@exodus/stasis-plugins 1.0.0-beta.12 → 1.0.0-beta.13
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/metro-babel-transformer.js +28 -5
- package/src/metro-transformer.js +9 -5
- package/src/metro.js +54 -7
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.13",
|
|
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.13"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -30,11 +30,21 @@ function isDependencyCall(callee) {
|
|
|
30
30
|
callee.property.type === 'Identifier' && REQUIRE_MEMBERS.has(callee.property.name)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// The
|
|
34
|
-
//
|
|
33
|
+
// The string `node` holds as a constant: a literal, a template with no substitutions, or a `+` of
|
|
34
|
+
// such, which Metro's collectDependencies evaluates to the specifier as it does a literal.
|
|
35
|
+
function constantString(node) {
|
|
36
|
+
if (node?.type === 'StringLiteral') return node.value
|
|
37
|
+
if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) return node.quasis[0].value.cooked ?? undefined
|
|
38
|
+
if (node?.type !== 'BinaryExpression' || node.operator !== '+') return undefined
|
|
39
|
+
const left = constantString(node.left)
|
|
40
|
+
const right = left === undefined ? undefined : constantString(node.right)
|
|
41
|
+
return right === undefined ? undefined : left + right
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The specifier a dependency node holds as a constant: an import/export's source, a call's first
|
|
45
|
+
// argument. A plugin rewriting it replaces the literal, never the node holding it.
|
|
35
46
|
function specifierOf(node) {
|
|
36
|
-
|
|
37
|
-
return literal?.type === 'StringLiteral' ? literal.value : undefined
|
|
47
|
+
return constantString(node.type === 'CallExpression' ? node.arguments[0] : node.source)
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
function asWrittenPlugin({ types: t }) {
|
|
@@ -42,10 +52,22 @@ function asWrittenPlugin({ types: t }) {
|
|
|
42
52
|
name: 'stasis-as-written',
|
|
43
53
|
pre(file) {
|
|
44
54
|
const { slots } = transformContext.getStore()
|
|
55
|
+
let evaluate = false
|
|
45
56
|
t.traverseFast(file.ast, (node) => {
|
|
46
57
|
if (node.type === 'CallExpression' ? !isDependencyCall(node.callee) : !DECLARATIONS.has(node.type)) return
|
|
47
58
|
const written = specifierOf(node)
|
|
48
59
|
if (written !== undefined) slots.push({ node, written })
|
|
60
|
+
else if (node.type === 'CallExpression' && node.arguments.length > 0) evaluate = true
|
|
61
|
+
})
|
|
62
|
+
// Metro takes any argument Babel evaluates to a string with confidence, one bound to a constant
|
|
63
|
+
// too (`const name = './x'; require(name)`), which only a path, and its scope, can evaluate.
|
|
64
|
+
if (!evaluate) return
|
|
65
|
+
file.path.traverse({
|
|
66
|
+
CallExpression(path) {
|
|
67
|
+
if (!isDependencyCall(path.node.callee) || specifierOf(path.node) !== undefined || path.node.arguments.length === 0) return
|
|
68
|
+
const { confident, value } = path.get('arguments.0').evaluate()
|
|
69
|
+
if (confident && typeof value === 'string') slots.push({ node: path.node, written: value })
|
|
70
|
+
},
|
|
49
71
|
})
|
|
50
72
|
},
|
|
51
73
|
visitor: {
|
|
@@ -73,7 +95,8 @@ export function transform(args) {
|
|
|
73
95
|
const collect = (result) => {
|
|
74
96
|
for (const { node, written } of slots) {
|
|
75
97
|
if (DECLARATIONS.has(node.type) && !kept.has(node)) continue
|
|
76
|
-
|
|
98
|
+
// One evaluated through its scope stays as written: no plugin rewrites what isn't a literal.
|
|
99
|
+
const left = specifierOf(node) ?? (node.type === 'CallExpression' ? written : undefined)
|
|
77
100
|
if (left === undefined) continue
|
|
78
101
|
if (!context.asWritten.has(left)) context.asWritten.set(left, new Set())
|
|
79
102
|
context.asWritten.get(left).add(written)
|
package/src/metro-transformer.js
CHANGED
|
@@ -88,14 +88,18 @@ function resolveBabelTransformer(base, path) {
|
|
|
88
88
|
return isAbsolute(path) ? path : createRequire(base.file).resolve(path)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// Each dependency named by a specifier Babel rewrote
|
|
92
|
-
// Metro dependency can stand for several, written apart and rewritten
|
|
93
|
-
// `./src/abc`)
|
|
91
|
+
// Each dependency as the source wrote it. One named by a specifier Babel rewrote carries the ones
|
|
92
|
+
// written in the source: one Metro dependency can stand for several, written apart and rewritten
|
|
93
|
+
// alike (`#local/abc` and `./src/abc`). One no specifier in the source names carries
|
|
94
|
+
// `stasisInserted`: Babel added it (a helper from @babel/runtime, the JSX runtime), or Metro did (its
|
|
95
|
+
// asyncRequire for an `import()`, an asset's registry). A dependency written as Metro names it is
|
|
96
|
+
// left as is.
|
|
94
97
|
function withAsWritten(result, asWritten) {
|
|
95
|
-
if (
|
|
98
|
+
if (!Array.isArray(result?.dependencies)) return result
|
|
96
99
|
const dependencies = result.dependencies.map((dep) => {
|
|
97
100
|
const written = asWritten.get(dep.name)
|
|
98
|
-
if (written === undefined
|
|
101
|
+
if (written === undefined) return { ...dep, stasisInserted: true }
|
|
102
|
+
if (written.size === 1 && written.has(dep.name)) return dep
|
|
99
103
|
return { ...dep, stasisAsWritten: [...written] }
|
|
100
104
|
})
|
|
101
105
|
return { ...result, dependencies }
|
package/src/metro.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isUtf8 } from 'node:buffer'
|
|
|
3
3
|
import { spawnSync } from 'node:child_process'
|
|
4
4
|
import { existsSync } from 'node:fs'
|
|
5
5
|
import { createRequire } from 'node:module'
|
|
6
|
-
import { isAbsolute, join } from 'node:path'
|
|
6
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
8
8
|
|
|
9
9
|
import { resolvePluginState } from './plugins.js'
|
|
@@ -40,6 +40,23 @@ const AUTO_INCLUDES = [
|
|
|
40
40
|
'@react-native-community/cli/setup_env.sh',
|
|
41
41
|
]
|
|
42
42
|
|
|
43
|
+
// Metro's own transform plugins, as its transform worker names them for its cache key: the file
|
|
44
|
+
// `metro-transform-plugins` resolves to from the project, else through metro and its transform
|
|
45
|
+
// worker, which depend on it (a pnpm install links it only beside them). -> [] where none resolves
|
|
46
|
+
function metroTransformPluginFiles(projectDir) {
|
|
47
|
+
for (const chain of [['metro-transform-plugins'], ['metro', 'metro-transform-worker', 'metro-transform-plugins']]) {
|
|
48
|
+
let file
|
|
49
|
+
try {
|
|
50
|
+
for (const name of chain) file = require.resolve(name, { paths: [file === undefined ? projectDir : dirname(file)] })
|
|
51
|
+
} catch {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
const { getTransformPluginCacheKeyFiles } = require(file)
|
|
55
|
+
return typeof getTransformPluginCacheKeyFiles === 'function' ? getTransformPluginCacheKeyFiles() : []
|
|
56
|
+
}
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
// The worker-side toolchain that only Metro's transform workers load, named in both the
|
|
44
61
|
// --child-process assert and the transform-cache warning so the two can't describe different sets.
|
|
45
62
|
const WORKER_TOOLCHAIN = 'babel.config.js, @babel/core, the RN preset + plugins'
|
|
@@ -252,17 +269,18 @@ export class StasisMetro {
|
|
|
252
269
|
// AFTER the graph so #seen already holds graph modules -- an auto-include that landed in the
|
|
253
270
|
// graph (asyncRequire, when the app uses dynamic import()) is deduped, not re-recorded.
|
|
254
271
|
this.#captureAutoIncludes()
|
|
272
|
+
this.#captureTransformPlugins()
|
|
255
273
|
// AFTER the graph + auto-includes (same dedupe reason): the native pass only adds the native
|
|
256
274
|
// build-input surface (podspec/gradle/sources) that no module graph carries.
|
|
257
275
|
this.#captureNativeModules()
|
|
258
276
|
}
|
|
259
277
|
|
|
260
|
-
// Shared epilogue for the classify-driven capture paths: record under the given format/resource
|
|
261
|
-
// `format` null/undefined lets addFile pick (loader format for JS unless
|
|
262
|
-
// byte-derived for a resource); addFile already asserts UTF-8 for a
|
|
263
|
-
// no guard is needed here.
|
|
264
|
-
#recordCapture(full, source, { format, resource, inferFormat = true }) {
|
|
265
|
-
this.#state.addFile(pathToFileURL(full).toString(), { source, format: format ?? undefined, resource, inferFormat, reason
|
|
278
|
+
// Shared epilogue for the classify-driven capture paths: record under the given format/resource,
|
|
279
|
+
// attributed to `reason`. `format` null/undefined lets addFile pick (loader format for JS unless
|
|
280
|
+
// `inferFormat` is false, byte-derived for a resource); addFile already asserts UTF-8 for a
|
|
281
|
+
// non-resource (code) source, so no guard is needed here.
|
|
282
|
+
#recordCapture(full, source, { format, resource, inferFormat = true, reason = 'metro' }) {
|
|
283
|
+
this.#state.addFile(pathToFileURL(full).toString(), { source, format: format ?? undefined, resource, inferFormat, reason })
|
|
266
284
|
}
|
|
267
285
|
|
|
268
286
|
// Attest the AUTO_INCLUDES list. Per entry, skip: unresolvable, already-seen, or resolving
|
|
@@ -293,6 +311,31 @@ export class StasisMetro {
|
|
|
293
311
|
}
|
|
294
312
|
}
|
|
295
313
|
|
|
314
|
+
// Attest each of Metro's own transform plugins (metroTransformPluginFiles). Its transform worker
|
|
315
|
+
// hashes every one into its cache key, but loads one only as the build's options call for it
|
|
316
|
+
// (import-export-plugin under `experimentalImportSupport`, inline-requires-plugin under
|
|
317
|
+
// `inlineRequires`, constant-folding-plugin outside dev, normalizePseudoGlobals when minifying), so
|
|
318
|
+
// a capture loads them all, as Metro would: under `stasis run` the loader then records each with
|
|
319
|
+
// what it imports, so a bundle builds under any of those options. Each is recorded besides, as the
|
|
320
|
+
// Node module it is, where no loader records it, attributed to `run` as one Metro loads is: the
|
|
321
|
+
// build's toolchain, none of it the app's. One outside the project root is passed over.
|
|
322
|
+
#captureTransformPlugins() {
|
|
323
|
+
for (const file of metroTransformPluginFiles(this.#projectDir)) {
|
|
324
|
+
if (this.#seen.has(file)) continue
|
|
325
|
+
try {
|
|
326
|
+
this.#state.relative(file)
|
|
327
|
+
} catch {
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
330
|
+
this.#seen.add(file)
|
|
331
|
+
try {
|
|
332
|
+
require(file)
|
|
333
|
+
} catch { /* one that can't load can't run under Metro either: its bytes are still recorded */ }
|
|
334
|
+
// Real reader: plugin bookkeeping, not a program fs read -- must not be re-recorded by --fs.
|
|
335
|
+
this.#recordCapture(file, realReadFileSync(file), { reason: 'run' })
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
296
339
|
// Attest each native dependency's build-input surface. Discovery is `react-native config`; each
|
|
297
340
|
// dep root is walked (isSkippedNativeWalkDir pruned) and its native sources are attested as code,
|
|
298
341
|
// other assets as resources, plus each package.json. Node-runnable JS/TS is skipped (already in
|
|
@@ -386,9 +429,13 @@ export class StasisMetro {
|
|
|
386
429
|
// (`dep.data.name`) by the specifier Babel left; where a plugin rewrote it, the stasis transformer
|
|
387
430
|
// (./metro-transformer.js) carries the ones written in the source. Code and resource targets alike,
|
|
388
431
|
// as `stasis bundle --metro` and the esbuild plugin record them: an asset is resolved as code is.
|
|
432
|
+
// One the source doesn't write (`stasisInserted`: a Babel helper, the JSX runtime, Metro's
|
|
433
|
+
// asyncRequire) is no edge, as `stasis bundle --metro` has none: its file is carried, as every
|
|
434
|
+
// module of the graph is, and under load Metro resolves it unasserted.
|
|
389
435
|
#edges(module) {
|
|
390
436
|
const edges = []
|
|
391
437
|
for (const dep of module.dependencies?.values?.() ?? []) {
|
|
438
|
+
if (dep?.data?.stasisInserted === true) continue
|
|
392
439
|
const target = dep?.absolutePath
|
|
393
440
|
if (typeof target !== 'string' || !isAbsolute(target) || !existsSync(target)) continue
|
|
394
441
|
if (classifyExtension(target, this.#resources) === 'unknown') continue
|