diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index 79a39e94951..f9372c3b76d 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -157,7 +157,7 @@ In case a dynamic import is not passed a string as argument, this hook gets acce Note that the return value of this hook will not be passed to `resolveId` afterwards; if you need access to the static resolution algorithm, you can use [`this.resolve(source, importer)`](guide/en/#thisresolvesource-string-importer-string-options-skipself-boolean-custom-plugin-string-any--promiseid-string-external-boolean-modulesideeffects-boolean--no-treeshake-syntheticnamedexports-boolean--string-meta-plugin-string-any--null) on the plugin context. #### `resolveId` -Type: `(source: string, importer: string | undefined, options: {custom?: {[plugin: string]: any}) => string | false | null | {id: string, external?: boolean, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}`
+Type: `(source: string, importer: string | undefined, options: {custom?: {[plugin: string]: any}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}`
Kind: `async, first`
Previous Hook: [`buildStart`](guide/en/#buildstart) if we are resolving an entry point, [`moduleParsed`](guide/en/#moduleparsed) if we are resolving an import, or as fallback for [`resolveDynamicImport`](guide/en/#resolvedynamicimport). Additionally this hook can be triggered during the build phase from plugin hooks by calling [`this.emitFile`](guide/en/#thisemitfileemittedfile-emittedchunk--emittedasset--string) to emit an entry point or at any time by calling [`this.resolve`](guide/en/#thisresolvesource-string-importer-string-options-skipself-boolean-custom-plugin-string-any--promiseid-string-external-boolean-modulesideeffects-boolean--no-treeshake-syntheticnamedexports-boolean--string-meta-plugin-string-any--null) to manually resolve an id.
Next Hook: [`load`](guide/en/#load) if the resolved id that has not yet been loaded, otherwise [`buildEnd`](guide/en/#buildend). @@ -208,7 +208,7 @@ resolveId(source) { } ``` -Relative ids, i.e. starting with `./` or `../`, will **not** be renormalized when returning an object. If you want this behaviour, return an absolute file system location as `id` instead. +If `external` is `true`, then absolute ids will be converted to relative ids based on the user's choice for the [`makeAbsoluteExternalsRelative`](guide/en/#makeabsoluteexternalsrelative) option. This choice can be overridden by passing either `external: "relative"` to always convert an absolute id to a relative id or `external: "absolute"` to keep it as an absolute id. When returning an object, relative external ids, i.e. ids starting with `./` or `../`, will *not* be internally converted to an absolute id and converted back to a relative id in the output, but are instead included in the output unchanged. If you do not want this behaviour, return an absolute file system location as `id` instead and choose `external: "relative"`. If `false` is returned for `moduleSideEffects` in the first hook that resolves a module id and no other module imports anything from this module, then this module will not be included even if the module would have side-effects. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `"no-treeshake"` is returned, treeshaking will be turned off for this module and it will also be included in one of the generated chunks even if it is empty. If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the `treeshake.moduleSideEffects` option or default to `true`. The `load` and `transform` hooks can override this. @@ -695,8 +695,8 @@ An object containing potentially useful Rollup metadata: Use Rollup's internal acorn instance to parse code to an AST. -#### `this.resolve(source: string, importer?: string, options?: {skipSelf?: boolean, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean, moduleSideEffects: boolean | 'no-treeshake', syntheticNamedExports: boolean | string, meta: {[plugin: string]: any}} | null>` -Resolve imports to module ids (i.e. file names) using the same plugins that Rollup uses, and determine if an import should be external. If `null` is returned, the import could not be resolved by Rollup or any plugin but was not explicitly marked as external by the user. +#### `this.resolve(source: string, importer?: string, options?: {skipSelf?: boolean, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", moduleSideEffects: boolean | 'no-treeshake', syntheticNamedExports: boolean | string, meta: {[plugin: string]: any}} | null>` +Resolve imports to module ids (i.e. file names) using the same plugins that Rollup uses, and determine if an import should be external. If `null` is returned, the import could not be resolved by Rollup or any plugin but was not explicitly marked as external by the user. If an absolute external id is returned that should remain absolute in the output either via the [`makeAbsoluteExternalsRelative`](guide/en/#makeabsoluteexternalsrelative) option or by explicit plugin choice in the [`resolveId`](guide/en/#resolveid) hook, `external` will be `"absolute"` instead of `true`. If you pass `skipSelf: true`, then the `resolveId` hook of the plugin from which `this.resolve` is called will be skipped when resolving. When other plugins themselves also call `this.resolve` in their `resolveId` hooks with the *exact same `source` and `importer`* while handling the original `this.resolve` call, then the `resolveId` hook of the original plugin will be skipped for those calls as well. The rationale here is that the plugin already stated that it "does not know" how to resolve this particular combination of `source` and `importer` at this point in time. If you do not want this behaviour, do not use `skipSelf` but implement your own infinite loop prevention mechanism if necessary. diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index fc8785b7320..4e8c7d97432 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -319,6 +319,23 @@ buildWithCache() }) ``` +#### makeAbsoluteExternalsRelative +Type: `boolean | "ifRelativeSource"`
+CLI: `--makeAbsoluteExternalsRelative`/`--no-makeAbsoluteExternalsRelative`
+Default: `true` + +Determines if absolute external paths should be converted to relative paths in the output. This does not only apply to paths that are absolute in the source but also to paths that are resolved to an absolute path by either a plugin or Rollup core. + +For `true`, an external import like `import "/Users/Rollup/project/relative.js"` would be converted to a relative path. When converting an absolute path to a relative path, Rollup does *not* take the `file` or `dir` options into account, because those may not be present e.g. for builds using the JavaScript API. Instead, it assumes that the root of the generated bundle is located at the common shared parent directory of all modules that were included in the bundle. Assuming that the common parent directory of all modules is `"/Users/Rollup/project"`, the import from above would likely be converted to `import "./relative.js"` in the output. If the output chunk is itself nested in a sub-directory by choosing e.g. `chunkFileNames: "chunks/[name].js"`, the import would be `"../relative.js"`. + +As stated before, this would also apply to originally relative imports like `import "./relative.js"` that are resolved to an absolute path before they are marked as external by the [`external`](guide/en/#external) option. + +One common problem is that this mechanism will also apply to imports like `import "/absolute.js'"`, resulting in unexpected relative paths in the output. + +For this case, choosing `"ifRelativeSource"` will check if the original import was a relative import and only then convert it to a relative import in the output. Choosing `false` will keep all paths as absolute paths in the output. + +Note that when a relative path is directly marked as "external" using the [`external`](guide/en/#external) option, then it will be the same relative path in the output. When it is resolved first via a plugin or Rollup core and then marked as external, the above logic will apply. + #### onwarn Type: `(warning: RollupWarning, defaultHandler: (warning: string | RollupWarning) => void) => void;` @@ -360,7 +377,6 @@ export default { }; ``` - #### output.assetFileNames Type: `string | ((assetInfo: AssetInfo) => string)`
CLI: `--assetFileNames `
diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index ed6148899a9..cad73ea8473 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -391,24 +391,12 @@ export class ModuleLoader { } } - /* For plugins when resolveIdResult.id is absolute (otherwise external is true) - external | normalizeExternalPaths | result - true | true | true - true | 'relative' | 'absolute' - true | false | 'absolute' - 'normalize' | true | true - 'normalize' | 'relative' | true - 'normalize' | false | true - 'absolute' | true | 'absolute' - 'absolute' | 'relative' | 'absolute' - 'absolute' | false | 'absolute' - */ private getNormalizedResolvedIdWithoutDefaults( resolveIdResult: ResolveIdResult, importer: string | undefined, source: string ): NormalizedResolveIdWithoutDefaults | null { - const { normalizeExternalPaths } = this.options; + const { makeAbsoluteExternalsRelative } = this.options; if (resolveIdResult) { if (typeof resolveIdResult === 'object') { const external = @@ -417,10 +405,10 @@ export class ModuleLoader { ...resolveIdResult, external: external && - (external === 'normalize' || + (external === 'relative' || !isAbsolute(resolveIdResult.id) || (external === true && - isNotAbsoluteExternal(resolveIdResult.id, source, normalizeExternalPaths)) || + isNotAbsoluteExternal(resolveIdResult.id, source, makeAbsoluteExternalsRelative)) || 'absolute') }; } @@ -429,20 +417,23 @@ export class ModuleLoader { return { external: external && - (isNotAbsoluteExternal(resolveIdResult, source, normalizeExternalPaths) || 'absolute'), + (isNotAbsoluteExternal(resolveIdResult, source, makeAbsoluteExternalsRelative) || + 'absolute'), id: - external && normalizeExternalPaths + external && makeAbsoluteExternalsRelative ? normalizeRelativeExternalId(resolveIdResult, importer) : resolveIdResult }; } - const id = normalizeExternalPaths ? normalizeRelativeExternalId(source, importer) : source; + const id = makeAbsoluteExternalsRelative + ? normalizeRelativeExternalId(source, importer) + : source; if (resolveIdResult !== false && !this.options.external(id, importer, true)) { return null; } return { - external: isNotAbsoluteExternal(id, source, normalizeExternalPaths) || 'absolute', + external: isNotAbsoluteExternal(id, source, makeAbsoluteExternalsRelative) || 'absolute', id }; } @@ -584,11 +575,11 @@ function addChunkNamesToModule( function isNotAbsoluteExternal( id: string, source: string, - normalizeExternalPaths: boolean | 'relative' + makeAbsoluteExternalsRelative: boolean | 'ifRelativeSource' ) { return ( - normalizeExternalPaths === true || - (normalizeExternalPaths === 'relative' && isRelative(source)) || + makeAbsoluteExternalsRelative === true || + (makeAbsoluteExternalsRelative === 'ifRelativeSource' && isRelative(source)) || !isAbsolute(id) ); } diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 0b0c44f9597..b9dda715aa8 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -228,7 +228,7 @@ export interface ResolvedIdMap { } interface PartialResolvedId extends Partial> { - external?: boolean | 'absolute' | 'normalize'; + external?: boolean | 'absolute' | 'relative'; id: string; } @@ -528,10 +528,10 @@ export interface InputOptions { /** @deprecated Use the "inlineDynamicImports" output option instead. */ inlineDynamicImports?: boolean; input?: InputOption; + makeAbsoluteExternalsRelative?: boolean | 'ifRelativeSource'; /** @deprecated Use the "manualChunks" output option instead. */ manualChunks?: ManualChunksOption; moduleContext?: ((id: string) => string | null | undefined) | { [id: string]: string }; - normalizeExternalPaths?: boolean | 'relative'; onwarn?: WarningHandlerWithDefault; perf?: boolean; plugins?: Plugin[]; @@ -555,10 +555,10 @@ export interface NormalizedInputOptions { /** @deprecated Use the "inlineDynamicImports" output option instead. */ inlineDynamicImports: boolean | undefined; input: string[] | { [entryAlias: string]: string }; + makeAbsoluteExternalsRelative: boolean | 'ifRelativeSource'; /** @deprecated Use the "manualChunks" output option instead. */ manualChunks: ManualChunksOption | undefined; moduleContext: (id: string) => string; - normalizeExternalPaths: boolean | 'relative'; onwarn: WarningHandler; perf: boolean; plugins: Plugin[]; diff --git a/src/utils/options/mergeOptions.ts b/src/utils/options/mergeOptions.ts index 4a782442727..822e26c0f8f 100644 --- a/src/utils/options/mergeOptions.ts +++ b/src/utils/options/mergeOptions.ts @@ -106,9 +106,9 @@ function mergeInputOptions( external: getExternal(config, overrides), inlineDynamicImports: getOption('inlineDynamicImports'), input: getOption('input') || [], + makeAbsoluteExternalsRelative: getOption('makeAbsoluteExternalsRelative'), manualChunks: getOption('manualChunks'), moduleContext: getOption('moduleContext'), - normalizeExternalPaths: getOption('normalizeExternalPaths'), onwarn: getOnWarn(config, defaultOnWarnHandler), perf: getOption('perf'), plugins: ensureArray(config.plugins) as Plugin[], diff --git a/src/utils/options/normalizeInputOptions.ts b/src/utils/options/normalizeInputOptions.ts index e6a62786836..b85e848cc74 100644 --- a/src/utils/options/normalizeInputOptions.ts +++ b/src/utils/options/normalizeInputOptions.ts @@ -49,10 +49,10 @@ export function normalizeInputOptions( external: getIdMatcher(config.external as ExternalOption), inlineDynamicImports: getInlineDynamicImports(config, onwarn, strictDeprecations), input: getInput(config), + makeAbsoluteExternalsRelative: + (config.makeAbsoluteExternalsRelative as boolean | 'ifRelativeSource' | undefined) ?? true, manualChunks: getManualChunks(config, onwarn, strictDeprecations), moduleContext: getModuleContext(config, context), - normalizeExternalPaths: - (config.normalizeExternalPaths as boolean | 'relative' | undefined) ?? true, onwarn, perf: (config.perf as boolean | undefined) || false, plugins: ensureArray(config.plugins) as Plugin[], diff --git a/test/form/samples/normalize-external-paths/normalize-false/_config.js b/test/form/samples/normalize-external-paths/normalize-false/_config.js index cc09710af0a..8dcad6c0ed6 100644 --- a/test/form/samples/normalize-external-paths/normalize-false/_config.js +++ b/test/form/samples/normalize-external-paths/normalize-false/_config.js @@ -1,21 +1,35 @@ const path = require('path'); +const assert = require('assert'); + +const ID_MAIN = path.join(__dirname, 'main.js'); module.exports = { description: 'does not normalize external paths when set to false', options: { - normalizeExternalPaths: false, + makeAbsoluteExternalsRelative: false, external(id) { if (['./relativeUnresolved.js', '../relativeUnresolved.js', '/absolute.js'].includes(id)) return true; }, plugins: { + async buildStart() { + const testExternal = async (source, expected) => + assert.deepStrictEqual((await this.resolve(source, ID_MAIN)).external, expected, source); + + await testExternal('./relativeUnresolved.js', true); + await testExternal('/absolute.js', 'absolute'); + await testExternal('./pluginDirect.js', true); + await testExternal('./pluginTrue.js', 'absolute'); + await testExternal('./pluginAbsolute.js', 'absolute'); + await testExternal('./pluginNormalize.js', true); + }, resolveId(source) { if (source.endsWith('/pluginDirect.js')) return false; if (source.endsWith('/pluginTrue.js')) return { id: '/pluginTrue.js', external: true }; if (source.endsWith('/pluginAbsolute.js')) return { id: '/pluginAbsolute.js', external: 'absolute' }; if (source.endsWith('/pluginNormalize.js')) - return { id: path.join(__dirname, 'pluginNormalize.js'), external: 'normalize' }; + return { id: path.join(__dirname, 'pluginNormalize.js'), external: 'relative' }; } } } diff --git a/test/form/samples/normalize-external-paths/normalize-relative/_config.js b/test/form/samples/normalize-external-paths/normalize-relative/_config.js index da7c8fc356c..c5443def0a9 100644 --- a/test/form/samples/normalize-external-paths/normalize-relative/_config.js +++ b/test/form/samples/normalize-external-paths/normalize-relative/_config.js @@ -1,10 +1,13 @@ const path = require('path'); +const assert = require('assert'); + +const ID_MAIN = path.join(__dirname, 'main.js'); module.exports = { description: - 'only normalizes external paths that were originally relative when set to "relative"', + 'only normalizes external paths that were originally relative when set to "ifRelativeSource"', options: { - normalizeExternalPaths: 'relative', + makeAbsoluteExternalsRelative: 'ifRelativeSource', external(id) { if ( [ @@ -18,6 +21,19 @@ module.exports = { return true; }, plugins: { + async buildStart() { + const testExternal = async (source, expected) => + assert.deepStrictEqual((await this.resolve(source, ID_MAIN)).external, expected, source); + + await testExternal('./relativeUnresolved.js', true); + await testExternal('./relativeMissing.js', true); + await testExternal('./relativeExisting.js', true); + await testExternal('/absolute.js', 'absolute'); + await testExternal('./pluginDirect.js', true); + await testExternal('./pluginTrue.js', true); + await testExternal('./pluginAbsolute.js', 'absolute'); + await testExternal('./pluginNormalize.js', true); + }, resolveId(source) { if (source.endsWith('/pluginDirect.js')) return false; if (source.endsWith('/pluginTrue.js')) @@ -25,7 +41,7 @@ module.exports = { if (source.endsWith('/pluginAbsolute.js')) return { id: '/pluginAbsolute.js', external: 'absolute' }; if (source.endsWith('/pluginNormalize.js')) - return { id: path.join(__dirname, 'pluginNormalize.js'), external: 'normalize' }; + return { id: path.join(__dirname, 'pluginNormalize.js'), external: 'relative' }; } } } diff --git a/test/form/samples/normalize-external-paths/normalize-true/_config.js b/test/form/samples/normalize-external-paths/normalize-true/_config.js index 386bde4c372..80349737f6b 100644 --- a/test/form/samples/normalize-external-paths/normalize-true/_config.js +++ b/test/form/samples/normalize-external-paths/normalize-true/_config.js @@ -1,9 +1,12 @@ const path = require('path'); +const assert = require('assert'); + +const ID_MAIN = path.join(__dirname, 'main.js'); module.exports = { description: 'normalizes both relative and absolute external paths when set to true', options: { - normalizeExternalPaths: true, + makeAbsoluteExternalsRelative: true, external(id) { if ( [ @@ -17,6 +20,19 @@ module.exports = { return true; }, plugins: { + async buildStart() { + const testExternal = async (source, expected) => + assert.deepStrictEqual((await this.resolve(source, ID_MAIN)).external, expected, source); + + await testExternal('./relativeUnresolved.js', true); + await testExternal('./relativeMissing.js', true); + await testExternal('./relativeExisting.js', true); + await testExternal('/absolute.js', true); + await testExternal('./pluginDirect.js', true); + await testExternal('./pluginTrue.js', true); + await testExternal('./pluginAbsolute.js', 'absolute'); + await testExternal('./pluginNormalize.js', true); + }, resolveId(source) { if (source.endsWith('/pluginDirect.js')) return false; if (source.endsWith('/pluginTrue.js')) @@ -24,7 +40,7 @@ module.exports = { if (source.endsWith('/pluginAbsolute.js')) return { id: '/pluginAbsolute.js', external: 'absolute' }; if (source.endsWith('/pluginNormalize.js')) - return { id: path.join(__dirname, 'pluginNormalize.js'), external: 'normalize' }; + return { id: path.join(__dirname, 'pluginNormalize.js'), external: 'relative' }; if (source === '/absolute.js') return path.join(__dirname, 'absolute.js'); } } diff --git a/test/function/samples/options-hook/_config.js b/test/function/samples/options-hook/_config.js index 9a1dab37078..8c2289935d3 100644 --- a/test/function/samples/options-hook/_config.js +++ b/test/function/samples/options-hook/_config.js @@ -19,7 +19,7 @@ module.exports = { context: 'undefined', experimentalCacheExpiry: 10, input: ['used'], - normalizeExternalPaths: true, + makeAbsoluteExternalsRelative: true, perf: false, plugins: [ { diff --git a/test/misc/optionList.js b/test/misc/optionList.js index 7eeb54b2025..72c376d9724 100644 --- a/test/misc/optionList.js +++ b/test/misc/optionList.js @@ -1,6 +1,6 @@ exports.input = - 'acorn, acornInjectPlugins, cache, context, experimentalCacheExpiry, external, inlineDynamicImports, input, manualChunks, moduleContext, normalizeExternalPaths, onwarn, perf, plugins, preserveEntrySignatures, preserveModules, preserveSymlinks, shimMissingExports, strictDeprecations, treeshake, watch'; + 'acorn, acornInjectPlugins, cache, context, experimentalCacheExpiry, external, inlineDynamicImports, input, makeAbsoluteExternalsRelative, manualChunks, moduleContext, onwarn, perf, plugins, preserveEntrySignatures, preserveModules, preserveSymlinks, shimMissingExports, strictDeprecations, treeshake, watch'; exports.flags = - 'acorn, acornInjectPlugins, amd, assetFileNames, banner, c, cache, chunkFileNames, compact, config, context, d, dir, dynamicImportFunction, e, entryFileNames, environment, esModule, experimentalCacheExpiry, exports, extend, external, externalLiveBindings, f, failAfterWarnings, file, footer, format, freeze, g, globals, h, hoistTransitiveImports, i, indent, inlineDynamicImports, input, interop, intro, m, manualChunks, minifyInternalExports, moduleContext, n, name, namespaceToStringTag, noConflict, normalizeExternalPaths, o, onwarn, outro, p, paths, perf, plugin, plugins, preferConst, preserveEntrySignatures, preserveModules, preserveModulesRoot, preserveSymlinks, shimMissingExports, silent, sourcemap, sourcemapExcludeSources, sourcemapFile, stdin, strict, strictDeprecations, systemNullSetters, treeshake, v, validate, w, waitForBundleInput, watch'; + 'acorn, acornInjectPlugins, amd, assetFileNames, banner, c, cache, chunkFileNames, compact, config, context, d, dir, dynamicImportFunction, e, entryFileNames, environment, esModule, experimentalCacheExpiry, exports, extend, external, externalLiveBindings, f, failAfterWarnings, file, footer, format, freeze, g, globals, h, hoistTransitiveImports, i, indent, inlineDynamicImports, input, interop, intro, m, makeAbsoluteExternalsRelative, manualChunks, minifyInternalExports, moduleContext, n, name, namespaceToStringTag, noConflict, o, onwarn, outro, p, paths, perf, plugin, plugins, preferConst, preserveEntrySignatures, preserveModules, preserveModulesRoot, preserveSymlinks, shimMissingExports, silent, sourcemap, sourcemapExcludeSources, sourcemapFile, stdin, strict, strictDeprecations, systemNullSetters, treeshake, v, validate, w, waitForBundleInput, watch'; exports.output = 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, entryFileNames, esModule, exports, extend, externalLiveBindings, file, footer, format, freeze, globals, hoistTransitiveImports, indent, inlineDynamicImports, interop, intro, manualChunks, minifyInternalExports, name, namespaceToStringTag, noConflict, outro, paths, plugins, preferConst, preserveModules, preserveModulesRoot, sourcemap, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict, systemNullSetters, validate';