diff --git a/packages/plugin-rsc/src/transforms/index.ts b/packages/plugin-rsc/src/transforms/index.ts index 6147430da..7a185a783 100644 --- a/packages/plugin-rsc/src/transforms/index.ts +++ b/packages/plugin-rsc/src/transforms/index.ts @@ -1,4 +1,5 @@ export * from './hoist' +export * from './module-exports' export * from './module-export-effect' export * from './wrap-export' export * from './proxy-export' diff --git a/packages/plugin-rsc/src/transforms/module-export-effect.ts b/packages/plugin-rsc/src/transforms/module-export-effect.ts index 5d208aa9b..5a051dd0c 100644 --- a/packages/plugin-rsc/src/transforms/module-export-effect.ts +++ b/packages/plugin-rsc/src/transforms/module-export-effect.ts @@ -1,50 +1,22 @@ import { tinyassert } from '@hiogawa/utils' -import type { ExportDefaultDeclaration, Node, Program } from 'estree' +import type { Identifier } from 'estree' import MagicString from 'magic-string' import type { ESTree } from 'vite' -import { extractNames, validateNonAsyncFunction } from './utils' +import { scanModuleExports, type ModuleExportMeta } from './module-exports' +import { validateNonAsyncFunction } from './utils' // TODO: Metadata, filtering, and returned reference contexts are currently // ported only for transformWrapExport compatibility. Remove them if no // module-export-effect consumer needs this API surface. -export type TransformModuleExportEffectMeta = { - /** - * The local declaration name when statically available. - * - * - `"Page"` for `export function Page() {}` - * - `"Page"` for `export const Page = () => {}` - * - `undefined` for `export default () => {}` - * - `undefined` for `export { Page }` - */ - localName?: string - /** - * Whether the exported value is statically known to be a function. - * - * - `true` for `export const Page = () => {}` - * - `false` for `export const value = 1` - * - `undefined` for `export const value = getValue()` - * - `undefined` for `export default Page` - */ - isFunction?: boolean - /** - * The local identifier referenced by a default export. - * - * - `"Page"` for `const Page = () => {}; export default Page` - * - `undefined` for `export default function Page() {}` - * - `undefined` for `export default () => {}` - */ - defaultExportIdentifierName?: string -} - export type TransformModuleExportEffectFilter = ( name: string, - meta: TransformModuleExportEffectMeta, + meta: ModuleExportMeta, ) => boolean export type TransformModuleExportEffectContext = { binding: string exportName: string - meta: TransformModuleExportEffectMeta + meta: ModuleExportMeta } export type TransformModuleExportEffectOptions = { @@ -100,7 +72,6 @@ export function transformModuleExportEffect( viteAst: ESTree.Program, options: TransformModuleExportEffectOptions, ): TransformModuleExportEffectResult { - const ast = viteAst as unknown as Program const output = new MagicString(input) const filter = options.filter ?? (() => true) const references: TransformModuleExportEffectContext[] = [] @@ -132,141 +103,81 @@ export function transformModuleExportEffect( } } - for (const node of ast.body) { - if (node.type === 'ExportNamedDeclaration') { - if (node.declaration) { - if ( - node.declaration.type === 'FunctionDeclaration' || - node.declaration.type === 'ClassDeclaration' - ) { - /** - * export function foo() {} - * export class Foo {} - */ - tinyassert(node.declaration.id) - const binding = node.declaration.id.name - const meta: TransformModuleExportEffectMeta = { - localName: binding, - isFunction: getIsFunction(node.declaration), + for (const group of scanModuleExports(viteAst)) { + if (group.type === 'declaration') { + const [entry] = group.exports + const { localName: binding, exportName, meta } = entry + if (!filter(exportName, meta)) continue + validateNonAsyncFunction(options, group.declaration) + replaceAndMove( + group.node.start, + group.declaration.start, + input.length, + `${generate({ binding, exportName, meta })}\nexport { ${binding} };`, + ) + } else if (group.type === 'variable-declaration') { + const exportNames: string[] = [] + const effects: string[] = [] + for (const declarator of group.declarators) { + let validate = false + for (const entry of declarator.exports) { + const { localName: binding, exportName, meta } = entry + exportNames.push(exportName) + if (filter(exportName, meta)) { + validate = true + effects.push(generate({ binding, exportName, meta })) } - if (!filter(binding, meta)) continue - validateNonAsyncFunction(options, node.declaration) - replaceAndMove( - node.start, - node.declaration.start, - input.length, - `${generate({ binding, exportName: binding, meta })}\nexport { ${binding} };`, + } + if (validate && declarator.node.init) { + validateNonAsyncFunction(options, declarator.node.init) + } + } + if (effects.length > 0) { + replaceAndMove( + group.node.start, + group.declaration.start, + input.length, + `${effects.join('\n')}\nexport { ${exportNames.join(', ')} };`, + ) + } + } else if (group.type === 'specifiers') { + for (const entry of group.exports) { + tinyassert(entry.node.local.type === 'Identifier') + if (entry.node.exported.type !== 'Identifier') { + throw Object.assign( + new Error('unsupported string literal export name'), + { pos: entry.node.exported.start }, ) - } else if (node.declaration.type === 'VariableDeclaration') { - /** - * export const foo = 1, bar = 2 - */ - const exportNames: string[] = [] - const effects: string[] = [] - for (const declaration of node.declaration.declarations) { - const names = extractNames(declaration.id) - exportNames.push(...names) - const isFunction = - declaration.id.type === 'Identifier' && declaration.init - ? getIsFunction(declaration.init) - : undefined - let validate = false - for (const binding of names) { - const meta: TransformModuleExportEffectMeta = { - localName: binding, - isFunction, - } - if (filter(binding, meta)) { - validate = true - effects.push(generate({ binding, exportName: binding, meta })) - } - } - if (validate && declaration.init) { - validateNonAsyncFunction(options, declaration.init) - } - } - if (effects.length > 0) { - replaceAndMove( - node.start, - node.declaration.start, - input.length, - `${effects.join('\n')}\nexport { ${exportNames.join(', ')} };`, - ) - } } - } else { - /** - * export { foo, bar as baz } - * export { foo, bar as baz } from './dep' - */ - for (const specifier of node.specifiers) { - tinyassert(specifier.local.type === 'Identifier') - if (specifier.exported.type !== 'Identifier') { - throw Object.assign( - new Error('unsupported string literal export name'), - { pos: specifier.exported.start }, - ) - } - const exportName = specifier.exported.name - const meta: TransformModuleExportEffectMeta = {} - if (!filter(exportName, meta)) continue + const { exportName, meta } = entry + if (!filter(exportName, meta)) continue - let binding = specifier.local.name - if (node.source) { - binding = `$$effect_import_${exportName}` - // TODO: Preserve import attributes from the original re-export. - output.append( - `\nimport { ${specifier.local.name} as ${binding} } from ${node.source.raw};`, - ) - } - output.append(`\n${generate({ binding, exportName, meta })}`) + let binding = entry.localName + if (group.node.source) { + binding = `$$effect_import_${exportName}` + // TODO: Preserve import attributes from the original re-export. + output.append( + `\nimport { ${entry.localName} as ${binding} } from ${group.node.source.raw};`, + ) } + output.append(`\n${generate({ binding, exportName, meta })}`) } - } else if (node.type === 'ExportAllDeclaration') { - /** - * export * as ns from './dep' - * export * from './dep' - */ + } else if (group.type === 'export-all') { if (options.exportAll !== 'preserve') { throw Object.assign(new Error('unsupported ExportAllDeclaration'), { - pos: node.start, + pos: group.node.start, }) } - } else if (node.type === 'ExportDefaultDeclaration') { - /** - * export default function foo() {} - * export default class Foo {} - * export default foo - * export default () => {} - */ - let binding: string - let meta: TransformModuleExportEffectMeta - if ( - (node.declaration.type === 'FunctionDeclaration' || - node.declaration.type === 'ClassDeclaration') && - node.declaration.id - ) { - // export default function foo() {} - // export default class Foo {} - binding = node.declaration.id.name - meta = { - localName: binding, - isFunction: getIsFunction(node.declaration), - } - } else if (node.declaration.type === 'Identifier') { - // export default foo - binding = '$$effect_default' - meta = { defaultExportIdentifierName: node.declaration.name } - } else { - // export default () => {} - binding = '$$effect_default' - meta = { isFunction: getIsFunction(node.declaration) } - } + } else if (group.type === 'default') { + const node = group.node + const binding = group.localName ?? '$$effect_default' + const meta = group.meta if (!filter('default', meta)) continue validateNonAsyncFunction(options, node.declaration) const effect = generate({ binding, exportName: 'default', meta }) - if (node.declaration.type === 'Identifier') { + if (group.kind === 'identifier') { + const declaration = node.declaration as Identifier // export default foo // ^^^^^^^^^^^ // ⬇️ (replace `default foo`) @@ -276,7 +187,7 @@ export function transformModuleExportEffect( output.update( exportTokenEnd, node.end, - `const ${binding} = ${node.declaration.name};`, + `const ${binding} = ${declaration.name};`, ) // export const $$effect_default = foo // ^^^^^^ @@ -290,11 +201,13 @@ export function transformModuleExportEffect( input.length, `${effect}\nexport default ${binding};`, ) - } else if ( - (node.declaration.type === 'FunctionDeclaration' || - node.declaration.type === 'ClassDeclaration') && - node.declaration.id - ) { + } else if (group.kind === 'named-declaration') { + // export default function foo() {} + // ^^^^^^^^^^^^^^ + // ⬇️ + // function foo() {} + // registerServerReference(foo, 'default'); // << effect + // export default foo; // << export replaceAndMove( node.start, node.declaration.start, @@ -335,24 +248,3 @@ export function transformModuleExportEffect( referenceNames: references.map((reference) => reference.exportName), } } - -function getIsFunction( - node: Node | ExportDefaultDeclaration['declaration'], -): boolean | undefined { - if ( - node.type === 'FunctionDeclaration' || - node.type === 'FunctionExpression' || - node.type === 'ArrowFunctionExpression' - ) { - return true - } - if ( - node.type === 'ClassDeclaration' || - node.type === 'Literal' || - node.type === 'ObjectExpression' || - node.type === 'ArrayExpression' || - node.type === 'ClassExpression' - ) { - return false - } -} diff --git a/packages/plugin-rsc/src/transforms/module-exports.test.ts b/packages/plugin-rsc/src/transforms/module-exports.test.ts new file mode 100644 index 000000000..db83d638a --- /dev/null +++ b/packages/plugin-rsc/src/transforms/module-exports.test.ts @@ -0,0 +1,137 @@ +import { parseAstAsync } from 'vite' +import { expect, test } from 'vitest' +import { scanModuleExports } from './module-exports' + +test(scanModuleExports, async () => { + const ast = await parseAstAsync(` +export async function action() {} +export const loader = async () => {}, value = 1 +export const { item } = source +export { loader as renamed } +export { remote as reexported } from './dep' +export default action +export * from './all' +`) + + const groups = scanModuleExports(ast) + + expect(groups).toHaveLength(7) + expect(groups[0]).toMatchObject({ + type: 'declaration', + declaration: { type: 'FunctionDeclaration' }, + exports: [ + { + localName: 'action', + exportName: 'action', + meta: { + localName: 'action', + isFunction: true, + }, + }, + ], + }) + expect(groups[1]).toMatchObject({ + type: 'variable-declaration', + declaration: { kind: 'const' }, + declarators: [ + { + exports: [ + { + localName: 'loader', + exportName: 'loader', + meta: { + localName: 'loader', + isFunction: true, + }, + }, + ], + }, + { + exports: [ + { + localName: 'value', + exportName: 'value', + meta: { + localName: 'value', + isFunction: false, + }, + }, + ], + }, + ], + }) + expect(groups[2]).toMatchObject({ + type: 'variable-declaration', + declarators: [ + { + exports: [ + { + localName: 'item', + exportName: 'item', + meta: { + localName: 'item', + isFunction: undefined, + }, + }, + ], + }, + ], + }) + expect(groups[3]).toMatchObject({ + type: 'specifiers', + node: { source: null }, + exports: [{ localName: 'loader', exportName: 'renamed', meta: {} }], + }) + expect(groups[4]).toMatchObject({ + type: 'specifiers', + node: { source: { value: './dep' } }, + exports: [{ localName: 'remote', exportName: 'reexported', meta: {} }], + }) + expect(groups[5]).toMatchObject({ + type: 'default', + kind: 'identifier', + localName: undefined, + meta: { defaultExportIdentifierName: 'action' }, + }) + expect(groups[6]).toMatchObject({ type: 'export-all' }) +}) + +test.each([ + ['export default function action() {}', 'named-declaration'], + ['export default action', 'identifier'], + ['export default () => {}', 'other'], +] as const)('classifies %s', async (source, kind) => { + const ast = await parseAstAsync(source) + + expect(scanModuleExports(ast)).toMatchObject([{ type: 'default', kind }]) +}) + +test('flags string literal export names as unsupported', async () => { + const ast = await parseAstAsync(` +export { local as "public name" } +export { "remote name" as remote } from './dep' +`) + + expect(scanModuleExports(ast)).toMatchObject([ + { + type: 'specifiers', + exports: [ + { + localName: 'local', + exportName: '__unsupported_string_export__', + node: { exported: { type: 'Literal' } }, + }, + ], + }, + { + type: 'specifiers', + exports: [ + { + localName: '__unsupported_string_export__', + exportName: 'remote', + node: { local: { type: 'Literal' } }, + }, + ], + }, + ]) +}) diff --git a/packages/plugin-rsc/src/transforms/module-exports.ts b/packages/plugin-rsc/src/transforms/module-exports.ts new file mode 100644 index 000000000..c3b8ccf2a --- /dev/null +++ b/packages/plugin-rsc/src/transforms/module-exports.ts @@ -0,0 +1,250 @@ +import { tinyassert } from '@hiogawa/utils' +import type { + ExportAllDeclaration, + ExportDefaultDeclaration, + ExportNamedDeclaration, + ExportSpecifier, + FunctionDeclaration, + ClassDeclaration, + Node, + Program, + VariableDeclaration, + VariableDeclarator, +} from 'estree' +import type { ESTree } from 'vite' +import { extractNames } from './utils' + +export type ModuleExportMeta = { + /** + * The local declaration name when statically available. + * + * - `"Page"` for `export function Page() {}` + * - `"Page"` for `export const Page = () => {}` + * - `undefined` for `export default () => {}` + * - `undefined` for `export { Page }` + */ + localName?: string + /** + * Whether the exported value is statically known to be a function. + * + * - `true` for `export const Page = () => {}` + * - `false` for `export const value = 1` + * - `undefined` for `export const value = getValue()` + * - `undefined` for `export default Page` + */ + isFunction?: boolean + /** + * The local identifier referenced by a default export. + * + * - `"Page"` for `const Page = () => {}; export default Page` + * - `undefined` for `export default function Page() {}` + * - `undefined` for `export default () => {}` + */ + defaultExportIdentifierName?: string +} + +export type ModuleExportEntry = { + localName: string + exportName: string + meta: ModuleExportMeta +} + +export type ModuleExportSpecifier = { + node: ExportSpecifier + localName: string + exportName: string + meta: ModuleExportMeta +} + +type ModuleExportDefaultKind = 'named-declaration' | 'identifier' | 'other' + +export type ModuleExportGroup = + | { + /** + * export function foo() {} + * export class Foo {} + */ + type: 'declaration' + node: ExportNamedDeclaration + declaration: FunctionDeclaration | ClassDeclaration + exports: [ModuleExportEntry] + } + | { + /** + * export const foo = 1, bar = 2 + */ + type: 'variable-declaration' + node: ExportNamedDeclaration + declaration: VariableDeclaration + declarators: { + node: VariableDeclarator + exports: ModuleExportEntry[] + }[] + } + | { + /** + * export { foo as bar } + * export { foo as bar } from './dep' + */ + type: 'specifiers' + node: ExportNamedDeclaration + exports: ModuleExportSpecifier[] + } + | { + /** + * export * from './dep' + * export * as ns from './dep' + */ + type: 'export-all' + node: ExportAllDeclaration + } + | { + /** + * `named-declaration`: export default function foo() {} + * `identifier`: export default value + * `other`: export default function () {} + * `other`: export default () => {} + */ + type: 'default' + kind: ModuleExportDefaultKind + node: ExportDefaultDeclaration + localName?: string + meta: ModuleExportMeta + } + +export function scanModuleExports( + viteAst: ESTree.Program, +): ModuleExportGroup[] { + const ast = viteAst as unknown as Program + const groups: ModuleExportGroup[] = [] + + for (const node of ast.body) { + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + if (node.declaration.type === 'VariableDeclaration') { + // export const foo = 1, bar = 2 + const declaration = node.declaration + groups.push({ + type: 'variable-declaration', + node, + declaration, + declarators: declaration.declarations.map((declarator) => { + const isFunction = + declarator.id.type === 'Identifier' && declarator.init + ? getIsFunction(declarator.init) + : undefined + return { + node: declarator, + exports: extractNames(declarator.id).map((name) => ({ + localName: name, + exportName: name, + meta: { + localName: name, + isFunction, + }, + })), + } + }), + }) + } else { + // export function foo() {} + // export class Foo {} + tinyassert(node.declaration.id) + const name = node.declaration.id.name + groups.push({ + type: 'declaration', + node, + declaration: node.declaration, + exports: [ + { + localName: name, + exportName: name, + meta: { + localName: name, + isFunction: getIsFunction(node.declaration), + }, + }, + ], + }) + } + } else { + // export { foo as bar } + // export { foo as bar } from './dep' + groups.push({ + type: 'specifiers', + node, + exports: node.specifiers.map((specifier) => { + // String-literal export names are unsupported. Callers must check + // the returned node's local and exported types before rewriting. + return { + node: specifier, + localName: + specifier.local.type === 'Identifier' + ? specifier.local.name + : '__unsupported_string_export__', + exportName: + specifier.exported.type === 'Identifier' + ? specifier.exported.name + : '__unsupported_string_export__', + meta: {}, + } + }), + }) + } + } else if (node.type === 'ExportAllDeclaration') { + // export * from './dep' + // export * as ns from './dep' + groups.push({ type: 'export-all', node }) + } else if (node.type === 'ExportDefaultDeclaration') { + // export default function foo() {} + // export default value + let kind: ModuleExportDefaultKind + let localName: string | undefined + let meta: ModuleExportMeta + if ( + (node.declaration.type === 'FunctionDeclaration' || + node.declaration.type === 'ClassDeclaration') && + node.declaration.id + ) { + kind = 'named-declaration' + localName = node.declaration.id.name + meta = { + localName: node.declaration.id.name, + isFunction: getIsFunction(node.declaration), + } + } else if (node.declaration.type === 'Identifier') { + kind = 'identifier' + meta = { defaultExportIdentifierName: node.declaration.name } + } else { + // export default function () {} + // export default () => {} + kind = 'other' + meta = { isFunction: getIsFunction(node.declaration) } + } + groups.push({ type: 'default', kind, node, localName, meta }) + } + } + + return groups +} + +function getIsFunction( + node: Node | ExportDefaultDeclaration['declaration'], +): boolean | undefined { + if ( + node.type === 'FunctionDeclaration' || + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' + ) { + return true + } + if ( + node.type === 'ClassDeclaration' || + node.type === 'Literal' || + node.type === 'ObjectExpression' || + node.type === 'ArrayExpression' || + node.type === 'ClassExpression' + ) { + return false + } +} diff --git a/packages/plugin-rsc/src/transforms/wrap-export.ts b/packages/plugin-rsc/src/transforms/wrap-export.ts index 07f0f1c30..bf383f65f 100644 --- a/packages/plugin-rsc/src/transforms/wrap-export.ts +++ b/packages/plugin-rsc/src/transforms/wrap-export.ts @@ -1,8 +1,12 @@ import { tinyassert } from '@hiogawa/utils' -import type { ExportDefaultDeclaration, Node, Program } from 'estree' import MagicString from 'magic-string' import type { ESTree } from 'vite' -import { extractNames, validateNonAsyncFunction } from './utils' +import { + scanModuleExports, + type ModuleExportEntry, + type ModuleExportMeta, +} from './module-exports' +import { validateNonAsyncFunction } from './utils' type ExportMeta = { /** @@ -35,8 +39,6 @@ type ExportMeta = { defaultExportIdentifierName?: string } -type ExportWithMeta = { name: string; meta: ExportMeta } - export type TransformWrapExportFilter = ( name: string, meta: ExportMeta, @@ -57,21 +59,28 @@ export function transformWrapExport( exportNames: string[] output: MagicString } { - const ast = viteAst as unknown as Program const output = new MagicString(input) const exportNames: string[] = [] const toAppend: string[] = [] const filter = options.filter ?? (() => true) - function wrapSimple(start: number, end: number, exports: ExportWithMeta[]) { - const filteredExports = exports.map((item) => ({ - ...item, - shouldWrap: filter(item.name, item.meta), - })) + function wrapSimple( + start: number, + end: number, + exports: ModuleExportEntry[], + ) { + const filteredExports = exports.map((item) => { + const meta = getExportMeta(item.meta) + return { + ...item, + meta, + shouldWrap: filter(item.exportName, meta), + } + }) exportNames.push( ...filteredExports .filter((item) => item.shouldWrap) - .map((item) => item.name), + .map((item) => item.exportName), ) // update code and move to preserve `registerServerReference` position // e.g. @@ -85,12 +94,12 @@ export function transformWrapExport( const newCode = filteredExports .map((e) => [ e.shouldWrap && - `${e.name} = /* #__PURE__ */ ${options.runtime( - e.name, - e.name, + `${e.localName} = /* #__PURE__ */ ${options.runtime( + e.localName, + e.exportName, e.meta, )};\n`, - `export { ${e.name} };\n`, + `export { ${e.localName} };\n`, ]) .flat() .filter(Boolean) @@ -116,154 +125,109 @@ export function transformWrapExport( ) } - for (const node of ast.body) { - // named exports - if (node.type === 'ExportNamedDeclaration') { - if (node.declaration) { + for (const group of scanModuleExports(viteAst)) { + if (group.type === 'declaration') { + const [entry] = group.exports + if (filter(entry.exportName, getExportMeta(entry.meta))) { + validateNonAsyncFunction(options, group.declaration) + } + wrapSimple(group.node.start, group.declaration.start, group.exports) + } else if (group.type === 'variable-declaration') { + if (group.declaration.kind === 'const') { + output.update( + group.declaration.start, + group.declaration.start + 5, + 'let', + ) + } + const exports: ModuleExportEntry[] = [] + for (const declarator of group.declarators) { + exports.push(...declarator.exports) if ( - node.declaration.type === 'FunctionDeclaration' || - node.declaration.type === 'ClassDeclaration' + declarator.node.init && + declarator.exports.some(({ exportName, meta }) => + filter(exportName, getExportMeta(meta)), + ) ) { - /** - * export function foo() {} - */ - const name = node.declaration.id.name - const meta: ExportMeta = { - isFunction: getIsFunction(node.declaration), - declName: name, - } - if (filter(name, meta)) { - validateNonAsyncFunction(options, node.declaration) - } - wrapSimple(node.start, node.declaration.start, [{ name, meta }]) - } else if (node.declaration.type === 'VariableDeclaration') { - /** - * export const foo = 1, bar = 2 - */ - if (node.declaration.kind === 'const') { - output.update( - node.declaration.start, - node.declaration.start + 5, - 'let', + validateNonAsyncFunction(options, declarator.node.init) + } + } + wrapSimple(group.node.start, group.declaration.start, exports) + } else if (group.type === 'specifiers') { + if (group.node.source) { + output.remove(group.node.start, group.node.end) + for (const entry of group.exports) { + tinyassert(entry.node.local.type === 'Identifier') + if (entry.node.exported.type !== 'Identifier') { + throw Object.assign( + new Error('unsupported string literal export name'), + { pos: entry.node.exported.start }, ) } - const exports: ExportWithMeta[] = [] - for (const decl of node.declaration.declarations) { - const isFunction = - decl.id.type === 'Identifier' && decl.init - ? getIsFunction(decl.init) - : undefined - const declarationExports: ExportWithMeta[] = extractNames( - decl.id, - ).map((name) => ({ - name, - meta: { isFunction, declName: name }, - })) - exports.push(...declarationExports) - if ( - decl.init && - declarationExports.some(({ name, meta }) => filter(name, meta)) - ) { - validateNonAsyncFunction(options, decl.init) - } - } - wrapSimple(node.start, node.declaration.start, exports) - } else { - node.declaration satisfies never + toAppend.push( + `import { ${entry.localName} as $$import_${entry.localName} } from ${group.node.source.raw}`, + ) + wrapExport( + `$$import_${entry.localName}`, + entry.exportName, + getExportMeta(entry.meta), + ) } } else { - if (node.source) { - /** - * export { foo, bar as car } from './foo' - */ - output.remove(node.start, node.end) - for (const spec of node.specifiers) { - tinyassert(spec.local.type === 'Identifier') - if (spec.exported.type !== 'Identifier') { - throw Object.assign( - new Error('unsupported string literal export name'), - { pos: spec.exported.start }, - ) - } - const name = spec.local.name - toAppend.push( - `import { ${name} as $$import_${name} } from ${node.source.raw}`, + output.remove(group.node.start, group.node.end) + for (const entry of group.exports) { + tinyassert(entry.node.local.type === 'Identifier') + if (entry.node.exported.type !== 'Identifier') { + throw Object.assign( + new Error('unsupported string literal export name'), + { pos: entry.node.exported.start }, ) - wrapExport(`$$import_${name}`, spec.exported.name) - } - } else { - /** - * export { foo, bar as car } - */ - output.remove(node.start, node.end) - for (const spec of node.specifiers) { - tinyassert(spec.local.type === 'Identifier') - if (spec.exported.type !== 'Identifier') { - throw Object.assign( - new Error('unsupported string literal export name'), - { pos: spec.exported.start }, - ) - } - wrapExport(spec.local.name, spec.exported.name) } + wrapExport( + entry.localName, + entry.exportName, + getExportMeta(entry.meta), + ) } } - } - - /** - * export * as ns from './foo' - * export * from './foo' - */ - // vue sfc uses ExportAllDeclaration to re-export setup script. - // for now we just give an option to not throw for this case. - // https://github.com/vitejs/vite-plugin-vue/blob/30a97c1ddbdfb0e23b7dc14a1d2fb609668b9987/packages/plugin-vue/src/main.ts#L372 - if (node.type === 'ExportAllDeclaration') { + } else if (group.type === 'export-all') { + // Vue SFC uses ExportAllDeclaration to re-export its setup script, so + // consumers can opt out of rejecting this form. + // https://github.com/vitejs/vite-plugin-vue/blob/30a97c1ddbdfb0e23b7dc14a1d2fb609668b9987/packages/plugin-vue/src/main.ts#L372 if (!options.ignoreExportAllDeclaration) { throw Object.assign(new Error('unsupported ExportAllDeclaration'), { - pos: node.start, + pos: group.node.start, }) } - } - - /** - * export default function foo() {} - * export default class Foo {} - * export default () => {} - */ - if (node.type === 'ExportDefaultDeclaration') { - let localName: string - let isFunction: boolean | undefined - let declName: string | undefined - let defaultExportIdentifierName: string | undefined - if ( - (node.declaration.type === 'FunctionDeclaration' || - node.declaration.type === 'ClassDeclaration') && - node.declaration.id - ) { + } else if (group.type === 'default') { + const localName = group.localName ?? '$$default' + if (group.kind === 'named-declaration') { // preserve name scope for `function foo() {}` and `class Foo {}` - localName = node.declaration.id.name - output.remove(node.start, node.declaration.start) - isFunction = getIsFunction(node.declaration) - declName = node.declaration.id.name + // e.g. + // export default foo() {} + // ^^^^^^^^^^^^^^ + //. ⬇️ (remove `export default`) + // function foo() {} + output.remove(group.node.start, group.node.declaration.start) } else { // otherwise we can introduce new variable - localName = '$$default' - output.update(node.start, node.declaration.start, 'const $$default = ') - if (node.declaration.type === 'Identifier') { - defaultExportIdentifierName = node.declaration.name - } else { - isFunction = getIsFunction(node.declaration) - } + // e.g. + // export default foo + // ^^^^^^^^^^^^^^ + //. ⬇️ (replace `export default`) + // const $$default = foo + // ^^^^^^^^^^^^^^^^^ + output.update( + group.node.start, + group.node.declaration.start, + 'const $$default = ', + ) } - const defaultMeta: ExportMeta = { - isFunction, - declName, - defaultExportIdentifierName, + const meta = getExportMeta(group.meta) + if (filter('default', meta)) { + validateNonAsyncFunction(options, group.node.declaration) } - if (filter('default', defaultMeta)) { - validateNonAsyncFunction(options, node.declaration) - } - wrapExport(localName, 'default', defaultMeta) + wrapExport(localName, 'default', meta) } } @@ -274,23 +238,6 @@ export function transformWrapExport( return { exportNames, output } } -function getIsFunction( - node: Node | ExportDefaultDeclaration['declaration'], -): boolean | undefined { - if ( - node.type === 'FunctionDeclaration' || - node.type === 'ArrowFunctionExpression' || - node.type === 'FunctionExpression' - ) { - return true - } - if ( - node.type === 'ClassDeclaration' || - node.type === 'Literal' || - node.type === 'ObjectExpression' || - node.type === 'ArrayExpression' || - node.type === 'ClassExpression' - ) { - return false - } +function getExportMeta({ localName, ...meta }: ModuleExportMeta): ExportMeta { + return { ...meta, ...(localName && { declName: localName }) } }