tw-plugin-rollup 1.0.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 +373 -0
- package/README.md +222 -0
- package/dist/cjs/index.cjs +356 -0
- package/dist/esm/index.d.ts +171 -0
- package/dist/esm/index.js +303 -0
- package/package.json +82 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import magic_string from "magic-string";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* tw-plugin-rollup — bundle a multi-file TurboWarp / Scratch extension into a
|
|
10
|
+
* single unsandboxed-extension file with **Rollup**, **Rolldown**, or **Vite**
|
|
11
|
+
* (including Vite 8, which builds on Rolldown).
|
|
12
|
+
*
|
|
13
|
+
* Write your extension across as many ES modules as you like, `export default`
|
|
14
|
+
* the extension class (or an already-constructed instance) from the entry
|
|
15
|
+
* module, and this plugin wraps the bundle in the standard TurboWarp IIFE
|
|
16
|
+
* template and calls `Scratch.extensions.register()` for you:
|
|
17
|
+
*
|
|
18
|
+
* ```js
|
|
19
|
+
* (function (Scratch) {
|
|
20
|
+
* "use strict";
|
|
21
|
+
* // ...all of your bundled modules, inlined...
|
|
22
|
+
* Scratch.extensions.register(new MyExtension());
|
|
23
|
+
* })(Scratch);
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Because the entire bundle lives inside the IIFE, every bare reference to the
|
|
27
|
+
* `Scratch` global inside your code resolves to the local parameter — exactly
|
|
28
|
+
* the "personal copy of the Scratch API" the TurboWarp docs recommend.
|
|
29
|
+
*
|
|
30
|
+
* The plugin uses only the standard Rollup plugin API plus Node's `fs`, so the
|
|
31
|
+
* exact same object works in Rollup, Rolldown, and Vite without depending on
|
|
32
|
+
* any one of them.
|
|
33
|
+
*
|
|
34
|
+
* @module tw-plugin-rollup
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const PLUGIN_NAME = 'turbowarp-extension';
|
|
38
|
+
// Assets imported by the extension are inlined as base64 `data:` URIs by
|
|
39
|
+
// default — a TurboWarp extension is a single file and can't reference separate
|
|
40
|
+
// asset files. SVG/PNG/etc. imports become the `data:` strings you hand to
|
|
41
|
+
// `menuIconURI` / `blockIconURI`.
|
|
42
|
+
const DEFAULT_ASSET_PATTERN = /\.(svg|png|jpe?g|gif|webp|avif)$/i;
|
|
43
|
+
// Maps the file extensions we inline to the MIME type the `data:` URI advertises.
|
|
44
|
+
const MIME_TYPES = {
|
|
45
|
+
svg: 'image/svg+xml',
|
|
46
|
+
png: 'image/png',
|
|
47
|
+
jpg: 'image/jpeg',
|
|
48
|
+
jpeg: 'image/jpeg',
|
|
49
|
+
gif: 'image/gif',
|
|
50
|
+
webp: 'image/webp',
|
|
51
|
+
avif: 'image/avif'
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Registry metadata. Each field becomes a `// Key: Value` comment line at the
|
|
55
|
+
* very top of the file — the header the
|
|
56
|
+
* [TurboWarp extensions gallery](https://github.com/TurboWarp/extensions)
|
|
57
|
+
* requires for submission:
|
|
58
|
+
*
|
|
59
|
+
* ```js
|
|
60
|
+
* // Name: Consoles
|
|
61
|
+
* // ID: sipcconsole
|
|
62
|
+
* // Description: Blocks that interact with the developer console.
|
|
63
|
+
* // By: -SIPC-
|
|
64
|
+
* // License: MIT
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @typedef {object} TurboWarpExtensionMetadata
|
|
68
|
+
* @property {string} [name] Display name shown in the extension list.
|
|
69
|
+
* @property {string} [id] Unique extension id. **Must match** the `id` your
|
|
70
|
+
* `getInfo()` returns.
|
|
71
|
+
* @property {string} [description] One-line description for the gallery.
|
|
72
|
+
* @property {string | string[]} [by] Author(s). Each entry becomes its own
|
|
73
|
+
* `// By:` line and may include a profile link, e.g.
|
|
74
|
+
* `"GarboMuffin <https://scratch.mit.edu/users/GarboMuffin/>"`.
|
|
75
|
+
* @property {string | string[]} [original] Original author(s) when this is a
|
|
76
|
+
* derivative — one `// Original:` line each.
|
|
77
|
+
* @property {string} [license] SPDX license id, e.g. `"MPL-2.0"`.
|
|
78
|
+
* @property {string} [context] Extra `// Context:` line.
|
|
79
|
+
*/ /**
|
|
80
|
+
* @typedef {object} TurboWarpExtensionPluginOptions
|
|
81
|
+
* @property {boolean} [register=true] Append a
|
|
82
|
+
* `Scratch.extensions.register(...)` call for the entry's chosen export. Set
|
|
83
|
+
* to `false` if you would rather call `register()` yourself somewhere in your
|
|
84
|
+
* own code (it still runs inside the IIFE, so the `Scratch` global is
|
|
85
|
+
* available there too).
|
|
86
|
+
* @property {boolean} [unsandboxed=false] Emit a guard at the top of the bundle
|
|
87
|
+
* that throws unless the extension is running unsandboxed
|
|
88
|
+
* (`Scratch.extensions.unsandboxed`). Use this for extensions that require
|
|
89
|
+
* direct access to the VM.
|
|
90
|
+
* @property {TurboWarpExtensionMetadata} [metadata] Registry metadata injected
|
|
91
|
+
* as the `// Name:` / `// ID:` / … comment header the TurboWarp gallery reads.
|
|
92
|
+
* Omit it for extensions you only ever load manually.
|
|
93
|
+
* @property {boolean | RegExp} [inlineAssets=true] Have the plugin resolve asset
|
|
94
|
+
* imports to a base64 `data:` URI — the form TurboWarp wants for
|
|
95
|
+
* `menuIconURI` / `blockIconURI`. With this on you can
|
|
96
|
+
* `import iconURI from './icon.svg'` and use `iconURI` directly. Defaults to
|
|
97
|
+
* matching `svg`, `png`, `jpg`, `gif`, `webp`, `avif`; pass a `RegExp` to use
|
|
98
|
+
* your own test, or `false` to leave asset handling to your own config (e.g.
|
|
99
|
+
* Vite's, which already inlines assets).
|
|
100
|
+
* @property {string} [name] Human-readable name used in the unsandboxed-guard
|
|
101
|
+
* error message. Defaults to `metadata.name` when set, otherwise
|
|
102
|
+
* `"This extension"`.
|
|
103
|
+
* @property {string} [varName="__turbowarpExtension__"] Identifier the bundle's
|
|
104
|
+
* export is assigned to before registration. Only change it if it somehow
|
|
105
|
+
* collides with a global your extension relies on.
|
|
106
|
+
* @property {string | string[]} [libraryExport="default"] Which export of the
|
|
107
|
+
* entry module is the extension. Defaults to the default export; pass a named
|
|
108
|
+
* export (or a path like `['nested', 'Extension']`) to use something else.
|
|
109
|
+
*/ /**
|
|
110
|
+
* Rollup / Rolldown / Vite plugin that turns a normal multi-module bundle into
|
|
111
|
+
* a single-file TurboWarp unsandboxed extension.
|
|
112
|
+
*
|
|
113
|
+
* Returns a plain Rollup plugin object, so it slots straight into a Rollup or
|
|
114
|
+
* Rolldown `plugins` array, or a Vite config's `plugins`. It carries an
|
|
115
|
+
* `enforce: 'pre'` hint so that, under Vite, its asset `load` hook runs before
|
|
116
|
+
* Vite's own asset handling.
|
|
117
|
+
*
|
|
118
|
+
* @param {TurboWarpExtensionPluginOptions} [options]
|
|
119
|
+
* @returns {import('rollup').Plugin}
|
|
120
|
+
*/ function turbowarpExtension(options = {}) {
|
|
121
|
+
/** @type {Required<TurboWarpExtensionPluginOptions>} */ const resolved = {
|
|
122
|
+
register: true,
|
|
123
|
+
unsandboxed: false,
|
|
124
|
+
metadata: null,
|
|
125
|
+
inlineAssets: true,
|
|
126
|
+
name: undefined,
|
|
127
|
+
varName: '__turbowarpExtension__',
|
|
128
|
+
libraryExport: 'default',
|
|
129
|
+
...options
|
|
130
|
+
};
|
|
131
|
+
// Fall back to the metadata name so the guard message reads naturally without
|
|
132
|
+
// having to repeat the name in two places.
|
|
133
|
+
resolved.name = resolved.name ?? resolved.metadata?.name ?? 'This extension';
|
|
134
|
+
const assetPattern = resolved.inlineAssets instanceof RegExp ? resolved.inlineAssets : DEFAULT_ASSET_PATTERN;
|
|
135
|
+
const prefix = buildPrefix(resolved);
|
|
136
|
+
const suffix = buildSuffix(resolved);
|
|
137
|
+
return {
|
|
138
|
+
name: PLUGIN_NAME,
|
|
139
|
+
// Vite-only hint (ignored by Rollup/Rolldown): run our `load` hook ahead of
|
|
140
|
+
// Vite's asset plugins so `import icon from './icon.svg'` reaches us first.
|
|
141
|
+
enforce: 'pre',
|
|
142
|
+
// Shape the bundle so the entry's export is reachable: a single
|
|
143
|
+
// self-executing IIFE that captures the chosen export in `varName`, which
|
|
144
|
+
// the wrapper below reads and registers.
|
|
145
|
+
outputOptions (outputOptions) {
|
|
146
|
+
outputOptions.format = 'iife';
|
|
147
|
+
outputOptions.name = resolved.varName;
|
|
148
|
+
// `default` exposes the bare default export as `varName`; any other export
|
|
149
|
+
// selector needs the namespace object so we can index into it (see
|
|
150
|
+
// `exportAccessor`).
|
|
151
|
+
outputOptions.exports = resolved.libraryExport === 'default' ? 'default' : 'named';
|
|
152
|
+
// Single pasteable file: never split, even if the graph has dynamic imports.
|
|
153
|
+
outputOptions.inlineDynamicImports = true;
|
|
154
|
+
return outputOptions;
|
|
155
|
+
},
|
|
156
|
+
// Inline asset imports as base64 `data:` URIs so `import icon from
|
|
157
|
+
// './icon.svg'` yields a string usable as `menuIconURI` — and so nothing is
|
|
158
|
+
// emitted as a separate file (the extension must be self-contained).
|
|
159
|
+
load (id) {
|
|
160
|
+
if (!resolved.inlineAssets) return null;
|
|
161
|
+
// Strip any query/hash suffix bundlers tack on (e.g. Vite's `?import`).
|
|
162
|
+
const path = id.replace(/[?#].*$/, '');
|
|
163
|
+
if (!assetPattern.test(path)) return null;
|
|
164
|
+
const ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase();
|
|
165
|
+
const mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
166
|
+
const base64 = readFileSync(path).toString('base64');
|
|
167
|
+
const dataUri = `data:${mime};base64,${base64}`;
|
|
168
|
+
return {
|
|
169
|
+
code: `export default ${JSON.stringify(dataUri)};`,
|
|
170
|
+
map: null
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
// Wrap the entry chunk in the TurboWarp IIFE template. Runs after Rollup has
|
|
174
|
+
// produced the inner `var varName = (function () { … })()` IIFE, so the
|
|
175
|
+
// export is already captured by the time the registration suffix reads it.
|
|
176
|
+
renderChunk (code, chunk, outputOptions) {
|
|
177
|
+
if (!chunk.isEntry) return null;
|
|
178
|
+
const magic = new magic_string(code);
|
|
179
|
+
magic.prepend(`${prefix}\n`);
|
|
180
|
+
magic.append(`\n${suffix}`);
|
|
181
|
+
return {
|
|
182
|
+
code: magic.toString(),
|
|
183
|
+
map: outputOptions.sourcemap ? magic.generateMap({
|
|
184
|
+
hires: true
|
|
185
|
+
}) : null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The registry metadata header (`// Name:` / `// ID:` / …), in the order the
|
|
192
|
+
* gallery conventionally lists them, followed by the opening of the TurboWarp
|
|
193
|
+
* IIFE template and an optional unsandboxed guard.
|
|
194
|
+
*
|
|
195
|
+
* @param {Required<TurboWarpExtensionPluginOptions>} options
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/ function buildPrefix(options) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
const header = buildMetadataHeader(options.metadata);
|
|
200
|
+
if (header) lines.push(header, ''); // blank line between header and code
|
|
201
|
+
lines.push('(function (Scratch) {', '"use strict";');
|
|
202
|
+
if (options.unsandboxed) {
|
|
203
|
+
const message = JSON.stringify(`${options.name} must be run unsandboxed.`);
|
|
204
|
+
lines.push(`if (!Scratch.extensions.unsandboxed) { throw new Error(${message}); }`);
|
|
205
|
+
}
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
// Known metadata fields, paired with the exact label the gallery expects, in
|
|
209
|
+
// conventional order. Unlisted fields are emitted afterwards using their key
|
|
210
|
+
// verbatim as the label.
|
|
211
|
+
const METADATA_FIELDS = [
|
|
212
|
+
[
|
|
213
|
+
'name',
|
|
214
|
+
'Name'
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
'id',
|
|
218
|
+
'ID'
|
|
219
|
+
],
|
|
220
|
+
[
|
|
221
|
+
'description',
|
|
222
|
+
'Description'
|
|
223
|
+
],
|
|
224
|
+
[
|
|
225
|
+
'by',
|
|
226
|
+
'By'
|
|
227
|
+
],
|
|
228
|
+
[
|
|
229
|
+
'original',
|
|
230
|
+
'Original'
|
|
231
|
+
],
|
|
232
|
+
[
|
|
233
|
+
'license',
|
|
234
|
+
'License'
|
|
235
|
+
],
|
|
236
|
+
[
|
|
237
|
+
'context',
|
|
238
|
+
'Context'
|
|
239
|
+
]
|
|
240
|
+
];
|
|
241
|
+
/**
|
|
242
|
+
* Render registry metadata as a block of `// Key: Value` comment lines.
|
|
243
|
+
* Array-valued fields (e.g. `by`) produce one line per entry, and every value
|
|
244
|
+
* is collapsed onto a single line so it can't break out of the comment.
|
|
245
|
+
*
|
|
246
|
+
* @param {TurboWarpExtensionMetadata | null | undefined} metadata
|
|
247
|
+
* @returns {string} The joined comment lines, or `''` when there's nothing.
|
|
248
|
+
*/ function buildMetadataHeader(metadata) {
|
|
249
|
+
if (!metadata) return '';
|
|
250
|
+
const lines = [];
|
|
251
|
+
const emit = (label, value)=>{
|
|
252
|
+
if (value == null) return;
|
|
253
|
+
for (const entry of Array.isArray(value) ? value : [
|
|
254
|
+
value
|
|
255
|
+
]){
|
|
256
|
+
const text = String(entry).replace(/[\r\n]+/g, ' ').trim();
|
|
257
|
+
if (text) lines.push(`// ${label}: ${text}`);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const known = new Set();
|
|
261
|
+
for (const [key, label] of METADATA_FIELDS){
|
|
262
|
+
known.add(key);
|
|
263
|
+
if (key in metadata) emit(label, metadata[key]);
|
|
264
|
+
}
|
|
265
|
+
for (const key of Object.keys(metadata)){
|
|
266
|
+
if (!known.has(key)) emit(key, metadata[key]);
|
|
267
|
+
}
|
|
268
|
+
return lines.join('\n');
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Build the JS expression that reads the chosen export off `varName`. For the
|
|
272
|
+
* default export that's just `varName`; a named export (or a nested path)
|
|
273
|
+
* indexes into the namespace object Rollup assigns to `varName`.
|
|
274
|
+
*
|
|
275
|
+
* @param {Required<TurboWarpExtensionPluginOptions>} options
|
|
276
|
+
* @returns {string}
|
|
277
|
+
*/ function exportAccessor(options) {
|
|
278
|
+
if (options.libraryExport === 'default') return options.varName;
|
|
279
|
+
const path = Array.isArray(options.libraryExport) ? options.libraryExport : [
|
|
280
|
+
options.libraryExport
|
|
281
|
+
];
|
|
282
|
+
return path.reduce((expr, key)=>`${expr}[${JSON.stringify(key)}]`, options.varName);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Closing of the IIFE template. When `register` is enabled, the entry's export
|
|
286
|
+
* is registered — instantiated first if it is a class (a function), or passed
|
|
287
|
+
* straight through if it is already an instance.
|
|
288
|
+
*
|
|
289
|
+
* @param {Required<TurboWarpExtensionPluginOptions>} options
|
|
290
|
+
* @returns {string}
|
|
291
|
+
*/ function buildSuffix(options) {
|
|
292
|
+
const lines = [];
|
|
293
|
+
if (options.register) {
|
|
294
|
+
lines.push('(function () {', ` var extension = ${exportAccessor(options)};`, ' Scratch.extensions.register(', ' typeof extension === "function" ? new extension() : extension', ' );', '})();');
|
|
295
|
+
}
|
|
296
|
+
lines.push('})(Scratch);');
|
|
297
|
+
return lines.join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* export default */ const src = (turbowarpExtension);
|
|
301
|
+
|
|
302
|
+
export { turbowarpExtension };
|
|
303
|
+
export default src;
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tw-plugin-rollup",
|
|
3
|
+
"description": "Bundle a multi-file TurboWarp/Scratch extension into a single unsandboxed-extension file with Rollup, Rolldown, or Vite.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MPL-2.0",
|
|
7
|
+
"author": "playforge-coding",
|
|
8
|
+
"homepage": "https://github.com/playforge-coding/scratch4js/tree/main/packages/tw-plugin-rollup#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/playforge-coding/scratch4js.git",
|
|
12
|
+
"directory": "packages/tw-plugin-rollup"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/playforge-coding/scratch4js/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"turbowarp",
|
|
19
|
+
"scratch",
|
|
20
|
+
"extension",
|
|
21
|
+
"rollup",
|
|
22
|
+
"rollup-plugin",
|
|
23
|
+
"rolldown",
|
|
24
|
+
"vite",
|
|
25
|
+
"vite-plugin",
|
|
26
|
+
"bundler"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/esm/index.d.ts",
|
|
32
|
+
"default": "./dist/esm/index.js"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/esm/index.d.ts",
|
|
36
|
+
"default": "./dist/cjs/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"main": "./dist/cjs/index.cjs",
|
|
41
|
+
"module": "./dist/esm/index.js",
|
|
42
|
+
"types": "./dist/esm/index.d.ts",
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"magic-string": "^0.30.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"rollup": "^3.0.0 || ^4.0.0",
|
|
57
|
+
"rolldown": "*",
|
|
58
|
+
"vite": ">=4"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"rollup": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"rolldown": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"vite": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@rsbuild/core": "^1.3.22",
|
|
73
|
+
"@rslib/core": "^0.22.0",
|
|
74
|
+
"rollup": "^4.0.0",
|
|
75
|
+
"typescript": "^5.9.0"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "rslib build",
|
|
79
|
+
"dev": "rslib build --watch",
|
|
80
|
+
"build:example": "node examples/multi-file-extension/build.mjs"
|
|
81
|
+
}
|
|
82
|
+
}
|