Skip to content

Commit

Permalink
[v3.0] Rework file name patterns when preserving modules (#4565)
Browse files Browse the repository at this point in the history
* Put preserveModules path into Chunk name

* Use regular entryFileNames logic

* Clarify documentation for `preserveModules`

* Improve coverage

* Improve wording in docs
  • Loading branch information
lukastaegert committed Jul 15, 2022
1 parent 088eca6 commit 7ab6e21
Show file tree
Hide file tree
Showing 131 changed files with 485 additions and 253 deletions.
37 changes: 29 additions & 8 deletions docs/999-big-list-of-options.md
Expand Up @@ -91,6 +91,31 @@ export default {
};
```
If you want to convert a set of file to another format while maintaining the file structure and export signatures, the recommended way—instead of using [`output.preserveModules`](guide/en/#outputpreservemodules) that may tree-shake exports as well as emit virtual files created by plugins—is to turn every file into an entry point. You can do so dynamically e.g. via the `glob` package:
```js
import glob from 'glob';
import path from 'path';
import { fileURLToPath } from 'url';

export default {
input: Object.fromEntries(
glob.sync('src/**/*.js').map(file => [
// This remove `src/` as well as the file extension from each file, so e.g.
// src/nested/foo.js becomes nested/foo
path.relative('src', file.slice(0, file.length - path.extname(file).length)),
// This expands the relative paths to absolute paths, so e.g.
// src/nested/foo becomes /project/src/nested/foo.js
fileURLToPath(new URL(file, import.meta.url))
])
),
output: {
format: 'es',
dir: 'dist'
}
};
```
The option can be omitted if some plugin emits at least one chunk (using [`this.emitFile`](guide/en/#thisemitfile)) by the end of the [`buildStart`](guide/en/#buildstart) hook.
When using the command line interface, multiple inputs can be provided by using the option multiple times. When provided as the first options, it is equivalent to not prefix them with `--input`:
Expand Down Expand Up @@ -451,13 +476,7 @@ The pattern to use for chunks created from entry points, or a function that is c
Forward slashes `/` can be used to place files in sub-directories. When using a function, `chunkInfo` is a reduced version of the one in [`generateBundle`](guide/en/#generatebundle) without properties that depend on file names and no information about the rendered modules as rendering only happens after file names have been generated. You can however access a list of included `moduleIds`. See also [`output.assetFileNames`](guide/en/#outputassetfilenames), [`output.chunkFileNames`](guide/en/#outputchunkfilenames).
This pattern will also be used when setting the [`output.preserveModules`](guide/en/#outputpreservemodules) option. Here a different set of placeholders is available, though:
- `[format]`: The rendering format defined in the output options.
- `[name]`: The file name (without extension) of the file.
- `[ext]`: The extension of the file.
- `[extname]`: The extension of the file, prefixed by `.` if it is not empty.
- `[assetExtname]`: The extension of the file, prefixed by `.` if it is not empty and it is not one of `js`, `jsx`, `ts` or `tsx`.
This pattern will also be used for every file when setting the [`output.preserveModules`](guide/en/#outputpreservemodules) option. Note that in this case, `[name]` will include the relative path from the output root and possibly the original file extension if it was not one of `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, or `.cts`.
#### output.extend
Expand Down Expand Up @@ -1007,7 +1026,9 @@ define(['https://d3js.org/d3.v4.min'], function (d3) {
Type: `boolean`<br> CLI: `--preserveModules`/`--no-preserveModules`<br> Default: `false`
Instead of creating as few chunks as possible, this mode will create separate chunks for all modules using the original module names as file names. Requires the [`output.dir`](guide/en/#outputdir) option. Tree-shaking will still be applied, suppressing files that are not used by the provided entry points or do not have side effects when executed. This mode can be used to transform a file structure to a different module format.
Instead of creating as few chunks as possible, this mode will create separate chunks for all modules using the original module names as file names. Requires the [`output.dir`](guide/en/#outputdir) option. Tree-shaking will still be applied, suppressing files that are not used by the provided entry points or do not have side effects when executed and removing unused exports of files that are not entry points. On the other hand, if plugins (like `@rollup/plugin-commonjs`) emit additional "virtual" files to achieve certain results, those files will be emitted as actual files using a pattern `_virtual/fileName.js`.
It is therefore not recommended to blindly use this option to transform an entire file structure to another format if you directly want to import from those files as expected exports may be missing. In that case, you should rather designate all files explicitly as entry points by adding them to the [`input` option object](guide/en/#input), see the example there for how to do that.
Note that when transforming to `cjs` or `amd` format, each file will by default be treated as an entry point with [`output.exports`](guide/en/#outputexports) set to `auto`. This means that e.g. for `cjs`, a file that only contains a default export will be rendered as
Expand Down
1 change: 0 additions & 1 deletion rollup.config.ts
Expand Up @@ -97,7 +97,6 @@ export default async function (
chunkFileNames: 'shared/[name].js',
dir: 'dist',
entryFileNames: '[name]',
// TODO Only loadConfigFile is using default exports mode; this should be changed in Rollup@3
exports: 'auto',
externalLiveBindings: false,
format: 'cjs',
Expand Down
1 change: 0 additions & 1 deletion src/Bundle.ts
Expand Up @@ -44,7 +44,6 @@ export default class Bundle {
const outputBundle: OutputBundleWithPlaceholders = Object.create(null);
this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions);

// TODO Lukas rethink time measuring points
try {
await this.pluginDriver.hookParallel('renderStart', [this.outputOptions, this.inputOptions]);

Expand Down
95 changes: 40 additions & 55 deletions src/Chunk.ts
Expand Up @@ -51,7 +51,7 @@ import {
isDefaultAProperty,
namespaceInteropHelpersByInteropType
} from './utils/interopHelpers';
import { basename, dirname, extname, isAbsolute, normalize } from './utils/path';
import { basename, extname, isAbsolute } from './utils/path';
import relativeId, { getAliasName, getImportPath } from './utils/relativeId';
import type { RenderOptions } from './utils/renderHelpers';
import { makeUnique, renderNamePattern } from './utils/renderNamePattern';
Expand Down Expand Up @@ -125,7 +125,7 @@ interface FacadeName {

type RenderedDependencies = Map<Chunk | ExternalChunk, ChunkDependency>;

const NON_ASSET_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx'];
const NON_ASSET_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts'];

function getGlobalName(
chunk: ExternalChunk,
Expand Down Expand Up @@ -402,7 +402,11 @@ export default class Chunk {
if (module.preserveSignature) {
this.strictFacade = needsStrictFacade;
}
this.assignFacadeName(requiredFacades.shift()!, module);
this.assignFacadeName(
requiredFacades.shift()!,
module,
this.outputOptions.preserveModules
);
}
}

Expand Down Expand Up @@ -504,13 +508,11 @@ export default class Chunk {
const { chunkFileNames, entryFileNames, file, format, preserveModules } = this.outputOptions;
if (file) {
fileName = basename(file);
} else if (preserveModules) {
fileName = this.generateIdPreserveModules();
} else if (this.fileName !== null) {
fileName = this.fileName;
} else {
const [pattern, patternName] =
this.facadeModule && this.facadeModule.isUserDefinedEntryPoint
preserveModules || this.facadeModule?.isUserDefinedEntryPoint
? [entryFileNames, 'output.entryFileNames']
: [chunkFileNames, 'output.chunkFileNames'];
fileName = renderNamePattern(
Expand Down Expand Up @@ -605,8 +607,6 @@ export default class Chunk {
renderedExports.length !== 0 ||
renderedDependencies.some(dep => (dep.reexports && dep.reexports.length !== 0)!);

// TODO Lukas Note: Mention in docs, that users/plugins are responsible to do their own caching
// TODO Lukas adapt plugin hook graphs and order in docs
const { intro, outro, banner, footer } = await createAddons(
outputOptions,
pluginDriver,
Expand Down Expand Up @@ -664,12 +664,19 @@ export default class Chunk {
}
}

private assignFacadeName({ fileName, name }: FacadeName, facadedModule: Module): void {
private assignFacadeName(
{ fileName, name }: FacadeName,
facadedModule: Module,
preservePath?: boolean
): void {
if (fileName) {
this.fileName = fileName;
} else {
this.name = this.outputOptions.sanitizeFileName(
name || getChunkNameFromModule(facadedModule)
name ||
(preservePath
? this.getPreserveModulesChunkNameFromModule(facadedModule)
: getChunkNameFromModule(facadedModule))
);
}
}
Expand Down Expand Up @@ -727,48 +734,6 @@ export default class Chunk {
}
}

private generateIdPreserveModules(): string {
const [{ id }] = this.orderedModules;
const { entryFileNames, format, preserveModulesRoot, sanitizeFileName } = this.outputOptions;
const sanitizedId = sanitizeFileName(id.split(QUERY_HASH_REGEX, 1)[0]);
let path: string;

const patternOpt = this.unsetOptions.has('entryFileNames')
? '[name][assetExtname].js'
: entryFileNames;
const pattern =
typeof patternOpt === 'function' ? patternOpt(this.getPreRenderedChunkInfo()) : patternOpt;

if (isAbsolute(sanitizedId)) {
const currentDir = dirname(sanitizedId);
const extension = extname(sanitizedId);
const fileName = renderNamePattern(pattern, 'output.entryFileNames', {
assetExtname: () => (NON_ASSET_EXTENSIONS.includes(extension) ? '' : extension),
ext: () => extension.substring(1),
extname: () => extension,
format: () => format as string,
name: () => this.getChunkName()
});
const currentPath = `${currentDir}/${fileName}`;
if (preserveModulesRoot && currentPath.startsWith(preserveModulesRoot)) {
path = currentPath.slice(preserveModulesRoot.length).replace(/^[\\/]/, '');
} else {
path = relative(this.inputBase, currentPath);
}
} else {
const extension = extname(sanitizedId);
const fileName = renderNamePattern(pattern, 'output.entryFileNames', {
assetExtname: () => (NON_ASSET_EXTENSIONS.includes(extension) ? '' : extension),
ext: () => extension.substring(1),
extname: () => extension,
format: () => format as string,
name: () => getAliasName(sanitizedId)
});
path = `_virtual/${fileName}`;
}
return makeUnique(normalize(path), this.bundle);
}

private generateVariableName(): string {
if (this.manualChunkAlias) {
return this.manualChunkAlias;
Expand Down Expand Up @@ -978,6 +943,24 @@ export default class Chunk {
});
}

private getPreserveModulesChunkNameFromModule(module: Module): string {
const predefinedChunkName = getPredefinedChunkNameFromModule(module);
if (predefinedChunkName) return predefinedChunkName;
const { preserveModulesRoot, sanitizeFileName } = this.outputOptions;
const sanitizedId = sanitizeFileName(module.id.split(QUERY_HASH_REGEX, 1)[0]);
const extName = extname(sanitizedId);
const idWithoutExtension = NON_ASSET_EXTENSIONS.includes(extName)
? sanitizedId.slice(0, -extName.length)
: sanitizedId;
if (isAbsolute(idWithoutExtension)) {
return preserveModulesRoot && idWithoutExtension.startsWith(preserveModulesRoot)
? idWithoutExtension.slice(preserveModulesRoot.length).replace(/^[\\/]/, '')
: relative(this.inputBase, idWithoutExtension);
} else {
return `_virtual/${basename(idWithoutExtension)}`;
}
}

private getReexportSpecifiers(): Map<Chunk | ExternalChunk, ReexportSpecifier[]> {
const { externalLiveBindings, interop } = this.outputOptions;
const reexportSpecifiers = new Map<Chunk | ExternalChunk, ReexportSpecifier[]>();
Expand Down Expand Up @@ -1363,10 +1346,12 @@ export default class Chunk {
}

function getChunkNameFromModule(module: Module): string {
return getPredefinedChunkNameFromModule(module) ?? getAliasName(module.id);
}

function getPredefinedChunkNameFromModule(module: Module): string {
return (
module.chunkNames.find(({ isUserDefined }) => isUserDefined)?.name ??
module.chunkNames[0]?.name ??
getAliasName(module.id)
module.chunkNames.find(({ isUserDefined }) => isUserDefined)?.name ?? module.chunkNames[0]?.name
);
}

Expand Down
Expand Up @@ -4,7 +4,7 @@ module.exports = {
strictDeprecations: false,
input: 'src/main.ts',
output: {
entryFileNames: 'entry-[name]-[format]-[ext][extname].js'
entryFileNames: '[name]-[format]-[hash].js'
},
preserveModules: true
},
Expand Down

This file was deleted.

@@ -0,0 +1,12 @@
define(['exports', './foo-amd-0f9dc16c', './nested/bar-amd-f038b10c', './nested/baz-amd-d3de4cc0', './no-ext-amd-9d2c6ef6'], (function (exports, foo, bar, baz, noExt) { 'use strict';



exports.foo = foo;
exports.bar = bar;
exports.baz = baz;
exports.noExt = noExt;

Object.defineProperty(exports, '__esModule', { value: true });

}));

This file was deleted.

@@ -0,0 +1,15 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var foo = require('./foo-cjs-0d5774b5.js');
var bar = require('./nested/bar-cjs-abedc91d.js');
var baz = require('./nested/baz-cjs-4a9c02fc.js');
var noExt = require('./no-ext-cjs-500f8f81.js');



exports.foo = foo;
exports.bar = bar;
exports.baz = baz;
exports.noExt = noExt;

This file was deleted.

@@ -0,0 +1,4 @@
export { default as foo } from './foo-es-3585f3eb.js';
export { default as bar } from './nested/bar-es-bd5e2ae1.js';
export { default as baz } from './nested/baz-es-a913ab4d.js';
export { default as noExt } from './no-ext-es-1f34b6e8.js';
@@ -1,4 +1,4 @@
System.register(['./entry-foo-system-ts.ts.js', './nested/entry-bar-system-ts.ts.js', './nested/entry-baz-system-ts.ts.js', './entry-no-ext-system-.js'], (function (exports) {
System.register(['./foo-system-0e2d8e48.js', './nested/bar-system-a72f6c95.js', './nested/baz-system-71d114fd.js', './no-ext-system-0cf938a8.js'], (function (exports) {
'use strict';
return {
setters: [function (module) {
Expand Down
@@ -1,11 +1,28 @@
const assert = require('assert');
const path = require('path');

const expectedNames = new Set([
'nested/a',
'b.str',
'c',
'd',
'e',
'f',
'g',
'h',
'main',
'no-ext'
]);

module.exports = {
description: 'entryFileNames pattern supported in combination with preserveModules',
options: {
input: 'src/main.ts',
input: 'src/main.js',
output: {
entryFileNames: 'entry-[name]-[format]-[ext][extname][assetExtname].js',
entryFileNames({ name }) {
assert.ok(expectedNames.has(name), `Unexpected name ${name}.`);
return '[name]-[format]-[hash].js';
},
preserveModules: true
},
plugins: [
Expand Down
@@ -0,0 +1,7 @@
define((function () { 'use strict';

var b = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

return b;

}));
@@ -1,7 +1,7 @@
define((function () { 'use strict';

var foo = 42;
var c = 42;

return foo;
return c;

}));
@@ -1,7 +1,7 @@
define((function () { 'use strict';

var bar = 'banana';
var d = 42;

return bar;
return d;

}));
@@ -1,7 +1,7 @@
define((function () { 'use strict';

var baz = 'whatever';
var e = 42;

return baz;
return e;

}));

0 comments on commit 7ab6e21

Please sign in to comment.