@statorjs/stator 2.5.1 → 2.7.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/README.md +1 -1
- package/package.json +5 -5
- package/src/build/build.ts +93 -115
- package/src/build/head.ts +11 -4
- package/src/build/index.ts +9 -0
- package/src/build/islands.ts +228 -0
- package/src/build/source-id.ts +15 -0
- package/src/cli/commands/build.ts +11 -1
- package/src/cli/commands/dev.ts +7 -3
- package/src/cli/commands/start.ts +2 -1
- package/src/cli/run.ts +1 -1
- package/src/cli/stator.js +9 -0
- package/src/client/inspector.css +198 -0
- package/src/client/inspector.ts +330 -7
- package/src/compiler/diagnostics.ts +10 -0
- package/src/compiler/index.ts +2 -1
- package/src/compiler/regions.ts +30 -0
- package/src/engine/actor.ts +10 -3
- package/src/engine/describe.ts +129 -0
- package/src/engine/index.ts +7 -0
- package/src/engine/types.ts +24 -1
- package/src/server/create-app.ts +7 -1
- package/src/server/dev-loader.mjs +124 -0
- package/src/server/dev-native.ts +562 -0
- package/src/server/dev-vite.ts +362 -0
- package/src/server/dev.ts +44 -318
- package/src/server/discovery.ts +45 -1
- package/src/server/effects.ts +10 -1
- package/src/server/http.ts +29 -1
- package/src/server/index.ts +3 -0
- package/src/server/inspect.ts +101 -0
- package/src/server/machine-hash.ts +195 -0
- package/src/server/machine-store.ts +9 -2
- package/src/server/session-runtime.ts +9 -4
- package/src/server/snapshot-policy.ts +110 -0
- package/src/vite/index.ts +10 -4
- package/src/vite/plugin.ts +12 -32
- package/src/vite/stub.ts +4 -1
|
@@ -63,3 +63,13 @@ export function locAt(source: string, offset: number, file?: string): Diagnostic
|
|
|
63
63
|
const { line, column } = offsetToLineCol(source, offset)
|
|
64
64
|
return { file, line, column, frame: codeFrame(source, line, column) }
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/** Render a `CompileError` for a terminal or a dev overlay: the message, then
|
|
68
|
+
* `file:line:column` and the code frame when the error is located. Plain text
|
|
69
|
+
* on purpose — it is the one shape that survives crossing a loader/worker
|
|
70
|
+
* thread, where an Error's custom properties (`loc`) are dropped. */
|
|
71
|
+
export function formatCompileError(err: CompileError): string {
|
|
72
|
+
if (!err.loc) return err.message
|
|
73
|
+
const { file, line, column, frame } = err.loc
|
|
74
|
+
return `${err.message}\n\n${file ?? '<source>'}:${line}:${column}\n${frame}`
|
|
75
|
+
}
|
package/src/compiler/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ export type { DiagnosticLocation } from './diagnostics.ts'
|
|
|
29
29
|
export {
|
|
30
30
|
CompileError,
|
|
31
31
|
codeFrame,
|
|
32
|
+
formatCompileError,
|
|
32
33
|
locAt,
|
|
33
34
|
offsetToLineCol,
|
|
34
35
|
} from './diagnostics.ts'
|
|
@@ -36,7 +37,7 @@ export { generateDts } from './dts.ts'
|
|
|
36
37
|
export { scopeHash } from './hash.ts'
|
|
37
38
|
export type { LowerMeta, LowerOptions } from './lower.ts'
|
|
38
39
|
export { lowerTemplate } from './lower.ts'
|
|
39
|
-
export { componentImportSpecifier, declaredRegions } from './regions.ts'
|
|
40
|
+
export { componentImportSpecifier, declaredRegions, regionResolverFor } from './regions.ts'
|
|
40
41
|
export type { ParsedStator, ScannedRegions, SourceRegion } from './split.ts'
|
|
41
42
|
export { scanRegions, splitStator } from './split.ts'
|
|
42
43
|
export { scopeCss } from './styles.ts'
|
package/src/compiler/regions.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
1
3
|
import ts from 'typescript'
|
|
2
4
|
import { splitStator } from './split.ts'
|
|
3
5
|
|
|
@@ -77,3 +79,31 @@ export function componentImportSpecifier(
|
|
|
77
79
|
}
|
|
78
80
|
return null
|
|
79
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a region resolver for a file: maps a component identifier used in
|
|
85
|
+
* `file` to the named regions its imported `.stator` declares, so `compile()`
|
|
86
|
+
* can reject a `child="x"` the callee never declares. Reads sibling files
|
|
87
|
+
* synchronously (resolution happens mid-compile). Returns null when the
|
|
88
|
+
* identifier isn't a `.stator` default import or the file can't be read.
|
|
89
|
+
*
|
|
90
|
+
* Shared by every compile path — the Vite plugin, `buildApp`, and the native
|
|
91
|
+
* dev loader — so the check is identical in dev and prod.
|
|
92
|
+
*/
|
|
93
|
+
export function regionResolverFor(
|
|
94
|
+
file: string,
|
|
95
|
+
source: string,
|
|
96
|
+
): (componentName: string) => Set<string> | null {
|
|
97
|
+
const { frontmatter } = splitStator(source)
|
|
98
|
+
return (componentName) => {
|
|
99
|
+
const spec = componentImportSpecifier(frontmatter, componentName)
|
|
100
|
+
if (!spec) return null
|
|
101
|
+
const target = spec.startsWith('.') ? resolve(dirname(file), spec) : null
|
|
102
|
+
if (!target) return null
|
|
103
|
+
try {
|
|
104
|
+
return declaredRegions(readFileSync(target, 'utf8'))
|
|
105
|
+
} catch {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/engine/actor.ts
CHANGED
|
@@ -160,7 +160,8 @@ export function createActor<C extends object, E extends EventObject, S extends s
|
|
|
160
160
|
effectId,
|
|
161
161
|
kind: 'entry',
|
|
162
162
|
stateKey,
|
|
163
|
-
run: (signal) =>
|
|
163
|
+
run: (signal, session) =>
|
|
164
|
+
Promise.resolve(entry(ctxSnapshot, { effectId, signal, ...(session ? { session } : {}) })),
|
|
164
165
|
}
|
|
165
166
|
if (opts.onEffect) {
|
|
166
167
|
opts.onEffect(invocation)
|
|
@@ -289,8 +290,14 @@ export function createActor<C extends object, E extends EventObject, S extends s
|
|
|
289
290
|
machineName: def.name,
|
|
290
291
|
effectId,
|
|
291
292
|
kind: 'transition',
|
|
292
|
-
run: (signal) =>
|
|
293
|
-
Promise.resolve(
|
|
293
|
+
run: (signal, session) =>
|
|
294
|
+
Promise.resolve(
|
|
295
|
+
effect(ctxSnapshot, evSnapshot as never, {
|
|
296
|
+
effectId,
|
|
297
|
+
signal,
|
|
298
|
+
...(session ? { session } : {}),
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
294
301
|
}
|
|
295
302
|
if (opts.onEffect) {
|
|
296
303
|
opts.onEffect(invocation)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* describeMachine — a machine def serialized to plain data.
|
|
3
|
+
*
|
|
4
|
+
* The def already IS the statechart: states, per-state `on` maps, guards,
|
|
5
|
+
* effects, emits, selectors and `serverOnly` are all present at runtime (only
|
|
6
|
+
* the event-union *type* is erased). This walks that structure into a
|
|
7
|
+
* JSON-able description — the SHAPE half of introspection. Consumers: the dev
|
|
8
|
+
* inspector's machine view; later the manifest, `stator check`, and viz.
|
|
9
|
+
*
|
|
10
|
+
* Closures stay opaque by design: a guard/action/effect reports presence,
|
|
11
|
+
* never its body. The walk normalizes the three authored transition forms
|
|
12
|
+
* (bare action fn, config object, ordered candidate array) exactly as
|
|
13
|
+
* `actor.send` does.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
AnyMachineDef,
|
|
18
|
+
EventObject,
|
|
19
|
+
Lifecycle,
|
|
20
|
+
OnMap,
|
|
21
|
+
StateNode,
|
|
22
|
+
Transition,
|
|
23
|
+
TransitionConfig,
|
|
24
|
+
} from './types.ts'
|
|
25
|
+
|
|
26
|
+
export interface TransitionDescription {
|
|
27
|
+
/** Target state; absent = self-transition (action only, no state change). */
|
|
28
|
+
to?: string
|
|
29
|
+
guarded: boolean
|
|
30
|
+
action: boolean
|
|
31
|
+
emits: string[]
|
|
32
|
+
effect: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StateDescription {
|
|
36
|
+
/** Event type → ordered guarded candidates (first whose guard passes wins). */
|
|
37
|
+
on: Record<string, TransitionDescription[]>
|
|
38
|
+
entry: boolean
|
|
39
|
+
after: Array<{ delay: number | 'dynamic'; send: string }>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MachineDescription {
|
|
43
|
+
name: string
|
|
44
|
+
lifecycle: Lifecycle
|
|
45
|
+
persist: boolean
|
|
46
|
+
initial: string
|
|
47
|
+
states: Record<string, StateDescription>
|
|
48
|
+
/** Machine-level fallback handlers (consulted when the state declares none). */
|
|
49
|
+
on: Record<string, TransitionDescription[]>
|
|
50
|
+
/** Every event type the chart handles somewhere (state ∪ machine level), sorted. */
|
|
51
|
+
events: string[]
|
|
52
|
+
serverOnly: string[]
|
|
53
|
+
emits: string[]
|
|
54
|
+
selectors: string[]
|
|
55
|
+
reads: string[]
|
|
56
|
+
subscribes: Array<{ from: string; event: string; dispatch: string }>
|
|
57
|
+
/** The def's initial context (the shape a fresh instance starts from). */
|
|
58
|
+
context: unknown
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type AnyTransition = Transition<unknown, EventObject, string>
|
|
62
|
+
type AnyOnMap = OnMap<unknown, EventObject, string>
|
|
63
|
+
|
|
64
|
+
function describeTransitions(value: AnyTransition | AnyTransition[]): TransitionDescription[] {
|
|
65
|
+
const candidates = Array.isArray(value) ? value : [value]
|
|
66
|
+
return candidates.map((entry) => {
|
|
67
|
+
// A bare function is sugar for `{ do: fn }` — same normalization as actor.send.
|
|
68
|
+
const config: TransitionConfig<unknown, EventObject, string> =
|
|
69
|
+
typeof entry === 'function' ? { do: entry } : entry
|
|
70
|
+
const emits =
|
|
71
|
+
config.emit === undefined ? [] : Array.isArray(config.emit) ? [...config.emit] : [config.emit]
|
|
72
|
+
return {
|
|
73
|
+
...(config.to !== undefined ? { to: config.to } : {}),
|
|
74
|
+
guarded: config.when !== undefined,
|
|
75
|
+
action: config.do !== undefined,
|
|
76
|
+
emits,
|
|
77
|
+
effect: config.effect !== undefined,
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function describeOn(on: AnyOnMap | undefined): Record<string, TransitionDescription[]> {
|
|
83
|
+
const out: Record<string, TransitionDescription[]> = {}
|
|
84
|
+
for (const [type, value] of Object.entries(on ?? {})) {
|
|
85
|
+
if (value !== undefined) out[type] = describeTransitions(value as AnyTransition)
|
|
86
|
+
}
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Serialize a def to plain JSON-able data. Pure; never invokes app code. */
|
|
91
|
+
export function describeMachine(def: AnyMachineDef): MachineDescription {
|
|
92
|
+
const states: Record<string, StateDescription> = {}
|
|
93
|
+
for (const [name, node] of Object.entries(
|
|
94
|
+
def.states as Record<string, StateNode<unknown, EventObject, string>>,
|
|
95
|
+
)) {
|
|
96
|
+
states[name] = {
|
|
97
|
+
on: describeOn(node.on),
|
|
98
|
+
entry: node.entry !== undefined,
|
|
99
|
+
after: (node.after ?? []).map((a) => ({
|
|
100
|
+
delay: typeof a.delay === 'number' ? a.delay : 'dynamic',
|
|
101
|
+
send: a.send.type,
|
|
102
|
+
})),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const on = describeOn(def.on)
|
|
106
|
+
const events = new Set<string>(Object.keys(on))
|
|
107
|
+
for (const state of Object.values(states)) {
|
|
108
|
+
for (const type of Object.keys(state.on)) events.add(type)
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
name: def.name,
|
|
112
|
+
lifecycle: def.lifecycle,
|
|
113
|
+
persist: def.persist,
|
|
114
|
+
initial: def.initial,
|
|
115
|
+
states,
|
|
116
|
+
on,
|
|
117
|
+
events: [...events].sort(),
|
|
118
|
+
serverOnly: [...def.serverOnly],
|
|
119
|
+
emits: Object.keys(def.emits),
|
|
120
|
+
selectors: Object.keys(def.selectors),
|
|
121
|
+
reads: def.reads.map((d) => d.name),
|
|
122
|
+
subscribes: def.subscribes.map((s) => ({
|
|
123
|
+
from: s.from.name,
|
|
124
|
+
event: s.event,
|
|
125
|
+
dispatch: typeof s.dispatch === 'string' ? s.dispatch : s.dispatch.type,
|
|
126
|
+
})),
|
|
127
|
+
context: def.context,
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/engine/index.ts
CHANGED
|
@@ -10,6 +10,12 @@ export type { Actor, AnyActor, CreateActorOptions } from './actor.ts'
|
|
|
10
10
|
export { createActor } from './actor.ts'
|
|
11
11
|
export type { DefineMachineConfig } from './define-machine.ts'
|
|
12
12
|
export { defineMachine } from './define-machine.ts'
|
|
13
|
+
export type {
|
|
14
|
+
MachineDescription,
|
|
15
|
+
StateDescription,
|
|
16
|
+
TransitionDescription,
|
|
17
|
+
} from './describe.ts'
|
|
18
|
+
export { describeMachine } from './describe.ts'
|
|
13
19
|
export type {
|
|
14
20
|
Action,
|
|
15
21
|
ActionHelpers,
|
|
@@ -19,6 +25,7 @@ export type {
|
|
|
19
25
|
Effect,
|
|
20
26
|
EffectInvocation,
|
|
21
27
|
EffectMeta,
|
|
28
|
+
EffectSession,
|
|
22
29
|
EmitDeclaration,
|
|
23
30
|
EmitsConfig,
|
|
24
31
|
EntryEffect,
|
package/src/engine/types.ts
CHANGED
|
@@ -61,6 +61,18 @@ export type Guard<C, E extends EventObject, R = Record<string, any>> = (
|
|
|
61
61
|
export interface EffectMeta {
|
|
62
62
|
effectId: string
|
|
63
63
|
signal?: AbortSignal
|
|
64
|
+
/** The session this effect runs for — set by the server host for SESSION
|
|
65
|
+
* machines only (app machines and client islands have none). Lets an entry
|
|
66
|
+
* effect reload a durable fact by identity after a fresh start or a
|
|
67
|
+
* snapshot reset, with no client round trip: `loadCart(meta.session.claims<Me>().userId)`. */
|
|
68
|
+
session?: EffectSession
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The session an effect runs for. `claims` mirrors `stator(c).claims<T>()`:
|
|
72
|
+
* the app-defined claims as of the moment the effect started, or `undefined`. */
|
|
73
|
+
export interface EffectSession {
|
|
74
|
+
readonly id: string
|
|
75
|
+
claims<T = unknown>(): T | undefined
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
/**
|
|
@@ -127,7 +139,9 @@ export interface EffectInvocation {
|
|
|
127
139
|
kind: 'entry' | 'transition'
|
|
128
140
|
/** The owning state — set for entry effects, for exit-abort targeting. */
|
|
129
141
|
stateKey?: string
|
|
130
|
-
|
|
142
|
+
/** Run the effect. The host passes the abort signal and, for session
|
|
143
|
+
* machines, the session — merged into the effect's `meta`. */
|
|
144
|
+
run: (signal?: AbortSignal, session?: EffectSession) => Promise<EventObject | null>
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
/** Object form of a transition. A bare `Action` is sugar for `{ do: fn }`.
|
|
@@ -216,6 +230,15 @@ export interface Snapshot<C> {
|
|
|
216
230
|
* effect (the load role is re-runnable by contract) instead of leaving the
|
|
217
231
|
* machine wedged waiting for a completion that died with another process. */
|
|
218
232
|
pendingEntry?: { effectId: string }
|
|
233
|
+
/** Snapshot format version, stamped by the host on persist. Absent ⇒ the
|
|
234
|
+
* original (current) format. Lets the shape itself evolve (hierarchy in
|
|
235
|
+
* `value`) with a known-old snapshot recognisable as such. */
|
|
236
|
+
format?: number
|
|
237
|
+
/** Code hash of the machine that wrote this snapshot, stamped by the host on
|
|
238
|
+
* persist. Hydration rejects a snapshot whose hash differs from the running
|
|
239
|
+
* machine's — sessions never outlive the code that made them. Absent
|
|
240
|
+
* (pre-existing snapshots) ⇒ mismatch. */
|
|
241
|
+
code?: string
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
export type Lifecycle = 'app' | 'session'
|
package/src/server/create-app.ts
CHANGED
|
@@ -84,6 +84,10 @@ export interface CreateAppConfig extends DeprecatedFlatConfig {
|
|
|
84
84
|
/** Build identifier for the deploy-aware reload handshake. `stator start`
|
|
85
85
|
* passes the id baked into the build manifest; absent → no handshake. */
|
|
86
86
|
buildId?: string
|
|
87
|
+
/** Machine code hashes from the build manifest (`loadProductionHead(dist).machines`),
|
|
88
|
+
* keyed by file relative to `machinesDir`. Omitted ⇒ hashed live at boot;
|
|
89
|
+
* a discovered machine missing from a supplied map is a boot error. */
|
|
90
|
+
machineHashes?: Readonly<Record<string, string>>
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
export interface StatorApp {
|
|
@@ -120,7 +124,9 @@ export async function createApp(config: CreateAppConfig): Promise<StatorApp> {
|
|
|
120
124
|
// the scoped children (http/sse/…), which a bare `logger.level =` would miss.
|
|
121
125
|
setLogLevel(process.env.LOG_LEVEL ?? resolved.logLevel ?? 'warn')
|
|
122
126
|
setSessionSameSite(resolved.sameSite ?? 'Lax')
|
|
123
|
-
const { defs } = await discoverMachines(machinesDir
|
|
127
|
+
const { defs } = await discoverMachines(machinesDir, undefined, {
|
|
128
|
+
...(config.machineHashes ? { hashes: config.machineHashes } : {}),
|
|
129
|
+
})
|
|
124
130
|
const sessionStore = resolved.session ?? new InMemoryStore()
|
|
125
131
|
const store = new MachineStore(defs, sessionStore, {
|
|
126
132
|
sessionTtlSeconds: resolved.sessionTtlSeconds,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Native dev-server module loader — the Vite exit's dev half. Three jobs, all
|
|
2
|
+
// on Node's module-customization hooks thread:
|
|
3
|
+
//
|
|
4
|
+
// 1. `.stator` compile-on-load: a `.stator` import is read from the SOURCE tree,
|
|
5
|
+
// compiled by the framework compiler (server render module), and type-
|
|
6
|
+
// stripped by esbuild. No mirror, no on-disk `.stator.ts` — the server runs
|
|
7
|
+
// from the app's real files, so `import.meta.url`-relative paths (a SQLite
|
|
8
|
+
// file, a data dir) mean the same thing in dev as in the app's source.
|
|
9
|
+
// 2. TS-on-import, like the CLI's shipped `cli/loader.js`.
|
|
10
|
+
// 3. App-scoped `?v=` propagation — the one piece the prod loader lacks.
|
|
11
|
+
//
|
|
12
|
+
// Invalidation model — importer-only: every app module is imported at
|
|
13
|
+
// `?v=<its own version>`. The dev server keeps a per-file version map, bumps
|
|
14
|
+
// a changed file AND its transitive importers (a static reverse import graph),
|
|
15
|
+
// and pushes the new versions here over a MessagePort before re-importing
|
|
16
|
+
// entries. `resolve` stamps each app-local child with its current version, so
|
|
17
|
+
// a changed subtree re-evaluates fresh while everything else keeps its cached
|
|
18
|
+
// module instance — a `lib/db.ts` that opens a connection at top level runs
|
|
19
|
+
// once per session, not once per edit, and the ESM registry (which never
|
|
20
|
+
// releases a module) grows by the affected subtree instead of the whole app.
|
|
21
|
+
//
|
|
22
|
+
// The versioning boundary is CRITICAL: it applies only to files under the app
|
|
23
|
+
// root (`initialize({ appDir })`) and never inside a `node_modules` directory.
|
|
24
|
+
// Versioning framework internals would fork their singletons (render context,
|
|
25
|
+
// registries) into a second instance and break `read()` — the same dual-
|
|
26
|
+
// instance hazard as the old Vite fence, from the other side. Both checks are
|
|
27
|
+
// needed: under pnpm the framework realpaths OUT of node_modules (so the app-
|
|
28
|
+
// root allowlist catches it), under a flat install it sits INSIDE the app root
|
|
29
|
+
// (so the node_modules denylist catches it).
|
|
30
|
+
//
|
|
31
|
+
// This module imports the TypeScript compiler source, so it must be registered
|
|
32
|
+
// after a TS loader (the dev server registers `cli/loader.js` first).
|
|
33
|
+
import { readFile } from 'node:fs/promises'
|
|
34
|
+
import { sep } from 'node:path'
|
|
35
|
+
import { fileURLToPath } from 'node:url'
|
|
36
|
+
import { transform } from 'esbuild'
|
|
37
|
+
import { sourceId } from '../build/source-id.ts'
|
|
38
|
+
import { CompileError, compile, formatCompileError, regionResolverFor } from '../compiler/index.ts'
|
|
39
|
+
|
|
40
|
+
const TS = /\.(?:ts|tsx|mts|cts)$/
|
|
41
|
+
const STATOR = /\.stator$/
|
|
42
|
+
/** Extensions whose app-local imports carry the build version. */
|
|
43
|
+
const VERSIONED = /\.(?:stator|ts|tsx|mts|cts|js|mjs)$/
|
|
44
|
+
|
|
45
|
+
/** Absolute (real) app root under which versioning applies. Unset ⇒ this loader
|
|
46
|
+
* behaves like the prod TS loader plus `.stator` compile (no versioning). */
|
|
47
|
+
let appDir
|
|
48
|
+
/** Absolute file path → current version, pushed by the dev server. */
|
|
49
|
+
const versions = new Map()
|
|
50
|
+
|
|
51
|
+
export async function initialize(data) {
|
|
52
|
+
appDir = data?.appDir
|
|
53
|
+
const port = data?.port
|
|
54
|
+
if (!port) return
|
|
55
|
+
port.on('message', (msg) => {
|
|
56
|
+
if (msg?.type !== 'versions') return
|
|
57
|
+
for (const [file, v] of msg.entries) versions.set(file, v)
|
|
58
|
+
port.postMessage({ type: 'ack', id: msg.id })
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const inApp = (path) =>
|
|
63
|
+
Boolean(appDir) && path.startsWith(appDir + sep) && !path.includes(`${sep}node_modules${sep}`)
|
|
64
|
+
|
|
65
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
66
|
+
const result = await nextResolve(specifier, context)
|
|
67
|
+
const url = new URL(result.url)
|
|
68
|
+
const own = TS.test(url.pathname) || STATOR.test(url.pathname)
|
|
69
|
+
const format = own ? 'module' : result.format
|
|
70
|
+
if (!VERSIONED.test(url.pathname) || format !== 'module') return result
|
|
71
|
+
|
|
72
|
+
// Stamp an app-local module with its current version (entries arrive already
|
|
73
|
+
// stamped by the dev server; children get theirs here).
|
|
74
|
+
if (url.protocol === 'file:' && !url.searchParams.has('v')) {
|
|
75
|
+
const path = fileURLToPath(url)
|
|
76
|
+
if (inApp(path)) {
|
|
77
|
+
url.searchParams.set('v', String(versions.get(path) ?? 1))
|
|
78
|
+
return { url: url.href, format, shortCircuit: true }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return own ? { ...result, format, shortCircuit: true } : result
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function load(url, context, nextLoad) {
|
|
85
|
+
if (!url.startsWith('file:')) return nextLoad(url, context)
|
|
86
|
+
// `fileURLToPath` ignores the `?v=` query, so cache-busted URLs read the real file.
|
|
87
|
+
const path = fileURLToPath(url)
|
|
88
|
+
|
|
89
|
+
if (STATOR.test(path)) {
|
|
90
|
+
const source = await readFile(path, 'utf8')
|
|
91
|
+
const { id, kind } = appDir
|
|
92
|
+
? sourceId(appDir, path)
|
|
93
|
+
: { id: path, kind: /[\\/]routes[\\/]/.test(path) ? 'route' : 'component' }
|
|
94
|
+
let result
|
|
95
|
+
try {
|
|
96
|
+
result = compile(source, { id, kind, resolveRegions: regionResolverFor(path, source) })
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// Errors cross back to the main thread as plain Errors (custom props are
|
|
99
|
+
// dropped), so bake file:line:column + the code frame into the message.
|
|
100
|
+
throw err instanceof CompileError ? new Error(formatCompileError(err)) : err
|
|
101
|
+
}
|
|
102
|
+
const { code } = await transform(result.serverCode, {
|
|
103
|
+
loader: 'ts',
|
|
104
|
+
format: 'esm',
|
|
105
|
+
target: 'node20',
|
|
106
|
+
sourcemap: 'inline',
|
|
107
|
+
sourcefile: path,
|
|
108
|
+
})
|
|
109
|
+
return { format: 'module', source: code, shortCircuit: true }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (TS.test(path)) {
|
|
113
|
+
const source = await readFile(path, 'utf8')
|
|
114
|
+
const { code } = await transform(source, {
|
|
115
|
+
loader: path.endsWith('x') ? 'tsx' : 'ts',
|
|
116
|
+
format: 'esm',
|
|
117
|
+
target: 'node20',
|
|
118
|
+
sourcemap: 'inline',
|
|
119
|
+
sourcefile: path,
|
|
120
|
+
})
|
|
121
|
+
return { format: 'module', source: code, shortCircuit: true }
|
|
122
|
+
}
|
|
123
|
+
return nextLoad(url, context)
|
|
124
|
+
}
|