UNPKG

vite-plugin-top-level-await

Version:

Transform code to support top-level await in normal browsers for Vite.

319 lines (318 loc) 14.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.transformModule = transformModule; const error_1 = require("./utils/error"); const make_node_1 = require("./utils/make-node"); const random_identifier_1 = require("./utils/random-identifier"); const resolve_import_1 = require("./utils/resolve-import"); const resolve_pattern_1 = require("./utils/resolve-pattern"); function transformByType(node, type, filter) { for (const key of Array.isArray(node) ? node.keys() : Object.keys(node)) { if (["span", "type"].includes(key)) continue; if (!node[key]) continue; if (typeof node[key] === "object") node[key] = transformByType(node[key], type, filter); } if (node["type"] === type) return filter(node); return node; } function declarationToExpression(decl) { if (decl.type === "FunctionDeclaration") { return { ...decl, identifier: null, type: "FunctionExpression" }; } else if (decl.type === "ClassDeclaration") { return { ...decl, identifier: null, type: "ClassExpression" }; } else { /* istanbul ignore next */ (0, error_1.raiseUnexpectedNode)("declaration", decl.type); } } function expressionToDeclaration(expr) { if (expr.type === "FunctionExpression") { return { ...expr, type: "FunctionDeclaration" }; } else if (expr.type === "ClassExpression") { return { ...expr, type: "ClassDeclaration" }; } else { /* istanbul ignore next */ (0, error_1.raiseUnexpectedNode)("expression", expr.type); } } function transformModule(code, ast, moduleName, bundleInfo, options) { var _a, _b; const randomIdentifier = new random_identifier_1.RandomIdentifierGenerator(code); // Extract import declarations const imports = ast.body.filter((item) => item.type === "ImportDeclaration"); // Handle `export { named as renamed } from "module";` const exportFroms = ast.body.filter((item) => item.type === "ExportNamedDeclaration" && !!item.source); const newImportsByExportFroms = []; const exportMap = {}; // Extract export declarations // In Rollup's output, there should be only one, and as the last top-level statement // But some plugins (e.g. @vitejs/plugin-legacy) may inject others like "export function" const namedExports = ast.body.filter((item, i) => { switch (item.type) { /* istanbul ignore next */ case "ExportAllDeclaration": (0, error_1.raiseUnexpectedNode)("top-level statement", item.type); case "ExportDefaultExpression": // Convert to a variable const identifier = randomIdentifier.generate(); ast.body[i] = (0, make_node_1.makeVariableInitDeclaration)(identifier, item.expression); exportMap["default"] = identifier; return false; case "ExportDefaultDeclaration": if (item.decl.type === "FunctionExpression" || item.decl.type === "ClassExpression") { // Convert to a declaration or variable if (item.decl.identifier) { ast.body[i] = expressionToDeclaration(item.decl); exportMap["default"] = item.decl.identifier.value; } else { const identifier = randomIdentifier.generate(); ast.body[i] = (0, make_node_1.makeVariableInitDeclaration)(identifier, item.decl); exportMap["default"] = identifier; } } else { /* istanbul ignore next */ (0, error_1.raiseUnexpectedNode)("top-level export declaration", item.decl.type); } return false; case "ExportDeclaration": if (item.declaration.type === "FunctionDeclaration" || item.declaration.type === "ClassDeclaration") { // Remove the "export" keyword from this statement ast.body[i] = item.declaration; exportMap[item.declaration.identifier.value] = item.declaration.identifier.value; } else { /* istanbul ignore next */ (0, error_1.raiseUnexpectedNode)("top-level export declaration", item.declaration.type); } return false; // Handle `export { named as renamed };` without "from" case "ExportNamedDeclaration": if (!item.source) { item.specifiers.forEach(specifier => { /* istanbul ignore if */ if (specifier.type !== "ExportSpecifier") { (0, error_1.raiseUnexpectedNode)("export specifier", specifier.type); } exportMap[(specifier.exported || specifier.orig).value] = specifier.orig.value; }); } return true; } return false; }); const exportedNameSet = new Set(Object.values(exportMap)); const exportedNames = Array.from(exportedNameSet); /* * Move ALL top-level statements to an async IIFE: * * ```js * export let __tla = Promise.all([ * // imported TLA promises * ]).then(async () => { * // original top-level statements here * }); * ``` * * And add variable declarations for exported names to new top-level, before the IIFE. * * ```js * let x; * export let __tla = Promise.all([ * // imported TLA promises * ]).then(async () => { * // const x = 1; * x = 1; * }); * export { x as someExport }; * ``` */ const topLevelStatements = ast.body.filter((item) => !imports.includes(item) && !namedExports.includes(item)); const importedNames = new Set(imports.flatMap(importStmt => importStmt.specifiers.map(specifier => specifier.local.value))); const exportFromedNames = new Set(exportFroms.flatMap(exportStmt => exportStmt.specifiers.map(specifier => { if (specifier.type === "ExportNamespaceSpecifier") { return specifier.name.value; } else if (specifier.type === "ExportDefaultSpecifier") { // When will this happen? return specifier.exported.value; } else { return (specifier.exported || specifier.orig).value; } }))); const exportedNamesDeclaration = (0, make_node_1.makeVariablesDeclaration)(exportedNames.filter(name => !importedNames.has(name) && !exportFromedNames.has(name))); const warppedStatements = topLevelStatements.flatMap(stmt => { if (stmt.type === "VariableDeclaration") { const declaredNames = stmt.declarations.flatMap(decl => (0, resolve_pattern_1.resolvePattern)(decl.id)); const exportedDeclaredNames = declaredNames.filter(name => exportedNameSet.has(name)); const unexportedDeclaredNames = declaredNames.filter(name => !exportedNameSet.has(name)); // None is exported in the declared names, no need to transform if (exportedDeclaredNames.length === 0) return stmt; // Generate assignment statements for init-ed declarators const assignmentStatements = stmt.declarations .filter(decl => decl.init) .map(decl => (0, make_node_1.makeAssignmentStatement)(decl.id, decl.init)); // Generate variable declarations for unexported variables const unexportedDeclarations = (0, make_node_1.makeVariablesDeclaration)(unexportedDeclaredNames); return unexportedDeclarations ? [unexportedDeclarations, ...assignmentStatements] : assignmentStatements; } else if (stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration") { const name = stmt.identifier.value; if (!exportedNameSet.has(name)) return stmt; return (0, make_node_1.makeAssignmentStatement)((0, make_node_1.makeIdentifier)(name), declarationToExpression(stmt)); } else { return stmt; } }); /* * Process dynamic imports. * * ```js * [ * import("some-module-with-tla"), * import("some-module-without-tla"), * import(dynamicModuleName) * ] * ``` * * The expression evaluates to a promise, which will resolve after module loaded, but not after * out `__tla` promise resolved. * * We can check the target module. If the argument is string literial and the target module has NO * top-level await, we won't need to transform it. * * ```js * [ * import("some-module-with-tla").then(async m => { await m.__tla; return m; }), * import("some-module-without-tla"), * import(dynamicModuleName).then(async m => { await m.__tla; return m; }) * ] * ``` */ transformByType(warppedStatements, "CallExpression", (call) => { var _a; if (call.callee.type === "Import") { const argument = call.arguments[0].expression; if (argument.type === "StringLiteral") { const importedModuleName = (0, resolve_import_1.resolveImport)(moduleName, argument.value); // Skip transform if (importedModuleName && !((_a = bundleInfo[importedModuleName]) === null || _a === void 0 ? void 0 : _a.transformNeeded)) return call; } return (0, make_node_1.makeCallExpression)((0, make_node_1.makeMemberExpression)(call, "then"), [ (0, make_node_1.makeArrowFunction)(["m"], [ (0, make_node_1.makeStatement)((0, make_node_1.makeAwaitExpression)((0, make_node_1.makeMemberExpression)("m", options.promiseExportName))), (0, make_node_1.makeReturnStatement)((0, make_node_1.makeIdentifier)("m")) ], true) ]); } return call; }); /* * Import and await the promise "__tla" from each imported module with TLA transform enabled. * * ```js * import { ..., __tla as __tla_0 } from "..."; * import { ..., __tla as __tla_1 } from "..."; * ``` * * To work with circular dependency, wrap each imported promise with try-catch. * Promises from circular dependencies will not be imported and awaited. * * ```js * export let __tla = Promise.all([ * (() => { try { return __tla_0; } catch {} })(), * (() => { try { return __tla_1; } catch {} })() * ]).then(async () => { * // original top-level statements here * }); * ``` */ // Add import of TLA promises from imported modules let importedPromiseCount = 0; for (const declaration of [...imports, ...exportFroms]) { const importedModuleName = (0, resolve_import_1.resolveImport)(moduleName, declaration.source.value); if (!importedModuleName || !bundleInfo[importedModuleName]) continue; if (bundleInfo[importedModuleName].transformNeeded) { let targetImportDeclaration; if (declaration.type === "ImportDeclaration") { targetImportDeclaration = declaration; } else { targetImportDeclaration = (0, make_node_1.makeImportDeclaration)(declaration.source); newImportsByExportFroms.push(targetImportDeclaration); } targetImportDeclaration.specifiers.push((0, make_node_1.makeImportSpecifier)(options.promiseExportName, options.promiseImportName(importedPromiseCount))); importedPromiseCount++; } } const importedPromiseArray = importedPromiseCount === 0 ? null : (0, make_node_1.makeArrayExpression)([...Array(importedPromiseCount).keys()].map(i => (0, make_node_1.makeCallExpression)((0, make_node_1.makeArrowFunction)([], [(0, make_node_1.makeTryCatchStatement)([(0, make_node_1.makeReturnStatement)((0, make_node_1.makeIdentifier)(options.promiseImportName(i)))], [])])))); // The `async () => { /* original top-level statements */ }` function const wrappedTopLevelFunction = (0, make_node_1.makeArrowFunction)([], warppedStatements, true); // `Promise.all([ /* ... */]).then(async () => { /* ... */ })` or `(async () => {})()` const promiseExpression = importedPromiseArray ? (0, make_node_1.makeCallExpression)((0, make_node_1.makeMemberExpression)((0, make_node_1.makeCallExpression)((0, make_node_1.makeMemberExpression)("Promise", "all"), [importedPromiseArray]), "then"), [wrappedTopLevelFunction]) : (0, make_node_1.makeCallExpression)(wrappedTopLevelFunction); /* * New top-level after transformation: * * import { ..., __tla as __tla_0 } from "some-module-with-TLA"; * import { ... } from "some-module-without-TLA"; * * let some, variables, exported, from, original, top, level; * * let __tla = Promise.all([ ... ]).then(async () => { * ... * }); * * export { ..., __tla }; */ const newTopLevel = [ ...imports, ...newImportsByExportFroms, ...exportFroms, exportedNamesDeclaration ]; if (exportedNames.length > 0 || ((_b = (_a = bundleInfo[moduleName]) === null || _a === void 0 ? void 0 : _a.importedBy) === null || _b === void 0 ? void 0 : _b.length) > 0) { // If the chunk is being imported, append export of the TLA promise to export list const promiseDeclaration = (0, make_node_1.makeVariableInitDeclaration)(options.promiseExportName, promiseExpression); exportMap[options.promiseExportName] = options.promiseExportName; newTopLevel.push(promiseDeclaration, (0, make_node_1.makeExportListDeclaration)(Object.entries(exportMap))); } else { // If the chunk is an entry, just execute the promise expression newTopLevel.push((0, make_node_1.makeStatement)(promiseExpression)); } ast.body = newTopLevel.filter(x => x); return ast; }