diff --git a/src/builders/rollup/build.ts b/src/builders/rollup/build.ts index f7c4988..0326ffc 100644 --- a/src/builders/rollup/build.ts +++ b/src/builders/rollup/build.ts @@ -9,7 +9,7 @@ import { getRollupOptions } from "./config"; import { getChunkFilename } from "./utils"; import { rollupStub } from "./stub"; import { rollupWatch } from "./watch"; -import { fixCJSExportTypePlugin } from "./plugins/cjs"; +import { fixDefaultCJSExportsPlugin } from "./plugins/cjs"; export async function rollupBuild(ctx: BuildContext): Promise { // Stub mode @@ -82,7 +82,7 @@ export async function rollupBuild(ctx: BuildContext): Promise { ...rollupOptions.plugins, dts(ctx.options.rollup.dts), removeShebangPlugin(), - ctx.options.rollup.emitCJS && fixCJSExportTypePlugin(), + ctx.options.rollup.emitCJS && fixDefaultCJSExportsPlugin(ctx), ].filter(Boolean); await ctx.hooks.callHook("rollup:dts:options", ctx, rollupOptions); diff --git a/src/builders/rollup/plugins/cjs.md b/src/builders/rollup/plugins/cjs.md new file mode 100644 index 0000000..bc311ce --- /dev/null +++ b/src/builders/rollup/plugins/cjs.md @@ -0,0 +1,129 @@ +# Default CJS exports + +This document will cover all the types of default exports that can be generated by [rollup-plugin-dts](https://github.com/Swatinem/rollup-plugin-dts) plugin. + +### Export default without specifier from the module itself (index.ts in the fixture) + +The following example can be found in the [reexport-types](../../../../test/cjs-types-fixture/reexport-types) fixture, check `index.ts` module. + +Given the following code: +```ts +// module.ts +export default function foo() {} +``` + +`rollup-plugin-dts` will generate: +```ts +// module.d.[c]ts +declare function foo(): void; + +export { foo as default }; +``` + +we need to convert it to: +```ts +// module.d.[c]ts +declare function foo(): void; + +export = foo; +``` + +### Exporting default from default import + +The following example can be found in the [reexport-default](../../../../test/cjs-types-fixture/reexport-default) fixture, check `index.ts` module. + +Given the following code: +```ts +// module.ts +import MagicString from "magic-string"; +export default MagicString; +``` + +`rollup-plugin-dts` will generate: +```ts +// module.d.[c]ts +import MagicString from 'magic-string'; +export { default } from 'magic-string'; +``` + +we need to convert it to (doesn't require adding the import): +```ts +// module.d.[c]ts +import MagicString from 'magic-string'; +export = MagicString; +``` + +### Exporting default with specifier + +The following example can be found in the [reexport-types](../../../../test/cjs-types-fixture/reexport-types) fixture, check `all.ts` module. + +Given the following code: +```ts +// module.ts +export type * from "./index.ts"; +export { default } from "./index.ts"; +``` + +`rollup-plugin-dts` will generate: +```ts +// module.d.[c]ts +// otherexports and otherexporttypes when present +export { otherexports, type otherexporttypes, default } from './index.js'; +``` + +we need to convert it to (requires adding the import): +```ts +// module.d.[c]ts +import _default from './index.js'; +export = _default; +// otherexports and otherexporttypes when present +export { otherexports, type otherexporttypes } from './index.js'; +``` + +### Exporting default from named import as default export with specifier + +The following example can be found in the [reexport-default](../../../../test/cjs-types-fixture/reexport-default) fixture, check `asdefault.ts` module. + +Given the following code: +```ts +// module.ts +import { resolve } from "pathe"; +export default resolve; +``` + +`rollup-plugin-dts` will generate: +```ts +// module.d.[c]ts +import { resolve } from 'pathe'; +export { resolve as default } from 'pathe'; +``` + +we need to convert it to (doesn't require adding the import): +```ts +// module.d.[c]ts +import { resolve } from 'pathe'; +export = resolve; +``` + +### Exporting named export as default with specifier + +The following example can be found in the [reexport-default](../../../../test/cjs-types-fixture/reexport-default) fixture, check `asdefault.ts` module. + +Given the following code: +```ts +// module.ts +export { resolve as default } from "pathe"; +``` + +`rollup-plugin-dts` will generate: +```ts +// module.d.[c]ts +export { resolve as default } from 'pathe'; +``` + +we need to convert it to (requires adding the import): +```ts +// module.d.[c]ts +import { resolve } from 'pathe'; +export = resolve; +``` diff --git a/src/builders/rollup/plugins/cjs.ts b/src/builders/rollup/plugins/cjs.ts index 18a8fa4..96b2c0e 100644 --- a/src/builders/rollup/plugins/cjs.ts +++ b/src/builders/rollup/plugins/cjs.ts @@ -1,6 +1,8 @@ -import type { Plugin } from "rollup"; -import { findExports, findStaticImports } from "mlly"; +import type { Plugin, RenderedChunk } from "rollup"; +import { findExports, findStaticImports, parseStaticImport } from "mlly"; +import type { ESMExport, ParsedStaticImport } from "mlly"; import MagicString from "magic-string"; +import type { BuildContext } from "../../../types"; export function cjsPlugin(_opts?: any): Plugin { return { @@ -14,10 +16,7 @@ export function cjsPlugin(_opts?: any): Plugin { } as Plugin; } -export function fixCJSExportTypePlugin(): Plugin { - const regexp = /export\s*\{([^}]*)\}/; - const defaultExportRegexp = /\s*as\s+default\s*/; - const typeExportRegexp = /\s*type\s+/; +export function fixDefaultCJSExportsPlugin(ctx: BuildContext): Plugin { return { name: "unbuild-fix-cjs-export-type", renderChunk(code, info) { @@ -33,65 +32,7 @@ export function fixCJSExportTypePlugin(): Plugin { return; } - const defaultExport = findExports(code).find((e) => - e.names.includes("default"), - ); - - if (!defaultExport) { - return; - } - - const match = defaultExport.code.match(regexp); - if (!match?.length) { - return; - } - - let defaultAlias: string | undefined; - const exportsEntries: string[] = []; - for (const exp of match[1].split(",").map((e) => e.trim())) { - const m = exp.match(defaultExportRegexp); - if (m) { - defaultAlias = exp.replace(m[0], ""); - } else { - exportsEntries.push(exp); - } - } - - if (!defaultAlias) { - return; - } - - let exportStatement = exportsEntries.length > 0 ? undefined : ""; - - // replace export { type A, type B, type ... } with export type { A, B, ... } - // that's, if all remaining exports are type exports, replace export {} with export type {} - if (exportStatement === undefined) { - let someExternalExport = false; - const allRemainingExports = exportsEntries.map((exp) => { - if (someExternalExport) { - return [exp, ""] as const; - } - if (!info.imports.includes(exp)) { - const m = exp.match(typeExportRegexp); - if (m) { - const name = exp.replace(m[0], "").trim(); - if (!info.imports.includes(name)) { - return [exp, name] as const; - } - } - } - someExternalExport = true; - return [exp, ""] as const; - }); - exportStatement = someExternalExport - ? `\nexport { ${allRemainingExports.map(([e, _]) => e).join(", ")} }` - : `\nexport type { ${allRemainingExports.map(([_, t]) => t).join(", ")} }`; - } - - return code.replace( - defaultExport.code, - `export = ${defaultAlias}${exportStatement}`, - ); + return pathDefaultCJSExportsPlugin(code, info, ctx); }, } as Plugin; } @@ -125,3 +66,298 @@ function CJSToESM(code: string): { code: string; map: any } | null { map: s.generateMap(), }; } + +interface ParsedExports { + defaultExport: ESMExport; + defaultAlias: string; + exports: string[]; +} + +function extractExports( + code: string, + info: RenderedChunk, + ctx: BuildContext, +): ParsedExports | undefined { + const defaultExport = findExports(code).find((e) => + e.names.includes("default"), + ); + + if (!defaultExport) { + ctx.warnings.add( + `No default export found in ${info.fileName}, it contains default export but cannot be parsed.`, + ); + return; + } + + const regexp = /export\s*\{([^}]*)\}/; + const defaultExportRegexp = /\s*as\s+default\s*/; + const match = defaultExport.code.match(regexp); + if (!match?.length) { + ctx.warnings.add( + `No default export found in ${info.fileName}, it contains default export but cannot be parsed.`, + ); + return; + } + + let defaultAlias: string | undefined; + const exportsEntries: string[] = []; + for (const exp of match[1].split(",").map((e) => e.trim())) { + if (exp === "default") { + defaultAlias = exp; + continue; + } + const m = exp.match(defaultExportRegexp); + if (m) { + defaultAlias = exp.replace(m[0], ""); + } else { + exportsEntries.push(exp); + } + } + + if (!defaultAlias) { + ctx.warnings.add( + `No default export found in ${info.fileName}, it contains default export but cannot be parsed.`, + ); + return; + } + + return { + defaultExport, + defaultAlias, + exports: exportsEntries, + }; +} + +// export { default } from "magic-string"; +// or +// import MagicString from 'magic-string'; +// export default MagicString; +function handleDefaultCJSExportAsDefault( + code: string, + { defaultExport, exports }: ParsedExports, + imports: ParsedStaticImport[], + defaultImport?: ParsedStaticImport, +): string | undefined { + if (defaultImport) { + return exports.length === 0 + ? code.replace( + defaultExport.code, + `export = ${defaultImport.defaultImport}`, + ) + : code.replace( + defaultExport.code, + `export = ${defaultImport.defaultImport};\nexport { ${exports.join(", ")} } from '${defaultExport.specifier}'`, + ); + } else { + const magicString = new MagicString(code); + // add the import after last import in the code + const lastImportPosition = + imports.length > 0 ? imports.at(-1)?.end || 0 : 0; + if (lastImportPosition > 0) { + magicString.appendRight( + lastImportPosition, + `\nimport _default from '${defaultExport.specifier}';\n`, + ); + } else { + magicString.prepend( + `import _default from '${defaultExport.specifier}';\n`, + ); + } + + return exports.length > 0 + ? magicString + .replace( + defaultExport.code, + `export = _default;\nexport { ${exports.join(", ")} } from '${defaultExport.specifier}'`, + ) + .toString() + : magicString.replace(defaultExport.code, "export = _default").toString(); + } +} + +// export { resolve as default } from "pathe"; +function handleDefaultNamedCJSExport( + code: string, + info: RenderedChunk, + parsedExports: ParsedExports, + imports: ParsedStaticImport[], + ctx: BuildContext, + defaultImport?: ParsedStaticImport | undefined, +): string | undefined { + const { defaultAlias, defaultExport, exports } = parsedExports; + + // export { default } from "magic-string"; + if (defaultAlias === "default") { + // mlly parsing with _type='named', but always as default + // { + // type: 'default', + // exports: ' default', + // specifier: 'magic-string', + // names: [ 'default' ], + // name: 'default', + // _type: 'named' + // } + + // doesn't matter the type, it's always default (maybe mlly bug?) + + // export { resolve as default } from 'pathe'; + // { + // type: 'default', + // exports: ' resolve as default', + // specifier: 'pathe', + // names: [ 'default' ], + // name: 'default', + // _type: 'named' + // } + + // prevent calling handleDefaultCJSExportAsDefault + // since we don't have the import name for the default export + // defaultImport should be undefined + if (defaultImport && !defaultImport.defaultImport) { + ctx.warnings.add( + `Cannot parse default export name from ${defaultImport.specifier} import at ${info.fileName}!.`, + ); + return undefined; + } + + return handleDefaultCJSExportAsDefault( + code, + parsedExports, + imports, + defaultImport, + ); + } + + if (defaultImport) { + // we need to add the named import to the default import + const namedExports = defaultImport.namedImports; + if (namedExports?.[defaultAlias] === defaultAlias) { + return exports.length === 0 + ? code.replace(defaultExport.code, `export = ${defaultAlias}`) + : code.replace( + defaultExport.code, + `export = ${defaultAlias};\nexport { ${exports.join(", ")} }`, + ); + } else { + ctx.warnings.add( + `Cannot parse "${defaultAlias}" named export from ${defaultImport.specifier} import at ${info.fileName}!.`, + ); + return undefined; + } + } + + // we need to add the import + const magicString = new MagicString(code); + // add the import after last import in the code + const lastImportPosition = imports.length > 0 ? imports.at(-1)?.end || 0 : 0; + if (lastImportPosition > 0) { + magicString.appendRight( + lastImportPosition, + `\nimport { ${defaultAlias} } from '${defaultExport.specifier}';\n`, + ); + } else { + magicString.prepend( + `import { ${defaultAlias} } from '${defaultExport.specifier}';\n`, + ); + } + + return exports.length > 0 + ? magicString + .replace( + defaultExport.code, + `export = ${defaultAlias};\nexport { ${exports.join(", ")} } from '${defaultExport.specifier}'`, + ) + .toString() + : magicString + .replace(defaultExport.code, `export = ${defaultAlias}`) + .toString(); +} + +// export { xxx as default }; +function handleNoSpecifierDefaultCJSExport( + code: string, + info: RenderedChunk, + { defaultAlias, defaultExport, exports }: ParsedExports, +): string | undefined { + let exportStatement = exports.length > 0 ? undefined : ""; + + // replace export { type A, type B, type ... } with export type { A, B, ... } + // that's, if all remaining exports are type exports, replace export {} with export type {} + if (exportStatement === undefined) { + let someExternalExport = false; + const typeExportRegexp = /\s*type\s+/; + const allRemainingExports = exports.map((exp) => { + if (someExternalExport) { + return [exp, ""] as const; + } + if (!info.imports.includes(exp)) { + const m = exp.match(typeExportRegexp); + if (m) { + const name = exp.replace(m[0], "").trim(); + if (!info.imports.includes(name)) { + return [exp, name] as const; + } + } + } + someExternalExport = true; + return [exp, ""] as const; + }); + exportStatement = someExternalExport + ? `;\nexport { ${allRemainingExports.map(([e, _]) => e).join(", ")} }` + : `;\nexport type { ${allRemainingExports.map(([_, t]) => t).join(", ")} }`; + } + + return code.replace( + defaultExport.code, + `export = ${defaultAlias}${exportStatement}`, + ); +} + +function pathDefaultCJSExportsPlugin( + code: string, + info: RenderedChunk, + ctx: BuildContext, +): string | undefined { + const parsedExports = extractExports(code, info, ctx); + if (!parsedExports) { + return; + } + + if (parsedExports.defaultExport.specifier) { + const imports: ParsedStaticImport[] = []; + for (const imp of findStaticImports(code)) { + // don't add empty imports like import 'pathe'; + if (!imp.imports) { + continue; + } + imports.push(parseStaticImport(imp)); + } + const specifier = parsedExports.defaultExport.specifier; + const defaultImport = imports.find((i) => i.specifier === specifier); + return parsedExports.defaultExport._type === "named" + ? // export { resolve as default } from "pathe"; + // or (handleDefaultNamedCJSExport will call handleDefaultCJSExportAsDefault) + // export { default } from "magic-string"; + handleDefaultNamedCJSExport( + code, + info, + parsedExports, + imports, + ctx, + defaultImport, + ) + : // export { default } from "magic-string"; + // or + // import MagicString from 'magic-string'; + // export default MagicString; + handleDefaultCJSExportAsDefault( + code, + parsedExports, + imports, + defaultImport, + ); + } else { + // export { xxx as default }; + return handleNoSpecifierDefaultCJSExport(code, info, parsedExports); + } +} diff --git a/test/__snapshots__/cjs-exports.test.ts.snap b/test/__snapshots__/cjs-exports.test.ts.snap index add3fae..5650f8e 100644 --- a/test/__snapshots__/cjs-exports.test.ts.snap +++ b/test/__snapshots__/cjs-exports.test.ts.snap @@ -7,7 +7,7 @@ exports[`Node10 and Node16 Default Exports Types > Mixed Declaration Types 1`] = [ { "code": "export type { A, B, C, Options }", - "end": 259, + "end": 260, "exports": " A, B, C, Options", "names": [ "A", @@ -16,18 +16,38 @@ exports[`Node10 and Node16 Default Exports Types > Mixed Declaration Types 1`] = "Options", ], "specifier": undefined, - "start": 227, + "start": 228, "type": "named", }, ], [], + "interface A { + name: string; +} +interface B { + name: string; +} +interface C { + name: string; +} +interface Options { + a?: A; + b?: B; + c?: C; +} +declare function plugin(options?: Options): Options; + +export = plugin; +export type { A, B, C, Options }; +", + [], ], [ "index.d.ts", [ { "code": "export type { A, B, C, Options }", - "end": 259, + "end": 260, "exports": " A, B, C, Options", "names": [ "A", @@ -36,17 +56,135 @@ exports[`Node10 and Node16 Default Exports Types > Mixed Declaration Types 1`] = "Options", ], "specifier": undefined, - "start": 227, + "start": 228, "type": "named", }, ], [], + "interface A { + name: string; +} +interface B { + name: string; +} +interface C { + name: string; +} +interface Options { + a?: A; + b?: B; + c?: C; +} +declare function plugin(options?: Options): Options; + +export = plugin; +export type { A, B, C, Options }; +", + [], ], ] `; exports[`Node10 and Node16 Default Exports Types > Re-Export Types 1`] = ` [ + [ + "all.d.cts", + [], + [ + { + "code": "export { ResolvedOptions, default as plugin } from './index.cjs'", + "end": 119, + "exports": " ResolvedOptions, default as plugin", + "names": [ + "ResolvedOptions", + "plugin", + ], + "specifier": "./index.cjs", + "start": 55, + "type": "named", + }, + { + "code": "export { A, B, CC as C, CC, Options } from './types.cjs'", + "end": 177, + "exports": " A, B, CC as C, CC, Options", + "names": [ + "A", + "B", + "C", + "CC", + "Options", + ], + "specifier": "./types.cjs", + "start": 121, + "type": "named", + }, + ], + "import _default from './index.cjs'; +export = _default; +export { ResolvedOptions, default as plugin } from './index.cjs'; +export { A, B, CC as C, CC, Options } from './types.cjs'; +", + [ + { + "code": "import _default from './index.cjs'; +", + "end": 36, + "imports": "_default ", + "specifier": "./index.cjs", + "start": 0, + "type": "static", + }, + ], + ], + [ + "all.d.ts", + [], + [ + { + "code": "export { ResolvedOptions, default as plugin } from './index.js'", + "end": 117, + "exports": " ResolvedOptions, default as plugin", + "names": [ + "ResolvedOptions", + "plugin", + ], + "specifier": "./index.js", + "start": 54, + "type": "named", + }, + { + "code": "export { A, B, CC as C, CC, Options } from './types.js'", + "end": 174, + "exports": " A, B, CC as C, CC, Options", + "names": [ + "A", + "B", + "C", + "CC", + "Options", + ], + "specifier": "./types.js", + "start": 119, + "type": "named", + }, + ], + "import _default from './index.js'; +export = _default; +export { ResolvedOptions, default as plugin } from './index.js'; +export { A, B, CC as C, CC, Options } from './types.js'; +", + [ + { + "code": "import _default from './index.js'; +", + "end": 35, + "imports": "_default ", + "specifier": "./index.js", + "start": 0, + "type": "static", + }, + ], + ], [ "index.d.cts", [], @@ -66,18 +204,40 @@ exports[`Node10 and Node16 Default Exports Types > Re-Export Types 1`] = ` "type": "named", }, { - "code": "export { Options, type ResolvedOptions }", - "end": 271, - "exports": " Options, type ResolvedOptions", - "name": "Options", + "code": "export { Options, type ResolvedOptions, plugin }", + "end": 280, + "exports": " Options, type ResolvedOptions, plugin", "names": [ "Options", + "plugin", ], "specifier": undefined, - "start": 231, + "start": 232, "type": "named", }, ], + "import { Options } from './types.cjs'; +export { A, B, CC as C, CC } from './types.cjs'; + +interface ResolvedOptions extends Options { + name: string; +} +declare function plugin(options?: Options): ResolvedOptions; + +export = plugin; +export { Options, type ResolvedOptions, plugin }; +", + [ + { + "code": "import { Options } from './types.cjs'; +", + "end": 39, + "imports": "{ Options } ", + "specifier": "./types.cjs", + "start": 0, + "type": "static", + }, + ], ], [ "index.d.ts", @@ -98,18 +258,40 @@ exports[`Node10 and Node16 Default Exports Types > Re-Export Types 1`] = ` "type": "named", }, { - "code": "export { Options, type ResolvedOptions }", - "end": 269, - "exports": " Options, type ResolvedOptions", - "name": "Options", + "code": "export { Options, type ResolvedOptions, plugin }", + "end": 278, + "exports": " Options, type ResolvedOptions, plugin", "names": [ "Options", + "plugin", ], "specifier": undefined, - "start": 229, + "start": 230, "type": "named", }, ], + "import { Options } from './types.js'; +export { A, B, CC as C, CC } from './types.js'; + +interface ResolvedOptions extends Options { + name: string; +} +declare function plugin(options?: Options): ResolvedOptions; + +export = plugin; +export { Options, type ResolvedOptions, plugin }; +", + [ + { + "code": "import { Options } from './types.js'; +", + "end": 38, + "imports": "{ Options } ", + "specifier": "./types.js", + "start": 0, + "type": "static", + }, + ], ], [ "types.d.cts", @@ -131,6 +313,25 @@ exports[`Node10 and Node16 Default Exports Types > Re-Export Types 1`] = ` }, ], [], + "interface A { + name: string; +} +interface B { + name: string; +} +interface C { + name: string; +} + +interface Options { + a?: A; + b?: B; + c?: C; +} + +export type { A, B, C, C as CC, Options }; +", + [], ], [ "types.d.ts", @@ -152,6 +353,230 @@ exports[`Node10 and Node16 Default Exports Types > Re-Export Types 1`] = ` }, ], [], + "interface A { + name: string; +} +interface B { + name: string; +} +interface C { + name: string; +} + +interface Options { + a?: A; + b?: B; + c?: C; +} + +export type { A, B, C, C as CC, Options }; +", + [], + ], +] +`; + +exports[`Node10 and Node16 Default Exports Types > Re-Export as default 1`] = ` +[ + [ + "asdefault.d.cts", + [], + [], + "import { resolve } from 'pathe'; +export = resolve; +", + [ + { + "code": "import { resolve } from 'pathe'; +", + "end": 33, + "imports": "{ resolve } ", + "specifier": "pathe", + "start": 0, + "type": "static", + }, + ], + ], + [ + "asdefault.d.ts", + [], + [], + "import { resolve } from 'pathe'; +export = resolve; +", + [ + { + "code": "import { resolve } from 'pathe'; +", + "end": 33, + "imports": "{ resolve } ", + "specifier": "pathe", + "start": 0, + "type": "static", + }, + ], + ], + [ + "index.d.cts", + [], + [ + { + "code": "export { MagicStringOptions } from 'magic-string'", + "end": 111, + "exports": " MagicStringOptions", + "name": "MagicStringOptions", + "names": [ + "MagicStringOptions", + ], + "specifier": "magic-string", + "start": 62, + "type": "named", + }, + ], + "import MagicString from 'magic-string'; +export = MagicString; +export { MagicStringOptions } from 'magic-string'; +", + [ + { + "code": "import MagicString from 'magic-string'; +", + "end": 40, + "imports": "MagicString ", + "specifier": "magic-string", + "start": 0, + "type": "static", + }, + ], + ], + [ + "index.d.ts", + [], + [ + { + "code": "export { MagicStringOptions } from 'magic-string'", + "end": 111, + "exports": " MagicStringOptions", + "name": "MagicStringOptions", + "names": [ + "MagicStringOptions", + ], + "specifier": "magic-string", + "start": 62, + "type": "named", + }, + ], + "import MagicString from 'magic-string'; +export = MagicString; +export { MagicStringOptions } from 'magic-string'; +", + [ + { + "code": "import MagicString from 'magic-string'; +", + "end": 40, + "imports": "MagicString ", + "specifier": "magic-string", + "start": 0, + "type": "static", + }, + ], + ], + [ + "magicstringasdefault.d.cts", + [], + [], + "import _default from 'magic-string'; +export = _default; +import 'pathe'; +", + [ + { + "code": "import _default from 'magic-string'; +", + "end": 37, + "imports": "_default ", + "specifier": "magic-string", + "start": 0, + "type": "static", + }, + { + "code": "import 'pathe'; +", + "end": 72, + "imports": undefined, + "specifier": "pathe", + "start": 56, + "type": "static", + }, + ], + ], + [ + "magicstringasdefault.d.ts", + [], + [], + "import _default from 'magic-string'; +export = _default; +import 'pathe'; +", + [ + { + "code": "import _default from 'magic-string'; +", + "end": 37, + "imports": "_default ", + "specifier": "magic-string", + "start": 0, + "type": "static", + }, + { + "code": "import 'pathe'; +", + "end": 72, + "imports": undefined, + "specifier": "pathe", + "start": 56, + "type": "static", + }, + ], + ], + [ + "resolveasdefault.d.cts", + [], + [], + "import { resolve } from 'pathe'; +export = resolve; +", + [ + { + "code": "import { resolve } from 'pathe'; +", + "end": 33, + "imports": "{ resolve } ", + "specifier": "pathe", + "start": 0, + "type": "static", + }, + ], + ], + [ + "resolveasdefault.d.ts", + [], + [], + "import { resolve } from 'pathe'; +export = resolve; +", + [ + { + "code": "import { resolve } from 'pathe'; +", + "end": 33, + "imports": "{ resolve } ", + "specifier": "pathe", + "start": 0, + "type": "static", + }, + ], ], ] `; diff --git a/test/cjs-exports.test.ts b/test/cjs-exports.test.ts index 5ffcb89..a8d9db4 100644 --- a/test/cjs-exports.test.ts +++ b/test/cjs-exports.test.ts @@ -3,21 +3,42 @@ import { build } from "../src"; import { resolve } from "pathe"; import { fileURLToPath } from "node:url"; import { readdir, readFile } from "node:fs/promises"; -import { type ESMExport, findExports, findTypeExports } from "mlly"; +import { + type ESMExport, + findExports, + findStaticImports, + findTypeExports, + parseStaticImport, + type StaticImport, +} from "mlly"; describe("Node10 and Node16 Default Exports Types", () => { const dtsFiles = /\.d\.(c)?ts$/; async function readDtsFiles( dist: string, - ): Promise<[name: string, types: ESMExport[], exports: ESMExport[]][]> { + ): Promise< + [ + name: string, + types: ESMExport[], + exports: ESMExport[], + content: string, + imports: StaticImport[], + ][] + > { const files = await readdir(dist).then((files) => files.filter((f) => dtsFiles.test(f)).map((f) => [f, resolve(dist, f)]), ); return await Promise.all( files.map(async ([name, path]) => { const content = await readFile(path, "utf8"); - return [name, findTypeExports(content), findExports(content)]; + return [ + name, + findTypeExports(content), + findExports(content), + content, + findStaticImports(content), + ]; }), ); } @@ -66,8 +87,8 @@ describe("Node10 and Node16 Default Exports Types", () => { expect(warnings[0]).toBe('Generated an empty chunk: "types".'); expect(warnings[1]).toBe('Generated an empty chunk: "types".'); const files = await readDtsFiles(resolve(root, "dist")); - expect(files).toHaveLength(4); - for (const [name, types, exports] of files) { + expect(files).toHaveLength(6); + for (const [name, types, exports, content, imports] of files) { if (name.startsWith("types")) { expect(exports).toHaveLength(0); expect(types).toHaveLength(1); @@ -75,13 +96,84 @@ describe("Node10 and Node16 Default Exports Types", () => { types.find((e) => e.names.includes("default")), `${name} should not have a default export`, ).toBeUndefined(); - } else { + } else if (name.startsWith("index")) { + expect(exports).toHaveLength(2); + expect(types).toHaveLength(0); + expect(imports).toHaveLength(1); + expect( + exports.find((e) => e.names.includes("default")), + `${name} should not have a default export`, + ).toBeUndefined(); + expect(content).toMatch("export = plugin"); + } else if (name.startsWith("all")) { expect(exports).toHaveLength(2); expect(types).toHaveLength(0); + expect(imports).toHaveLength(1); + expect( + exports.find((e) => e.names.includes("default")), + `${name} should not have a default export`, + ).toBeUndefined(); + const defaultImport = parseStaticImport(imports[0]); + expect(defaultImport.defaultImport).toBe("_default"); + expect(content).toMatch(`export = ${defaultImport.defaultImport}`); + } + } + expect(files).toMatchSnapshot(); + }); + + it("Re-Export as default", async () => { + const root = resolve( + fileURLToPath(import.meta.url), + "../cjs-types-fixture/reexport-default", + ); + await build(root, false); + const files = await readDtsFiles(resolve(root, "dist")); + expect(files).toHaveLength(8); + for (const [name, types, exports, content, imports] of files) { + if (name.startsWith("asdefault")) { + expect(exports).toHaveLength(0); + expect(types).toHaveLength(0); + expect(imports).toHaveLength(1); + expect( + types.find((e) => e.names.includes("default")), + `${name} should not have a default export`, + ).toBeUndefined(); + const defaultImport = parseStaticImport(imports[0]); + expect(defaultImport.namedImports?.resolve).toBeDefined(); + expect(content).toMatch(`export = resolve`); + } else if (name.startsWith("index")) { + expect(exports).toHaveLength(1); + expect(types).toHaveLength(0); + expect(imports).toHaveLength(1); + expect( + exports.find((e) => e.names.includes("default")), + `${name} should not have a default export`, + ).toBeUndefined(); + const defaultImport = parseStaticImport(imports[0]); + expect(defaultImport.defaultImport).toBe("MagicString"); + expect(content).toMatch(`export = ${defaultImport.defaultImport}`); + } else if (name.startsWith("magicstringasdefault")) { + expect(exports).toHaveLength(0); + expect(types).toHaveLength(0); + expect(imports.filter((i) => !!i.imports)).toHaveLength(1); + expect( + exports.find((e) => e.names.includes("default")), + `${name} should not have a default export`, + ).toBeUndefined(); + const defaultImport = parseStaticImport(imports[0]); + expect(defaultImport.defaultImport).toBe("_default"); + expect(content).toMatch(`export = ${defaultImport.defaultImport}`); + } else if (name.startsWith("resolvedasdefault")) { + expect(exports).toHaveLength(0); + expect(types).toHaveLength(0); + expect(imports.filter((i) => !!i.imports)).toHaveLength(1); expect( exports.find((e) => e.names.includes("default")), `${name} should not have a default export`, ).toBeUndefined(); + const defaultImport = parseStaticImport(imports[0]); + expect(defaultImport.defaultImport).toBe("resolve"); + expect(content).toMatch(`export = ${defaultImport.defaultImport}`); } } expect(files).toMatchSnapshot(); diff --git a/test/cjs-types-fixture/reexport-default/asdefault.ts b/test/cjs-types-fixture/reexport-default/asdefault.ts new file mode 100644 index 0000000..2170d36 --- /dev/null +++ b/test/cjs-types-fixture/reexport-default/asdefault.ts @@ -0,0 +1,4 @@ +import { resolve } from "pathe"; + +// eslint-disable-next-line +export default resolve; diff --git a/test/cjs-types-fixture/reexport-default/build.config.ts b/test/cjs-types-fixture/reexport-default/build.config.ts new file mode 100644 index 0000000..3fb8486 --- /dev/null +++ b/test/cjs-types-fixture/reexport-default/build.config.ts @@ -0,0 +1,27 @@ +import { defineBuildConfig } from "../../../src"; + +export default defineBuildConfig({ + entries: [ + "./index.ts", + "./asdefault.ts", + "./magicstringasdefault.ts", + "./resolveasdefault.ts", + ], + declaration: true, + clean: true, + // avoid exit code 1 on warnings + failOnWarn: false, + externals: ["magic-string", "pathe"], + rollup: { + emitCJS: true, + dts: { + respectExternal: true, + compilerOptions: { + composite: false, + preserveSymlinks: false, + module: 200, + moduleResolution: 100, + }, + }, + }, +}); diff --git a/test/cjs-types-fixture/reexport-default/index.ts b/test/cjs-types-fixture/reexport-default/index.ts new file mode 100644 index 0000000..245577c --- /dev/null +++ b/test/cjs-types-fixture/reexport-default/index.ts @@ -0,0 +1,6 @@ +import MagicString, { type MagicStringOptions } from "magic-string"; + +// eslint-disable-next-line +export default MagicString; +// eslint-disable-next-line +export type { MagicStringOptions }; diff --git a/test/cjs-types-fixture/reexport-default/magicstringasdefault.ts b/test/cjs-types-fixture/reexport-default/magicstringasdefault.ts new file mode 100644 index 0000000..08444f7 --- /dev/null +++ b/test/cjs-types-fixture/reexport-default/magicstringasdefault.ts @@ -0,0 +1 @@ +export { default } from "magic-string"; diff --git a/test/cjs-types-fixture/reexport-default/package.json b/test/cjs-types-fixture/reexport-default/package.json new file mode 100644 index 0000000..e9eedd0 --- /dev/null +++ b/test/cjs-types-fixture/reexport-default/package.json @@ -0,0 +1,67 @@ +{ + "name": "cjs-types-reexport-default-fixture", + "version": "0.0.0", + "private": "true", + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./asdefault": { + "import": { + "types": "./dist/asdefault.d.mts", + "default": "./dist/asdefault.mjs" + }, + "require": { + "types": "./dist/asdefault.d.cts", + "default": "./dist/asdefault.cjs" + } + }, + "./magicstringasdefault": { + "import": { + "types": "./dist/magicstringasdefault.d.mts", + "default": "./dist/magicstringasdefault.mjs" + }, + "require": { + "types": "./dist/magicstringasdefault.d.cts", + "default": "./dist/magicstringasdefault.cjs" + } + }, + "./resolveasdefault": { + "import": { + "types": "./dist/resolveasdefault.d.mts", + "default": "./dist/resolveasdefault.mjs" + }, + "require": { + "types": "./dist/resolveasdefault.d.cts", + "default": "./dist/resolveasdefault.cjs" + } + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "typesVersions": { + "*": { + "asdefault": [ + "./dist/asdefault.d.ts" + ], + "magicstringasdefault": [ + "./dist/magicstringasdefault.d.ts" + ], + "resolveasdefault": [ + "./dist/resolveasdefault.d.ts" + ] + } + }, + "files": [ + "dist" + ] +} diff --git a/test/cjs-types-fixture/reexport-default/resolveasdefault.ts b/test/cjs-types-fixture/reexport-default/resolveasdefault.ts new file mode 100644 index 0000000..9dedfb5 --- /dev/null +++ b/test/cjs-types-fixture/reexport-default/resolveasdefault.ts @@ -0,0 +1 @@ +export { resolve as default } from "pathe"; diff --git a/test/cjs-types-fixture/reexport-types/all.ts b/test/cjs-types-fixture/reexport-types/all.ts new file mode 100644 index 0000000..25b0730 --- /dev/null +++ b/test/cjs-types-fixture/reexport-types/all.ts @@ -0,0 +1,2 @@ +export type * from "./index.ts"; +export { default } from "./index.ts"; diff --git a/test/cjs-types-fixture/reexport-types/build.config.ts b/test/cjs-types-fixture/reexport-types/build.config.ts index 17aaa78..d87a817 100644 --- a/test/cjs-types-fixture/reexport-types/build.config.ts +++ b/test/cjs-types-fixture/reexport-types/build.config.ts @@ -1,7 +1,7 @@ import { defineBuildConfig } from "../../../src"; export default defineBuildConfig({ - entries: ["./index.ts", "./types.ts"], + entries: ["./index.ts", "./types.ts", "./all.ts"], declaration: true, clean: true, // avoid exit code 1 on warnings diff --git a/test/cjs-types-fixture/reexport-types/index.ts b/test/cjs-types-fixture/reexport-types/index.ts index 7301062..4d50410 100644 --- a/test/cjs-types-fixture/reexport-types/index.ts +++ b/test/cjs-types-fixture/reexport-types/index.ts @@ -6,9 +6,12 @@ export interface ResolvedOptions extends Options { name: string; } -export default function plugin(options: Options = {}): ResolvedOptions { +function plugin(options: Options = {}): ResolvedOptions { return { ...options, name: "plugin", }; } + +export default plugin; +export { plugin }; diff --git a/test/cjs-types-fixture/reexport-types/package.json b/test/cjs-types-fixture/reexport-types/package.json index 1030e42..ffb9106 100644 --- a/test/cjs-types-fixture/reexport-types/package.json +++ b/test/cjs-types-fixture/reexport-types/package.json @@ -14,6 +14,16 @@ "default": "./dist/index.cjs" } }, + "./all": { + "import": { + "types": "./dist/all.d.mts", + "default": "./dist/all.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, "./types": { "types": { "import": "./dist/types.d.mts", @@ -26,6 +36,9 @@ "types": "./dist/index.d.ts", "typesVersions": { "*": { + "all": [ + "./dist/all.d.ts" + ], "types": [ "./dist/types.d.ts" ]