Skip to content

Commit

Permalink
String import/export specifier (#12091)
Browse files Browse the repository at this point in the history
* feat: parse moduleExportName
* feat: add validators
* Support string specifier name in commonjs transform
* Support string specifier name in export-ns-from
* test: add loose testcases
* test: add testcases for amd and umd
* feat: support systemjs
* test: update fixtures fixed in #12110
* add plugin name typings
* test: rename test layout
* feat: implement under moduleStringNames flag
* chore: add plugin syntax module string names
* feat: support ModuleExportName as ModuleExportName
* test: update test fixtures
* fix flow errors
* docs: update AST spec
* feat: support { "some imports" as "some exports" }
* feat: support { "some imports" as "some exports" } in systemjs
* test: add test on `import { "foo" }`
* Address review comments
* add moduleStringNames to missing plugin helpers
* Apply suggestions from code review
* update test fixtures
* Update packages/babel-parser/src/parser/error-message.js
* update test fixtures

Co-Authored-By: Kai Cataldo <kai@kaicataldo.com>
Co-authored-by: Brian Ng <bng412@gmail.com>
  • Loading branch information
3 people authored and nicolo-ribaudo committed Oct 14, 2020
1 parent 1b90d90 commit 21d7ee2
Show file tree
Hide file tree
Showing 152 changed files with 1,872 additions and 96 deletions.
6 changes: 6 additions & 0 deletions packages/babel-core/src/parser/util/missing-plugin-helper.js
Expand Up @@ -135,6 +135,12 @@ const pluginNameMap = {
url: "https://git.io/JfK3k",
},
},
moduleStringNames: {
syntax: {
name: "@babel/plugin-syntax-module-string-names",
url: "https://git.io/JTL8G",
},
},
numericSeparator: {
syntax: {
name: "@babel/plugin-syntax-numeric-separator",
Expand Down
1 change: 1 addition & 0 deletions packages/babel-helper-module-transforms/package.json
Expand Up @@ -19,6 +19,7 @@
"@babel/helper-replace-supers": "workspace:^7.10.4",
"@babel/helper-simple-access": "workspace:^7.10.4",
"@babel/helper-split-export-declaration": "workspace:^7.11.0",
"@babel/helper-validator-identifier": "workspace:^7.10.4",
"@babel/template": "workspace:^7.10.4",
"@babel/types": "workspace:^7.11.0",
"lodash": "^4.17.19"
Expand Down
94 changes: 65 additions & 29 deletions packages/babel-helper-module-transforms/src/index.js
Expand Up @@ -10,6 +10,8 @@ import rewriteLiveReferences from "./rewrite-live-references";
import normalizeAndLoadModuleMetadata, {
hasExports,
isSideEffectImport,
type ModuleMetadata,
type SourceModuleMetadata,
} from "./normalize-and-load-metadata";

export { default as getModuleName } from "./get-module-name";
Expand Down Expand Up @@ -180,33 +182,58 @@ export function buildNamespaceInitStatements(
return statements;
}

const getTemplateForReexport = loose => {
return loose
? template.statement`EXPORTS.EXPORT_NAME = NAMESPACE.IMPORT_NAME;`
: template`
Object.defineProperty(EXPORTS, "EXPORT_NAME", {
enumerable: true,
get: function() {
return NAMESPACE.IMPORT_NAME;
},
});
`;
const ReexportTemplate = {
loose: template.statement`EXPORTS.EXPORT_NAME = NAMESPACE_IMPORT;`,
looseComputed: template.statement`EXPORTS["EXPORT_NAME"] = NAMESPACE_IMPORT;`,
spec: template`
Object.defineProperty(EXPORTS, "EXPORT_NAME", {
enumerable: true,
get: function() {
return NAMESPACE_IMPORT;
},
});
`,
};

const buildReexportsFromMeta = (meta, metadata, loose) => {
const buildReexportsFromMeta = (
meta: ModuleMetadata,
metadata: SourceModuleMetadata,
loose,
) => {
const namespace = metadata.lazy
? t.callExpression(t.identifier(metadata.name), [])
: t.identifier(metadata.name);

const templateForCurrentMode = getTemplateForReexport(loose);
return Array.from(metadata.reexports, ([exportName, importName]) =>
templateForCurrentMode({
const { stringSpecifiers } = meta;
return Array.from(metadata.reexports, ([exportName, importName]) => {
let NAMESPACE_IMPORT;
if (stringSpecifiers.has(importName)) {
NAMESPACE_IMPORT = t.memberExpression(
t.cloneNode(namespace),
t.stringLiteral(importName),
true,
);
} else {
NAMESPACE_IMPORT = NAMESPACE_IMPORT = t.memberExpression(
t.cloneNode(namespace),
t.identifier(importName),
);
}
const astNodes = {
EXPORTS: meta.exportName,
EXPORT_NAME: exportName,
NAMESPACE: t.cloneNode(namespace),
IMPORT_NAME: importName,
}),
);
NAMESPACE_IMPORT,
};
if (loose) {
if (stringSpecifiers.has(exportName)) {
return ReexportTemplate.looseComputed(astNodes);
} else {
return ReexportTemplate.loose(astNodes);
}
} else {
return ReexportTemplate.spec(astNodes);
}
});
};

/**
Expand Down Expand Up @@ -363,16 +390,25 @@ function buildExportInitializationStatements(
* Given a set of export names, create a set of nested assignments to
* initialize them all to a given expression.
*/
function buildInitStatement(metadata, exportNames, initExpr) {
const InitTemplate = {
computed: template.expression`EXPORTS["NAME"] = VALUE`,
default: template.expression`EXPORTS.NAME = VALUE`,
};

function buildInitStatement(metadata: ModuleMetadata, exportNames, initExpr) {
const { stringSpecifiers, exportName: EXPORTS } = metadata;
return t.expressionStatement(
exportNames.reduce(
(acc, exportName) =>
template.expression`EXPORTS.NAME = VALUE`({
EXPORTS: metadata.exportName,
NAME: exportName,
VALUE: acc,
}),
initExpr,
),
exportNames.reduce((acc, exportName) => {
const params = {
EXPORTS,
NAME: exportName,
VALUE: acc,
};
if (stringSpecifiers.has(exportName)) {
return InitTemplate.computed(params);
} else {
return InitTemplate.default(params);
}
}, initExpr),
);
}
@@ -1,5 +1,6 @@
import { basename, extname } from "path";

import { isIdentifierName } from "@babel/helper-validator-identifier";
import splitExportDeclaration from "@babel/helper-split-export-declaration";

export type ModuleMetadata = {
Expand All @@ -15,6 +16,12 @@ export type ModuleMetadata = {

// Lookup of source file to source file metadata.
source: Map<string, SourceModuleMetadata>,

// List of names that should only be printed as string literals.
// i.e. `import { "any unicode" as foo } from "some-module"`
// `stringSpecifiers` is Set(1) ["any unicode"]
// In most cases `stringSpecifiers` is an empty Set
stringSpecifiers: Set<string>,
};

export type InteropType = "default" | "namespace" | "none";
Expand Down Expand Up @@ -87,13 +94,18 @@ export default function normalizeModuleAndLoadMetadata(
if (!exportName) {
exportName = programPath.scope.generateUidIdentifier("exports").name;
}
const stringSpecifiers = new Set();

nameAnonymousExports(programPath);

const { local, source, hasExports } = getModuleMetadata(programPath, {
loose,
lazy,
});
const { local, source, hasExports } = getModuleMetadata(
programPath,
{
loose,
lazy,
},
stringSpecifiers,
);

removeModuleDeclarations(programPath);

Expand Down Expand Up @@ -124,17 +136,48 @@ export default function normalizeModuleAndLoadMetadata(
hasExports,
local,
source,
stringSpecifiers,
};
}

function getExportSpecifierName(
path: NodePath,
stringSpecifiers: Set<string>,
): string {
if (path.isIdentifier()) {
return path.node.name;
} else if (path.isStringLiteral()) {
const stringValue = path.node.value;
// add specifier value to `stringSpecifiers` only when it can not be converted to an identifier name
// i.e In `import { "foo" as bar }`
// we do not consider `"foo"` to be a `stringSpecifier` because we can treat it as
// `import { foo as bar }`
// This helps minimize the size of `stringSpecifiers` and reduce overhead of checking valid identifier names
// when building transpiled code from metadata
if (!isIdentifierName(stringValue)) {
stringSpecifiers.add(stringValue);
}
return stringValue;
} else {
throw new Error(
`Expected export specifier to be either Identifier or StringLiteral, got ${path.node.type}`,
);
}
}

/**
* Get metadata about the imports and exports present in this module.
*/
function getModuleMetadata(
programPath: NodePath,
{ loose, lazy }: { loose: boolean, lazy: boolean },
stringSpecifiers: Set<string>,
) {
const localData = getLocalExportMetadata(programPath, loose);
const localData = getLocalExportMetadata(
programPath,
loose,
stringSpecifiers,
);

const sourceData = new Map();
const getData = sourceNode => {
Expand Down Expand Up @@ -199,7 +242,10 @@ function getModuleMetadata(
});
}
} else if (spec.isImportSpecifier()) {
const importName = spec.get("imported").node.name;
const importName = getExportSpecifierName(
spec.get("imported"),
stringSpecifiers,
);
const localName = spec.get("local").node.name;

data.imports.set(localName, importName);
Expand Down Expand Up @@ -231,8 +277,14 @@ function getModuleMetadata(
if (!spec.isExportSpecifier()) {
throw spec.buildCodeFrameError("Unexpected export specifier type");
}
const importName = spec.get("local").node.name;
const exportName = spec.get("exported").node.name;
const importName = getExportSpecifierName(
spec.get("local"),
stringSpecifiers,
);
const exportName = getExportSpecifierName(
spec.get("exported"),
stringSpecifiers,
);

data.reexports.set(exportName, importName);

Expand Down Expand Up @@ -310,6 +362,7 @@ function getModuleMetadata(
function getLocalExportMetadata(
programPath: NodePath,
loose: boolean,
stringSpecifiers: Set<string>,
): Map<string, LocalExportMetadata> {
const bindingKindLookup = new Map();

Expand Down Expand Up @@ -392,11 +445,13 @@ function getLocalExportMetadata(
child.get("specifiers").forEach(spec => {
const local = spec.get("local");
const exported = spec.get("exported");
const localMetadata = getLocalMetadata(local);
const exportName = getExportSpecifierName(exported, stringSpecifiers);

if (exported.node.name === "__esModule") {
if (exportName === "__esModule") {
throw exported.buildCodeFrameError('Illegal export "__esModule".');
}
getLocalMetadata(local).names.push(exported.node.name);
localMetadata.names.push(exportName);
});
}
} else if (child.isExportDefaultDeclaration()) {
Expand Down
Expand Up @@ -3,7 +3,7 @@ import * as t from "@babel/types";
import template from "@babel/template";
import simplifyAccess from "@babel/helper-simple-access";

import type { ModuleMetadata } from "./";
import type { ModuleMetadata } from "./normalize-and-load-metadata";

export default function rewriteLiveReferences(
programPath: NodePath,
Expand Down Expand Up @@ -71,7 +71,13 @@ export default function rewriteLiveReferences(
let namespace = t.identifier(meta.name);
if (meta.lazy) namespace = t.callExpression(namespace, []);

return t.memberExpression(namespace, t.identifier(importName));
const computed = metadata.stringSpecifiers.has(importName);

return t.memberExpression(
namespace,
computed ? t.stringLiteral(importName) : t.identifier(importName),
computed,
);
},
});
}
Expand Down Expand Up @@ -135,11 +141,14 @@ const buildBindingExportAssignmentExpression = (
// class Foo {} export { Foo, Foo as Bar };
// as
// class Foo {} exports.Foo = exports.Bar = Foo;
const { stringSpecifiers } = metadata;
const computed = stringSpecifiers.has(exportName);
return t.assignmentExpression(
"=",
t.memberExpression(
t.identifier(metadata.exportName),
t.identifier(exportName),
computed ? t.stringLiteral(exportName) : t.identifier(exportName),
/* computed */ computed,
),
expr,
);
Expand Down
17 changes: 10 additions & 7 deletions packages/babel-parser/ast/spec.md
Expand Up @@ -1291,7 +1291,7 @@ interface ImportDeclaration <: ModuleDeclaration {
type: "ImportDeclaration";
importKind: null | "type" | "typeof" | "value";
specifiers: [ ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier ];
source: Literal;
source: StringLiteral;
attributes?: [ ImportAttribute ];
}
```
Expand All @@ -1305,7 +1305,7 @@ An import declaration, e.g., `import foo from "mod";`.
```js
interface ImportSpecifier <: ModuleSpecifier {
type: "ImportSpecifier";
imported: Identifier;
imported: Identifier | StringLiteral;
}
```

Expand Down Expand Up @@ -1352,21 +1352,24 @@ interface ExportNamedDeclaration <: ModuleDeclaration {
type: "ExportNamedDeclaration";
declaration: Declaration | null;
specifiers: [ ExportSpecifier ];
source: Literal | null;
source: StringLiteral | null;
}
```

An export named declaration, e.g., `export {foo, bar};`, `export {foo} from "mod";`, `export var foo = 1;` or `export * as foo from "bar";`.

_Note: Having `declaration` populated with non-empty `specifiers` or non-null `source` results in an invalid state._
Note:

- Having `declaration` populated with non-empty `specifiers` or non-null `source` results in an invalid state.
- If `source` is `null`, for each `specifier` of `specifiers`, `specifier.local` can not be a `StringLiteral`.

### ExportSpecifier

```js
interface ExportSpecifier <: ModuleSpecifier {
type: "ExportSpecifier";
exported: Identifier;
local?: Identifier;
exported: Identifier | StringLiteral;
local?: Identifier | StringLiteral;
}
```

Expand Down Expand Up @@ -1396,7 +1399,7 @@ An export default declaration, e.g., `export default function () {};` or `export
```js
interface ExportAllDeclaration <: ModuleDeclaration {
type: "ExportAllDeclaration";
source: Literal;
source: StringLiteral;
}
```

Expand Down

0 comments on commit 21d7ee2

Please sign in to comment.