@exodus/stasis-plugins 1.0.0-beta.8 → 1.0.0-rc.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/package.json +6 -3
- package/src/esbuild.js +168 -115
- package/src/metro-babel-transformer.js +129 -0
- package/src/metro-native.js +201 -0
- package/src/metro-transformer.js +68 -55
- package/src/metro.js +258 -282
- package/src/plugins.js +102 -111
- package/src/rollup.js +64 -188
- package/src/webpack.js +61 -181
package/src/plugins.js
CHANGED
|
@@ -1,49 +1,54 @@
|
|
|
1
|
-
// Plugin <-> preload coordination (resolvePluginState): decides which State a bundler plugin
|
|
2
|
-
// drives, given its options and the ambient preload.
|
|
3
|
-
|
|
4
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import { isUtf8 } from 'node:buffer'
|
|
5
3
|
|
|
6
|
-
import { DEFAULT_LOCK,
|
|
4
|
+
import { DEFAULT_LOCK, assertKnownKeys, assertOption } from '@exodus/stasis-core/config'
|
|
7
5
|
import { ensureStateForLoader, isLoaderInstalled } from '@exodus/stasis-core/hooks'
|
|
8
6
|
import { State } from '@exodus/stasis-core/state'
|
|
9
|
-
import { extSetsEqual, parseResourcesOption } from '@exodus/stasis-core/util'
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
import { classifyExtension, extSetsEqual, parseResourcesOption } from '@exodus/stasis-core/util'
|
|
8
|
+
|
|
9
|
+
// The Config options a plugin takes; not `lockFile`, as the lockfile is always the preload's.
|
|
10
|
+
const PLUGIN_OPTION_KEYS = ['scope', 'lock', 'bundle', 'bundleFile', 'resourcesBundleFile', 'debug', 'childProcess', 'packageJSON', 'resources']
|
|
11
|
+
|
|
12
|
+
// Validated as Config validates them, without constructing one (which has side effects).
|
|
13
|
+
export function validatePluginOptions(label, options) {
|
|
14
|
+
assertKnownKeys(Object.keys(options), PLUGIN_OPTION_KEYS, label)
|
|
15
|
+
for (const key of PLUGIN_OPTION_KEYS) {
|
|
16
|
+
if (options[key] === undefined) continue
|
|
17
|
+
if (key === 'resources') parseResourcesOption(label, options.resources)
|
|
18
|
+
else assertOption(key, options[key])
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The active Config is authoritative; `resources`, a Set, is coordinated in resolvePluginState instead.
|
|
23
|
+
export function assertOptionsMatchConfig(config, options) {
|
|
24
|
+
try {
|
|
25
|
+
for (const key of PLUGIN_OPTION_KEYS) {
|
|
26
|
+
if (key !== 'resources' && options[key] !== undefined) assert.equal(config[key === 'bundle' ? 'bundleMode' : key], options[key])
|
|
27
|
+
}
|
|
28
|
+
} catch (cause) {
|
|
29
|
+
throw new Error(`Plugin options conflict with active stasis state: ${cause.message}`, { cause })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The State a bundler plugin runs against, given its options and the ambient preload: { state: null, isNoop: true }
|
|
34
|
+
// (inert: register no hooks), else { state, isNoop: false }. The lockfile is always the preload's; only the bundle
|
|
35
|
+
// may split off, to a sidecar State at its own bundleFile.
|
|
23
36
|
export function resolvePluginState(label, options, cwd) {
|
|
24
37
|
validatePluginOptions(label, options)
|
|
25
|
-
const {
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// stasis-core copy -- that would mint a phantom State.preload racing the real loader's write.
|
|
38
|
+
const { lock, bundle, bundleFile, resourcesBundleFile, resources } = options
|
|
39
|
+
// The loader is installed but its lazy init hasn't fired (webpack builds plugins before user code). Never for a
|
|
40
|
+
// separate stasis-core copy: that would mint a phantom State.preload racing the real loader's write.
|
|
29
41
|
if (!State.preload && isLoaderInstalled()) ensureStateForLoader()
|
|
30
42
|
const ambient = State.preload
|
|
31
43
|
|
|
32
44
|
if (!ambient) {
|
|
33
|
-
//
|
|
34
|
-
// process-wide signal regardless of which stasis-core copy hosts this code.
|
|
45
|
+
// `stasis run` always exports EXODUS_STASIS_LOCK, whichever stasis-core copy hosts this code.
|
|
35
46
|
const underStasisRun = process.env.EXODUS_STASIS_LOCK !== undefined
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// plugin sits inertly in a plain build. Capture is governed elsewhere (env or explicit options).
|
|
39
|
-
if (!underStasisRun && lock === undefined && bundle === undefined) {
|
|
47
|
+
// Inert in a plain build, as where both are 'none' (a framework plugin switched off by env).
|
|
48
|
+
if ((!underStasisRun && lock === undefined && bundle === undefined) || (lock === 'none' && bundle === 'none')) {
|
|
40
49
|
return { state: null, isNoop: true }
|
|
41
50
|
}
|
|
42
|
-
|
|
43
|
-
if (lock === 'none' && bundle === 'none') return { state: null, isNoop: true } // Rule 7
|
|
44
|
-
// A partial opt-out (one field 'none', the other omitted) would otherwise hit a misleading
|
|
45
|
-
// Rule 1 throw or Config's generic error; catch it explicitly. An active bundle mode paired
|
|
46
|
-
// with lock='none' is a legitimate "bundle as trust root" config, so only fire when the other is undefined.
|
|
51
|
+
// A partial opt-out would otherwise meet a misleading throw; an active bundle with lock='none' is legitimate.
|
|
47
52
|
if ((lock === 'none' && bundle === undefined) || (bundle === 'none' && lock === undefined)) {
|
|
48
53
|
throw new Error(
|
|
49
54
|
`${label}: to opt out of stasis without a preload, pass BOTH lock='none' and bundle='none' ` +
|
|
@@ -51,14 +56,10 @@ export function resolvePluginState(label, options, cwd) {
|
|
|
51
56
|
`bundle=${bundle === undefined ? 'default' : `'${bundle}'`})`
|
|
52
57
|
)
|
|
53
58
|
}
|
|
54
|
-
// A writing
|
|
55
|
-
// (whose loader gives full coverage). `lock` may be undefined here if only a writing `bundle`
|
|
56
|
-
// was passed, so the throw below distinguishes a defaulted mode from an explicit one.
|
|
59
|
+
// A writing or verifying lock would miss the bundler's own deps without a preload, but under `stasis run`.
|
|
57
60
|
const effectiveLock = lock ?? DEFAULT_LOCK
|
|
58
61
|
if (!underStasisRun && effectiveLock !== 'none' && effectiveLock !== 'ignore') {
|
|
59
|
-
const phrasing = lock === undefined
|
|
60
|
-
? `the default lockfile mode ('${DEFAULT_LOCK}')`
|
|
61
|
-
: `lockfile mode '${lock}'`
|
|
62
|
+
const phrasing = lock === undefined ? `the default lockfile mode ('${DEFAULT_LOCK}')` : `lockfile mode '${lock}'`
|
|
62
63
|
throw new Error(
|
|
63
64
|
`${label}: ${phrasing} requires a stasis preload; ` +
|
|
64
65
|
`pass lock='none' or lock='ignore' to opt out, or run under the stasis loader`
|
|
@@ -67,26 +68,15 @@ export function resolvePluginState(label, options, cwd) {
|
|
|
67
68
|
return { state: new State(cwd, options), isNoop: false }
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
//
|
|
71
|
+
// These must agree with the preload, which a reuse adopts and a sidecar copies.
|
|
71
72
|
const pc = ambient.config
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
throw new Error(`${label}: scope='${scope}' conflicts with active preload scope='${pc.scope}'`)
|
|
78
|
-
}
|
|
79
|
-
if (debug !== undefined && debug !== pc.debug) {
|
|
80
|
-
throw new Error(`${label}: debug=${debug} conflicts with active preload debug=${pc.debug}`)
|
|
81
|
-
}
|
|
82
|
-
if (childProcess !== undefined && childProcess !== pc.childProcess) {
|
|
83
|
-
throw new Error(`${label}: childProcess=${childProcess} conflicts with active preload childProcess=${pc.childProcess}`)
|
|
84
|
-
}
|
|
85
|
-
if (packageJSON !== undefined && packageJSON !== pc.packageJSON) {
|
|
86
|
-
throw new Error(`${label}: packageJSON=${packageJSON} conflicts with active preload packageJSON=${pc.packageJSON}`)
|
|
73
|
+
const shown = (value) => (typeof value === 'string' ? `'${value}'` : value)
|
|
74
|
+
for (const key of ['lock', 'scope', 'debug', 'childProcess', 'packageJSON']) {
|
|
75
|
+
if (options[key] !== undefined && options[key] !== pc[key]) {
|
|
76
|
+
throw new Error(`${label}: ${key}=${shown(options[key])} conflicts with active preload ${key}=${shown(pc[key])}`)
|
|
77
|
+
}
|
|
87
78
|
}
|
|
88
|
-
// resources is process-wide, so
|
|
89
|
-
// parsed sets (order/dupes/case-insensitive) to prevent one plugin silently widening the asset set.
|
|
79
|
+
// resources is process-wide: compared as parsed sets, so that no plugin silently widens the asset set.
|
|
90
80
|
if (resources !== undefined) {
|
|
91
81
|
const parsed = parseResourcesOption(label, resources)
|
|
92
82
|
if (!extSetsEqual(parsed, pc.resources)) {
|
|
@@ -96,74 +86,75 @@ export function resolvePluginState(label, options, cwd) {
|
|
|
96
86
|
}
|
|
97
87
|
}
|
|
98
88
|
|
|
99
|
-
//
|
|
100
|
-
// fs-READ during the build from genuine run dependencies recorded before any plugin existed.
|
|
89
|
+
// So the preload's `reason` map can tell files merely fs-read during the build from the run's own dependencies.
|
|
101
90
|
ambient.markPluginAttached()
|
|
102
91
|
|
|
92
|
+
// Sidecars skip stasis.config.json discovery, so carry the preload's settings over.
|
|
93
|
+
const sidecar = (mode) => {
|
|
94
|
+
const inherited = Object.fromEntries(['scope', 'lock', 'debug', 'childProcess', 'packageJSON', 'brotliQuality'].map((key) => [key, pc[key]]))
|
|
95
|
+
const state = new State(cwd, { parent: ambient, ...inherited, bundle: mode, bundleFile, resourcesBundleFile, resources: [...pc.resources] })
|
|
96
|
+
return { state, isNoop: false }
|
|
97
|
+
}
|
|
98
|
+
|
|
103
99
|
if (pc.bundle) {
|
|
104
|
-
// Preload has bundle on -- plugin can't disable, must match mode.
|
|
105
|
-
if (bundle === 'none' || bundle === 'ignore') {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`${label}: bundle='${bundle}' conflicts with active preload bundle='${pc.bundleMode}'`
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
100
|
if (bundle !== undefined && bundle !== pc.bundleMode) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
`${label}: bundle='${bundle}' conflicts with active preload bundle='${pc.bundleMode}'`
|
|
113
|
-
)
|
|
101
|
+
throw new Error(`${label}: bundle='${bundle}' conflicts with active preload bundle='${pc.bundleMode}'`)
|
|
114
102
|
}
|
|
115
|
-
if (bundleFile
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return { state: ambient, isNoop: false }
|
|
119
|
-
}
|
|
120
|
-
// Rule 6: different path -- sidecar inherits preload's modes, overrides bundleFile.
|
|
121
|
-
const sidecar = new State(cwd, {
|
|
122
|
-
parent: ambient,
|
|
123
|
-
scope: pc.scope,
|
|
124
|
-
lock: pc.lock,
|
|
125
|
-
bundle: pc.bundleMode,
|
|
126
|
-
bundleFile,
|
|
127
|
-
resourcesBundleFile,
|
|
128
|
-
debug: pc.debug,
|
|
129
|
-
childProcess: pc.childProcess,
|
|
130
|
-
packageJSON: pc.packageJSON,
|
|
131
|
-
resources: [...pc.resources],
|
|
132
|
-
// carried over: sidecars skip stasis.config.json discovery
|
|
133
|
-
brotliQuality: pc.brotliQuality,
|
|
134
|
-
})
|
|
135
|
-
return { state: sidecar, isNoop: false }
|
|
103
|
+
if (bundleFile !== undefined && bundleFile !== pc.bundleFile) return sidecar(pc.bundleMode)
|
|
104
|
+
assertOptionsMatchConfig(pc, options)
|
|
105
|
+
return { state: ambient, isNoop: false }
|
|
136
106
|
}
|
|
137
107
|
|
|
138
|
-
// Preload exists but has no bundle.
|
|
139
108
|
if (bundle === undefined || bundle === 'none' || bundle === 'ignore') {
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// the assert pins that strip to today's no-bundle values so a future mode can't silently mask a disagreement.
|
|
109
|
+
// 'ignore' and the preload's 'none' both mean no bundle, so the field leaves the cross-check; the assert pins
|
|
110
|
+
// that to today's no-bundle modes, so that a future one can't silently mask a disagreement.
|
|
143
111
|
assert.ok(pc.bundleMode === 'none' || pc.bundleMode === 'ignore',
|
|
144
112
|
`unexpected preload bundleMode='${pc.bundleMode}' in the no-bundle branch`)
|
|
145
113
|
const { bundle: _b, ...rest } = options
|
|
146
114
|
assertOptionsMatchConfig(pc, rest)
|
|
147
115
|
return { state: ambient, isNoop: false }
|
|
148
116
|
}
|
|
149
|
-
// Plugin wants its own bundle alongside preload's lockfile -- sidecar.
|
|
150
117
|
if (!bundleFile) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
|
|
153
|
-
|
|
118
|
+
throw new Error(`${label}: bundle='${bundle}' alongside a preload without bundle requires an explicit bundleFile`)
|
|
119
|
+
}
|
|
120
|
+
return sidecar(bundle)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// A State the caller owns (`stasis build`'s), else resolvePluginState's; a foreign copy's State fails the instanceof.
|
|
124
|
+
export const ownOrResolvedState = (label, options) =>
|
|
125
|
+
(options instanceof State ? options : resolvePluginState(label, options, process.cwd()).state)
|
|
126
|
+
|
|
127
|
+
// Dedupe is by path, so a rebuild with new bytes would emit them while the bundle and lockfile keep the old ones.
|
|
128
|
+
export const rebuildRefused = (label) =>
|
|
129
|
+
`${label}: watch/rebuild is not supported for capture -- run a one-shot build (rebuilds would silently attest stale content)`
|
|
130
|
+
|
|
131
|
+
export const unsupportedExtension = (label, what) =>
|
|
132
|
+
`${label}: unsupported extension for ${what} -- add its extension or filename to the plugin's resources option or stop importing it`
|
|
133
|
+
|
|
134
|
+
// A file's kind (code|resource), or a throw naming it as `what` where it's neither.
|
|
135
|
+
export function classifyOrThrow(label, path, resources, what = `'${path}'`) {
|
|
136
|
+
const kind = classifyExtension(path, resources)
|
|
137
|
+
if (kind === 'unknown') throw new Error(unsupportedExtension(label, what))
|
|
138
|
+
return kind
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Refuse non-UTF-8 code rather than silently encode it as a resource.
|
|
142
|
+
export function assertCodeUtf8(label, path, source) {
|
|
143
|
+
assert.ok(isUtf8(source), `${label}: code-classified file has non-UTF-8 bytes: ${path}`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Only the `type` import attribute persists to the lockfile.
|
|
147
|
+
export function assertPersistableAttributes(attrs) {
|
|
148
|
+
for (const k of Object.keys(attrs)) assert.equal(k, 'type', `unsupported import attribute: ${k}`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// `state.getImport(...)`, or undefined where the bundle attests no such edge: an EXTERNAL at capture, which the
|
|
152
|
+
// bundler resolves as without us (file BYTES still fail closed where the plugin loads them).
|
|
153
|
+
export function attestedImport(state, parentURL, specifier, options) {
|
|
154
|
+
try {
|
|
155
|
+
return state.getImport(parentURL, specifier, options)
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err?.code === 'ERR_MODULE_NOT_FOUND') return undefined
|
|
158
|
+
throw err
|
|
154
159
|
}
|
|
155
|
-
const sidecar = new State(cwd, {
|
|
156
|
-
parent: ambient,
|
|
157
|
-
scope: pc.scope,
|
|
158
|
-
lock: pc.lock,
|
|
159
|
-
bundle,
|
|
160
|
-
bundleFile,
|
|
161
|
-
resourcesBundleFile,
|
|
162
|
-
debug: pc.debug,
|
|
163
|
-
childProcess: pc.childProcess,
|
|
164
|
-
packageJSON: pc.packageJSON,
|
|
165
|
-
resources: [...pc.resources],
|
|
166
|
-
brotliQuality: pc.brotliQuality, // carried over, like the Rule 6 sidecar above
|
|
167
|
-
})
|
|
168
|
-
return { state: sidecar, isNoop: false }
|
|
169
160
|
}
|