fix: incorrect content hash

This commit is contained in:
chenchen32 2023-08-27 19:29:59 +08:00
parent 38e1e3d097
commit 7940ccd71f
3 changed files with 185 additions and 94 deletions

View file

@ -1,11 +1,9 @@
import type * as webpack from 'webpack'; import type * as webpack from 'webpack';
import * as path from 'path'; import * as path from 'path';
import * as loaderUtils from 'loader-utils';
import * as fs from 'fs';
import { AddWorkerEntryPointPlugin } from './plugins/AddWorkerEntryPointPlugin'; import { AddWorkerEntryPointPlugin } from './plugins/AddWorkerEntryPointPlugin';
import { IFeatureDefinition } from './types'; import { IFeatureDefinition } from './types';
import { ILoaderOptions } from './loaders/include'; 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'); const INCLUDE_LOADER_PATH = require.resolve('./loaders/include');
@ -37,19 +35,6 @@ function resolveMonacoPath(filePath: string, monacoEditorPath: string | undefine
return require.resolve(filePath); 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(<any>{ resourcePath: entry }, filename, {
content: fs.readFileSync(resolveMonacoPath(entry, monacoEditorPath))
});
}
interface EditorMetadata { interface EditorMetadata {
features: IFeatureDefinition[]; features: IFeatureDefinition[];
languages: IFeatureDefinition[]; languages: IFeatureDefinition[];
@ -147,6 +132,7 @@ declare namespace MonacoEditorWebpackPlugin {
globalAPI?: boolean; globalAPI?: boolean;
} }
} }
interface IInternalMonacoEditorWebpackPluginOpts { interface IInternalMonacoEditorWebpackPluginOpts {
languages: IFeatureDefinition[]; languages: IFeatureDefinition[];
features: IFeatureDefinition[]; features: IFeatureDefinition[];
@ -156,8 +142,18 @@ interface IInternalMonacoEditorWebpackPluginOpts {
globalAPI: boolean; globalAPI: boolean;
} }
type IWorkerPathsMap = Record<string, string>;
interface IWorkerTaskAction {
resolve: () => void;
reject: (err: Error) => void;
}
class MonacoEditorWebpackPlugin implements webpack.WebpackPluginInstance { class MonacoEditorWebpackPlugin implements webpack.WebpackPluginInstance {
private readonly options: IInternalMonacoEditorWebpackPluginOpts; private readonly options: IInternalMonacoEditorWebpackPluginOpts;
public workerTaskActions: Record<string, IWorkerTaskAction>;
public workersTask: Promise<void[]>;
public workerPathsMap: IWorkerPathsMap;
constructor(options: MonacoEditorWebpackPlugin.IMonacoEditorWebpackPluginOpts = {}) { constructor(options: MonacoEditorWebpackPlugin.IMonacoEditorWebpackPluginOpts = {}) {
const monacoEditorPath = options.monacoEditorPath; const monacoEditorPath = options.monacoEditorPath;
@ -172,6 +168,9 @@ class MonacoEditorWebpackPlugin implements webpack.WebpackPluginInstance {
publicPath: options.publicPath || '', publicPath: options.publicPath || '',
globalAPI: options.globalAPI || false globalAPI: options.globalAPI || false
}; };
this.workerTaskActions = {};
this.workersTask = Promise.resolve([]);
this.workerPathsMap = {};
} }
apply(compiler: webpack.Compiler): void { 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( const rules = createLoaderRules(
languages, languages,
features, features,
workers,
filename,
monacoEditorPath, monacoEditorPath,
publicPath, publicPath,
compilationPublicPath, compilationPublicPath,
globalAPI globalAPI,
this
); );
const plugins = createPlugins(compiler, workers, filename, monacoEditorPath);
addCompilerRules(compiler, rules); addCompilerRules(compiler, rules);
addCompilerPlugins(compiler, plugins); addCompilerPlugins(compiler, plugins);
compiler.hooks.thisCompilation.tap(AddWorkerEntryPointPlugin.name, () => {
const tasks: Promise<void>[] = [];
Object.keys(this.workerPathsMap).forEach((workerKey) => {
const task = new Promise<void>((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 ''; 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( function createLoaderRules(
languages: IFeatureDefinition[], languages: IFeatureDefinition[],
features: IFeatureDefinition[], features: IFeatureDefinition[],
workers: ILabeledWorkerDefinition[],
filename: string,
monacoEditorPath: string | undefined, monacoEditorPath: string | undefined,
pluginPublicPath: string, pluginPublicPath: string,
compilationPublicPath: string, compilationPublicPath: string,
globalAPI: boolean globalAPI: boolean,
pluginInstance: MonacoEditorWebpackPlugin
): webpack.RuleSetRule[] { ): webpack.RuleSetRule[] {
if (!languages.length && !features.length) { if (!languages.length && !features.length) {
return []; return [];
} }
const languagePaths = flatArr(coalesce(languages.map((language) => language.entry))); const languagePaths = flatArr(coalesce(languages.map((language) => language.entry)));
const featurePaths = flatArr(coalesce(features.map((feature) => feature.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: // Determine the public path from which to load worker JS files. In order of precedence:
// 1. Plugin-specific public path. // 1. Plugin-specific public path.
@ -279,35 +296,10 @@ function createLoaderRules(
`? __webpack_public_path__ ` + `? __webpack_public_path__ ` +
`: ${JSON.stringify(compilationPublicPath)}`; `: ${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 = { const options: ILoaderOptions = {
globals, pluginInstance,
publicPath: pathPrefix,
globalAPI,
pre: featurePaths.map((importPath) => resolveMonacoPath(importPath, monacoEditorPath)), pre: featurePaths.map((importPath) => resolveMonacoPath(importPath, monacoEditorPath)),
post: languagePaths.map((importPath) => resolveMonacoPath(importPath, monacoEditorPath)) post: languagePaths.map((importPath) => resolveMonacoPath(importPath, monacoEditorPath))
}; };
@ -328,18 +320,21 @@ function createPlugins(
compiler: webpack.Compiler, compiler: webpack.Compiler,
workers: ILabeledWorkerDefinition[], workers: ILabeledWorkerDefinition[],
filename: string, filename: string,
monacoEditorPath: string | undefined monacoEditorPath: string | undefined,
pluginInstance: MonacoEditorWebpackPlugin
): AddWorkerEntryPointPlugin[] { ): AddWorkerEntryPointPlugin[] {
const webpack = compiler.webpack ?? require('webpack'); const webpack = compiler.webpack ?? require('webpack');
return (<AddWorkerEntryPointPlugin[]>[]).concat( return (<AddWorkerEntryPointPlugin[]>[]).concat(
workers.map( workers.map(
({ id, entry }) => ({ id, entry, label }) =>
new AddWorkerEntryPointPlugin({ new AddWorkerEntryPointPlugin({
id, id,
entry: resolveMonacoPath(entry, monacoEditorPath), entry: resolveMonacoPath(entry, monacoEditorPath),
filename: getWorkerFilename(filename, entry, monacoEditorPath), label,
plugins: [new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })] filename,
plugins: [new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })],
pluginInstance
}) })
) )
); );

View file

@ -1,8 +1,11 @@
import type { PitchLoaderDefinitionFunction } from 'webpack'; import type { PitchLoaderDefinitionFunction } from 'webpack';
import * as loaderUtils from 'loader-utils'; import * as loaderUtils from 'loader-utils';
import type * as MonacoEditorWebpackPlugin from '../index';
export interface ILoaderOptions { export interface ILoaderOptions {
globals?: { [key: string]: string }; pluginInstance: MonacoEditorWebpackPlugin;
publicPath: string;
globalAPI: boolean;
pre?: string[]; pre?: string[];
post?: string[]; post?: string[];
} }
@ -10,7 +13,16 @@ export interface ILoaderOptions {
export const pitch: PitchLoaderDefinitionFunction<ILoaderOptions> = function pitch( export const pitch: PitchLoaderDefinitionFunction<ILoaderOptions> = function pitch(
remainingRequest 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 // 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 // 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<ILoaderOptions> = function pit
return loaderUtils.stringifyRequest(this, request); return loaderUtils.stringifyRequest(this, request);
}; };
return [ const getContent = () => {
...(globals const globals: Record<string, string> = {
? Object.keys(globals).map((key) => `self[${JSON.stringify(key)}] = ${globals[key]};`) MonacoEnvironment: `(function (paths) {
: []), function stripTrailingSlash(str) {
...pre.map((include: any) => `import ${stringifyRequest(include)};`), 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}`)}; import * as monaco from ${stringifyRequest(`!!${remainingRequest}`)};
export * from ${stringifyRequest(`!!${remainingRequest}`)}; export * from ${stringifyRequest(`!!${remainingRequest}`)};
export default monaco; export default monaco;
`, `,
...post.map((include: any) => `import ${stringifyRequest(include)};`) ...post.map((include: any) => `import ${stringifyRequest(include)};`)
].join('\n'); ].join('\n');
};
pluginInstance.workersTask.then(
() => {
callback(null, getContent());
},
(err) => {
callback(err, getContent());
}
);
}; };

View file

@ -1,23 +1,31 @@
import type * as webpack from 'webpack'; import type * as webpack from 'webpack';
import type * as MonacoEditorWebpackPlugin from '../index';
export interface IAddWorkerEntryPointPluginOptions { export interface IAddWorkerEntryPointPluginOptions {
id: string; id: string;
label: string;
entry: string; entry: string;
filename: string; filename: string;
chunkFilename?: string; chunkFilename?: string;
plugins: webpack.WebpackPluginInstance[]; plugins: webpack.WebpackPluginInstance[];
pluginInstance: MonacoEditorWebpackPlugin;
} }
function getCompilerHook( export function getCompilerHook(
compiler: webpack.Compiler, compiler: webpack.Compiler,
{ id, entry, filename, chunkFilename, plugins }: IAddWorkerEntryPointPluginOptions {
id,
entry,
label,
filename,
chunkFilename,
plugins,
pluginInstance
}: IAddWorkerEntryPointPluginOptions
) { ) {
const webpack = compiler.webpack ?? require('webpack'); const webpack = compiler.webpack ?? require('webpack');
return function ( return function (compilation: webpack.Compilation) {
compilation: webpack.Compilation,
callback: (error?: Error | null | false) => void
) {
const outputOptions = { const outputOptions = {
filename, filename,
chunkFilename, chunkFilename,
@ -29,11 +37,38 @@ function getCompilerHook(
new webpack.webworker.WebWorkerTemplatePlugin(), new webpack.webworker.WebWorkerTemplatePlugin(),
new webpack.LoaderTargetPlugin('webworker') new webpack.LoaderTargetPlugin('webworker')
]); ]);
childCompiler.hooks.compilation.tap(AddWorkerEntryPointPlugin.name, (childCompilation) => {
childCompilation.hooks.afterOptimizeAssets.tap(AddWorkerEntryPointPlugin.name, (_assets) => {
// one entry may generate many assetswe can not distinguish which is the entry bundleso 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; 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)); 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({ constructor({
id, id,
label,
entry, entry,
filename, filename,
chunkFilename = undefined, chunkFilename = undefined,
plugins plugins,
pluginInstance
}: IAddWorkerEntryPointPluginOptions) { }: IAddWorkerEntryPointPluginOptions) {
this.options = { id, entry, filename, chunkFilename, plugins }; this.options = {
id,
label,
entry,
filename,
chunkFilename,
plugins,
pluginInstance
};
} }
apply(compiler: webpack.Compiler) { apply(compiler: webpack.Compiler) {
@ -57,7 +102,7 @@ export class AddWorkerEntryPointPlugin implements webpack.WebpackPluginInstance
if (parseInt(majorVersion) < 4) { if (parseInt(majorVersion) < 4) {
(<any>compiler).plugin('make', compilerHook); (<any>compiler).plugin('make', compilerHook);
} else { } else {
compiler.hooks.make.tapAsync('AddWorkerEntryPointPlugin', compilerHook); compiler.hooks.make.tap(AddWorkerEntryPointPlugin.name, compilerHook);
} }
} }
} }