@rollup-extras/plugin-script-loader 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +313 -0
- package/index.d.ts +27 -0
- package/index.d.ts.map +9 -0
- package/index.js +267 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Konstantin Shutkin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Plugin Script Loader
|
|
2
|
+
|
|
3
|
+
Rollup plugin to mimic Webpack's `script-loader` inline behavior. Inlines raw scripts into the bundle in import order, or emits them as a separate asset file for non-module, non-strict mode execution.
|
|
4
|
+
|
|
5
|
+
[Changelog](./CHANGELOG.md)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Using npm:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm install --save-dev @rollup-extras/plugin-script-loader
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## The Problem
|
|
16
|
+
|
|
17
|
+
In legacy codebases using libraries like d3 or AngularJS, you need to:
|
|
18
|
+
|
|
19
|
+
- Load scripts in a **stable, deterministic order** (e.g., d3 before d3 plugins, AngularJS before Angular modules)
|
|
20
|
+
- **Concatenate** everything into a single bundle
|
|
21
|
+
- Pass all code through the **optimization layer** (terser) — not hidden inside `eval()` strings
|
|
22
|
+
- Support **legacy libraries** that aren't proper ES modules
|
|
23
|
+
- Optionally, run legacy code in **sloppy mode** (non-strict) outside the main ES module bundle
|
|
24
|
+
|
|
25
|
+
In Webpack, this is solved with `script-loader`:
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
import "script-loader!d3";
|
|
29
|
+
import "script-loader!d3-plugin1";
|
|
30
|
+
import "script-loader!angular";
|
|
31
|
+
import "script-loader!angular-route";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This plugin brings the same pattern to Rollup.
|
|
35
|
+
|
|
36
|
+
## How It Works
|
|
37
|
+
|
|
38
|
+
The plugin intercepts imports that start with a configurable prefix (`script!` by default), resolves the underlying module, and processes it based on the `emit` option:
|
|
39
|
+
|
|
40
|
+
- **`emit: 'inline'`** (default) — Inlines the raw source code directly into the bundle at the import site
|
|
41
|
+
- **`emit: 'asset'`** — Collects all scripts and emits them as a separate concatenated asset file
|
|
42
|
+
|
|
43
|
+
Because the code is handled as real JavaScript (not wrapped in `eval()` or `new Function()`), it:
|
|
44
|
+
|
|
45
|
+
- Preserves **import order** — scripts appear exactly as imported
|
|
46
|
+
- Is fully visible to **terser** and other optimization plugins
|
|
47
|
+
- Gets **concatenated** into a single output
|
|
48
|
+
- Is **CSP-safe** — no dynamic code evaluation
|
|
49
|
+
|
|
50
|
+
## Example: Inline Mode (Default)
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
import scriptLoader from "@rollup-extras/plugin-script-loader";
|
|
54
|
+
|
|
55
|
+
export default {
|
|
56
|
+
input: "src/index.js",
|
|
57
|
+
|
|
58
|
+
output: {
|
|
59
|
+
format: "iife",
|
|
60
|
+
file: "dist/bundle.js",
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
plugins: [scriptLoader()],
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
In your entry point:
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
// Legacy libraries — inlined in this exact order
|
|
71
|
+
import "script!d3";
|
|
72
|
+
import "script!d3-tip";
|
|
73
|
+
import "script!angular";
|
|
74
|
+
import "script!angular-route";
|
|
75
|
+
import "script!angular-sanitize";
|
|
76
|
+
|
|
77
|
+
// Your application code (regular ES module imports)
|
|
78
|
+
import { app } from "./app.js";
|
|
79
|
+
|
|
80
|
+
app.init();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Example: Asset Mode (Emit Separate File)
|
|
84
|
+
|
|
85
|
+
For legacy code that must run in true sloppy (non-strict) mode, use `emit: 'asset'` to emit a separate classic script file:
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
import scriptLoader from "@rollup-extras/plugin-script-loader";
|
|
89
|
+
|
|
90
|
+
export default {
|
|
91
|
+
input: "src/index.js",
|
|
92
|
+
|
|
93
|
+
output: {
|
|
94
|
+
format: "es",
|
|
95
|
+
dir: "dist",
|
|
96
|
+
sourcemap: true,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
plugins: [
|
|
100
|
+
scriptLoader({
|
|
101
|
+
emit: "asset",
|
|
102
|
+
name: "vendor.js",
|
|
103
|
+
sourcemap: true,
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This emits `vendor.js` as a separate asset file containing all concatenated scripts. Include it in your HTML before the main bundle:
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<script src="vendor.js"></script>
|
|
113
|
+
<script src="main.js" type="module"></script>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### With Content Hashing
|
|
117
|
+
|
|
118
|
+
To let Rollup apply its `assetFileNames` pattern for content hashing:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
scriptLoader({
|
|
122
|
+
emit: "asset",
|
|
123
|
+
name: "vendor.js",
|
|
124
|
+
exactFileName: false, // Use Rollup's assetFileNames pattern
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
With `output.assetFileNames: 'assets/[name].[hash].[ext]'`, this outputs `assets/vendor.abc123.js`.
|
|
129
|
+
|
|
130
|
+
### With Minification
|
|
131
|
+
|
|
132
|
+
Provide a custom `minify` function to minify the concatenated output:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
import { minify } from "terser";
|
|
136
|
+
|
|
137
|
+
scriptLoader({
|
|
138
|
+
emit: "asset",
|
|
139
|
+
name: "vendor.js",
|
|
140
|
+
minify: async (code, map) => {
|
|
141
|
+
const result = await minify(code, {
|
|
142
|
+
sourceMap: map ? { content: map } : false,
|
|
143
|
+
});
|
|
144
|
+
return { code: result.code, map: result.map };
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or with oxc-minify:
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
import { minify } from "oxc-minify";
|
|
153
|
+
|
|
154
|
+
scriptLoader({
|
|
155
|
+
emit: "asset",
|
|
156
|
+
minify: async (code) => {
|
|
157
|
+
const result = await minify("vendor.js", code);
|
|
158
|
+
return { code: result.code, map: result.map };
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Integration with plugin-html
|
|
164
|
+
|
|
165
|
+
Use with `@rollup-extras/plugin-html` to automatically inject the vendor bundle:
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
import scriptLoader from "@rollup-extras/plugin-script-loader";
|
|
169
|
+
import html from "@rollup-extras/plugin-html";
|
|
170
|
+
import {
|
|
171
|
+
simpleES5Script,
|
|
172
|
+
combineAssetFactories,
|
|
173
|
+
} from "@rollup-extras/plugin-html/asset-factories";
|
|
174
|
+
|
|
175
|
+
export default {
|
|
176
|
+
input: "src/main.js",
|
|
177
|
+
output: {
|
|
178
|
+
format: "es",
|
|
179
|
+
dir: "dist",
|
|
180
|
+
sourcemap: true,
|
|
181
|
+
assetFileNames: "assets/[name].[hash].[ext]",
|
|
182
|
+
},
|
|
183
|
+
plugins: [
|
|
184
|
+
scriptLoader({
|
|
185
|
+
emit: "asset",
|
|
186
|
+
name: "vendor.js",
|
|
187
|
+
exactFileName: false,
|
|
188
|
+
sourcemap: true,
|
|
189
|
+
}),
|
|
190
|
+
|
|
191
|
+
html({
|
|
192
|
+
template: "index.html",
|
|
193
|
+
assetsFactory: combineAssetFactories(
|
|
194
|
+
simpleES5Script(/vendor\..*\.js$/) // Inject vendor as classic script
|
|
195
|
+
),
|
|
196
|
+
}),
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Configuration
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
type ScriptLoaderPluginOptions = {
|
|
205
|
+
// Existing options
|
|
206
|
+
prefix?: string; // 'script!' by default
|
|
207
|
+
useStrict?: boolean; // true by default (for inline mode)
|
|
208
|
+
pluginName?: string; // '@rollup-extras/plugin-script-loader' by default
|
|
209
|
+
verbose?: boolean; // false by default
|
|
210
|
+
|
|
211
|
+
// Emit mode options
|
|
212
|
+
emit?: "inline" | "asset"; // 'inline' by default
|
|
213
|
+
name?: string; // 'vendor.js' by default
|
|
214
|
+
exactFileName?: boolean; // true by default
|
|
215
|
+
sourcemap?: boolean; // true by default when emit: 'asset'
|
|
216
|
+
minify?: (
|
|
217
|
+
code: string,
|
|
218
|
+
sourcemap?: SourceMap
|
|
219
|
+
) => Promise<{ code: string; map?: SourceMap }>;
|
|
220
|
+
};
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### `emit`
|
|
224
|
+
|
|
225
|
+
- **`'inline'`** (default) — Scripts are inlined into the main bundle (current behavior)
|
|
226
|
+
- **`'asset'`** — Scripts are concatenated and emitted as a separate asset file
|
|
227
|
+
|
|
228
|
+
### `name`
|
|
229
|
+
|
|
230
|
+
Base name for the emitted asset file. Only used when `emit: 'asset'`.
|
|
231
|
+
|
|
232
|
+
Default: `'vendor.js'`
|
|
233
|
+
|
|
234
|
+
### `exactFileName`
|
|
235
|
+
|
|
236
|
+
Controls how the asset filename is determined. Only used when `emit: 'asset'`.
|
|
237
|
+
|
|
238
|
+
- **`true`** (default) — Uses Rollup's `fileName` property; asset emitted with exact name (e.g., `vendor.js`)
|
|
239
|
+
- **`false`** — Uses Rollup's `name` property; Rollup applies `output.assetFileNames` pattern (e.g., `assets/vendor.abc123.js`)
|
|
240
|
+
|
|
241
|
+
### `sourcemap`
|
|
242
|
+
|
|
243
|
+
Whether to generate sourcemaps for the emitted asset. Only used when `emit: 'asset'`.
|
|
244
|
+
|
|
245
|
+
Default: `true` when `emit: 'asset'`
|
|
246
|
+
|
|
247
|
+
When enabled, generates a concatenated sourcemap (`vendor.js.map`) pointing back to original source files.
|
|
248
|
+
|
|
249
|
+
### `minify`
|
|
250
|
+
|
|
251
|
+
Optional async function to minify the concatenated code. Only used when `emit: 'asset'`.
|
|
252
|
+
|
|
253
|
+
Receives the code and optionally the sourcemap, returns minified code and optionally a new sourcemap.
|
|
254
|
+
|
|
255
|
+
### `useStrict`
|
|
256
|
+
|
|
257
|
+
Controls whether the inlined script is parsed in strict mode or sloppy (non-strict) mode. Only applies to `emit: 'inline'` mode.
|
|
258
|
+
|
|
259
|
+
- **`true`** (default) — appends `export {}` to the loaded code, which makes Rollup's parser treat the file as an ES module (strict mode)
|
|
260
|
+
- **`false`** — does not append `export {}`, so the parser uses script (sloppy) mode
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
// Sloppy mode for legacy libraries that break under strict parsing
|
|
264
|
+
scriptLoader({ useStrict: false });
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
> **Note:** `useStrict` only controls _parse-time_ mode. For the output to actually run in non-strict mode at runtime, you also need `output.strict: false` in your Rollup config (applies to `iife`, `cjs`, and `umd` formats). The `es` format is always strict per spec. For true sloppy mode at runtime, use `emit: 'asset'` instead.
|
|
268
|
+
|
|
269
|
+
### `prefix`
|
|
270
|
+
|
|
271
|
+
The import prefix that triggers script loading.
|
|
272
|
+
|
|
273
|
+
Default: `'script!'`
|
|
274
|
+
|
|
275
|
+
For Webpack migration compatibility:
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
scriptLoader({ prefix: "script-loader!" });
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### `verbose`
|
|
282
|
+
|
|
283
|
+
Enables detailed logging of processed scripts.
|
|
284
|
+
|
|
285
|
+
Default: `false`
|
|
286
|
+
|
|
287
|
+
## Notes
|
|
288
|
+
|
|
289
|
+
### Global Scope
|
|
290
|
+
|
|
291
|
+
With `format: 'iife'`, all inlined code shares the same function scope. Libraries that explicitly assign to `window` (e.g., `window.d3 = ...`, `window.angular = ...`) work correctly.
|
|
292
|
+
|
|
293
|
+
With `emit: 'asset'`, the emitted file runs as a classic script in true global scope — ideal for legacy libraries.
|
|
294
|
+
|
|
295
|
+
### Works With node-resolve
|
|
296
|
+
|
|
297
|
+
The plugin delegates module resolution to other plugins via Rollup's `this.resolve()`. This means `script!d3` will be resolved by `@rollup/plugin-node-resolve` (if present) just like a regular import — picking up the file from `node_modules`.
|
|
298
|
+
|
|
299
|
+
### Works With terser
|
|
300
|
+
|
|
301
|
+
Since the code is handled as real JavaScript, `@rollup/plugin-terser` can process inline mode output. For asset mode, use the `minify` option to apply minification to the concatenated output.
|
|
302
|
+
|
|
303
|
+
### Watch Mode
|
|
304
|
+
|
|
305
|
+
The plugin properly clears its state between builds, fully supporting Rollup's watch mode.
|
|
306
|
+
|
|
307
|
+
## Prior Art
|
|
308
|
+
|
|
309
|
+
- [script-loader](https://github.com/webpack-contrib/script-loader) (Webpack, deprecated)
|
|
310
|
+
|
|
311
|
+
# License
|
|
312
|
+
|
|
313
|
+
[MIT](https://github.com/kshutkin/rollup-extras/blob/main/LICENSE)
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
declare module '@rollup-extras/plugin-script-loader' {
|
|
2
|
+
import type { Plugin, SourceMap } from 'rollup';
|
|
3
|
+
export default function _default(options?: ScriptLoaderPluginOptions): Plugin;
|
|
4
|
+
export type ScriptLoaderPluginOptions = {
|
|
5
|
+
prefix?: string;
|
|
6
|
+
useStrict?: boolean;
|
|
7
|
+
pluginName?: string;
|
|
8
|
+
verbose?: boolean;
|
|
9
|
+
emit?: "inline" | "asset";
|
|
10
|
+
name?: string;
|
|
11
|
+
exactFileName?: boolean;
|
|
12
|
+
sourcemap?: boolean;
|
|
13
|
+
minify?: (code: string, sourcemap?: SourceMap) => Promise<{
|
|
14
|
+
code: string;
|
|
15
|
+
map?: SourceMap;
|
|
16
|
+
}>;
|
|
17
|
+
};
|
|
18
|
+
export type ScriptEntry = {
|
|
19
|
+
filePath: string;
|
|
20
|
+
code: string;
|
|
21
|
+
originalMap?: SourceMap;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import MagicString, { Bundle } from 'magic-string';
|
|
5
|
+
|
|
6
|
+
import { LogLevel } from '@niceties/logger';
|
|
7
|
+
import logger from '@rollup-extras/utils/logger';
|
|
8
|
+
import { getOptionsObject } from '@rollup-extras/utils/options';
|
|
9
|
+
import statistics from '@rollup-extras/utils/statistics';
|
|
10
|
+
|
|
11
|
+
const scriptPrefix = '\0script-loader:';
|
|
12
|
+
|
|
13
|
+
const factories = { logger };
|
|
14
|
+
|
|
15
|
+
export default function (options = {}) {
|
|
16
|
+
const normalizedOptions = getOptionsObject(
|
|
17
|
+
options,
|
|
18
|
+
{
|
|
19
|
+
pluginName: '@rollup-extras/plugin-script-loader',
|
|
20
|
+
prefix: 'script!',
|
|
21
|
+
useStrict: (true),
|
|
22
|
+
verbose: (false),
|
|
23
|
+
emit: ('inline'),
|
|
24
|
+
name: 'vendor.js',
|
|
25
|
+
exactFileName: (true),
|
|
26
|
+
sourcemap: (undefined),
|
|
27
|
+
minify: (
|
|
28
|
+
undefined
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
factories
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const { pluginName, prefix, useStrict, verbose, logger, emit, name, exactFileName, minify } = normalizedOptions;
|
|
35
|
+
|
|
36
|
+
const sourcemap = normalizedOptions.sourcemap ?? emit === 'asset';
|
|
37
|
+
|
|
38
|
+
const statisticsCollector = statistics(
|
|
39
|
+
false,
|
|
40
|
+
( result) => `inlined ${typeof result === 'number' ? `${result} scripts` : result.join(', ')}`
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const scripts = new Map();
|
|
44
|
+
|
|
45
|
+
const importOrder = [];
|
|
46
|
+
|
|
47
|
+
async function tryReadSourcemap(filePath, code) {
|
|
48
|
+
|
|
49
|
+
const inlineMatch = code.match(
|
|
50
|
+
/\/\/[#@]\s*sourceMappingURL=data:application\/json;(?:charset=utf-8;)?base64,([A-Za-z0-9+/=]+)\s*$/
|
|
51
|
+
);
|
|
52
|
+
if (inlineMatch) {
|
|
53
|
+
try {
|
|
54
|
+
const decoded = Buffer.from(inlineMatch[1], 'base64').toString('utf8');
|
|
55
|
+
return JSON.parse(decoded);
|
|
56
|
+
} catch {
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const externalMatch = code.match(/\/\/[#@]\s*sourceMappingURL=(.+?)\s*$/);
|
|
62
|
+
if (externalMatch && !externalMatch[1].startsWith('data:')) {
|
|
63
|
+
try {
|
|
64
|
+
const mapPath = `${filePath}.map`;
|
|
65
|
+
const mapContent = await readFile(mapPath, 'utf8');
|
|
66
|
+
return JSON.parse(mapContent);
|
|
67
|
+
} catch {
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stripSourcemapComment(code) {
|
|
76
|
+
return code.replace(/\n?\/\/[#@]\s*sourceMappingURL=.+?\s*$/, '');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return ({
|
|
80
|
+
name: pluginName,
|
|
81
|
+
|
|
82
|
+
async resolveId(source, importer, resolveOptions) {
|
|
83
|
+
if (!source.startsWith(prefix)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const bareSpecifier = source.slice(prefix.length);
|
|
88
|
+
|
|
89
|
+
const resolved = await this.resolve(bareSpecifier, importer, { ...resolveOptions, skipSelf: true });
|
|
90
|
+
|
|
91
|
+
if (!resolved) {
|
|
92
|
+
this.warn(`Could not resolve "${bareSpecifier}" (from "${source}")`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let resolvedPath = resolved.id;
|
|
97
|
+
if (resolvedPath.startsWith('\0')) {
|
|
98
|
+
resolvedPath = resolvedPath.slice(1);
|
|
99
|
+
}
|
|
100
|
+
const queryIndex = resolvedPath.indexOf('?');
|
|
101
|
+
if (queryIndex !== -1) {
|
|
102
|
+
resolvedPath = resolvedPath.slice(0, queryIndex);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const virtualId = scriptPrefix + resolvedPath;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
id: virtualId,
|
|
109
|
+
moduleSideEffects: true,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
moduleParsed(moduleInfo) {
|
|
114
|
+
if (emit !== 'asset') {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const importedId of moduleInfo.importedIds) {
|
|
119
|
+
if (importedId.startsWith(scriptPrefix) && !importOrder.includes(importedId)) {
|
|
120
|
+
importOrder.push(importedId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async load(id) {
|
|
126
|
+
if (!id.startsWith(scriptPrefix)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const realPath = id.slice(scriptPrefix.length);
|
|
131
|
+
|
|
132
|
+
this.addWatchFile(realPath);
|
|
133
|
+
|
|
134
|
+
logger.start('inlining scripts', verbose ? LogLevel.info : LogLevel.verbose);
|
|
135
|
+
|
|
136
|
+
const code = (await readFile(realPath)).toString();
|
|
137
|
+
|
|
138
|
+
const shortName = (realPath.split('/').pop());
|
|
139
|
+
if (verbose) {
|
|
140
|
+
logger(`\t${realPath}`, LogLevel.info);
|
|
141
|
+
}
|
|
142
|
+
statisticsCollector(shortName);
|
|
143
|
+
|
|
144
|
+
logger.finish( (statisticsCollector()));
|
|
145
|
+
|
|
146
|
+
if (emit === 'asset') {
|
|
147
|
+
|
|
148
|
+
const originalMap = sourcemap ? await tryReadSourcemap(realPath, code) : undefined;
|
|
149
|
+
const cleanCode = stripSourcemapComment(code);
|
|
150
|
+
|
|
151
|
+
scripts.set(id, {
|
|
152
|
+
filePath: realPath,
|
|
153
|
+
code: cleanCode,
|
|
154
|
+
originalMap,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
code: `/* [script-loader] externalized: ${realPath} */\n`,
|
|
159
|
+
map: null,
|
|
160
|
+
moduleSideEffects: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
code: useStrict ? `${code}\nexport {}\n` : code,
|
|
166
|
+
map: null,
|
|
167
|
+
moduleSideEffects: true,
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
async generateBundle(_outputOptions, bundle) {
|
|
172
|
+
if (emit !== 'asset' || scripts.size === 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sortedEntries = Array.from(scripts.entries())
|
|
177
|
+
.sort(([idA], [idB]) => {
|
|
178
|
+
const orderA = importOrder.indexOf(idA);
|
|
179
|
+
const orderB = importOrder.indexOf(idB);
|
|
180
|
+
|
|
181
|
+
if (orderA !== -1 && orderB !== -1) {
|
|
182
|
+
return orderA - orderB;
|
|
183
|
+
}
|
|
184
|
+
if (orderA !== -1) return -1;
|
|
185
|
+
if (orderB !== -1) return 1;
|
|
186
|
+
return 0;
|
|
187
|
+
})
|
|
188
|
+
.map(([, entry]) => entry);
|
|
189
|
+
|
|
190
|
+
const magicBundle = new Bundle({ separator: '\n' });
|
|
191
|
+
|
|
192
|
+
for (const entry of sortedEntries) {
|
|
193
|
+
const ms = new MagicString(entry.code, {
|
|
194
|
+
filename: entry.filePath,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
magicBundle.addSource({
|
|
198
|
+
filename: entry.filePath,
|
|
199
|
+
content: ms,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let concatenatedCode = magicBundle.toString();
|
|
204
|
+
|
|
205
|
+
let generatedMap;
|
|
206
|
+
|
|
207
|
+
if (sourcemap) {
|
|
208
|
+
generatedMap = (
|
|
209
|
+
magicBundle.generateMap({
|
|
210
|
+
file: name,
|
|
211
|
+
source: name,
|
|
212
|
+
includeContent: true,
|
|
213
|
+
hires: true,
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (minify) {
|
|
219
|
+
try {
|
|
220
|
+
const minified = await minify(concatenatedCode, generatedMap);
|
|
221
|
+
concatenatedCode = minified.code;
|
|
222
|
+
if (minified.map) {
|
|
223
|
+
generatedMap = minified.map;
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
this.error(`Minification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const assetRefId = this.emitFile({
|
|
231
|
+
type: 'asset',
|
|
232
|
+
[exactFileName ? 'fileName' : 'name']: name,
|
|
233
|
+
source: concatenatedCode,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const finalFileName = this.getFileName(assetRefId);
|
|
237
|
+
|
|
238
|
+
if (sourcemap && generatedMap) {
|
|
239
|
+
|
|
240
|
+
const mapFileName = `${finalFileName}.map`;
|
|
241
|
+
|
|
242
|
+
generatedMap.file = basename(finalFileName);
|
|
243
|
+
|
|
244
|
+
this.emitFile({
|
|
245
|
+
type: 'asset',
|
|
246
|
+
fileName: mapFileName,
|
|
247
|
+
source: JSON.stringify(generatedMap),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const bundleEntry = bundle[finalFileName];
|
|
251
|
+
if (bundleEntry && bundleEntry.type === 'asset' && typeof bundleEntry.source === 'string') {
|
|
252
|
+
bundleEntry.source += `\n//# sourceMappingURL=${basename(mapFileName)}`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (verbose) {
|
|
257
|
+
const fileList = sortedEntries.map(e => basename(e.filePath)).join(', ');
|
|
258
|
+
logger(`emitted ${name} containing: ${fileList}`, LogLevel.info);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
closeBundle() {
|
|
263
|
+
scripts.clear();
|
|
264
|
+
importOrder.length = 0;
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.1.0",
|
|
3
|
+
"license": "MIT",
|
|
4
|
+
"name": "@rollup-extras/plugin-script-loader",
|
|
5
|
+
"author": "Konstantin Shutkin",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"main": "./index.js",
|
|
16
|
+
"module": "./index.js",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/kshutkin/rollup-extras.git",
|
|
23
|
+
"directory": "plugin-script-loader"
|
|
24
|
+
},
|
|
25
|
+
"bugs": "https://github.com/kshutkin/rollup-extras/issues",
|
|
26
|
+
"homepage": "https://github.com/kshutkin/rollup-extras/blob/main/plugin-script-loader/README.md",
|
|
27
|
+
"readme": "README.md",
|
|
28
|
+
"description": "Rollup plugin to mimic Webpack's script-loader inline behavior. Inlines raw scripts into the bundle in import order, enabling legacy library loading with stable ordering, terser optimization and concatenation.",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"rollup-plugin",
|
|
31
|
+
"script-loader",
|
|
32
|
+
"legacy",
|
|
33
|
+
"inline",
|
|
34
|
+
"concatenation"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@niceties/logger": "^2.1.0",
|
|
38
|
+
"magic-string": "^0.30.21",
|
|
39
|
+
"@rollup-extras/utils": "^2.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"rollup": "^4.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"rollup": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|