Deduplicated modules + emitted assets lead to an error
charlag opened this issue · 9 comments
Stacktrace:
Server: Error:,TypeError: Cannot read property 'replace' of undefined,TypeError: Cannot read property 'replace' of undefined
at normalizePathDelimiter (/REDACTED/tutanota-3/node_modules/nollup/lib/impl/NollupCodeGenerator.js:28:15)
at /REDACTED/tutanota-3/node_modules/nollup/lib/impl/NollupCodeGenerator.js:485:28
at Array.find (<anonymous>)
at /REDACTED/tutanota-3/node_modules/nollup/lib/impl/NollupCodeGenerator.js:484:47
at String.replace (<anonymous>)
at NollupCodeGenerator.onGenerateModulePreChunk (/REDACTED/tutanota-3/node_modules/nollup/lib/impl/NollupCodeGenerator.js:483:30)
at /REDACTED/tutanota-3/node_modules/nollup/lib/impl/NollupCompiler.js:451:37
at Array.reduce (<anonymous>)
at Object.compile (/REDACTED/tutanota-3/node_modules/nollup/lib/impl/NollupCompiler.js:447:53)
at runMicrotasks (<anonymous>)
This happens because onGenerateModulePreChunk()
calls normalizePathDelimiter on each file and assets (and possibly some other files) do not have facadeModuleId
. This started happening after a fix for #204 because we optimize away unnecessary chunks.
Changeing onGenerateModulePreChunk()
like this seems to fix the issue:
let foundOutputChunk = bundle.find(b => {
return normalizePathDelimiter(/** @type {RollupOutputChunk} */ (b).facadeModuleId) === inner
});
but I'm not sure if it's correct.
On related note, it seems like it's a very deeply nested loop (find
is a loop inside replace
which is called from inside of reduce()
. Putting things in a map of [normalizePathDelimiter(b.facadeModuleId)]: RollupOutputFile
might speed up things quite a bit.
I tried to do this and it seems to work but I do not see much performance impact (sorry for the messy diff, there's something up with formatting):
--- node_modules/nollup/lib/impl/NollupCodeGenerator.js 1985-10-26 09:15:00.000000000 +0100
+++ nollup-bck/lib/impl/NollupCodeGenerator.js 2021-09-07 16:23:17.043068138 +0200
@@ -6,6 +6,7 @@
const RollupConfigContainer = require('./RollupConfigContainer');
let MagicString = require('magic-string').default;
let AcornParser = require('./AcornParser');
+const {normalizePathDelimiter} = require("./utils")
/**
@@ -24,10 +25,6 @@
return input.substring(start, end).replace(/[^\n\r]/g, ' ');
}
-function normalizePathDelimiter (id) {
- return id.replace(/\\/g, '/');
-}
-
function escapeCode (code) {
// Turning the code into eval statements, so we need
// to escape line breaks and quotes. Using a multiline
@@ -474,16 +471,14 @@
/**
* @param {NollupInternalModule} file
- * @param {RollupOutputFile[]} bundle
+ * @param {Map<string, RollupOutputFile>} facadeModuleIdToModules
* @param {Object<string, NollupInternalModule>} modules
* @return {string}
*/
- onGenerateModulePreChunk (file, bundle, modules) {
+ onGenerateModulePreChunk (file, facadeModuleIdToModules, modules) {
if (file.dynamicImports.length > 0) {
return file.code.replace(/require\.dynamic\((\\)?\'(.*?)(\\)?\'\)/g, (match, escapeLeft, inner, escapeRight) => {
- let foundOutputChunk = bundle.find(b => {
- return normalizePathDelimiter(/** @type {RollupOutputChunk} */ (b).facadeModuleId) === inner
- });
+ let foundOutputChunk = facadeModuleIdToModules.get(inner);
let fileName = foundOutputChunk? foundOutputChunk.fileName : '';
return 'require.dynamic(' + (escapeLeft? '\\' : '') + '\'' + fileName + (escapeRight? '\\' : '') +'\', ' + modules[path.normalize(inner)].index + ')';
diff --color -bur node_modules/nollup/lib/impl/NollupCompiler.js nollup-bck/lib/impl/NollupCompiler.js
--- node_modules/nollup/lib/impl/NollupCompiler.js 1985-10-26 09:15:00.000000000 +0100
+++ nollup-bck/lib/impl/NollupCompiler.js 2021-09-07 16:36:07.956681780 +0200
@@ -1,7 +1,7 @@
// @ts-check
let ImportExportResolver = require('./NollupImportExportResolver');
let ParseError = require('./ParseError');
-let { getNameFromFileName, emitAssetToBundle, formatFileName, yellow } = require('./utils');
+let { getNameFromFileName, emitAssetToBundle, formatFileName, yellow, normalizePathDelimiter } = require('./utils');
let path = require('path');
let NollupContext = require('./NollupContext');
let NollupCodeGenerator = require('./NollupCodeGenerator');
@@ -26,7 +26,7 @@
Object.keys(name_map).forEach(name => {
let entries = name_map[name];
entries.forEach((entry, index) => {
- let name = entry.name + (index > 0? (index + 1) : '');
+ let name = entry.name + (index > 0 ? (index + 1) : '');
if (entry.isEntry && bundleOutputTypes[entry.facadeModuleId] === 'entry') {
if (outputOptions.file) {
@@ -147,7 +147,7 @@
context.currentModuleEmittedChunksCache = emittedChunksCache;
let loaded = await context.plugins.hooks.load(filePath, parentFilePath);
- let transformed = await context.plugins.hooks.transform( loaded.code, filePath);
+ let transformed = await context.plugins.hooks.transform(loaded.code, filePath);
let resolved = await ImportExportResolver(context.plugins, transformed.code, filePath, generator, context.liveBindings);
file.transformed = resolved.code;
@@ -205,7 +205,8 @@
for (let i = 0; i < file.imports.length; i++) {
try {
circularTrace.push(file.imports[i].source);
- await compileModule(context, file.imports[i].source, filePath, depth + 1, emitted, bundleModuleIds, generator, file.imports[i].syntheticNamedExports, false, bundleEmittedAssets, circularTrace);
+ await compileModule(context, file.imports[i].source, filePath, depth
+ + 1, emitted, bundleModuleIds, generator, file.imports[i].syntheticNamedExports, false, bundleEmittedAssets, circularTrace);
circularTrace.pop();
} catch (e) {
throw new ParseError(file.imports[i].source, e);
@@ -226,7 +227,7 @@
* @param {boolean} isEntry
* @return {Promise<Object>}
*/
-async function compileInputTarget (context, filePath, bundleModuleIds, generator, bundleEmittedAssets, isEntry) {
+async function compileInputTarget(context, filePath, bundleModuleIds, generator, bundleEmittedAssets, isEntry) {
let emitted = {
modules: {}, // modules id this input contains
dynamicImports: [], // emitted dynamic ids
@@ -242,7 +243,7 @@
await compileModule(context, filePath, parentFilePath, depth, emitted, bundleModuleIds, generator, false, isEntry, bundleEmittedAssets, [filePath]);
- function hoistDependencies (deps) {
+ function hoistDependencies(deps) {
deps.forEach(d => {
let depFile = context.files[d];
if (!depFile.hoist) {
@@ -273,7 +274,7 @@
* @param {NollupCodeGenerator} generator
* @return {Promise<NollupCompileOutput>}
*/
- async compile (context, generator) {
+ async compile(context, generator) {
context.plugins.start();
let bundle = /** @type {RollupOutputFile[]} */ ([]);
@@ -341,7 +342,7 @@
context.currentPhase = 'build';
for (let i = 0; i < context.input.length; i++) {
- let { name, file } = context.input[i];
+ let {name, file} = context.input[i];
let emitted = await compileInputTarget(context, file, bundleModuleIds, generator, bundleEmittedAssets, true);
bundle.push({
@@ -425,7 +426,7 @@
console.warn([
yellow('(!) Circular dependencies'),
...show,
- hide.length > 0? `...and ${hide.length} more` : '',
+ hide.length > 0 ? `...and ${hide.length} more` : '',
yellow('Code may not run correctly. See https://github.com/PepsRyuu/nollup/blob/master/docs/circular.md')
].join('\n'));
}
@@ -443,18 +444,25 @@
try {
await context.plugins.hooks.renderStart(context.config.output, context.config);
+ const facadeModuleIdToModules = bundle.reduce((map, file) => {
+ if (file.facadeModuleId) {
+ map.set(normalizePathDelimiter(/** @type {RollupOutputChunk} */ file.facadeModuleId), file)
+ }
+ return map
+ }, new Map())
+
// clone files and their code
modules = Object.entries(context.files).reduce((acc, val) => {
- let [ id, file ] = val;
+ let [id, file] = val;
acc[id] = {
index: file.index,
- code: generator.onGenerateModulePreChunk(file, bundle, context.files),
+ code: generator.onGenerateModulePreChunk(file, facadeModuleIdToModules, context.files),
};
return acc;
}, {});
// Rendering hooks
- let [ banner, intro, outro, footer ] = await Promise.all([
+ let [banner, intro, outro, footer] = await Promise.all([
context.plugins.hooks.banner(),
context.plugins.hooks.intro(),
context.plugins.hooks.outro(),
@@ -468,13 +476,15 @@
metaNames.forEach(metaName => {
let resolved = resolveImportMetaProperty(context.plugins, moduleId, metaName, bundleEntry, bundleReferenceIdMap);
modules[moduleId].code = modules[moduleId].code.replace(
- metaName === null? new RegExp('import\\.meta') : new RegExp('import\\.meta\\.' + metaName, 'g'),
+ metaName === null ? new RegExp('import\\.meta') : new RegExp('import\\.meta\\.' + metaName, 'g'),
resolved
);
});
});
- bundleEntry.code = banner + '\n' + intro + '\n' + generator.onGenerateChunk(modules, bundleEntry, context.config.output, context.config) + '\n' + outro + '\n' + footer;
+ bundleEntry.code = banner + '\n' + intro + '\n'
+ + generator.onGenerateChunk(modules, bundleEntry, context.config.output, context.config) + '\n' + outro + '\n'
+ + footer;
await context.plugins.hooks.renderChunk(bundleEntry.code, bundleEntry, context.config.output);
}
@@ -503,7 +513,7 @@
})));
return {
- stats: { time: Date.now() - bundleStartTime },
+ stats: {time: Date.now() - bundleStartTime},
changes: changes,
output: bundle
}
diff --color -bur node_modules/nollup/lib/impl/utils.js nollup-bck/lib/impl/utils.js
--- node_modules/nollup/lib/impl/utils.js 1985-10-26 09:15:00.000000000 +0100
+++ nollup-bck/lib/impl/utils.js 2021-09-07 16:35:25.727720346 +0200
@@ -102,6 +102,9 @@
bundle.push(bundleEntry);
}
+function normalizePathDelimiter (id) {
+ return id.replace(/\\/g, '/');
+}
module.exports = {
@@ -111,5 +114,6 @@
formatFileName,
getNameFromFileName,
emitAssetToBundle,
- findChildNodes
+ findChildNodes,
+ normalizePathDelimiter,
};
\ No newline at end of file
So I understand the fix, but I'm trying to understand how to replicate this as a test case. I'm probably missing something obvious, but how do you trigger a chunk without a facadeModuleId
? What is this optimisation process?
So I understand the fix, but I'm trying to understand how to replicate this as a test case. I'm probably missing something obvious, but how do you trigger a chunk without a
facadeModuleId
? What is this optimisation process?
You can produce such a chunk with
this.emitFile({
type: 'asset',
name,
fileName: name,
source: content,
})
inside a plugin
Ah I'm not filtering out assets when checking. That makes sense! So the test needs to emit an asset first, then emit a chunk to trigger the issue. Will look into this! Much appreciated!
Thank you!
Released in 0.18.4
Thanks! I still think that calling find
in a deeply nested loop is not very performant though.
Probably not the most efficient no, but it can be something addressed as a separate issue outside of this bug, as I think there's lots of room for improvement across the board for performance. Are you finding drastically improved performance with the alternative solution?
You are right.
From my tests I didn't notice much difference but as it is squared complexity it can get bad really quick if you are unlucky. I wanted to take V8 performance profile but didn't find time to do it yet.