-
-
Notifications
You must be signed in to change notification settings - Fork 296
/
Copy pathrollup-plugin-import-meta-assets.js
209 lines (186 loc) · 7.68 KB
/
rollup-plugin-import-meta-assets.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
'use strict';
const fs = require('fs');
const path = require('path');
const { createFilter } = require('@rollup/pluginutils');
const { asyncWalk } = require('estree-walker');
const MagicString = require('magic-string');
const {
dynamicImportToGlob: dynamicURLToGlob,
VariableDynamicImportError: VariableURLError,
} = require('@rollup/plugin-dynamic-import-vars');
/**
* Extract the relative path from an AST node representing this kind of expression `new URL('./path/to/asset.ext', import.meta.url)`.
*
* @param {import('estree').Node} node - The AST node
* @returns {string} The relative path
*/
function getRelativeAssetPath(node) {
// either normal string expression or else it would be Template Literal with a single quasi
const browserPath = node.arguments[0].value ?? node.arguments[0].quasis[0].value.cooked;
return browserPath.split('/').join(path.sep);
}
/**
* Checks if a AST node represents this kind of expression: `new URL('./path/to/asset.ext', import.meta.url)`.
*
* @param {import('estree').Node} node - The AST node
* @returns {undefined | 'static' | 'dynamic'}
*/
function getImportMetaUrlType(node) {
const isNewURL =
node.type === 'NewExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'URL' &&
node.arguments.length === 2 &&
(node.arguments[0].type === 'Literal' ||
// Allow template literals, reuses @rollup/plugin-dynamic-import-vars logic
node.arguments[0].type === 'TemplateLiteral') &&
typeof getRelativeAssetPath(node) === 'string' &&
node.arguments[1].type === 'MemberExpression' &&
node.arguments[1].object.type === 'MetaProperty' &&
node.arguments[1].property.type === 'Identifier' &&
node.arguments[1].property.name === 'url';
if (!isNewURL) {
return undefined;
}
if (node.arguments[0].type === 'TemplateLiteral' && node.arguments[0].expressions.length > 0) {
return 'dynamic';
}
return 'static';
}
/**
* Detects assets references relative to modules using patterns such as `new URL('./path/to/asset.ext', import.meta.url)`.
* The assets are added to the rollup pipeline, allowing them to be transformed and hash the filenames.
* Patterns that represent directories are skipped.
*
* @param {object} options
* @param {string|string[]} [options.include] A picomatch pattern, or array of patterns, which specifies the files in the build the plugin should operate on. By default all files are targeted.
* @param {string|string[]} [options.exclude] A picomatch pattern, or array of patterns, which specifies the files in the build the plugin should _ignore_. By default no files are ignored.
* @param {boolean} [options.warnOnError] By default, the plugin quits the build process when it encounters an error. If you set this option to true, it will throw a warning instead and leave the code untouched.
* @param {function} [options.transform] A function to transform assets.
* @return {import('rollup').Plugin} A Rollup Plugin
*/
function importMetaAssets({ include, exclude, warnOnError, transform } = {}) {
const filter = createFilter(include, exclude);
return {
name: 'rollup-plugin-import-meta-assets',
async transform(code, id) {
if (!filter(id)) {
return null;
}
// Part 1: resolve dynamic template literal expressions
const parsed = this.parse(code);
let dynamicURLIndex = -1;
let ms;
await asyncWalk(parsed, {
enter: async node => {
const importMetaUrlType = getImportMetaUrlType(node);
if (importMetaUrlType !== 'dynamic') {
return;
}
dynamicURLIndex += 1;
try {
// see if this is a Template Literal with expressions inside, and generate a glob expression
const glob = dynamicURLToGlob(node.arguments[0], code.substring(node.start, node.end));
if (!glob) {
// this was not a variable dynamic url
return;
}
const { globby } = await import('globby');
// execute the glob
const result = await globby(glob, { cwd: path.dirname(id) });
const paths = result.map(r =>
r.startsWith('./') || r.startsWith('../') ? r : `./${r}`,
);
// create magic string if it wasn't created already
ms = ms || new MagicString(code);
// unpack variable dynamic url into a function with url statements per file, rollup
// will turn these into chunks automatically
ms.prepend(
`function __variableDynamicURLRuntime${dynamicURLIndex}__(path) {
switch (path) {
${paths.map(p => ` case '${p}': return new URL('${p}', import.meta.url);`).join('\n')}
${` default: return new Promise(function(resolve, reject) {
(typeof queueMicrotask === 'function' ? queueMicrotask : setTimeout)(
reject.bind(null, new Error("Unknown variable dynamic new URL statement: " + path))
);
})\n`} }
}\n\n`,
);
// call the runtime function instead of doing a dynamic url, the url specifier will
// be evaluated at runtime and the correct url will be returned by the injected function
ms.overwrite(
node.start,
node.start + 7,
`__variableDynamicURLRuntime${dynamicURLIndex}__`,
);
} catch (error) {
if (error instanceof VariableURLError && warnOnError) {
this.warn(error);
} else {
this.error(error);
}
}
},
});
let newCode = code;
if (ms && dynamicURLIndex !== -1) {
newCode = ms.toString();
}
// Part 2: emit asset files
const ast = this.parse(newCode);
const magicString = new MagicString(newCode);
let modifiedCode = false;
await asyncWalk(ast, {
enter: async node => {
const importMetaUrlType = getImportMetaUrlType(node);
if (!importMetaUrlType) {
return;
}
if (importMetaUrlType === 'static') {
const absoluteScriptDir = path.dirname(id);
const relativeAssetPath = getRelativeAssetPath(node);
const absoluteAssetPath = path.resolve(absoluteScriptDir, relativeAssetPath);
const assetName = path.basename(absoluteAssetPath);
try {
const assetContents = await fs.promises.readFile(absoluteAssetPath);
const transformedAssetContents =
transform != null
? await transform(assetContents, absoluteAssetPath)
: assetContents;
if (transformedAssetContents === null) {
return;
}
const ref = this.emitFile({
type: 'asset',
name: assetName,
source: transformedAssetContents,
});
magicString.overwrite(
node.arguments[0].start,
node.arguments[0].end,
`import.meta.ROLLUP_FILE_URL_${ref}`,
);
modifiedCode = true;
} catch (error) {
// Do not process directories, just skip
if (error.code !== 'EISDIR') {
if (warnOnError) {
this.warn(error, node.arguments[0].start);
} else {
this.error(error, node.arguments[0].start);
}
}
}
}
},
});
return {
code: magicString.toString(),
map: modifiedCode ? magicString.generateMap({ hires: true, includeContent: true }) : null,
};
},
};
}
module.exports = { importMetaAssets };