Skip to content

Commit b26a37f

Browse files
feat: add resolvedBy field to ResolvedId (#4789)
* feat: add resolveBy field * test: fix previous tests * chore: tweak * chore: replace resolveBy to resolvedBy * feat: add hookFirstAndGetPlugin to PluginDriver * chore: tweak test * docs: add documentation for resolvedBy * docs: update documentation for resolvedBy * Improve docs and slightly streamline code Co-authored-by: Lukas Taegert-Atkinson <[email protected]> Co-authored-by: Lukas Taegert-Atkinson <[email protected]>
1 parent ade0ee3 commit b26a37f

26 files changed

Lines changed: 164 additions & 17 deletions

File tree

browser/src/resolveId.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export async function resolveId(
2626
assertions
2727
);
2828
if (pluginResult == null) {
29-
throwNoFileSystem('path.resolve');
29+
return throwNoFileSystem('path.resolve')();
3030
}
31-
return pluginResult;
31+
return pluginResult[0];
3232
}

docs/05-plugin-development.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ Note that the return value of this hook will not be passed to `resolveId` afterw
206206

207207
#### `resolveId`
208208

209-
**Type:** `(source: string, importer: string | undefined, options: {isEntry: boolean, assertions: {[key: string]: string}, custom?: {[plugin: string]: any}}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null}`<br> **Kind:** `async, first`<br> **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/#thisemitfile) to emit an entry point or at any time by calling [`this.resolve`](guide/en/#thisresolve) to manually resolve an id.<br> **Next Hook:** [`load`](guide/en/#load) if the resolved id has not yet been loaded, otherwise [`buildEnd`](guide/en/#buildend).
209+
**Type:** `(source: string, importer: string | undefined, options: {isEntry: boolean, assertions: {[key: string]: string}, custom?: {[plugin: string]: any}}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", assertions?: {[key: string]: string} | null, meta?: {[plugin: string]: any} | null, moduleSideEffects?: boolean | "no-treeshake" | null, resolvedBy?: string, syntheticNamedExports?: boolean | string | null}`<br> **Kind:** `async, first`<br> **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/#thisemitfile) to emit an entry point or at any time by calling [`this.resolve`](guide/en/#thisresolve) to manually resolve an id.<br> **Next Hook:** [`load`](guide/en/#load) if the resolved id has not yet been loaded, otherwise [`buildEnd`](guide/en/#buildend).
210210

211211
Defines a custom resolver. A resolver can be useful for e.g. locating third-party dependencies. Here `source` is the importee exactly as it is written in the import statement, i.e. for
212212

@@ -312,6 +312,8 @@ If `external` is `true`, then absolute ids will be converted to relative ids bas
312312

313313
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.
314314

