diff --git a/src/cli.js b/src/cli.js index 909e3c3..91cf4d0 100644 --- a/src/cli.js +++ b/src/cli.js @@ -11,9 +11,13 @@ const cli = cac("@atomico/exports").version("1.11.0"); cli.command("<...files>", "Build files") .option("--dist ", "Destination directory") - .option("--main ", "Nain file") + .option("--main ", "Main file") .option("--watch", "watch mode") .option("--wrappers", "enable the wrapper generator") + .option( + "--preserve-extensions", + "preserve file extensions in exports, for broader ESM compatibility" + ) .option("--ignore-types", "enable the wrapper generator") .option("--workspaces", "enable dependency merging") .option( @@ -35,8 +39,10 @@ cli.command("<...files>", "Build files") * @param {string[]} src * @param {object} flags * @param {boolean} flags.watch + * @param {string} flags.dist * @param {string} flags.main - * @param {string} flags.wrappers + * @param {boolean} flags.wrappers + * @param {boolean} flags.preserveExtensions * @param {boolean} flags.tmp * @param {boolean} flags.workspaces * @param {boolean} flags.publish @@ -49,6 +55,7 @@ cli.command("<...files>", "Build files") { watch, main = "index", + preserveExtensions, wrappers, dist, tmp, @@ -66,6 +73,7 @@ cli.command("<...files>", "Build files") src, main, wrappers, + preserveExtensions, dist, pkg: { src: tmp diff --git a/src/create-exports.js b/src/create-exports.js index 73e8cbb..19fd359 100644 --- a/src/create-exports.js +++ b/src/create-exports.js @@ -8,6 +8,7 @@ import { cleanPath, getModules, isJs, isTsDeclaration } from "./utils.js"; * @param {Pkg} options.pkg * @param {string} [options.main] * @param {string} [options.dist] + * @param {boolean} [options.preserveExtensions] * @param {boolean} [options.wrappers] * @param {boolean} [options.ignoreTypes] * @param {boolean} [options.centralizePackages] @@ -49,6 +50,7 @@ export async function createExports(options) { input: filesJs, scope: options.pkg.name, dist: options.dist, + preserveExtensions: options.preserveExtensions, centralizeWrappers: options.centralizeWrappers, main, }) @@ -130,12 +132,20 @@ export async function createExports(options) { return { pkg: { ...meta, - exports: filesJs.reduce( - (current, [path, file]) => ({ + exports: filesJs.reduce((current, [path, file]) => { + let resolvedPath = path; + + if (isJs(file) && path !== "." && options.preserveExtensions) { + resolvedPath += ".js"; + } + + return { ...current, [cleanPath( formatFirstDot( - path.startsWith(".") ? path : `./${path}` + resolvedPath.startsWith(".") + ? resolvedPath + : `./${resolvedPath}` ) )]: { ...(filesTsByPath[path] @@ -143,20 +153,26 @@ export async function createExports(options) { : {}), default: formatFirstDot(file), }, - }), - options.pkg?.exports || {} - ), + }; + }, options.pkg?.exports || {}), typesVersions: { ...options.pkg?.typesVersions, "*": filesTs .filter(([name]) => name) - .reduce( - (current, [path, file]) => ({ + .reduce((current, [path, file]) => { + let resolvedPath = path; + + if (options.preserveExtensions) { + resolvedPath += ".js"; + } + + return { ...current, - [cleanPath(path, { relative: true })]: [file], - }), - options.pkg?.typesVersions?.["*"] || {} - ), + [cleanPath(resolvedPath, { relative: true })]: [ + file, + ], + }; + }, options.pkg?.typesVersions?.["*"] || {}), }, }, wrappers, diff --git a/src/create-wrapper.js b/src/create-wrapper.js index 230b6fb..8f5a415 100644 --- a/src/create-wrapper.js +++ b/src/create-wrapper.js @@ -23,6 +23,7 @@ export const peerDependencies = [ * @param {[string,string][]} options.input * @param {string} [options.dist] * @param {string} [options.main] + * @param {boolean} [options.preserveExtensions] * @param {boolean} [options.centralizeWrappers] * @returns {ReturnType} */ @@ -35,6 +36,7 @@ export async function createWrappers(options) { createWrapper({ scope: options.scope, main: options.main, + preserveExtensions: options.preserveExtensions, input, path, dist, @@ -78,6 +80,7 @@ export async function createWrappers(options) { * @param {string} options.path * @param {string} options.dist * @param {string} options.main + * @param {boolean} options.preserveExtensions */ export async function createWrapper(options) { const code = await readFile(options.input, "utf8"); @@ -148,10 +151,16 @@ export async function createWrapper(options) { if (!elements.length) return; const imports = elements.map(([name]) => `${name} as _${name}`); + const subPath = + origin === options.main || !origin + ? "" + : options.preserveExtensions + ? `/${origin}.js` + : `/${origin}`; const originModule = `import { ${imports.join(", ")} } from "${ options.scope - }${origin === options.main || !origin ? "" : "/" + origin}";`; + }${subPath}";`; const tagNames = elements.map( ([name, { tagName }]) => diff --git a/src/merge-exports.js b/src/merge-exports.js index abc152e..b8f7745 100644 --- a/src/merge-exports.js +++ b/src/merge-exports.js @@ -2,12 +2,14 @@ import glob from "fast-glob"; import { createExports } from "./create-exports.js"; import { createPublish } from "./create-publish.js"; import { getJsonFormat, logger, read, write } from "./utils.js"; +import { extname } from "path"; /** * @param {object} options * @param {string[]} options.src * @param {string} options.main * @param {string} options.dist + * @param {boolean} options.preserveExtensions * @param {boolean} options.wrappers * @param {boolean} options.workspaces * @param {boolean} [options.publish] @@ -15,7 +17,7 @@ import { getJsonFormat, logger, read, write } from "./utils.js"; * @param {boolean} [options.ignoreTypes] * @param {boolean} [options.centralizePackages] * @param {boolean} [options.centralizeWrappers] - * @param {{src: string, snap: import("./create-exports").Pkg}} options.pkg + * @param {{src: string, snap: string}} options.pkg */ export async function mergeExports(options) { logger(`getting files...`); @@ -95,6 +97,7 @@ export async function mergeExports(options) { dist: options.dist, wrappers: options.wrappers, ignoreTypes: options.ignoreTypes, + preserveExtensions: options.preserveExtensions, centralizePackages: options.centralizePackages, centralizeWrappers: options.centralizeWrappers, }); diff --git a/tests/extensions-dist/module/package.json b/tests/extensions-dist/module/package.json new file mode 100644 index 0000000..49faace --- /dev/null +++ b/tests/extensions-dist/module/package.json @@ -0,0 +1,75 @@ +{ + "name": "demo", + "dependencies": { + "atomico": "latest" + }, + "peerDependencies": { + "@atomico/react": "*", + "@atomico/vue": "*" + }, + "peerDependenciesMeta": { + "@atomico/react": { + "optional": true + }, + "@atomico/vue": { + "optional": true + } + }, + "main": "./tests/module/atomico.js", + "module": "./tests/module/atomico.js", + "types": "./tests/module/atomico.d.ts", + "exports": { + "./atomico.js": { + "types": "./tests/module/atomico.d.ts", + "default": "./tests/module/atomico.js" + }, + "./components/a.js": { + "default": "./tests/module/components/a/a.js" + }, + "./components/b.js": { + "default": "./tests/module/components/b/b.js" + }, + "./components/c.js": { + "types": "./tests/module/components/c/c.d.ts", + "default": "./tests/module/components/c/c.js" + }, + "./react.js": { + "types": "./tests/extensions-dist/module/react.d.ts", + "default": "./tests/extensions-dist/module/react.js" + }, + "./preact.js": { + "types": "./tests/extensions-dist/module/preact.d.ts", + "default": "./tests/extensions-dist/module/preact.js" + }, + "./vue.js": { + "types": "./tests/extensions-dist/module/vue.d.ts", + "default": "./tests/extensions-dist/module/vue.js" + }, + ".": { + "types": "./tests/module/atomico.d.ts", + "default": "./tests/module/atomico.js" + }, + "./package.json": { + "default": "./tests/module/package.json" + } + }, + "typesVersions": { + "*": { + "atomico.js": [ + "./tests/module/atomico.d.ts" + ], + "components/c.js": [ + "./tests/module/components/c/c.d.ts" + ], + "react.js": [ + "./tests/extensions-dist/module/react.d.ts" + ], + "preact.js": [ + "./tests/extensions-dist/module/preact.d.ts" + ], + "vue.js": [ + "./tests/extensions-dist/module/vue.d.ts" + ] + } + } +} diff --git a/tests/extensions-dist/module/preact.d.ts b/tests/extensions-dist/module/preact.d.ts new file mode 100644 index 0000000..3f55233 --- /dev/null +++ b/tests/extensions-dist/module/preact.d.ts @@ -0,0 +1,8 @@ +import { MyComponent as _MyComponent } from "demo"; +import { Component } from "@atomico/react/preact"; +export const MyComponent: Component; +declare namespace JSX { + interface IntrinsicElements{ + "my-component": Component; + } +} \ No newline at end of file diff --git a/tests/extensions-dist/module/preact.js b/tests/extensions-dist/module/preact.js new file mode 100644 index 0000000..ee18340 --- /dev/null +++ b/tests/extensions-dist/module/preact.js @@ -0,0 +1,4 @@ +"use client"; +import { MyComponent as _MyComponent } from "demo"; +import { auto } from "@atomico/react/preact"; +export const MyComponent = auto(_MyComponent); \ No newline at end of file diff --git a/tests/extensions-dist/module/react.d.ts b/tests/extensions-dist/module/react.d.ts new file mode 100644 index 0000000..bc2cb21 --- /dev/null +++ b/tests/extensions-dist/module/react.d.ts @@ -0,0 +1,8 @@ +import { MyComponent as _MyComponent } from "demo"; +import { Component } from "@atomico/react"; +export const MyComponent: Component; +declare namespace JSX { + interface IntrinsicElements{ + "my-component": Component; + } +} \ No newline at end of file diff --git a/tests/extensions-dist/module/react.js b/tests/extensions-dist/module/react.js new file mode 100644 index 0000000..4ab4460 --- /dev/null +++ b/tests/extensions-dist/module/react.js @@ -0,0 +1,4 @@ +"use client"; +import { MyComponent as _MyComponent } from "demo"; +import { auto } from "@atomico/react"; +export const MyComponent = auto(_MyComponent); \ No newline at end of file diff --git a/tests/extensions-dist/module/vue.d.ts b/tests/extensions-dist/module/vue.d.ts new file mode 100644 index 0000000..3fe8e13 --- /dev/null +++ b/tests/extensions-dist/module/vue.d.ts @@ -0,0 +1,3 @@ +import { MyComponent as _MyComponent } from "demo"; +import { Component } from "@atomico/vue"; +export const MyComponent: Component; \ No newline at end of file diff --git a/tests/extensions-dist/module/vue.js b/tests/extensions-dist/module/vue.js new file mode 100644 index 0000000..f6ed0bb --- /dev/null +++ b/tests/extensions-dist/module/vue.js @@ -0,0 +1,4 @@ +"use client"; +import { MyComponent as _MyComponent } from "demo"; +import { auto } from "@atomico/vue"; +export const MyComponent = auto(_MyComponent); \ No newline at end of file diff --git a/tests/extensions-expect/module/package.json b/tests/extensions-expect/module/package.json new file mode 100644 index 0000000..49faace --- /dev/null +++ b/tests/extensions-expect/module/package.json @@ -0,0 +1,75 @@ +{ + "name": "demo", + "dependencies": { + "atomico": "latest" + }, + "peerDependencies": { + "@atomico/react": "*", + "@atomico/vue": "*" + }, + "peerDependenciesMeta": { + "@atomico/react": { + "optional": true + }, + "@atomico/vue": { + "optional": true + } + }, + "main": "./tests/module/atomico.js", + "module": "./tests/module/atomico.js", + "types": "./tests/module/atomico.d.ts", + "exports": { + "./atomico.js": { + "types": "./tests/module/atomico.d.ts", + "default": "./tests/module/atomico.js" + }, + "./components/a.js": { + "default": "./tests/module/components/a/a.js" + }, + "./components/b.js": { + "default": "./tests/module/components/b/b.js" + }, + "./components/c.js": { + "types": "./tests/module/components/c/c.d.ts", + "default": "./tests/module/components/c/c.js" + }, + "./react.js": { + "types": "./tests/extensions-dist/module/react.d.ts", + "default": "./tests/extensions-dist/module/react.js" + }, + "./preact.js": { + "types": "./tests/extensions-dist/module/preact.d.ts", + "default": "./tests/extensions-dist/module/preact.js" + }, + "./vue.js": { + "types": "./tests/extensions-dist/module/vue.d.ts", + "default": "./tests/extensions-dist/module/vue.js" + }, + ".": { + "types": "./tests/module/atomico.d.ts", + "default": "./tests/module/atomico.js" + }, + "./package.json": { + "default": "./tests/module/package.json" + } + }, + "typesVersions": { + "*": { + "atomico.js": [ + "./tests/module/atomico.d.ts" + ], + "components/c.js": [ + "./tests/module/components/c/c.d.ts" + ], + "react.js": [ + "./tests/extensions-dist/module/react.d.ts" + ], + "preact.js": [ + "./tests/extensions-dist/module/preact.d.ts" + ], + "vue.js": [ + "./tests/extensions-dist/module/vue.d.ts" + ] + } + } +} diff --git a/tests/extensions-expect/module/preact.d.ts b/tests/extensions-expect/module/preact.d.ts new file mode 100644 index 0000000..3f55233 --- /dev/null +++ b/tests/extensions-expect/module/preact.d.ts @@ -0,0 +1,8 @@ +import { MyComponent as _MyComponent } from "demo"; +import { Component } from "@atomico/react/preact"; +export const MyComponent: Component; +declare namespace JSX { + interface IntrinsicElements{ + "my-component": Component; + } +} \ No newline at end of file diff --git a/tests/extensions-expect/module/preact.js b/tests/extensions-expect/module/preact.js new file mode 100644 index 0000000..ee18340 --- /dev/null +++ b/tests/extensions-expect/module/preact.js @@ -0,0 +1,4 @@ +"use client"; +import { MyComponent as _MyComponent } from "demo"; +import { auto } from "@atomico/react/preact"; +export const MyComponent = auto(_MyComponent); \ No newline at end of file diff --git a/tests/extensions-expect/module/react.d.ts b/tests/extensions-expect/module/react.d.ts new file mode 100644 index 0000000..bc2cb21 --- /dev/null +++ b/tests/extensions-expect/module/react.d.ts @@ -0,0 +1,8 @@ +import { MyComponent as _MyComponent } from "demo"; +import { Component } from "@atomico/react"; +export const MyComponent: Component; +declare namespace JSX { + interface IntrinsicElements{ + "my-component": Component; + } +} \ No newline at end of file diff --git a/tests/extensions-expect/module/react.js b/tests/extensions-expect/module/react.js new file mode 100644 index 0000000..4ab4460 --- /dev/null +++ b/tests/extensions-expect/module/react.js @@ -0,0 +1,4 @@ +"use client"; +import { MyComponent as _MyComponent } from "demo"; +import { auto } from "@atomico/react"; +export const MyComponent = auto(_MyComponent); \ No newline at end of file diff --git a/tests/extensions-expect/module/vue.d.ts b/tests/extensions-expect/module/vue.d.ts new file mode 100644 index 0000000..3fe8e13 --- /dev/null +++ b/tests/extensions-expect/module/vue.d.ts @@ -0,0 +1,3 @@ +import { MyComponent as _MyComponent } from "demo"; +import { Component } from "@atomico/vue"; +export const MyComponent: Component; \ No newline at end of file diff --git a/tests/extensions-expect/module/vue.js b/tests/extensions-expect/module/vue.js new file mode 100644 index 0000000..f6ed0bb --- /dev/null +++ b/tests/extensions-expect/module/vue.js @@ -0,0 +1,4 @@ +"use client"; +import { MyComponent as _MyComponent } from "demo"; +import { auto } from "@atomico/vue"; +export const MyComponent = auto(_MyComponent); \ No newline at end of file diff --git a/tests/merge-export.test.js b/tests/merge-export.test.js index 62e6f53..f670e10 100644 --- a/tests/merge-export.test.js +++ b/tests/merge-export.test.js @@ -47,3 +47,47 @@ test("module/atomico", async (t) => { ) ); }); + +test("module/atomico - preserve extensions", async (t) => { + const snapPkg = await readFile("./tests/module/package.json", "utf8"); + + await mergeExports({ + main: "atomico", + src: ["./tests/module/**"], + dist: "./tests/extensions-dist/module", + preserveExtensions: true, + wrappers: true, + pkg: { + src: "./tests/extensions-dist/module/package.json", + snap: snapPkg, + }, + }); + + const baseExpect = "./tests/extensions-expect"; + + const baseDist = "./tests/extensions-dist"; + + const filesExpect = await glob(`${baseExpect}/**`); + + const filesDist = (await glob(`${baseDist}/**`)).reduce( + (files, file) => ({ + ...files, + [file.replace(baseDist, "")]: file, + }), + {} + ); + + await Promise.all( + filesExpect.map(async (file) => + t.is( + replaceLine(await readFile(file, "utf8")), + replaceLine( + await readFile( + filesDist[file.replace(baseExpect, "")], + "utf8" + ) + ) + ) + ) + ); +}); diff --git a/types/create-exports.d.ts b/types/create-exports.d.ts index 8b8cf3c..b5a3c07 100644 --- a/types/create-exports.d.ts +++ b/types/create-exports.d.ts @@ -4,6 +4,7 @@ * @param {Pkg} options.pkg * @param {string} [options.main] * @param {string} [options.dist] + * @param {boolean} [options.preserveExtensions] * @param {boolean} [options.wrappers] * @param {boolean} [options.ignoreTypes] * @param {boolean} [options.centralizePackages] @@ -15,6 +16,7 @@ export function createExports(options: { pkg: Pkg; main?: string; dist?: string; + preserveExtensions?: boolean; wrappers?: boolean; ignoreTypes?: boolean; centralizePackages?: boolean; diff --git a/types/merge-exports.d.ts b/types/merge-exports.d.ts index 9c54b96..3704452 100644 --- a/types/merge-exports.d.ts +++ b/types/merge-exports.d.ts @@ -3,6 +3,7 @@ * @param {string[]} options.src * @param {string} options.main * @param {string} options.dist + * @param {boolean} options.preserveExtensions * @param {boolean} options.wrappers * @param {boolean} options.workspaces * @param {boolean} [options.publish] @@ -10,12 +11,13 @@ * @param {boolean} [options.ignoreTypes] * @param {boolean} [options.centralizePackages] * @param {boolean} [options.centralizeWrappers] - * @param {{src: string, snap: import("./create-exports").Pkg}} options.pkg + * @param {{src: string, snap: string}} options.pkg */ export function mergeExports(options: { src: string[]; main: string; dist: string; + preserveExtensions: boolean; wrappers: boolean; workspaces: boolean; publish?: boolean; @@ -25,6 +27,6 @@ export function mergeExports(options: { centralizeWrappers?: boolean; pkg: { src: string; - snap: import("./create-exports").Pkg; + snap: string; }; }): Promise;