From 7940ccd71f40c3e5817da9678cde5138fb025d55 Mon Sep 17 00:00:00 2001 From: chenchen32 Date: Sun, 27 Aug 2023 19:29:59 +0800 Subject: [PATCH] fix: incorrect content hash --- webpack-plugin/src/index.ts | 141 +++++++++--------- webpack-plugin/src/loaders/include.ts | 71 +++++++-- .../src/plugins/AddWorkerEntryPointPlugin.ts | 67 +++++++-- 3 files changed, 185 insertions(+), 94 deletions(-) diff --git a/webpack-plugin/src/index.ts b/webpack-plugin/src/index.ts index 529dd044c5..077585e112 100644 --- a/webpack-plugin/src/index.ts +++ b/webpack-plugin/src/index.ts @@ -1,11 +1,9 @@ import type * as webpack from 'webpack'; import * as path from 'path'; -import * as loaderUtils from 'loader-utils'; -import * as fs from 'fs'; import { AddWorkerEntryPointPlugin } from './plugins/AddWorkerEntryPointPlugin'; import { IFeatureDefinition } from './types'; import { ILoaderOptions } from './loaders/include'; -import { EditorLanguage, EditorFeature, NegatedEditorFeature } from 'monaco-editor/esm/metadata'; +import { EditorFeature, EditorLanguage, NegatedEditorFeature } from 'monaco-editor/esm/metadata'; const INCLUDE_LOADER_PATH = require.resolve('./loaders/include'); @@ -37,19 +35,6 @@ function resolveMonacoPath(filePath: string, monacoEditorPath: string | undefine return require.resolve(filePath); } -/** - * Return the interpolated final filename for a worker, respecting the file name template. - */ -function getWorkerFilename( - filename: string, - entry: string, - monacoEditorPath: string | undefined -): string { - return loaderUtils.interpolateName({ resourcePath: entry }, filename, { - content: fs.readFileSync(resolveMonacoPath(entry, monacoEditorPath)) - }); -} - interface EditorMetadata { features: IFeatureDefinition[]; languages: IFeatureDefinition[]; @@ -147,6 +132,7 @@ declare namespace MonacoEditorWebpackPlugin { globalAPI?: boolean; } } + interface IInternalMonacoEditorWebpackPluginOpts { languages: IFeatureDefinition[]; features: IFeatureDefinition[]; @@ -156,8 +142,18 @@ interface IInternalMonacoEditorWebpackPluginOpts { globalAPI: boolean; } +type IWorkerPathsMap = Record; + +interface IWorkerTaskAction { + resolve: () => void; + reject: (err: Error) => void; +} + class MonacoEditorWebpackPlugin implements webpack.WebpackPluginInstance { private readonly options: IInternalMonacoEditorWebpackPluginOpts; + public workerTaskActions: Record; + public workersTask: Promise; + public workerPathsMap: IWorkerPathsMap; constructor(options: MonacoEditorWebpackPlugin.IMonacoEditorWebpackPluginOpts = {}) { const monacoEditorPath = options.monacoEditorPath; @@ -172,6 +168,9 @@ class MonacoEditorWebpackPlugin implements webpack.WebpackPluginInstance { publicPath: options.publicPath || '', globalAPI: options.globalAPI || false }; + this.workerTaskActions = {}; + this.workersTask = Promise.resolve([]); + this.workerPathsMap = {}; } apply(compiler: webpack.Compiler): void { @@ -188,19 +187,35 @@ class MonacoEditorWebpackPlugin implements webpack.WebpackPluginInstance { }); } }); + + this.workerPathsMap = collectWorkerPathsMap(workers); + + const plugins = createPlugins(compiler, workers, filename, monacoEditorPath, this); const rules = createLoaderRules( languages, features, - workers, - filename, monacoEditorPath, publicPath, compilationPublicPath, - globalAPI + globalAPI, + this ); - const plugins = createPlugins(compiler, workers, filename, monacoEditorPath); addCompilerRules(compiler, rules); addCompilerPlugins(compiler, plugins); + + compiler.hooks.thisCompilation.tap(AddWorkerEntryPointPlugin.name, () => { + const tasks: Promise[] = []; + Object.keys(this.workerPathsMap).forEach((workerKey) => { + const task = new Promise((resolve, reject) => { + this.workerTaskActions[workerKey] = { + resolve, + reject + }; + }); + tasks.push(task); + }); + this.workersTask = Promise.all(tasks); + }); } } @@ -235,39 +250,41 @@ function getCompilationPublicPath(compiler: webpack.Compiler): string { return ''; } +function collectWorkerPathsMap(workers: ILabeledWorkerDefinition[]): IWorkerPathsMap { + const map = fromPairs(workers.map(({ label }) => [label, ''])); + if (map['typescript']) { + // javascript shares the same worker + map['javascript'] = map['typescript']; + } + if (map['css']) { + // scss and less share the same worker + map['less'] = map['css']; + map['scss'] = map['css']; + } + + if (map['html']) { + // handlebars, razor and html share the same worker + map['handlebars'] = map['html']; + map['razor'] = map['html']; + } + + return map; +} + function createLoaderRules( languages: IFeatureDefinition[], features: IFeatureDefinition[], - workers: ILabeledWorkerDefinition[], - filename: string, monacoEditorPath: string | undefined, pluginPublicPath: string, compilationPublicPath: string, - globalAPI: boolean + globalAPI: boolean, + pluginInstance: MonacoEditorWebpackPlugin ): webpack.RuleSetRule[] { if (!languages.length && !features.length) { return []; } const languagePaths = flatArr(coalesce(languages.map((language) => language.entry))); const featurePaths = flatArr(coalesce(features.map((feature) => feature.entry))); - const workerPaths = fromPairs( - workers.map(({ label, entry }) => [label, getWorkerFilename(filename, entry, monacoEditorPath)]) - ); - if (workerPaths['typescript']) { - // javascript shares the same worker - workerPaths['javascript'] = workerPaths['typescript']; - } - if (workerPaths['css']) { - // scss and less share the same worker - workerPaths['less'] = workerPaths['css']; - workerPaths['scss'] = workerPaths['css']; - } - - if (workerPaths['html']) { - // handlebars, razor and html share the same worker - workerPaths['handlebars'] = workerPaths['html']; - workerPaths['razor'] = workerPaths['html']; - } // Determine the public path from which to load worker JS files. In order of precedence: // 1. Plugin-specific public path. @@ -279,35 +296,10 @@ function createLoaderRules( `? __webpack_public_path__ ` + `: ${JSON.stringify(compilationPublicPath)}`; - const globals = { - MonacoEnvironment: `(function (paths) { - function stripTrailingSlash(str) { - return str.replace(/\\/$/, ''); - } - return { - globalAPI: ${globalAPI}, - getWorkerUrl: function (moduleId, label) { - var pathPrefix = ${pathPrefix}; - var result = (pathPrefix ? stripTrailingSlash(pathPrefix) + '/' : '') + paths[label]; - if (/^((http:)|(https:)|(file:)|(\\/\\/))/.test(result)) { - var currentUrl = String(window.location); - var currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length); - if (result.substring(0, currentOrigin.length) !== currentOrigin) { - if(/^(\\/\\/)/.test(result)) { - result = window.location.protocol + result - } - var js = '/*' + label + '*/importScripts("' + result + '");'; - var blob = new Blob([js], { type: 'application/javascript' }); - return URL.createObjectURL(blob); - } - } - return result; - } - }; - })(${JSON.stringify(workerPaths, null, 2)})` - }; const options: ILoaderOptions = { - globals, + pluginInstance, + publicPath: pathPrefix, + globalAPI, pre: featurePaths.map((importPath) => resolveMonacoPath(importPath, monacoEditorPath)), post: languagePaths.map((importPath) => resolveMonacoPath(importPath, monacoEditorPath)) }; @@ -328,18 +320,21 @@ function createPlugins( compiler: webpack.Compiler, workers: ILabeledWorkerDefinition[], filename: string, - monacoEditorPath: string | undefined + monacoEditorPath: string | undefined, + pluginInstance: MonacoEditorWebpackPlugin ): AddWorkerEntryPointPlugin[] { const webpack = compiler.webpack ?? require('webpack'); return ([]).concat( workers.map( - ({ id, entry }) => + ({ id, entry, label }) => new AddWorkerEntryPointPlugin({ id, entry: resolveMonacoPath(entry, monacoEditorPath), - filename: getWorkerFilename(filename, entry, monacoEditorPath), - plugins: [new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })] + label, + filename, + plugins: [new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })], + pluginInstance }) ) ); diff --git a/webpack-plugin/src/loaders/include.ts b/webpack-plugin/src/loaders/include.ts index 4bebcce9f9..143d96556c 100644 --- a/webpack-plugin/src/loaders/include.ts +++ b/webpack-plugin/src/loaders/include.ts @@ -1,8 +1,11 @@ import type { PitchLoaderDefinitionFunction } from 'webpack'; import * as loaderUtils from 'loader-utils'; +import type * as MonacoEditorWebpackPlugin from '../index'; export interface ILoaderOptions { - globals?: { [key: string]: string }; + pluginInstance: MonacoEditorWebpackPlugin; + publicPath: string; + globalAPI: boolean; pre?: string[]; post?: string[]; } @@ -10,7 +13,16 @@ export interface ILoaderOptions { export const pitch: PitchLoaderDefinitionFunction = function pitch( remainingRequest ) { - const { globals = undefined, pre = [], post = [] } = (this.query as ILoaderOptions) || {}; + const { + pluginInstance, + publicPath, + globalAPI, + pre = [], + post = [] + } = (this.query as ILoaderOptions) || {}; + + this.cacheable(false); + const callback = this.async(); // HACK: NamedModulesPlugin overwrites existing modules when requesting the same module via // different loaders, so we need to circumvent this by appending a suffix to make the name unique @@ -26,16 +38,55 @@ export const pitch: PitchLoaderDefinitionFunction = function pit return loaderUtils.stringifyRequest(this, request); }; - return [ - ...(globals - ? Object.keys(globals).map((key) => `self[${JSON.stringify(key)}] = ${globals[key]};`) - : []), - ...pre.map((include: any) => `import ${stringifyRequest(include)};`), - ` + const getContent = () => { + const globals: Record = { + MonacoEnvironment: `(function (paths) { + function stripTrailingSlash(str) { + return str.replace(/\\/$/, ''); + } + return { + globalAPI: ${globalAPI}, + getWorkerUrl: function (moduleId, label) { + var pathPrefix = ${publicPath}; + var result = (pathPrefix ? stripTrailingSlash(pathPrefix) + '/' : '') + paths[label]; + if (/^((http:)|(https:)|(file:)|(\\/\\/))/.test(result)) { + var currentUrl = String(window.location); + var currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length); + if (result.substring(0, currentOrigin.length) !== currentOrigin) { + if(/^(\\/\\/)/.test(result)) { + result = window.location.protocol + result + } + var js = '/*' + label + '*/importScripts("' + result + '");'; + var blob = new Blob([js], { type: 'application/javascript' }); + return URL.createObjectURL(blob); + } + } + return result; + } + }; + })(${JSON.stringify(pluginInstance.workerPathsMap, null, 2)})` + }; + + return [ + ...(globals + ? Object.keys(globals).map((key) => `self[${JSON.stringify(key)}] = ${globals[key]};`) + : []), + ...pre.map((include: any) => `import ${stringifyRequest(include)};`), + ` import * as monaco from ${stringifyRequest(`!!${remainingRequest}`)}; export * from ${stringifyRequest(`!!${remainingRequest}`)}; export default monaco; `, - ...post.map((include: any) => `import ${stringifyRequest(include)};`) - ].join('\n'); + ...post.map((include: any) => `import ${stringifyRequest(include)};`) + ].join('\n'); + }; + + pluginInstance.workersTask.then( + () => { + callback(null, getContent()); + }, + (err) => { + callback(err, getContent()); + } + ); }; diff --git a/webpack-plugin/src/plugins/AddWorkerEntryPointPlugin.ts b/webpack-plugin/src/plugins/AddWorkerEntryPointPlugin.ts index 68ad845c73..b7ec702465 100644 --- a/webpack-plugin/src/plugins/AddWorkerEntryPointPlugin.ts +++ b/webpack-plugin/src/plugins/AddWorkerEntryPointPlugin.ts @@ -1,23 +1,31 @@ import type * as webpack from 'webpack'; +import type * as MonacoEditorWebpackPlugin from '../index'; export interface IAddWorkerEntryPointPluginOptions { id: string; + label: string; entry: string; filename: string; chunkFilename?: string; plugins: webpack.WebpackPluginInstance[]; + pluginInstance: MonacoEditorWebpackPlugin; } -function getCompilerHook( +export function getCompilerHook( compiler: webpack.Compiler, - { id, entry, filename, chunkFilename, plugins }: IAddWorkerEntryPointPluginOptions + { + id, + entry, + label, + filename, + chunkFilename, + plugins, + pluginInstance + }: IAddWorkerEntryPointPluginOptions ) { const webpack = compiler.webpack ?? require('webpack'); - return function ( - compilation: webpack.Compilation, - callback: (error?: Error | null | false) => void - ) { + return function (compilation: webpack.Compilation) { const outputOptions = { filename, chunkFilename, @@ -29,11 +37,38 @@ function getCompilerHook( new webpack.webworker.WebWorkerTemplatePlugin(), new webpack.LoaderTargetPlugin('webworker') ]); + + childCompiler.hooks.compilation.tap(AddWorkerEntryPointPlugin.name, (childCompilation) => { + childCompilation.hooks.afterOptimizeAssets.tap(AddWorkerEntryPointPlugin.name, (_assets) => { + // one entry may generate many assets,we can not distinguish which is the entry bundle,so we get the entry bundle by entrypoint + const entrypoint = childCompilation.entrypoints.get(entryNameWithoutExt); + const chunks = entrypoint?.chunks; + if (chunks) { + const chunk = [...chunks]?.[0]; + + pluginInstance.workerPathsMap[label] = [...chunk.files]?.[0]; + pluginInstance.workerTaskActions[label]?.resolve?.(); + } + }); + }); + const SingleEntryPlugin = webpack.EntryPlugin ?? webpack.SingleEntryPlugin; - new SingleEntryPlugin(compiler.context, entry, 'main').apply(childCompiler); + if (!label) { + console.warn(`Please set label in customLanguage (expected a string)`); + } + + const entryName = entry.split('/').pop(); + const entryNameWithoutExt = entryName?.split('.').slice(0, 1).join('.') ?? 'main'; + new SingleEntryPlugin(compiler.context, entry, entryNameWithoutExt).apply(childCompiler); + plugins.forEach((plugin) => plugin.apply(childCompiler)); - childCompiler.runAsChild((err?: Error | null) => callback(err)); + childCompiler.runAsChild((err?: Error | null) => { + if (err) { + console.error(`${AddWorkerEntryPointPlugin.name} childCompiler error`, err); + pluginInstance.workerTaskActions[label]?.reject?.(err); + } + }); }; } @@ -42,12 +77,22 @@ export class AddWorkerEntryPointPlugin implements webpack.WebpackPluginInstance constructor({ id, + label, entry, filename, chunkFilename = undefined, - plugins + plugins, + pluginInstance }: IAddWorkerEntryPointPluginOptions) { - this.options = { id, entry, filename, chunkFilename, plugins }; + this.options = { + id, + label, + entry, + filename, + chunkFilename, + plugins, + pluginInstance + }; } apply(compiler: webpack.Compiler) { @@ -57,7 +102,7 @@ export class AddWorkerEntryPointPlugin implements webpack.WebpackPluginInstance if (parseInt(majorVersion) < 4) { (compiler).plugin('make', compilerHook); } else { - compiler.hooks.make.tapAsync('AddWorkerEntryPointPlugin', compilerHook); + compiler.hooks.make.tap(AddWorkerEntryPointPlugin.name, compilerHook); } } }