diff --git a/README.md b/README.md index 09dca95..156c24d 100755 --- a/README.md +++ b/README.md @@ -223,6 +223,32 @@ To handle escape sequences in specifier strings, the `.n` field of imported spec For dynamic import expressions, this field will be empty if not a valid JS string. +### Star Re-exports + +`export * from 'module'` is both a dependency on `module` and a re-export of its +names. It is reported on both sides: + +```js +const [imports, exports] = parse(`export * from './core'`); + +// The exported name is "*" +exports[0].n; +// Returns "*" + +// The specifier is an import, typed StaticReexportStar (8) so it is not +// confused with a side-effect `import './core'` (which is type 1). +imports[0].t === 8; +imports[0].n; +// Returns "./core" + +// The two halves share the same statement range. +source.slice(imports[0].ss, imports[0].se); +// Returns "export * from './core'" +``` + +`export * as ns from 'module'` is unchanged: it already reports the namespace +name `ns` as the export, with the specifier as a normal static import. + ### Facade Detection Facade modules that only use import / export syntax can be detected via the third return value: diff --git a/lexer.js b/lexer.js index fae6cab..c676978 100644 --- a/lexer.js +++ b/lexer.js @@ -446,9 +446,15 @@ function tryParseExportStatement () { // export * // export * as X case 42/***/: + const starPos = pos; pos++; commentWhitespace(true); + const prevStarExport = exports.length; ch = readExportAs(pos, pos); + // A plain `export *` (no `as`) has no exported name to report, so record + // the star itself as the export name "*". `export * as X` already added X. + if (exports.length === prevStarExport) + addExport(starPos, starPos + 1, -1, -1); ch = commentWhitespace(true); break; } diff --git a/src/lexer.c b/src/lexer.c index f8c12bc..3bd3ff7 100755 --- a/src/lexer.c +++ b/src/lexer.c @@ -561,6 +561,7 @@ void readBindingPattern () { void tryParseExportStatement () { char16_t* sStartPos = pos; Export* prev_export_write_head = export_write_head; + bool reexportStar = false; pos += 6; @@ -621,9 +622,17 @@ void tryParseExportStatement () { // export * // export * as X else if (ch == '*') { + char16_t* starPos = pos; pos++; commentWhitespace(true); + Export* prevStarExport = export_write_head; ch = readExportAs(pos, pos); + // A plain `export *` (no `as`) has no exported name to report, so record + // the star itself as the export name "*". `export * as X` already added X. + if (export_write_head == prevStarExport) { + addExport(starPos, starPos + 1, NULL, NULL); + reexportStar = true; + } ch = commentWhitespace(true); } else { @@ -754,6 +763,11 @@ void tryParseExportStatement () { pos += 4; readImportString(sStartPos, commentWhitespace(true), false); + // Mark the specifier of a plain `export * from` so it is distinguishable + // from a side-effect `import 'x'`, which is otherwise the same shape. + if (reexportStar) + import_write_head->import_ty = StaticReexportStar; + // There were no local names. for (Export* exprt = prev_export_write_head == NULL ? first_export : prev_export_write_head->next; exprt != NULL; exprt = exprt->next) { exprt->local_start = exprt->local_end = NULL; diff --git a/src/lexer.h b/src/lexer.h index 2ef47dd..b0c2b07 100755 --- a/src/lexer.h +++ b/src/lexer.h @@ -23,6 +23,7 @@ enum ImportType { DynamicSourcePhase = 5, StaticDeferPhase = 6, DynamicDeferPhase = 7, + StaticReexportStar = 8, }; struct Attribute { diff --git a/src/lexer.ts b/src/lexer.ts index d812ad8..b53fbd2 100755 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -33,6 +33,15 @@ export enum ImportType { * import.defer('module') */ DynamicDeferPhase = 7, + /** + * The specifier of a star re-export + * export * from 'module' + * + * Reported as an import because it is a module dependency, but typed + * separately so it is not confused with a side-effect `import 'module'`. + * The matching export is reported with the name `*`. + */ + StaticReexportStar = 8, } export interface ImportSpecifier { @@ -138,6 +147,14 @@ export interface ExportSpecifier { * const [imports, exports] = parse(source); * exports[0].n; * // Returns "asdf" + * + * @example + * // A star re-export reports the name "*"; the re-exported specifier is the + * // matching import (ImportType.StaticReexportStar) sharing the statement range. + * const source = `export * from './core'`; + * const [imports, exports] = parse(source); + * exports[0].n; + * // Returns "*" */ readonly n: string; diff --git a/test/_unit.cjs b/test/_unit.cjs index 39110f4..ad3347e 100755 --- a/test/_unit.cjs +++ b/test/_unit.cjs @@ -1258,6 +1258,76 @@ function x() { assertExportIs(source, exports[1], { n: 'yy', ln: undefined }); }); + test('Export star reexport', () => { + const source = ` + export * from './core'; + export*from'minified'; + export * as ns from './named'; + export { a as b } from './picked'; + `; + const [imports, exports] = parse(source); + + // Every re-export still records its specifier as an import edge. + assert.strictEqual(imports.length, 4); + assert.strictEqual(imports[0].n, './core'); + assert.strictEqual(imports[1].n, 'minified'); + assert.strictEqual(imports[2].n, './named'); + assert.strictEqual(imports[3].n, './picked'); + + // The two `export *` specifiers are typed StaticReexportStar (8), so they + // are distinguishable from a side-effect `import './core'` (which is 1). + assert.strictEqual(imports[0].t, 8); + assert.strictEqual(imports[1].t, 8); + // `export * as ns` and `export { a } from` keep the plain Static type. + assert.strictEqual(imports[2].t, 1); + assert.strictEqual(imports[3].t, 1); + + // Plain `export *` now surfaces on the export side as the name "*". + // `ns` and `b` are the named re-exports as before. + assert.deepStrictEqual(exports.map(expt => expt.n), ['*', '*', 'ns', 'b']); + assertExportIs(source, exports[0], { n: '*', ln: undefined }); + assertExportIs(source, exports[1], { n: '*', ln: undefined }); + assertExportIs(source, exports[2], { n: 'ns', ln: undefined }); + assertExportIs(source, exports[3], { n: 'b', ln: undefined }); + + // The star export's name span points at the literal `*`, and its statement + // range matches the import's, so the two halves correlate. + assert.strictEqual(source.slice(exports[0].s, exports[0].e), '*'); + assert.strictEqual(imports[0].ss, source.indexOf(`export * from './core'`)); + assert.strictEqual(source.slice(imports[0].ss, imports[0].se), `export * from './core'`); + }); + + test('Export star reexport with comments between tokens', () => { + const source = `export /* a */ * /* b */ from './core'`; + const [imports, exports] = parse(source); + + assert.strictEqual(imports.length, 1); + assert.strictEqual(imports[0].t, 8); + assert.strictEqual(imports[0].n, './core'); + assert.deepStrictEqual(exports.map(expt => expt.n), ['*']); + assert.strictEqual(source.slice(imports[0].ss, imports[0].se), source); + }); + + test('Export star reexport is distinct from side-effect import', () => { + const [imports, exports] = parse(`import './core'`); + + assert.strictEqual(imports.length, 1); + // A side-effect import stays a plain Static import and contributes no + // export, so it never collides with a `export * from` star re-export. + assert.strictEqual(imports[0].t, 1); + assert.strictEqual(exports.length, 0); + }); + + test('Export star reexport is module syntax', () => { + const [,,, hasModuleSyntax] = parse(`export * from './core'`); + assert.strictEqual(hasModuleSyntax, true); + }); + + test('Export star reexport facade', () => { + const [,, facade] = parse(`export * from './core'`); + assert.strictEqual(facade, true); + }); + test('Export statement start', () => { const source = [ `export const x = 1;`,