315+
`resolvedBy` can be explicitly declared in the returned object. It will replace the corresponding field returned by [`this.resolve`](guide/en/#thisresolve).
316+
315317
If you return a value for `assertions` for an external module, this will determine how imports of this module will be rendered when generating `"es"` output. E.g. `{id: "foo", external: true, assertions: {type: "json"}}` will cause imports of this module appear as `import "foo" assert {type: "json"}`. If you do not pass a value, the value of the `assertions` input parameter will be used. Pass an empty object to remove any assertions. While `assertions` do not influence rendering for bundled modules, they still need to be consistent across all imports of a module, otherwise a warning is emitted. The `load` and `transform` hooks can override this.
316318

317319
See [synthetic named exports](guide/en/#synthetic-named-exports) for the effect of the `syntheticNamedExports` option. If `null` is returned or the flag is omitted, then `syntheticNamedExports` will default to `false`. The `load` and `transform` hooks can override this.
@@ -809,6 +811,7 @@ type ResolvedId = {
809811
assertions: { [key: string]: string }; // import assertions for this import
810812
meta: { [plugin: string]: any }; // custom module meta-data when resolving the module
811813
moduleSideEffects: boolean | 'no-treeshake'; // are side effects of the module observed, is tree-shaking enabled
814+
resolvedBy: string; // which plugin resolved this module, "rollup" if resolved by Rollup itself
812815
syntheticNamedExports: boolean | string; // does the module allow importing non-existing named exports
813816
};
814817
```
@@ -975,7 +978,7 @@ Use Rollup's internal acorn instance to parse code to an AST.
975978
976979
#### `this.resolve`
977980
978-
**Type:** `(source: string, importer?: string, options?: {skipSelf?: boolean, isEntry?: boolean, assertions?: {[key: string]: string}, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", assertions: {[key: string]: string}, meta: {[plugin: string]: any} | null, moduleSideEffects: boolean | "no-treeshake", syntheticNamedExports: boolean | string>`
981+
**Type:** `(source: string, importer?: string, options?: {skipSelf?: boolean, isEntry?: boolean, assertions?: {[key: string]: string}, custom?: {[plugin: string]: any}}) => Promise<{id: string, external: boolean | "absolute", assertions: {[key: string]: string}, meta: {[plugin: string]: any} | null, moduleSideEffects: boolean | "no-treeshake", resolvedBy: string, syntheticNamedExports: boolean | string>`
979982
980983
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`.
981984
@@ -989,6 +992,8 @@ If you pass an object for `assertions`, it will simulate resolving an import wit
989992
990993
When calling this function from a `resolveId` hook, you should always check if it makes sense for you to pass along the `isEntry`, `custom` and `assertions` options.
991994
995+
The value of `resolvedBy` refers to which plugin resolved this source. If it was resolved by Rollup itself, the value will be "rollup". If a `resolveId` hook in a plugin resolves this source, the value will be the name of the plugin unless it returned an explicit value for `resolvedBy`. This flag is only for debugging and documentation purposes and is not processed further by Rollup.
996+
992997
#### `this.setAssetSource`
993998
994999
**Type:** `(referenceId: string, source: string | Uint8Array) => void`

src/ModuleLoader.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export type ModuleLoaderResolveId = (
5656
type NormalizedResolveIdWithoutDefaults = Partial<PartialNull<ModuleOptions>> & {
5757
external?: boolean | 'absolute';
5858
id: string;
59+
resolvedBy?: string;
5960
};
6061

6162
type ResolveStaticDependencyPromise = Promise<readonly [source: string, resolvedId: ResolvedId]>;
@@ -586,6 +587,7 @@ export class ModuleLoader {
586587
meta: resolvedId.meta || {},
587588
moduleSideEffects:
588589
resolvedId.moduleSideEffects ?? this.hasModuleSideEffects(resolvedId.id, !!external),
590+
resolvedBy: resolvedId.resolvedBy ?? 'rollup',
589591
syntheticNamedExports: resolvedId.syntheticNamedExports ?? false
590592
};
591593
}
@@ -625,6 +627,7 @@ export class ModuleLoader {
625627
id: source,
626628
meta: {},
627629
moduleSideEffects: this.hasModuleSideEffects(source, true),
630+
resolvedBy: 'rollup',
628631
syntheticNamedExports: false
629632
};
630633
} else if (resolvedId.external && resolvedId.syntheticNamedExports) {

src/rollup/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export interface PluginContextMeta {
216216
export interface ResolvedId extends ModuleOptions {
217217
external: boolean | 'absolute';
218218
id: string;
219+
resolvedBy: string;
219220
}
220221

221222
export interface ResolvedIdMap {
@@ -225,10 +226,13 @@ export interface ResolvedIdMap {
225226
interface PartialResolvedId extends Partial<PartialNull<ModuleOptions>> {
226227
external?: boolean | 'absolute' | 'relative';
227228
id: string;
229+
resolvedBy?: string;
228230
}
229231

230232
export type ResolveIdResult = string | NullValue | false | PartialResolvedId;
231233

234+
export type ResolveIdResultWithoutNullValue = string | false | PartialResolvedId;
235+
232236
export type ResolveIdHook = (
233237
this: PluginContext,
234238
source: string,

src/utils/PluginDriver.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,24 @@ export class PluginDriver {
133133
replaceContext?: ReplaceContext | null,
134134
skipped?: ReadonlySet<Plugin> | null
135135
): Promise<ReturnType<FunctionPluginHooks[H]> | null> {
136-
let promise: Promise<ReturnType<FunctionPluginHooks[H]> | null> = Promise.resolve(null);
136+
return this.hookFirstAndGetPlugin(hookName, parameters, replaceContext, skipped).then(
137+
result => result && result[0]
138+
);
139+
}
140+
141+
// chains, first non-null result stops and returns result and last plugin
142+
async hookFirstAndGetPlugin<H extends AsyncPluginHooks & FirstPluginHooks>(
143+
hookName: H,
144+
parameters: Parameters<FunctionPluginHooks[H]>,
145+
replaceContext?: ReplaceContext | null,
146+
skipped?: ReadonlySet<Plugin> | null
147+
): Promise<[NonNullable<ReturnType<FunctionPluginHooks[H]>>, Plugin] | null> {
137148
for (const plugin of this.getSortedPlugins(hookName)) {
138-
if (skipped && skipped.has(plugin)) continue;
139-
promise = promise.then(result => {
140-
if (result != null) return result;
141-
return this.runHook(hookName, parameters, plugin, replaceContext);
142-
});
149+
if (skipped?.has(plugin)) continue;
150+
const result = await this.runHook(hookName, parameters, plugin, replaceContext);
151+
if (result != null) return [result, plugin];
143152
}
144-
return promise;
153+
return null;
145154
}
146155

147156
// chains synchronously, first non-null result stops and returns

src/utils/resolveId.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,23 @@ export async function resolveId(
2626
isEntry,
2727
assertions
2828
);
29-
if (pluginResult != null) return pluginResult;
29+
30+
if (pluginResult != null) {
31+
const [resolveIdResult, plugin] = pluginResult;
32+
if (typeof resolveIdResult === 'object' && !resolveIdResult.resolvedBy) {
33+
return {
34+
...resolveIdResult,
35+
resolvedBy: plugin.name
36+
};
37+
}
38+
if (typeof resolveIdResult === 'string') {
39+
return {
40+
id: resolveIdResult,
41+
resolvedBy: plugin.name
42+
};
43+
}
44+
return resolveIdResult;
45+
}
3046

3147
// external modules (non-entry modules that start with neither '.' or '/')
3248
// are skipped at this stage.

src/utils/resolveIdViaPlugins.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function resolveIdViaPlugins(
1212
customOptions: CustomPluginOptions | undefined,
1313
isEntry: boolean,
1414
assertions: Record<string, string>
15-
): Promise<ResolveIdResult> {
15+
): Promise<[NonNullable<ResolveIdResult>, Plugin] | null> {
1616
let skipped: Set<Plugin> | null = null;
1717
let replaceContext: ReplaceContext | null = null;
1818
if (skip) {
@@ -35,7 +35,7 @@ export function resolveIdViaPlugins(
3535
)
3636
});
3737
}
38-
return pluginDriver.hookFirst(
38+
return pluginDriver.hookFirstAndGetPlugin(
3939
'resolveId',
4040
[source, importer, { assertions, custom: customOptions, isEntry }],
4141
replaceContext,

test/browser/samples/missing-dependency-resolution/_config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ console.log(foo);`
99
})
1010
},
1111
error: {
12+
code: 'NO_FS_IN_BROWSER',
1213
message:
13-
'Unexpected warnings (UNRESOLVED_IMPORT): "dep" is imported by "main", but could not be resolved – treating it as an external dependency.\nIf you expect warnings, list their codes in config.expectedWarnings',
14+
'Cannot access the file system (via "path.resolve") when using the browser build of Rollup. Make sure you supply a plugin with custom resolveId and load hooks to Rollup.',
15+
url: 'https://rollupjs.org/guide/en/#a-simple-example',
1416
watchFiles: ['main']
1517
}
1618
};
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
module.exports = {
22
description: 'fails if an entry cannot be resolved',
33
error: {
4-
code: 'UNRESOLVED_ENTRY',
5-
message: 'Could not resolve entry module "main".'
4+
code: 'NO_FS_IN_BROWSER',
5+
message:
6+
'Cannot access the file system (via "path.resolve") when using the browser build of Rollup. Make sure you supply a plugin with custom resolveId and load hooks to Rollup.',
7+
url: 'https://rollupjs.org/guide/en/#a-simple-example'
68
}
79
};

test/chunking-form/samples/implicit-dependencies/implicitly-dependent-emitted-entry/_config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ module.exports = {
9090
id: ID_LIB,
9191
meta: {},
9292
moduleSideEffects: true,
93+
resolvedBy: 'rollup',
9394
syntheticNamedExports: false
9495
}
9596
],
@@ -167,6 +168,7 @@ module.exports = {
167168
id: ID_LIB,
168169
meta: {},
169170
moduleSideEffects: true,
171+
resolvedBy: 'rollup',
170172
syntheticNamedExports: false
171173
}
172174
],

0 commit comments

Comments
 (0)