fusion-cli/build/plugins/instrumented-import-dependency-template-plugin.js (208 lines of code) (raw):

/** Copyright (c) 2018 Uber Technologies, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ /* eslint-env node */ /*:: import type { ClientChunkMetadataState, ClientChunkMetadata, TranslationsManifest, } from "../types.js"; type InstrumentationPluginOpts = | ClientPluginOpts | ServerPluginOpts; type ServerPluginOpts = { compilation: "server", clientChunkMetadata: ClientChunkMetadataState }; type ClientPluginOpts = { compilation: "client", i18nManifest: TranslationsManifest }; */ const ConcatenatedModule = require('webpack/lib/optimize/ConcatenatedModule.js'); const ImportDependency = require('webpack/lib/dependencies/ImportDependency'); const ImportDependencyTemplate = require('webpack/lib/dependencies/ImportDependency') .Template; class InstrumentedImportDependency extends ImportDependency { constructor(dep, opts, moduleIdent) { super(dep.request, dep.originModule, dep.block); this.module = dep.module; this.loc = dep.loc; if (opts.compilation === 'client') { this.translationsManifest = opts.i18nManifest; } /** * Production builds may have no id at this point * This compilation phase is earlier than moduleIds, so * we must create our own and cache it based on the module identifier */ dep.module.id = createCachedModuleId(moduleIdent); } getInstrumentation() { // client-side, use built-in values this.chunkIds = getChunkGroupIds(this.block.chunkGroup); this.translationKeys = new Set(); if (this.translationsManifest) { const modules = getChunkGroupModules(this); for (const module of modules) { if (this.translationsManifest.has(module)) { const keys = this.translationsManifest.get(module); for (const key of keys) { this.translationKeys.add(key); } } } } return { chunkIds: this.chunkIds, translationKeys: Array.from(this.translationKeys), }; } updateHash(hash) { super.updateHash(hash); const {translationKeys} = this.getInstrumentation(); // Invalidate this dependency when the translation keys change // Necessary for HMR hash.update(JSON.stringify(translationKeys)); } } /** * We create an extension to the original ImportDependency template * that adds extra properties to the promise returned by import() * for its corresponding chunkId and module id. * * At a high level, if module `foo.js` had module id "abc" and was in chunk with id 5, we turn: * * import('./foo.js') * // Returns a promise * * into: * * Object.defineProperties(import('./foo.js'), {__CHUNK_IDS: [5], __MODULE_ID: "abc", __I18N_KEYS: ['key1']}) * // Also returns a promise, but with extra non-enumerable properties */ InstrumentedImportDependency.Template = class InstrumentedImportDependencyTemplate extends ImportDependencyTemplate { constructor(clientChunkIndex) { super(); if (clientChunkIndex) { this.clientChunkIndex = clientChunkIndex; } } /** * It may be possible to avoid duplicating code by extending `super`, but * for now, we'll just override this method entirely with a modified version * Based on https://github.com/webpack/webpack/blob/5e38646f589b5b6325556f3127e7b61df33d3cb9/lib/dependencies/ImportDependency.js */ apply(dep /*: any */, source /*: any */, runtime /*: any */) { const depBlock = dep.block; let chunkIds = []; let translationKeys = []; if (dep instanceof InstrumentedImportDependency) { const instrumentation = dep.getInstrumentation(); chunkIds = instrumentation.chunkIds; translationKeys = instrumentation.translationKeys; } else if (this.clientChunkIndex) { // Template invoked without InstrumentedImportDependency // server-side, use values from client bundle const ids = this.clientChunkIndex.get(getModuleResource(dep.module)); chunkIds = ids ? Array.from(ids) : []; } else { // Prevent future developers from creating a broken webpack state throw new Error( 'Dependency is not Instrumented and lacks a clientChunkIndex' ); } const content = runtime.moduleNamespacePromise({ block: dep.block, module: dep.module, request: dep.request, strict: dep.originModule.buildMeta.strictHarmonyModule, message: 'import()', }); // Add the following properties to the promise returned by import() // - `__CHUNK_IDS`: the webpack chunk ids for the dynamic import // - `__MODULE_ID`: the webpack module id of the dynamically imported module. Equivalent to require.resolveWeak(path) // - `__I18N_KEYS`: the translation keys used in the client chunk group for this import() const customContent = chunkIds ? `Object.defineProperties(${content}, { "__CHUNK_IDS": {value:${JSON.stringify(chunkIds)}}, "__MODULE_ID": {value:${JSON.stringify(dep.module.id)}}, "__I18N_KEYS": {value:${JSON.stringify(translationKeys)}} })` : content; // replace with `customContent` instead of `content` source.replace(depBlock.range[0], depBlock.range[1] - 1, customContent); } }; /** * Webpack plugin to replace standard ImportDependencyTemplate with custom one * See InstrumentedImportDependencyTemplate for more info */ class InstrumentedImportDependencyTemplatePlugin { /*:: opts: InstrumentationPluginOpts;*/ constructor(opts /*: InstrumentationPluginOpts*/) { this.opts = opts; } apply(compiler /*: any */) { const name = this.constructor.name; if (this.opts.compilation === 'server') { const {clientChunkMetadata} = this.opts; compiler.hooks.make.tapAsync(name, (compilation, done) => { clientChunkMetadata.result.then(metadata => { compilation.dependencyTemplates.set( ImportDependency, new InstrumentedImportDependency.Template(metadata.fileManifest) ); done(); }); }); } if (this.opts.compilation === 'client') { // Add a new template and factory for IntrumentedImportDependency compiler.hooks.compilation.tap(name, (compilation, params) => { compilation.dependencyFactories.set( InstrumentedImportDependency, params.normalModuleFactory ); compilation.dependencyTemplates.set( InstrumentedImportDependency, new InstrumentedImportDependency.Template() ); compilation.hooks.afterOptimizeDependencies.tap(name, modules => { // Replace ImportDependency with our Instrumented dependency for (const module of modules) { if (module.blocks) { module.blocks.forEach(block => { block.dependencies.forEach((dep, index) => { if (dep instanceof ImportDependency) { let moduleId = dep.module.id; if (dep.module.id === null && dep.module.libIdent) { moduleId = dep.module.libIdent({ context: compiler.options.context, }); } block.dependencies[ index ] = new InstrumentedImportDependency( dep, this.opts, moduleId ); } }); }); } } }); }); } /** * server and client * Ensure custom module ids are used instead of hashed module ids * Based on https://github.com/gogoair/custom-module-ids-webpack-plugin */ compiler.hooks.compilation.tap(name, (compilation, params) => { compilation.hooks.beforeModuleIds.tap(name, modules => { for (const module of modules) { if (module.id === null && module.libIdent) { // Some modules lose their id by this point // Reassign the cached module id so it matches the id used in the instrumentation const id = module.libIdent({ context: compiler.options.context, }); const moduleId = getCachedModuleId(id); if (moduleId) { module.id = moduleId; } } } }); }); } } module.exports = InstrumentedImportDependencyTemplatePlugin; const customModuleIds = new Map(); let moduleCounter = 0; /** * Create custom module id, cached based on the module identifier * id format: `__fusion__0` */ function createCachedModuleId(ident) { if (/^__fusion__\d+$/.test(ident)) { // This is already a cached identifier return ident; } if (customModuleIds.has(ident)) { return customModuleIds.get(ident); } const moduleId = `__fusion__${(moduleCounter++).toString()}`; customModuleIds.set(ident, moduleId); return moduleId; } function getCachedModuleId(ident) { return customModuleIds.get(ident); } /** * Adapted from * https://github.com/webpack/webpack/blob/5e38646f589b5b6325556f3127e7b61df33d3cb9/lib/dependencies/DepBlockHelpers.js */ function getChunkGroupIds(chunkGroup) { if (chunkGroup && !chunkGroup.isInitial()) { if (Array.isArray(chunkGroup.chunks)) { return chunkGroup.chunks.map(c => c.id); } return [chunkGroup.id]; } } function getModuleResource(module) { if (module instanceof ConcatenatedModule) { return module.rootModule.resource; } else { return module.resource; } } function getChunkGroupModules(dep) { const modulesSet = new Set(); if (dep.module && dep.module.dependencies) { modulesSet.add(getModuleResource(dep.module)); dep.module.dependencies.forEach(dependency => { if (dependency.module) { modulesSet.add(getModuleResource(dependency.module)); } }); } const {chunkGroup} = dep.block; if (chunkGroup && Array.isArray(chunkGroup.chunks)) { chunkGroup.chunks.forEach(chunk => { for (const module of chunk.getModules()) { modulesSet.add(getModuleResource(module)); if (module instanceof ConcatenatedModule) { module.buildInfo.fileDependencies.forEach(fileDep => { modulesSet.add(fileDep); }); } } }); } return modulesSet; }