Skip to content

Commit f080246

Browse files
feat: added support for watch mode
BREAKING CHANGE: The `options` parameter is now deprecated, the reason is that `exclude` and `include` do not make sense when importing the same asset from both excluded and included modules
1 parent 730ac5b commit f080246

File tree

6 files changed

+37
-119
lines changed

6 files changed

+37
-119
lines changed

rollup.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default {
2323
// ...Object.keys(pkg.peerDependencies),
2424
"fs",
2525
"path",
26+
"crypto",
2627
],
2728
plugins: [
2829
nodeResolve(),

src/index.ts

Lines changed: 35 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,12 @@
11
import fs from "fs";
22
import path from "path";
3+
import crypto from "crypto";
34
import { Plugin, OutputOptions } from "rollup";
45
import { createFilter, FilterPattern } from "@rollup/pluginutils";
56
import { parse, print, types, visit } from "recast";
67

7-
interface PluginOptions {
8-
/**
9-
* A picomatch pattern, or array of patterns,
10-
* which correspond to modules the plugin should operate on.
11-
* By default all modules are targeted.
12-
*/
13-
include?: FilterPattern;
14-
/**
15-
* A picomatch pattern, or array of patterns,
16-
* which correspond to modules the plugin should ignore.
17-
* By default no modules are ignored.
18-
*/
19-
exclude?: FilterPattern;
20-
}
21-
228
const PLUGIN_NAME = "external-assets";
23-
const REGEX_ESCAPED_PLUGIN_NAME = PLUGIN_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9+
const PREFIX = `\0${PLUGIN_NAME}:`;
2410

2511
function getOutputId(filename: string, outputOptions: OutputOptions) {
2612
// Extract output directory from outputOptions.
@@ -52,102 +38,71 @@ function getRelativeImportPath(from: string, to: string) {
5238
* which correspond to assets the plugin should operate on.
5339
* @param options - The options object.
5440
*/
55-
export default function externalAssets(pattern: FilterPattern, options?: PluginOptions): Plugin {
41+
export default function externalAssets(pattern: FilterPattern): Plugin {
5642
if (!pattern) throw new Error("please specify a pattern for targeted assets");
5743

58-
const importerFilter = createFilter(options?.include, options?.exclude);
59-
const sourceFilter = createFilter(pattern);
44+
const idFilter = createFilter(pattern);
45+
const hashToIdMap: Partial<Record<string, string>> = {};
6046

6147
return {
62-
async buildStart() {
63-
this.warn("'options' parameter is deprecated. Please update to the latest version.");
64-
},
65-
6648
name: PLUGIN_NAME,
6749

68-
async options(inputOptions) {
69-
const plugins = inputOptions.plugins;
70-
71-
// No transformations.
72-
if (!plugins) return null;
73-
74-
// Separate our plugin from other plugins.
75-
const externalAssetsPlugins: Plugin[] = [];
76-
const otherPlugins = plugins.filter(plugin => {
77-
if (plugin.name !== PLUGIN_NAME) return true;
78-
79-
externalAssetsPlugins.push(plugin);
80-
return false;
81-
});
50+
async resolveId(source, importer) {
51+
if (
52+
!importer // Skip entrypoints.
53+
|| !source.startsWith(PREFIX) // Not a hash that was calculated in the `load` hook.
54+
) return null;
8255

83-
// Re-position our plugin to be the first in the list.
84-
// Otherwise, if there's a plugin that resolves paths before ours,
85-
// non-external imports can trigger the load hook for assets that can't be parsed by other plugins.
8656
return {
87-
...inputOptions,
88-
plugins: [
89-
...externalAssetsPlugins,
90-
...otherPlugins,
91-
],
57+
id: source,
58+
external: true
9259
};
9360
},
9461

95-
async resolveId(source, importer, options) {
96-
// `this.resolve` was called from another instance of this plugin. skip to avoid infinite loop.
97-
// or skip resolving entrypoints.
98-
// or don't resolve imports from filtered out modules.
62+
async load(id) {
9963
if (
100-
options.custom?.[PLUGIN_NAME]?.skip
101-
|| !importer
102-
|| !importerFilter(importer)
64+
id.startsWith("\0") // Virtual module.
65+
|| id.includes("?") // Id reserved by some other plugin.
66+
|| !idFilter(id) // Filtered out id.
10367
) return null;
10468

105-
// We'll delegate resolving to other plugins (alias, node-resolve ...),
106-
// or eventually, rollup itself.
107-
// We need to skip this plugin to avoid an infinite loop.
108-
const resolution = await this.resolve(source, importer, {
109-
skipSelf: true,
110-
custom: {
111-
[PLUGIN_NAME]: {
112-
skip: true,
113-
}
114-
}
115-
});
69+
const hash = crypto.createHash('md5').update(id).digest('hex');
11670

117-
// If it cannot be resolved, or if the id is filtered out,
118-
// return `null` so that Rollup displays an error.
119-
if (!resolution || !sourceFilter(resolution.id)) return null;
71+
// In the output phase,
72+
// We'll use this mapping to replace the hash with a relative path from a chunk to the emitted asset.
73+
hashToIdMap[hash] = id;
12074

121-
return {
122-
...resolution,
123-
// We'll need `target_id` to emit the asset in the output phase.
124-
id: `${resolution.id}?${PLUGIN_NAME}&target_id=${resolution.id}`,
125-
external: true,
126-
};
75+
// Load a proxy module with a hash as the import.
76+
// The hash will be resolved as external.
77+
// The benefit of doing it this way, instead of resolving asset imports to external ids,
78+
// is that we get watch mode support out of the box.
79+
return `export * from "${PREFIX + hash}";\n`
80+
+ `export { default } from "${PREFIX + hash}";\n`;
12781
},
12882

12983
async renderChunk(code, chunk, outputOptions) {
13084
const chunk_id = getOutputId(chunk.fileName, outputOptions);
13185
const chunk_basename = path.basename(chunk_id);
13286

13387
const ast = parse(code, { sourceFileName: chunk_basename });
134-
const pattern = new RegExp(`.+\\?${REGEX_ESCAPED_PLUGIN_NAME}&target_id=(.+)`);
13588
const rollup_context = this;
13689

13790
visit(ast, {
13891
visitLiteral(nodePath) {
139-
const node = nodePath.node;
92+
const value = nodePath.node.value;
14093

141-
// We're only concerned with string literals.
142-
if (typeof node.value !== "string") return this.traverse(nodePath);
94+
if (
95+
typeof value !== "string" // We're only concerned with string literals.
96+
|| !value.startsWith(PREFIX) // Not a hash that was calculated in the `load` hook.
97+
) return this.traverse(nodePath);
14398

144-
const match = node.value.match(pattern);
99+
const hash = value.slice(PREFIX.length);
100+
const target_id = hashToIdMap[hash];
145101

146-
// This string does not refer to an import path that we resolved in the `resolveId` hook.
147-
if (!match) return this.traverse(nodePath);
102+
// The hash belongs to another instance of this plugin.
103+
if (!target_id) return this.traverse(nodePath);
148104

149105
// Emit the targeted asset.
150-
const target_id = match[1];
151106
const asset_reference_id = rollup_context.emitFile({
152107
type: "asset",
153108
source: fs.readFileSync(target_id),

tests/general.test.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const test = require("ava");
22
const { nodeResolve } = require("@rollup/plugin-node-resolve");
3-
const { rollup } = require("rollup");
43
const { outputSnapshotMacro } = require("./macros");
54
const externalAssets = require("..");
65

@@ -16,21 +15,6 @@ for (const value of falsy) {
1615
});
1716
}
1817

19-
// Solved by re-positioning the plugin to be the first on the list.
20-
test("Plugin works even if it's not the first in the list", async t => {
21-
await t.notThrowsAsync(
22-
rollup({
23-
input: "tests/fixtures/src/index2.js",
24-
plugins: [
25-
nodeResolve({
26-
moduleDirectories: ["tests/fixtures/node_modules"],
27-
}),
28-
externalAssets(["tests/fixtures/assets/*", /@fontsource\/open-sans/]),
29-
],
30-
})
31-
);
32-
});
33-
3418
test("Multiple instances of the plugin can be used at the same time", outputSnapshotMacro,
3519
{
3620
input: "tests/fixtures/src/index2.js",

tests/resolve.test.js

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const test = require("ava");
2-
const { rollup } = require("rollup");
32
const { outputSnapshotMacro } = require("./macros");
43
const { nodeResolve } = require("@rollup/plugin-node-resolve");
54
const alias = require("@rollup/plugin-alias");
@@ -12,27 +11,6 @@ test("Skips resolving entrypoints", async t => {
1211
t.is(resolution, null);
1312
});
1413

15-
// Rollup will not be able to parse assets imported from excluded modules.
16-
test("Doesn't process imports from excluded modules", async t => {
17-
await t.throwsAsync(
18-
rollup({
19-
input: "tests/fixtures/src/index1.js",
20-
plugins: [
21-
externalAssets(
22-
"tests/fixtures/assets/*",
23-
{
24-
exclude: /1\.js$/,
25-
include: "tests/fixtures/src/*.js",
26-
}
27-
),
28-
],
29-
}),
30-
{
31-
code: "PARSE_ERROR",
32-
}
33-
);
34-
});
35-
3614
test(`Resolve with @rollup/plugin-node-resolve`, outputSnapshotMacro,
3715
{
3816
input: "tests/fixtures/src/index2.js",

tests/snapshots/output.test.js.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10895,7 +10895,7 @@ Generated by [AVA](https://avajs.dev).
1089510895

1089610896
[
1089710897
{
10898-
code: `define(["./assets/image-0fc60877.png", "./assets/text-6d7076f2.txt", "./assets/styles-fc0ceb37.css"], function (png, text, styles_css) { 'use strict';␊
10898+
code: `define(["./assets/image-0fc60877.png", "./assets/text-6d7076f2.txt", "./assets/styles-fc0ceb37.css"], function (png, text, _externalAssets_4310739eb4843f3840c8bd0630aa60b0) { 'use strict';␊
1089910899
1090010900
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }␊
1090110901
42 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)