Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions lexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
14 changes: 14 additions & 0 deletions src/lexer.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/lexer.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum ImportType {
DynamicSourcePhase = 5,
StaticDeferPhase = 6,
DynamicDeferPhase = 7,
StaticReexportStar = 8,
};

struct Attribute {
Expand Down
17 changes: 17 additions & 0 deletions src/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down
70 changes: 70 additions & 0 deletions test/_unit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;`,
Expand Down
Loading