@exodus/stasis-plugins 1.0.0-beta.14 → 1.0.0-beta.16
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/package.json +2 -2
- package/src/esbuild.js +70 -160
- package/src/metro-babel-transformer.js +20 -41
- package/src/metro-native.js +44 -80
- package/src/metro-transformer.js +18 -46
- package/src/metro.js +102 -293
- package/src/plugins.js +82 -88
- package/src/rollup.js +58 -169
- package/src/webpack.js +56 -160
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/stasis-plugins",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.16",
|
|
4
4
|
"description": "Bundler plugins (webpack, esbuild, Rollup, Metro) for @exodus/stasis-core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://github.com/PreventiveMeasures/stasis#readme",
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@exodus/stasis-core": "1.0.0-beta.
|
|
39
|
+
"@exodus/stasis-core": "1.0.0-beta.16"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/esbuild.js
CHANGED
|
@@ -1,44 +1,31 @@
|
|
|
1
|
-
import { isUtf8 } from 'node:buffer'
|
|
2
1
|
import { dirname, extname, isAbsolute, join, relative, resolve as resolvePath } from 'node:path'
|
|
3
2
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
3
|
import assert from 'node:assert/strict'
|
|
5
4
|
|
|
6
5
|
import { Bundle } from '@exodus/stasis-core/bundle'
|
|
7
|
-
import { attestedImport,
|
|
6
|
+
import { assertCodeUtf8, assertPersistableAttributes, attestedImport, ownOrResolvedState, rebuildRefused, unsupportedExtension } from './plugins.js'
|
|
8
7
|
import { State } from '@exodus/stasis-core/state'
|
|
9
8
|
// Pre-patch snapshot, not `import { readFile }`: under --fs=async that builtin is patched, and
|
|
10
9
|
// this plugin loads after, so a direct import would route esbuild's reads through the capture hook.
|
|
11
10
|
import { realReadFile } from '@exodus/stasis-core/state-util'
|
|
12
|
-
import { EMPTY_MODULE_PATH, classifyExtension } from '@exodus/stasis-core/util'
|
|
11
|
+
import { EMPTY_MODULE_PATH, classifyExtension, upsert } from '@exodus/stasis-core/util'
|
|
13
12
|
|
|
14
|
-
// The build options esbuild's resolver reads, replayed into the disabled-import probe
|
|
15
|
-
// resolves exactly as the build does.
|
|
13
|
+
// The build options esbuild's resolver reads, replayed into the disabled-import probe.
|
|
16
14
|
const RESOLVE_OPTIONS = ['absWorkingDir', 'alias', 'conditions', 'external', 'mainFields', 'nodePaths',
|
|
17
15
|
'packages', 'platform', 'preserveSymlinks', 'resolveExtensions', 'tsconfig', 'tsconfigRaw']
|
|
18
16
|
|
|
19
17
|
// One import of `specifier` as `kind` would issue it, as the probe's stdin.
|
|
20
18
|
function probeSource(specifier, kind) {
|
|
21
19
|
const quoted = JSON.stringify(specifier)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return { contents: `import(${quoted})`, loader: 'js' }
|
|
28
|
-
case 'import-rule':
|
|
29
|
-
case 'composes-from':
|
|
30
|
-
return { contents: `@import ${quoted};`, loader: 'css' }
|
|
31
|
-
case 'url-token':
|
|
32
|
-
return { contents: `a { b: url(${quoted}) }`, loader: 'css' }
|
|
33
|
-
default:
|
|
34
|
-
return { contents: `import ${quoted}`, loader: 'js' }
|
|
35
|
-
}
|
|
20
|
+
if (kind === 'require-call' || kind === 'require-resolve') return { contents: `require(${quoted})`, loader: 'js' }
|
|
21
|
+
if (kind === 'dynamic-import') return { contents: `import(${quoted})`, loader: 'js' }
|
|
22
|
+
if (kind === 'import-rule' || kind === 'composes-from') return { contents: `@import ${quoted};`, loader: 'css' }
|
|
23
|
+
if (kind === 'url-token') return { contents: `a { b: url(${quoted}) }`, loader: 'css' }
|
|
24
|
+
return { contents: `import ${quoted}`, loader: 'js' }
|
|
36
25
|
}
|
|
37
26
|
|
|
38
|
-
// A `browser` map key or a path
|
|
39
|
-
//
|
|
40
|
-
// segments, every extension stripped, trailing index segments dropped. Equal loose keys are a
|
|
41
|
-
// SUPERSET of esbuild's matches -- only a probe decides.
|
|
27
|
+
// A `browser` map key or a path, loosened past esbuild's matching (bare, plus each resolve extension, plus `/index`,
|
|
28
|
+
// with or without './'): equal loose keys are a SUPERSET of esbuild's matches -- only a probe decides.
|
|
42
29
|
function looseBrowserKey(path) {
|
|
43
30
|
const parts = path.replaceAll('\\', '/').split('/').filter((part) => part !== '' && part !== '.')
|
|
44
31
|
while (parts.length > 0) {
|
|
@@ -58,7 +45,7 @@ async function browserFalseKeys(dir) {
|
|
|
58
45
|
try {
|
|
59
46
|
pkg = JSON.parse(await realReadFile(join(dir, 'package.json'), 'utf8'))
|
|
60
47
|
} catch {
|
|
61
|
-
return new Set()
|
|
48
|
+
return new Set()
|
|
62
49
|
}
|
|
63
50
|
const browser = pkg?.browser
|
|
64
51
|
if (browser === null || typeof browser !== 'object' || Array.isArray(browser)) return new Set()
|
|
@@ -73,40 +60,25 @@ export class StasisEsbuild {
|
|
|
73
60
|
#transform
|
|
74
61
|
#resource
|
|
75
62
|
#platform
|
|
76
|
-
#emptyModule
|
|
77
|
-
#browserScopes = new Map()
|
|
78
|
-
#probes = new Map()
|
|
79
|
-
|
|
80
|
-
// Build starts observed by this instance (across rebuilds and separate build()/context() calls);
|
|
81
|
-
// the second is refused in onStart.
|
|
63
|
+
#emptyModule // URL of the empty module disabled imports point at, once capture recorded it
|
|
64
|
+
#browserScopes = new Map() // dir -> Promise<[{ dir, keys }]>: `false` keys of it and its ancestors
|
|
65
|
+
#probes = new Map() // [kind, resolveDir, specifier] -> Promise<disabled path | null>
|
|
82
66
|
#captureBuildCount = 0
|
|
83
67
|
|
|
68
|
+
// Only for load mode: `loaders` per-extension overrides (RN's '.js' -> 'jsx'), `transform(source, { path, loader })`
|
|
69
|
+
// a code rewrite (`stasis build --platform=hermes`), `resource(source, { path })` a module for a resource no loader
|
|
70
|
+
// names (`stasis build --metro`'s asset module), `platform` the one a --metro per-platform edge resolves for.
|
|
84
71
|
constructor(options = {}, { loaders, transform, resource, platform } = {}) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const state = options instanceof State
|
|
88
|
-
? options
|
|
89
|
-
: resolvePluginState('StasisEsbuild', options, process.cwd()).state
|
|
90
|
-
this.#state = state // null when plugin should be inert
|
|
91
|
-
// Cache the resolved resources Set for the per-file classify hot path.
|
|
72
|
+
const state = ownOrResolvedState('StasisEsbuild', options)
|
|
73
|
+
this.#state = state
|
|
92
74
|
this.#resources = state?.config.resources ?? new Set()
|
|
93
|
-
// Per-extension esbuild loader overrides (e.g. { '.js' -> 'jsx' } for RN's JSX-in-.js),
|
|
94
|
-
// keyed by extname with the dot; falls back to #loaderFor.
|
|
95
75
|
this.#loaders = loaders ?? new Map()
|
|
96
|
-
// Load-mode-only code rewrite hook (e.g. `stasis build --platform=hermes` downleveling):
|
|
97
|
-
// (source, { path, loader }) => { contents, loader } | undefined, may be async. Never
|
|
98
|
-
// runs at capture -- capture attests the file's raw bytes.
|
|
99
76
|
this.#transform = transform
|
|
100
|
-
// Load-mode-only module builder for a resource no loader override names (e.g. `stasis build
|
|
101
|
-
// --metro`'s asset module): (source, { path }) => { contents, loader } | undefined, may be async.
|
|
102
77
|
this.#resource = resource
|
|
103
|
-
// Load mode: the platform whose target a --metro per-platform edge resolves to (none fails
|
|
104
|
-
// closed on one, as plain bundle=load does).
|
|
105
78
|
this.#platform = platform
|
|
106
79
|
}
|
|
107
80
|
|
|
108
|
-
//
|
|
109
|
-
// (.cjs/.mjs/.cts/.mts strip their module letter to js/ts).
|
|
81
|
+
// A configured override, else from the extension (.cjs/.mjs/.cts/.mts drop their module letter).
|
|
110
82
|
#loaderFor(path) {
|
|
111
83
|
const ext = extname(path)
|
|
112
84
|
return this.#loaders.get(ext) ?? ext.replace(/^\.[cm]?/u, '')
|
|
@@ -116,22 +88,19 @@ export class StasisEsbuild {
|
|
|
116
88
|
return 'stasis'
|
|
117
89
|
}
|
|
118
90
|
|
|
119
|
-
// Every directory from `dir` up whose
|
|
120
|
-
//
|
|
91
|
+
// Every directory from `dir` up whose `browser` map disables something, nearest first: a superset, as esbuild
|
|
92
|
+
// consults only the nearest map.
|
|
121
93
|
#browserFalseScopes(dir) {
|
|
122
|
-
|
|
123
|
-
if (scopes === undefined) {
|
|
94
|
+
return upsert(this.#browserScopes, dir, () => {
|
|
124
95
|
const parent = dirname(dir)
|
|
125
|
-
|
|
96
|
+
return Promise.all([browserFalseKeys(dir), parent === dir ? [] : this.#browserFalseScopes(parent)])
|
|
126
97
|
.then(([keys, above]) => (keys.size > 0 ? [{ dir, keys }, ...above] : above))
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
return scopes
|
|
98
|
+
})
|
|
130
99
|
}
|
|
131
100
|
|
|
132
|
-
// Whether some `browser` map `false` entry could match this resolution: the resolved path (and, for
|
|
133
|
-
//
|
|
134
|
-
//
|
|
101
|
+
// Whether some `browser` map `false` entry could match this resolution: the resolved path (and, for a relative
|
|
102
|
+
// specifier, the path it names) under each map's dir, or a bare specifier as is or as './<importer dir>/<specifier>'
|
|
103
|
+
// (a Browserify quirk esbuild keeps).
|
|
135
104
|
async #mayBeDisabled(path, specifier, resolveDir) {
|
|
136
105
|
const isPackagePath = !isAbsolute(specifier) && !/^\.\.?(?:\/|$)/u.test(specifier) && !specifier.startsWith('/')
|
|
137
106
|
const paths = isPackagePath ? [path] : [path, resolvePath(resolveDir, specifier)]
|
|
@@ -144,16 +113,13 @@ export class StasisEsbuild {
|
|
|
144
113
|
})
|
|
145
114
|
}
|
|
146
115
|
|
|
147
|
-
// esbuild's own verdict: a one-import build
|
|
148
|
-
//
|
|
149
|
-
// -> that path (absolute), or null when esbuild doesn't disable the import.
|
|
116
|
+
// esbuild's own verdict: a one-import build with the build's resolve options and every load stubbed empty, whose
|
|
117
|
+
// metafile still names a disabled module `(disabled):<path>`. -> that path (absolute), or null
|
|
150
118
|
#probeDisabled(specifier, kind, resolveDir, { esbuild, initialOptions }) {
|
|
151
|
-
|
|
152
|
-
let probe = this.#probes.get(key)
|
|
153
|
-
if (probe === undefined) {
|
|
119
|
+
return upsert(this.#probes, JSON.stringify([kind, resolveDir, specifier]), () => {
|
|
154
120
|
const options = Object.fromEntries(RESOLVE_OPTIONS.filter((k) => initialOptions[k] !== undefined).map((k) => [k, initialOptions[k]]))
|
|
155
121
|
const cwd = initialOptions.absWorkingDir ?? process.cwd()
|
|
156
|
-
|
|
122
|
+
return esbuild.build({
|
|
157
123
|
...options,
|
|
158
124
|
stdin: { ...probeSource(specifier, kind), resolveDir },
|
|
159
125
|
bundle: true,
|
|
@@ -166,94 +132,71 @@ export class StasisEsbuild {
|
|
|
166
132
|
if (!found?.startsWith('(disabled):')) return null
|
|
167
133
|
return resolvePath(cwd, found.slice('(disabled):'.length))
|
|
168
134
|
}, () => null)
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
return probe
|
|
135
|
+
})
|
|
172
136
|
}
|
|
173
137
|
|
|
174
|
-
// A `browser` field
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// could match (browser platform, see #mayBeDisabled).
|
|
138
|
+
// A `browser` field `false` makes esbuild resolve an import to an empty "disabled" module, but its resolve API
|
|
139
|
+
// drops that flag: a bare name comes back namespace-less, a file as its plain path, which a probe confirms where a
|
|
140
|
+
// `false` entry could match.
|
|
178
141
|
async #isDisabled(res, specifier, { kind, resolveDir }, build) {
|
|
179
142
|
if (res.external) return false
|
|
180
|
-
if (res.namespace === '') return true
|
|
143
|
+
if (res.namespace === '') return true
|
|
181
144
|
if (res.namespace !== 'file' || (build.initialOptions.platform ?? 'browser') !== 'browser') return false
|
|
182
145
|
if (!(await this.#mayBeDisabled(res.path, specifier, resolveDir))) return false
|
|
183
|
-
//
|
|
146
|
+
// A sibling plugin may have resolved the import to something else entirely.
|
|
184
147
|
return (await this.#probeDisabled(specifier, kind, resolveDir, build)) === res.path
|
|
185
148
|
}
|
|
186
149
|
|
|
187
150
|
setup = ({ onResolve, onLoad, onStart, onEnd, resolve, initialOptions, esbuild }) => {
|
|
188
|
-
if (!this.#state) return
|
|
151
|
+
if (!this.#state) return
|
|
152
|
+
const emptyModuleURL = pathToFileURL(resolvePath(this.#state.root, EMPTY_MODULE_PATH)).toString()
|
|
189
153
|
|
|
190
|
-
//
|
|
191
|
-
// changed bytes would emit new bytes while the bundle/lockfile keep the OLD ones. Rebuilds re-fire
|
|
192
|
-
// onStart against the same registrations, so an instance counter catches the second and errors.
|
|
154
|
+
// Rebuilds re-fire onStart against the same registrations.
|
|
193
155
|
if (!this.#state.config.loadBundle) {
|
|
194
156
|
onStart(() => {
|
|
195
157
|
this.#captureBuildCount += 1
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
errors: [{
|
|
199
|
-
text: 'StasisEsbuild: watch/rebuild is not supported for capture -- run a one-shot build (rebuilds would silently attest stale content)',
|
|
200
|
-
}],
|
|
201
|
-
}
|
|
158
|
+
return this.#captureBuildCount === 1 ? undefined : { errors: [{ text: rebuildRefused('StasisEsbuild') }] }
|
|
202
159
|
})
|
|
203
160
|
}
|
|
204
161
|
|
|
205
|
-
// The preload writes itself
|
|
206
|
-
// only on a clean build (a partial one would overwrite the user's good file).
|
|
162
|
+
// The preload writes itself; a failed build mustn't overwrite the user's good file.
|
|
207
163
|
if (this.#state !== State.preload) {
|
|
208
164
|
onEnd((result) => {
|
|
209
|
-
if (result.errors.length
|
|
210
|
-
this.#state.write()
|
|
165
|
+
if (result.errors.length === 0) this.#state.write()
|
|
211
166
|
})
|
|
212
167
|
}
|
|
213
168
|
|
|
214
|
-
// Load mode
|
|
215
|
-
//
|
|
216
|
-
// plugin-returned paths) so output bytes -- banners, asset names, source maps -- match a capture build.
|
|
169
|
+
// Load mode resolves through the bundle's import map: the file may not be on disk. namespace 'file' with the
|
|
170
|
+
// original path (esbuild doesn't stat a plugin's) keeps the output (banners, asset names, maps) a capture's.
|
|
217
171
|
onResolve({ filter: /$/ }, ({ path: specifier, importer, kind: resolveKind, with: attrs }) => {
|
|
218
172
|
if (!this.#state.config.loadBundle) return undefined
|
|
219
173
|
const isEntry = resolveKind === 'entry-point'
|
|
220
174
|
let url
|
|
221
175
|
if (isEntry) {
|
|
222
|
-
//
|
|
223
|
-
url = specifier.startsWith('file:')
|
|
224
|
-
? specifier
|
|
225
|
-
: pathToFileURL(resolvePath(process.cwd(), specifier)).toString()
|
|
176
|
+
// Anchored to cwd, as the run loader does, so the file needn't be on disk.
|
|
177
|
+
url = specifier.startsWith('file:') ? specifier : pathToFileURL(resolvePath(process.cwd(), specifier)).toString()
|
|
226
178
|
if (this.#state.config.full) this.#state.assertEntry(url)
|
|
227
179
|
} else {
|
|
180
|
+
// Looked up even for a built-in's name: a browser build records `fs` where a `browser` field disables it or a
|
|
181
|
+
// package by that name serves it. KNOWN GAP: a Node-loader-captured bundle keys an attributed edge under
|
|
182
|
+
// specific conditions, which this `*` lookup won't match.
|
|
228
183
|
const parentURL = pathToFileURL(importer).toString()
|
|
229
|
-
// Forward import attributes (`with { type: 'json' }`) so plugin-capture round-trips to
|
|
230
|
-
// plugin-load, both keyed under `* (with: ...)`. KNOWN GAP: a Node-loader-captured bundle
|
|
231
|
-
// keys attributed edges under specific conditions the plugin's `*` lookup won't match.
|
|
232
|
-
// The edge is looked up even for a built-in's name: a browser build records `fs` when a
|
|
233
|
-
// `browser` field disables it (an edge to the empty module) or a package by that name serves it.
|
|
234
|
-
// No attested edge (a built-in, or another external): esbuild's, which externalizes it as
|
|
235
|
-
// without us (built-ins under platform:'node'). File BYTES still fail closed at onLoad.
|
|
236
184
|
const attested = attestedImport(this.#state, parentURL, specifier, { importAttributes: attrs, platform: this.#platform })
|
|
237
185
|
if (attested === undefined) return undefined
|
|
238
186
|
;({ url } = attested)
|
|
239
187
|
}
|
|
240
188
|
const format = this.#state.getFormat(url)
|
|
241
|
-
const
|
|
242
|
-
const kind = isEmpty ? 'empty' : Bundle.isResourceFormat(format) ? 'resource' : 'code'
|
|
189
|
+
const kind = url === emptyModuleURL ? 'empty' : Bundle.isResourceFormat(format) ? 'resource' : 'code'
|
|
243
190
|
return { path: fileURLToPath(url), namespace: 'file', pluginData: { isEntry, kind } }
|
|
244
191
|
})
|
|
245
192
|
|
|
246
193
|
onResolve({ filter: /$/, namespace: 'file' }, async ({ path: specifier, with: attrs, ...args }) => {
|
|
247
194
|
const isEntry = !args.importer
|
|
248
|
-
|
|
249
|
-
// Recurse with a synthetic namespace so esbuild's default resolver runs without re-entering us
|
|
195
|
+
// A synthetic namespace runs esbuild's own resolver without re-entering this hook.
|
|
250
196
|
const res = await resolve(specifier, { ...args, with: attrs, namespace: 'stasis' })
|
|
251
|
-
// A miss is esbuild's to report
|
|
252
|
-
// optional supports-color). Decline so it re-resolves exactly as without us; no edge.
|
|
197
|
+
// A miss is esbuild's to report, or tolerate (a require() in a try/catch): decline, recording no edge.
|
|
253
198
|
if (res.errors.length > 0) return undefined
|
|
254
|
-
//
|
|
255
|
-
// on a bare name, so decline and let esbuild re-resolve it into its own empty module. Capture
|
|
256
|
-
// points the edge at the attested empty module, which is what bundle=load serves.
|
|
199
|
+
// Declined, esbuild re-resolves a disabled import into its own empty module, the attested one bundle=load serves.
|
|
257
200
|
if (!isEntry && await this.#isDisabled(res, specifier, args, { esbuild, initialOptions })) {
|
|
258
201
|
if (!this.#state.config.loadBundle) {
|
|
259
202
|
this.#emptyModule ??= this.#state.addEmptyModule({ reason: 'esbuild' })
|
|
@@ -261,8 +204,6 @@ export class StasisEsbuild {
|
|
|
261
204
|
}
|
|
262
205
|
return undefined
|
|
263
206
|
}
|
|
264
|
-
// A non-empty `suffix` (some CSS-modules plugins set one) can't be persisted to the bundle,
|
|
265
|
-
// so refuse it rather than silently dropping it at capture.
|
|
266
207
|
assert.equal(res.suffix, '',
|
|
267
208
|
`StasisEsbuild: a sibling plugin set res.suffix='${res.suffix}' on '${specifier}'; ` +
|
|
268
209
|
`stasis can't persist suffixes to the bundle. Disable that plugin or drop suffix from its resolve result.`)
|
|
@@ -270,77 +211,46 @@ export class StasisEsbuild {
|
|
|
270
211
|
let kind = 'skip'
|
|
271
212
|
if (res.namespace === 'file' && !res.external) {
|
|
272
213
|
kind = classifyExtension(res.path, this.#resources)
|
|
273
|
-
if (kind === 'unknown') {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// fall through to esbuild's disk resolver -- serving the asset UNATTESTED. kind is code|resource here.
|
|
279
|
-
if (!isEntry) {
|
|
280
|
-
const parentURL = pathToFileURL(args.importer).toString()
|
|
281
|
-
const url = pathToFileURL(res.path).toString()
|
|
282
|
-
// Forward import attributes symmetrically with load: addImport keys by attributes so
|
|
283
|
-
// getImport finds the same edge.
|
|
284
|
-
this.#state.addImport(parentURL, specifier, url, { importAttributes: attrs })
|
|
285
|
-
}
|
|
214
|
+
if (kind === 'unknown') return { errors: [{ text: unsupportedExtension('StasisEsbuild', `'${res.path}'`) }] }
|
|
215
|
+
// Resources too: load mode resolves every specifier through this map, so one with no edge would fall through
|
|
216
|
+
// to esbuild's disk resolver, unattested.
|
|
217
|
+
const url = pathToFileURL(res.path).toString()
|
|
218
|
+
if (!isEntry) this.#state.addImport(pathToFileURL(args.importer).toString(), specifier, url, { importAttributes: attrs })
|
|
286
219
|
}
|
|
287
|
-
|
|
288
220
|
return { ...res, pluginData: { ...res.pluginData, isEntry, kind } }
|
|
289
221
|
})
|
|
290
222
|
|
|
291
223
|
onLoad({ filter: /$/, namespace: 'file' }, async ({ path, pluginData, with: attrs }) => {
|
|
292
224
|
const kind = pluginData?.kind ?? 'skip'
|
|
293
225
|
if (kind === 'skip') return undefined
|
|
294
|
-
|
|
295
|
-
for (const k of Object.keys(attrs)) {
|
|
296
|
-
assert.equal(k, 'type', `unsupported import attribute: ${k}`)
|
|
297
|
-
}
|
|
226
|
+
assertPersistableAttributes(attrs)
|
|
298
227
|
|
|
299
|
-
//
|
|
300
|
-
// the bundle doesn't carry -- a missing in-scope file is a hard error, not a disk fallback.
|
|
228
|
+
// getFile verifies the hash and throws on a file the bundle doesn't carry: no disk fallback.
|
|
301
229
|
if (this.#state.config.loadBundle) {
|
|
302
230
|
const { source } = this.#state.getFile(pathToFileURL(path).toString())
|
|
303
|
-
//
|
|
304
|
-
// one a CSS @import, `composes` or url() accepts, so one empty module serves JS and CSS importers.
|
|
231
|
+
// esbuild's own loader for disabled modules, which a CSS @import, `composes` or url() accepts too.
|
|
305
232
|
if (kind === 'empty') return { contents: source, loader: 'empty' }
|
|
306
233
|
if (kind === 'resource') {
|
|
307
|
-
// A loader override for its extension (`stasis build --loader=.png:dataurl`) wins; else the
|
|
308
|
-
// resource hook's module, where it builds one.
|
|
309
234
|
const override = this.#loaders.get(extname(path))
|
|
310
235
|
if (override) return { contents: source, loader: override }
|
|
311
236
|
const built = await this.#resource?.(source, { path })
|
|
312
237
|
if (built) return built
|
|
313
|
-
//
|
|
314
|
-
// for the extension), so a resource none loads fails as at capture, not parsed as js.
|
|
238
|
+
// The build's own loader for it, so a resource none loads fails as at capture, not parsed as js.
|
|
315
239
|
return { contents: source, loader: 'default' }
|
|
316
240
|
}
|
|
317
241
|
const loader = this.#loaderFor(path)
|
|
318
|
-
|
|
319
|
-
const transformed = await this.#transform?.(source, { path, loader })
|
|
320
|
-
return transformed ?? { contents: source, loader }
|
|
242
|
+
return (await this.#transform?.(source, { path, loader })) ?? { contents: source, loader }
|
|
321
243
|
}
|
|
322
244
|
|
|
323
245
|
const source = await realReadFile(path)
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
// Mark as a resource (State picks 'resource' vs 'resource:base64' from bytes). Return no
|
|
327
|
-
// contents/loader: emission is esbuild's job via the build's own `loader` config.
|
|
328
|
-
if (!this.#seen.has(path)) {
|
|
329
|
-
this.#seen.add(path)
|
|
330
|
-
this.#state.addFile(pathToFileURL(path).toString(), { source, isEntry: pluginData?.isEntry, resource: true, reason: 'esbuild' })
|
|
331
|
-
}
|
|
332
|
-
return undefined
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Code-classified files must be UTF-8; refuse non-UTF-8 rather than silently encode as a resource.
|
|
336
|
-
assert.ok(isUtf8(source), `StasisEsbuild: code-classified file has non-UTF-8 bytes: ${path}`)
|
|
337
|
-
|
|
246
|
+
const resource = kind === 'resource'
|
|
247
|
+
if (!resource) assertCodeUtf8('StasisEsbuild', path, source)
|
|
338
248
|
if (!this.#seen.has(path)) {
|
|
339
249
|
this.#seen.add(path)
|
|
340
|
-
this.#state.addFile(pathToFileURL(path).toString(), { source, isEntry: pluginData?.isEntry, reason: 'esbuild' })
|
|
250
|
+
this.#state.addFile(pathToFileURL(path).toString(), { source, isEntry: pluginData?.isEntry, resource, reason: 'esbuild' })
|
|
341
251
|
}
|
|
342
|
-
|
|
343
|
-
return { contents: source, loader: this.#loaderFor(path) }
|
|
252
|
+
// A resource is emitted by esbuild, through the build's own `loader` config.
|
|
253
|
+
return resource ? undefined : { contents: source, loader: this.#loaderFor(path) }
|
|
344
254
|
})
|
|
345
255
|
}
|
|
346
256
|
}
|
|
@@ -2,30 +2,21 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
4
|
|
|
5
|
-
// Stands in for the project's Babel transformer
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// rewrite. This adds one plugin that records every specifier in its `pre` -- Babel runs every
|
|
10
|
-
// plugin's `pre` before any plugin's visitor, so nothing has rewritten one yet -- and reads each
|
|
11
|
-
// again once Babel is done, before Metro's own passes replace them in the AST, but one importing
|
|
12
|
-
// types alone that Babel erased. One a plugin consumed, gone from Babel's output with nothing left
|
|
13
|
-
// naming its module (`require('./messages.po')`, which @exodus/i18n's plugin-inline-po reads and
|
|
14
|
-
// inlines), it keeps apart. The real transformer runs as it would; its output is untouched.
|
|
5
|
+
// Stands in for the project's Babel transformer under ./metro-transformer.js, to keep each import as WRITTEN: Metro
|
|
6
|
+
// names a dependency by the specifier Babel leaves, so a rewrite (babel-plugin-module-resolver's) would leave it only
|
|
7
|
+
// that. An added plugin records each specifier in its `pre` (Babel runs every `pre` before any visitor) and reads it
|
|
8
|
+
// again once Babel is done; one a plugin consumed, with nothing left naming its module, is kept apart.
|
|
15
9
|
|
|
16
10
|
const require = createRequire(import.meta.url)
|
|
17
11
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// holds each one a plugin consumed, as Babel last left it.
|
|
12
|
+
// Per worker transform: `babelTransformerPath` the project's, `asWritten` each specifier Babel left -> the ones
|
|
13
|
+
// written, and `consumed` each one a plugin consumed, as Babel last left it.
|
|
21
14
|
export const transformContext = new AsyncLocalStorage()
|
|
22
15
|
|
|
23
|
-
// The nodes Metro's collectDependencies takes a specifier from
|
|
24
|
-
// below (`import()`, `require()`, `__prefetchImport()`, `require.<member>()`).
|
|
16
|
+
// The nodes Metro's collectDependencies takes a specifier from.
|
|
25
17
|
const DECLARATIONS = new Set(['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'])
|
|
26
18
|
const REQUIRE_MEMBERS = new Set(['context', 'resolveWeak', 'unstable_importMaybeSync'])
|
|
27
19
|
|
|
28
|
-
// Whether `node` is one of them.
|
|
29
20
|
function isDependency(node) {
|
|
30
21
|
if (node.type !== 'CallExpression') return DECLARATIONS.has(node.type)
|
|
31
22
|
const { callee } = node
|
|
@@ -36,8 +27,7 @@ function isDependency(node) {
|
|
|
36
27
|
callee.property.type === 'Identifier' && REQUIRE_MEMBERS.has(callee.property.name)
|
|
37
28
|
}
|
|
38
29
|
|
|
39
|
-
//
|
|
40
|
-
// such, which Metro's collectDependencies evaluates to the specifier as it does a literal.
|
|
30
|
+
// A literal, a template with no substitutions, or a `+` of such, which collectDependencies evaluates as a literal.
|
|
41
31
|
function constantString(node) {
|
|
42
32
|
if (node?.type === 'StringLiteral') return node.value
|
|
43
33
|
if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) return node.quasis[0].value.cooked ?? undefined
|
|
@@ -47,25 +37,19 @@ function constantString(node) {
|
|
|
47
37
|
return right === undefined ? undefined : left + right
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
function specifierOf(node) {
|
|
53
|
-
return constantString(node.type === 'CallExpression' ? node.arguments[0] : node.source)
|
|
54
|
-
}
|
|
40
|
+
// A plugin rewriting a specifier replaces the literal, never the node holding it.
|
|
41
|
+
const specifierOf = (node) => constantString(node.type === 'CallExpression' ? node.arguments[0] : node.source)
|
|
55
42
|
|
|
56
43
|
const TYPE_KINDS = new Set(['type', 'typeof'])
|
|
57
44
|
|
|
58
|
-
// A declaration of types alone, which Babel may erase
|
|
59
|
-
//
|
|
60
|
-
// whose every specifier is a type (`import { type A }`, which TypeScript's `onlyRemoveTypeImports`
|
|
61
|
-
// keeps as `import {} from`).
|
|
45
|
+
// A declaration of types alone, which Babel may erase: Flow's or TypeScript's `import type`, `export type ... from`,
|
|
46
|
+
// or one whose every specifier is a type (which TypeScript's `onlyRemoveTypeImports` keeps as `import {} from`).
|
|
62
47
|
function typesOnly(node) {
|
|
63
48
|
if (TYPE_KINDS.has(node.importKind ?? node.exportKind)) return true
|
|
64
49
|
const specifiers = node.specifiers ?? []
|
|
65
50
|
return specifiers.length > 0 && specifiers.every((specifier) => TYPE_KINDS.has(specifier.importKind ?? specifier.exportKind))
|
|
66
51
|
}
|
|
67
52
|
|
|
68
|
-
// The dependency nodes in Babel's output, and the specifiers they hold as constants.
|
|
69
53
|
function dependenciesLeft(t, ast) {
|
|
70
54
|
const nodes = new Set()
|
|
71
55
|
const specifiers = new Set()
|
|
@@ -92,8 +76,8 @@ function asWrittenPlugin({ types: t }) {
|
|
|
92
76
|
if (written !== undefined) slots.push({ node, written, types: node.type !== 'CallExpression' && typesOnly(node) })
|
|
93
77
|
else if (node.type === 'CallExpression' && node.arguments.length > 0) evaluate = true
|
|
94
78
|
})
|
|
95
|
-
// Metro takes
|
|
96
|
-
//
|
|
79
|
+
// Metro also takes an argument Babel evaluates to a string with confidence (`const name = './x'; require(name)`),
|
|
80
|
+
// which only a path, with its scope, can evaluate.
|
|
97
81
|
if (!evaluate) return
|
|
98
82
|
file.path.traverse({
|
|
99
83
|
CallExpression(path) {
|
|
@@ -104,10 +88,8 @@ function asWrittenPlugin({ types: t }) {
|
|
|
104
88
|
})
|
|
105
89
|
},
|
|
106
90
|
visitor: {
|
|
107
|
-
// Babel exits a node only if no plugin removed it on the way in, so a declaration
|
|
108
|
-
//
|
|
109
|
-
// plugin before the traversal gets there. One it kept (`import {} from` under
|
|
110
|
-
// `onlyRemoveTypeImports`) exits, still naming its module.
|
|
91
|
+
// Babel exits a node only if no plugin removed it on the way in, so a types-only declaration it erased never
|
|
92
|
+
// exits; one it kept (`import {} from`) does, still naming its module.
|
|
111
93
|
'ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration': {
|
|
112
94
|
exit(path) {
|
|
113
95
|
transformContext.getStore().kept.add(path.node)
|
|
@@ -123,12 +105,9 @@ export function transform(args) {
|
|
|
123
105
|
const slots = []
|
|
124
106
|
const kept = new Set()
|
|
125
107
|
Object.assign(context, { slots, kept })
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// the export-namespace-from transform), it names the same module, its specifier as rewritten
|
|
130
|
-
// before (module-resolver rewrites every one as the program enters). One gone from the output,
|
|
131
|
-
// with no node there naming its module, a plugin consumed in its stead.
|
|
108
|
+
// Each specifier as Babel left it in the node that held it as written, but an erased types-only declaration's. A
|
|
109
|
+
// node a plugin replaced (lowered to a require, or split in two) names the same module, its specifier as rewritten
|
|
110
|
+
// before; one gone from the output with no node there naming its module was consumed.
|
|
132
111
|
const collect = (result) => {
|
|
133
112
|
const left = result?.ast && context.types ? dependenciesLeft(context.types, result.ast) : null
|
|
134
113
|
for (const { node, written, types } of slots) {
|
|
@@ -144,7 +123,7 @@ export function transform(args) {
|
|
|
144
123
|
}
|
|
145
124
|
return result
|
|
146
125
|
}
|
|
147
|
-
// Sync or async, as the project's transformer is
|
|
126
|
+
// Sync or async, as the project's transformer is.
|
|
148
127
|
const result = require(context.babelTransformerPath).transform({ ...args, plugins: [...(args.plugins ?? []), asWrittenPlugin] })
|
|
149
128
|
return typeof result?.then === 'function' ? result.then(collect) : collect(result)
|
|
150
129
|
}
|