All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.RewriteDynamicImports Maven / Gradle / Ivy

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2021 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.AstFactory.type;
import static com.google.javascript.jscomp.ConvertChunksToESModules.DYNAMIC_IMPORT_CALLBACK_FN;
import static com.google.javascript.jscomp.ConvertChunksToESModules.UNABLE_TO_COMPUTE_RELATIVE_PATH;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.javascript.jscomp.CompilerOptions.ChunkOutputType;
import com.google.javascript.jscomp.ModuleRenaming.GlobalizedModuleName;
import com.google.javascript.jscomp.deps.ModuleLoader;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import com.google.javascript.jscomp.modules.Module;
import com.google.javascript.jscomp.modules.ModuleMap;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.TemplateType;
import com.google.javascript.rhino.jstype.TemplatizedType;
import java.util.Iterator;
import javax.annotation.Nullable;

/**
 * Rewrite dynamic import expressions to account for bundling and module rewriting. Since dynamic
 * imports cannot be fully polyfilled, optionally support replacing the expression with a function
 * call which indicates an external polyfill is utilized.
 *
 * 

If the import specifier is a string literal and the module resolver recognizes the target, the * pass retargets the specifier to the correct output chunk. * *

Example from: * *

 *   import('./input-module1.js');
 * 
* * To: * *
 *   imprt_('./output-chunk0.js').then(function() { return module$output$chunk0; });
 * 
*/ public class RewriteDynamicImports extends NodeTraversal.AbstractPostOrderCallback implements CompilerPass { static final DiagnosticType DYNAMIC_IMPORT_ALIASING_REQUIRED = DiagnosticType.warning( "JSC_DYNAMIC_IMPORT_ALIASING_REQUIRED", "Dynamic import expressions should be aliased for for language level. " + "Use the --dynamic_import_alias flag."); static final DiagnosticType DYNAMIC_IMPORT_INVALID_ALIAS = DiagnosticType.error( "JSC_DYNAMIC_IMPORT_INVALID_ALIAS", "Dynamic import alias is not a valid name"); private final AbstractCompiler compiler; private final AstFactory astFactory; private final String alias; private final boolean requiresAliasing; private final boolean shouldWrapDynamicImportCallbacks; private boolean dynamicImportsRemoved = false; private boolean arrowFunctionsAdded = false; private boolean wrappedDynamicImportCallback = false; /** @param compiler The compiler */ public RewriteDynamicImports( AbstractCompiler compiler, @Nullable String alias, ChunkOutputType chunkOutputType) { this.compiler = compiler; this.astFactory = compiler.createAstFactory(); this.alias = alias; this.requiresAliasing = !compiler.getOptions().getOutputFeatureSet().contains(Feature.DYNAMIC_IMPORT); this.shouldWrapDynamicImportCallbacks = chunkOutputType == ChunkOutputType.ES_MODULES; } private final boolean aliasIsValid() { return alias != null && (alias.equals("import") || NodeUtil.isValidQualifiedName(compiler.getFeatureSet(), alias)); } @Override public void process(Node externs, Node root) { dynamicImportsRemoved = false; checkArgument(externs.isRoot(), externs); checkArgument(root.isRoot(), root); NodeTraversal.traverse(compiler, root, this); if (wrappedDynamicImportCallback) { injectWrappingFunctionExtern(); } if (dynamicImportsRemoved) { // This pass removes dynamic import, but adds arrow functions. compiler.setFeatureSet(compiler.getFeatureSet().without(Feature.DYNAMIC_IMPORT)); if (this.requiresAliasing && aliasIsValid()) { NodeTraversal.traverse(compiler, externs, new AliasInjectingTraversal()); } } if (arrowFunctionsAdded) { compiler.setFeatureSet(compiler.getFeatureSet().with(Feature.ARROW_FUNCTIONS)); } } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.getToken() != Token.DYNAMIC_IMPORT) { return; } // If the module specifier is a string, attempt to resolve the module final ModuleMap moduleMap = compiler.getModuleMap(); final Node importSpecifier = n.getFirstChild(); if (importSpecifier.isStringLit() && moduleMap != null) { final ModulePath targetPath = t.getInput() .getPath() .resolveJsModule( importSpecifier.getString(), n.getSourceFileName(), n.getLineno(), n.getCharno()); final Module module = (targetPath == null) ? null : compiler.getModuleMap().getModule(targetPath); final String targetModuleVarName = (module == null) ? null : GlobalizedModuleName.create(module.metadata(), null, null).aliasName().join(); final Var targetModuleNS = (targetModuleVarName == null) ? null : t.getScope().getVar(targetModuleVarName); if (targetModuleNS != null) { final JSChunk targetModule = targetModuleNS.getInput().getChunk(); // If the target module is bundled into the same output chunk, replace the import statement // with a promise that resolves to the module namespace. // No further rewriting occurs for this case. if (t.getChunk() == targetModule) { replaceDynamicImportWithPromise(t, n, targetModuleNS); return; } else { // The target output chunk is recognized and different from the current chunk. // Retarget the import specifier path to the output chunk path and rewrite // the import to reference the rewritten global module namespace variable. retargetImportSpecifier(t, n, targetModule); if (NodeUtil.isExpressionResultUsed(n)) { addChainedThen(n, targetModuleNS); } } } } if (aliasIsValid()) { aliasDynamicImport(t, n); } else if (this.alias != null) { t.report(n, DYNAMIC_IMPORT_INVALID_ALIAS); } else if (requiresAliasing) { t.report(n, DYNAMIC_IMPORT_ALIASING_REQUIRED); } } /** * Replace a dynamic import expression with a promise resolving to the rewritten module namespace. * *

Before * *

   *   import('./foo.js');
   * 
* * After * *
   *   Promise.resolve(module$foo);
   * 
*/ private void replaceDynamicImportWithPromise( NodeTraversal t, Node dynamicImport, Var targetModuleNS) { final JSTypeRegistry registry = compiler.getTypeRegistry(); final Node promiseDotResolve = astFactory.createQName(compiler.getTranspilationNamespace(), "Promise.resolve"); final Node promiseResolveCall = astFactory.createCall(promiseDotResolve, type(dynamicImport)); final boolean isExpressionUsed = NodeUtil.isExpressionResultUsed(dynamicImport); if (isExpressionUsed) { JSType moduleNamespaceType; if (dynamicImport.getJSType() != null && dynamicImport.getJSType().isTemplatizedType()) { TemplatizedType templatizedType = (TemplatizedType) dynamicImport.getJSType(); moduleNamespaceType = templatizedType.getTemplateTypes().get(0); } else { moduleNamespaceType = registry.getNativeType(JSTypeNative.UNKNOWN_TYPE); } promiseDotResolve.setJSType( registry.createFunctionType( registry.createTemplatizedType( registry.getNativeObjectType(JSTypeNative.PROMISE_TYPE), moduleNamespaceType), registry.getNativeType(JSTypeNative.ALL_TYPE))); promiseResolveCall.addChildToBack(createModuleNamespaceNode(targetModuleNS)); promiseResolveCall.copyTypeFrom(dynamicImport); } else { promiseDotResolve.setJSType( registry.createFunctionType( registry.createTemplatizedType( registry.getNativeObjectType(JSTypeNative.PROMISE_TYPE), registry.getNativeType(JSTypeNative.VOID_TYPE)), registry.getNativeType(JSTypeNative.ALL_TYPE))); promiseResolveCall.setJSType( registry.createTemplatizedType( registry.getNativeObjectType(JSTypeNative.PROMISE_TYPE), registry.getNativeType(JSTypeNative.VOID_TYPE))); } promiseResolveCall.srcrefTree(dynamicImport); Node parent = dynamicImport.getParent(); dynamicImport.replaceWith(promiseResolveCall); t.reportCodeChange(parent); dynamicImportsRemoved = true; } private static String getChunkFileName(JSChunk chunk) { return chunk.getName() + ".js"; } /** * Rewrite the dynamic import specifier to the output chunk path * *

Before * *

   *   import('./foo.js');
   * 
* * After * *
   *   import('./chunk0.js');
   * 
*/ private void retargetImportSpecifier(NodeTraversal t, Node dynamicImport, JSChunk targetModule) { String retargetedSpecifier; String importingChunkFilename = getChunkFileName(t.getInput().getChunk()); String targetChunkFilename = getChunkFileName(targetModule); try { retargetedSpecifier = ModuleLoader.relativePathFrom(importingChunkFilename, targetChunkFilename); } catch (IllegalArgumentException e) { compiler.report( JSError.make( dynamicImport, UNABLE_TO_COMPUTE_RELATIVE_PATH, importingChunkFilename, targetChunkFilename)); return; } final Node originalSpecifierNode = dynamicImport.getFirstChild(); Node newSpecifier = astFactory.createString(retargetedSpecifier).srcref(originalSpecifierNode); originalSpecifierNode.replaceWith(newSpecifier); t.reportCodeChange(newSpecifier); } /** * Since a dynamic import expression is a promise resolving to the namespace export type, add a * ".then()" call after it and resolve to the rewritten module namespace. * *

Before * *

   *   import('./foo.js');
   * 
* * After * *
   *   import('./foo.js').then(() => module$foo);
   * 
*/ private void addChainedThen(Node dynamicImport, Var targetModuleNs) { JSTypeRegistry registry = compiler.getTypeRegistry(); final Node importParent = dynamicImport.getParent(); final Node placeholder = IR.empty(); dynamicImport.replaceWith(placeholder); final Node moduleNamespaceNode = createModuleNamespaceNode(targetModuleNs); final Node callbackFn = astFactory.createZeroArgArrowFunctionForExpression(moduleNamespaceNode); callbackFn.setJSType( registry.createFunctionType( registry.createTemplatizedType( registry.getNativeObjectType(JSTypeNative.PROMISE_TYPE), registry.getNativeType(JSTypeNative.UNKNOWN_TYPE)), registry.createFunctionType(registry.getNativeType(JSTypeNative.ALL_TYPE)))); Node thenArgument = callbackFn; if (shouldWrapDynamicImportCallbacks) { Node wrappingFunction = astFactory.createName( DYNAMIC_IMPORT_CALLBACK_FN, type(registry.createFunctionType(callbackFn.getJSType(), callbackFn.getJSType()))); thenArgument = astFactory.createCall(wrappingFunction, type(callbackFn), callbackFn); wrappedDynamicImportCallback = true; } final Node importThenCall = astFactory.createCall( astFactory.createGetPropWithUnknownType(dynamicImport, "then"), type(dynamicImport), thenArgument); importThenCall.srcrefTreeIfMissing(dynamicImport); if (dynamicImport.getJSType() != null) { importThenCall.copyTypeFrom(dynamicImport); } placeholder.replaceWith(importThenCall); compiler.reportChangeToChangeScope(callbackFn); compiler.reportChangeToEnclosingScope(importParent); arrowFunctionsAdded = true; } /** * Replace a dynamic import expression with a function call to the specified alias. * *

Before * *

   *   import('./foo.js');
   * 
* * After * *
   *   aliasedName('./foo.js');
   * 
*/ private void aliasDynamicImport(NodeTraversal t, Node dynamicImport) { checkNotNull(this.alias); final Node aliasNode = astFactory.createQNameWithUnknownType(this.alias); aliasNode.setOriginalName("import"); final Node moduleSpecifier = dynamicImport.removeFirstChild(); Node importAliasCall = astFactory .createCall(aliasNode, type(dynamicImport), moduleSpecifier) .srcrefTreeIfMissing(dynamicImport); if (dynamicImport.getJSType() != null) { importAliasCall.copyTypeFrom(dynamicImport); } dynamicImport.replaceWith(importAliasCall); t.reportCodeChange(importAliasCall); dynamicImportsRemoved = true; } /** For a given module, return a reference to the module namespace export */ private Node createModuleNamespaceNode(Var moduleVar) { final Node moduleVarNode = moduleVar.getNode(); Node moduleNamespace = moduleVarNode.cloneNode(); moduleNamespace.copyTypeFrom(moduleVarNode); return moduleNamespace; } /** For a given module, return a reference to the module namespace export */ private void injectWrappingFunctionExtern() { JSTypeRegistry registry = compiler.getTypeRegistry(); TemplateType templateT = registry.createTemplateType("T"); final Node wrappingFunctionDefinition = astFactory.createFunction( DYNAMIC_IMPORT_CALLBACK_FN, astFactory.createParamList("importCallback"), astFactory.createBlock(), type(registry.createFunctionType(templateT, templateT))); Node externsRoot = compiler.getSynthesizedExternsInput().getAstRoot(compiler); wrappingFunctionDefinition.srcrefTree(externsRoot); externsRoot.addChildToBack(wrappingFunctionDefinition); compiler.reportChangeToEnclosingScope(wrappingFunctionDefinition); } /** * A shallow traversal class for the externs to inject the alias. * *

Later passes require the names to be defined. For simple name aliases first check for an * existing definition. Qualified name aliases check for an existing definition of the root name * only. */ private class AliasInjectingTraversal extends NodeTraversal.AbstractPreOrderCallback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { checkState(aliasIsValid()); // If the extern is a simple name and already declared, there is nothing to do if (NodeUtil.isValidSimpleName(alias) && t.getScope().getVar(alias) != null) { return false; } // Create the type of the import function JSTypeRegistry registry = compiler.getTypeRegistry(); JSType unknownType = registry.getNativeType(JSTypeNative.UNKNOWN_TYPE); JSType promiseType = registry.getNativeType(JSTypeNative.PROMISE_TYPE); TemplatizedType promiseTemplatizedType = registry.createTemplatizedType(promiseType.toObjectType(), ImmutableList.of(unknownType)); JSType stringType = registry.getNativeType(JSTypeNative.STRING_TYPE); Node externsRoot = compiler.getSynthesizedExternsInput().getAstRoot(compiler); // "import" is not a valid JS name and will not parse. However we can manually create it // and inject it into the externs. if (alias.equals("import") || NodeUtil.isValidSimpleName(alias)) { Node aliasNode = astFactory.createFunction( alias, astFactory.createParamList("specifier"), astFactory.createBlock(), type(registry.createFunctionType(promiseTemplatizedType, stringType))); aliasNode.srcrefTree(externsRoot); externsRoot.addChildToBack(aliasNode); } else { String aliasRootName = NodeUtil.getRootOfQualifiedName(alias); Var aliasVar = t.getScope().getVar(aliasRootName); // If the namespace root is defined, just assume the whole name is properly defined. // This may need revisited in the future, but checking if the full qualified name // is properly defined is difficult here. As long as the root of the qualified name is // defined, other passes seem be ok even with missing properties. if (aliasVar != null) { return false; } Node aliasRootNode = astFactory.createSingleVarNameDeclaration(aliasRootName, astFactory.createObjectLit()); aliasRootNode.srcrefTree(externsRoot); externsRoot.addChildToBack(aliasRootNode); // Define the rest of the parts of the name Iterator aliasNameParts = Splitter.on(".").split(alias).iterator(); Node qName = aliasRootNode.getFirstChild().cloneNode(); aliasNameParts.next(); // skip over root name while (aliasNameParts.hasNext()) { qName = astFactory.createGetPropWithUnknownType(qName.cloneTree(), aliasNameParts.next()); Node assignedValue; if (!aliasNameParts.hasNext()) { assignedValue = astFactory.createFunction( "", astFactory.createParamList("specifier"), astFactory.createBlock(), type(registry.createFunctionType(promiseTemplatizedType, stringType))); } else { assignedValue = astFactory.createObjectLit(); } Node expr = astFactory.exprResult(astFactory.createAssign(qName, assignedValue)); expr.srcrefTree(externsRoot); externsRoot.addChildToBack(expr); } } compiler.reportChangeToChangeScope(externsRoot); return false; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy