@statorjs/stator 2.5.0 → 2.6.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/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/index.ts +1 -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 +561 -0
- package/src/server/dev-vite.ts +361 -0
- package/src/server/dev.ts +44 -313
- package/src/server/discovery.ts +45 -1
- package/src/server/effects.ts +10 -1
- package/src/server/http.ts +7 -1
- package/src/server/index.ts +1 -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
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { createServer as createHttpServer } from 'node:http'
|
|
5
|
+
import { relative, resolve } from 'node:path'
|
|
6
|
+
import { getRequestListener } from '@hono/node-server'
|
|
7
|
+
import type { Hono } from 'hono'
|
|
8
|
+
import { createServer as createViteServer, type ViteDevServer } from 'vite'
|
|
9
|
+
import { compile } from '../compiler/index.ts'
|
|
10
|
+
import type { AnyMachineDef } from '../engine/index.ts'
|
|
11
|
+
import { machineStub, stator } from '../vite/index.ts'
|
|
12
|
+
import { findFreePort, installGracefulShutdown, printDevBanner } from './banner.ts'
|
|
13
|
+
import { resolveAppConfig } from './config-compat.ts'
|
|
14
|
+
import type { DevApp, DevAppConfig } from './dev.ts'
|
|
15
|
+
import { loadDotenv } from './env.ts'
|
|
16
|
+
import { logger, setLogLevel } from './logger.ts'
|
|
17
|
+
import type { MachineStore } from './machine-store.ts'
|
|
18
|
+
import type { DiscoveredRoute } from './route-discovery.ts'
|
|
19
|
+
import { isStatorQueryRoute } from './routing.ts'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The TRANSITIONAL Vite-embedded dev server, kept for one minor behind
|
|
23
|
+
* `STATOR_VITE_DEV=1` while the native dev server (`dev-native.ts`, the
|
|
24
|
+
* default since the Vite exit) beds in. Deleted with the rest of the Vite
|
|
25
|
+
* implementation once the escape hatch retires.
|
|
26
|
+
*
|
|
27
|
+
* Embeds Vite in middleware mode so `.stator` (and TS) modules are compiled on
|
|
28
|
+
* the way in, loads machines + routes through `vite.ssrLoadModule`, and injects
|
|
29
|
+
* each route's scoped component CSS into `<head>` at render time.
|
|
30
|
+
*
|
|
31
|
+
* Critically, the framework runtime itself is loaded *through Vite*
|
|
32
|
+
* (`ssrLoadModule('@statorjs/stator/server')`), not imported natively — so the
|
|
33
|
+
* render-context, machine defs, routes, and templates all share one module
|
|
34
|
+
* instance. Importing the runtime natively here would create a second instance
|
|
35
|
+
* whose render-context doesn't match the one the templates resolve against, and
|
|
36
|
+
* `read()` would throw. (This mirrors how Astro/SvelteKit run SSR through Vite.)
|
|
37
|
+
*/
|
|
38
|
+
export async function createViteDevApp(config: DevAppConfig): Promise<DevApp> {
|
|
39
|
+
// Load .env before anything reads process.env. No-op if the CLI already
|
|
40
|
+
// loaded it; covers a direct `createDevApp` call (a hand-written dev entry).
|
|
41
|
+
loadDotenv(resolve(config.root))
|
|
42
|
+
// One build-id per dev boot — a `tsx` restart is a new process → new id → the
|
|
43
|
+
// client reloads on reconnect (the restart-without-reload gap). An inner Vite
|
|
44
|
+
// rebuild keeps the same id (no full reload; islands hot-reload via Vite).
|
|
45
|
+
const devBuildId = randomUUID()
|
|
46
|
+
// Vite's HMR websocket defaults to 24678 for EVERY dev server — two
|
|
47
|
+
// stator apps side by side would fight over it (the loser's live reload
|
|
48
|
+
// silently dies). Probe a free one instead.
|
|
49
|
+
const hmrPort = await findFreePort(24678)
|
|
50
|
+
const vite = await createViteServer({
|
|
51
|
+
root: resolve(config.root),
|
|
52
|
+
appType: 'custom',
|
|
53
|
+
// Ignore a user `vite.config.*` — the production build already sets this,
|
|
54
|
+
// and reading it only in dev is a dev/prod divergence footgun (a plugin
|
|
55
|
+
// configured there would run in `stator dev` and silently vanish in
|
|
56
|
+
// `stator build`). Stator's own plugins are passed inline below.
|
|
57
|
+
configFile: false,
|
|
58
|
+
server: { middlewareMode: true, hmr: { port: hmrPort } },
|
|
59
|
+
// machineStub keeps server machines out of browser module graphs: island
|
|
60
|
+
// imports of a machine resolve to a `{ name }` stub (SSR loads are
|
|
61
|
+
// untouched — `options.ssr` gates it).
|
|
62
|
+
plugins: [stator(), machineStub({ machinesDir: resolve(config.machinesDir) })],
|
|
63
|
+
// The framework's own source is TS that Vite must transform, not externalize.
|
|
64
|
+
ssr: { noExternal: [/@statorjs\/stator/] },
|
|
65
|
+
logLevel: 'warn',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Load the runtime through Vite so it shares an instance with the templates.
|
|
69
|
+
// (Type-only: the static import type is erased, so no second instance.)
|
|
70
|
+
const runtime = (await vite.ssrLoadModule(
|
|
71
|
+
'@statorjs/stator/server',
|
|
72
|
+
)) as typeof import('./index.ts')
|
|
73
|
+
const loader = (file: string) => vite.ssrLoadModule(file) as Promise<Record<string, unknown>>
|
|
74
|
+
|
|
75
|
+
const root = resolve(config.root)
|
|
76
|
+
const machinesDir = resolve(config.machinesDir)
|
|
77
|
+
const routesDir = resolve(config.routesDir)
|
|
78
|
+
const resolved = resolveAppConfig(config)
|
|
79
|
+
// Level precedence: LOG_LEVEL env > config > dev default (info). Apply to this
|
|
80
|
+
// module's logger AND the Vite-loaded runtime instance — SSE/http/effects log
|
|
81
|
+
// through the latter — via setLogLevel, which also covers the scoped children.
|
|
82
|
+
const logLevel = process.env.LOG_LEVEL ?? resolved.logLevel ?? 'info'
|
|
83
|
+
setLogLevel(logLevel)
|
|
84
|
+
runtime.setLogLevel(logLevel)
|
|
85
|
+
// The session cookie is written by the Vite-loaded runtime, so configure that
|
|
86
|
+
// instance's SameSite (the native one would set a different module's global).
|
|
87
|
+
runtime.setSessionSameSite(resolved.sameSite ?? 'Lax')
|
|
88
|
+
const inspectorOn = resolved.inspector ?? true
|
|
89
|
+
// One session store for the life of the dev process. Rebuilds swap the
|
|
90
|
+
// MachineStore AROUND it, so sessions survive an edit unless their machine's
|
|
91
|
+
// code hash changed (hydration policy). Creating the default store inside
|
|
92
|
+
// rebuildStore would silently reset every session on any machine-touching
|
|
93
|
+
// edit — the opposite of what the per-machine hash promises.
|
|
94
|
+
const sessionStore = resolved.session ?? new runtime.InMemoryStore()
|
|
95
|
+
|
|
96
|
+
const resultCache = new Map<string, ReturnType<typeof compile>>()
|
|
97
|
+
const compiledFor = async (file: string) => {
|
|
98
|
+
let r = resultCache.get(file)
|
|
99
|
+
if (r === undefined) {
|
|
100
|
+
// Match the plugin's kind detection so route-page frontmatter
|
|
101
|
+
// (Stator.reads etc.) compiles under the route capability set.
|
|
102
|
+
const kind = /[\\/]routes[\\/].*\.stator$/.test(file) ? 'route' : 'component'
|
|
103
|
+
r = compile(await readFile(file, 'utf8'), { id: file, kind })
|
|
104
|
+
resultCache.set(file, r)
|
|
105
|
+
}
|
|
106
|
+
return r
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const headExtras = async (routeFile: string) => {
|
|
110
|
+
const files = reachableStatorFiles(vite, routeFile)
|
|
111
|
+
let css = ''
|
|
112
|
+
const scripts: string[] = []
|
|
113
|
+
for (const f of files) {
|
|
114
|
+
const r = await compiledFor(f)
|
|
115
|
+
if (r.css) css += `/* ${f} */\n${r.css}\n`
|
|
116
|
+
if (r.isClient) {
|
|
117
|
+
// Vite's transform middleware compiles this URL because browsers send
|
|
118
|
+
// `Sec-Fetch-Dest: script` for module scripts (the `.stator` extension
|
|
119
|
+
// alone wouldn't match its JS-request check). curl-style probes must
|
|
120
|
+
// send that header or they'll see raw source and cry wolf.
|
|
121
|
+
const url = `/${relative(root, f).replace(/\\/g, '/')}?stator&type=client`
|
|
122
|
+
scripts.push(`<script type="module" src="${url}"></script>`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const head: string[] = []
|
|
126
|
+
// Vite's HMR client — pages are rendered by Hono, not Vite's
|
|
127
|
+
// transformIndexHtml, so it isn't injected for us. Without it the browser
|
|
128
|
+
// has no socket to receive the full-reload signal on a source change.
|
|
129
|
+
head.push('<script type="module" src="/@vite/client"></script>')
|
|
130
|
+
// The dev inspector toolbar (off in production — only the dev server injects it).
|
|
131
|
+
if (inspectorOn) head.push('<script src="/@stator/inspector.js" defer></script>')
|
|
132
|
+
if (css.trim()) head.push(`<style data-stator-dev>\n${css.trim()}\n</style>`)
|
|
133
|
+
head.push(...scripts)
|
|
134
|
+
return head.join('\n')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The app graph is rebuilt on a source change so edits don't need a restart.
|
|
138
|
+
let store: MachineStore
|
|
139
|
+
let routes: DiscoveredRoute[] = []
|
|
140
|
+
let machineCount = 0
|
|
141
|
+
let defs: AnyMachineDef[] = []
|
|
142
|
+
let app: Hono
|
|
143
|
+
|
|
144
|
+
// Which machines' code hashes changed across a store rebuild — the sessions
|
|
145
|
+
// of exactly those start fresh on their next request (hydration policy).
|
|
146
|
+
// Hashes are read through the Vite-loaded runtime: its registry instance is
|
|
147
|
+
// the one discovery populated (the native import would be a second, empty one).
|
|
148
|
+
const rebuildStore = async (): Promise<string[]> => {
|
|
149
|
+
const before = new Map(defs.map((d) => [d.name, runtime.codeHashOf(d)]))
|
|
150
|
+
defs = (await runtime.discoverMachines(machinesDir, loader)).defs
|
|
151
|
+
machineCount = defs.length
|
|
152
|
+
const changed = defs
|
|
153
|
+
.filter((d) => before.has(d.name) && before.get(d.name) !== runtime.codeHashOf(d))
|
|
154
|
+
.map((d) => d.name)
|
|
155
|
+
store = new runtime.MachineStore(defs, sessionStore, {
|
|
156
|
+
sessionTtlSeconds: resolved.sessionTtlSeconds,
|
|
157
|
+
appStore: resolved.app,
|
|
158
|
+
})
|
|
159
|
+
// Wire the effect scheduler before booting — a fresh app machine's
|
|
160
|
+
// initial-state entry effect fires during boot and needs a live scheduler.
|
|
161
|
+
runtime.wireAppEffects(store)
|
|
162
|
+
await store.bootAppMachines()
|
|
163
|
+
// Dev-only lint: a session machine whose `after` timer drives a loop with
|
|
164
|
+
// a data-loading entry effect is server-side polling that runs for
|
|
165
|
+
// sessions nobody is watching. Warn with the steer, don't block.
|
|
166
|
+
for (const f of runtime.findPollLoops(defs)) {
|
|
167
|
+
console.warn(
|
|
168
|
+
`stator: session machine "${f.machine}" self-reschedules through \`after\` ` +
|
|
169
|
+
`(${f.cycle.join(' → ')} via ${f.event}) with a data-loading entry effect on the loop. ` +
|
|
170
|
+
`This polls upstream for sessions nobody is watching. Prefer a client-owned clock ` +
|
|
171
|
+
`with a staleness guard — see the effects guide, "Who owns the clock".`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
return changed
|
|
175
|
+
}
|
|
176
|
+
// An edit touches a machine when the file is in that machine's hashed
|
|
177
|
+
// closure — the machine file itself or anything it imports.
|
|
178
|
+
const touchesMachine = (file: string): boolean =>
|
|
179
|
+
file.startsWith(machinesDir) || defs.some((d) => runtime.codeInputsOf(d).includes(file))
|
|
180
|
+
const rebuildRoutes = async (): Promise<void> => {
|
|
181
|
+
routes = await runtime.discoverRoutes(routesDir, loader)
|
|
182
|
+
// Vite serves `public/` at the root ahead of Hono in dev, so a static
|
|
183
|
+
// file at a data route's URL silently wins. Warn at discovery time.
|
|
184
|
+
// (The brand is a plain property, so this native-import check works on
|
|
185
|
+
// defs the Vite-loaded runtime created.)
|
|
186
|
+
for (const r of routes) {
|
|
187
|
+
if (!r.GET || !isStatorQueryRoute(r.GET) || r.paramNames.length > 0) continue
|
|
188
|
+
if (existsSync(resolve(root, 'public', r.urlPath.slice(1)))) {
|
|
189
|
+
console.warn(
|
|
190
|
+
`stator: public/${r.urlPath.slice(1)} shadows the data route ${r.urlPath} in dev — ` +
|
|
191
|
+
`the static file wins. Rename one of them.`,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const rebuildServer = async (): Promise<void> => {
|
|
197
|
+
// Re-discover `middleware.ts` each rebuild so edits to it take effect. Loaded
|
|
198
|
+
// through the Vite runtime so its handlers share the runtime's instance.
|
|
199
|
+
const middleware = await runtime.discoverMiddleware(resolve(root, 'middleware.ts'), loader)
|
|
200
|
+
app = await runtime.buildHonoApp({
|
|
201
|
+
routes,
|
|
202
|
+
store,
|
|
203
|
+
staticDir: config.staticDir,
|
|
204
|
+
headExtras,
|
|
205
|
+
inspector: inspectorOn,
|
|
206
|
+
trustedOrigins: resolved.trustedOrigins,
|
|
207
|
+
sameSite: resolved.sameSite,
|
|
208
|
+
origin: resolved.origin,
|
|
209
|
+
secret: resolved.secret,
|
|
210
|
+
buildId: devBuildId,
|
|
211
|
+
cors: resolved.cors,
|
|
212
|
+
middleware,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await rebuildStore()
|
|
217
|
+
await rebuildRoutes()
|
|
218
|
+
await rebuildServer()
|
|
219
|
+
|
|
220
|
+
// Discover boot.ts ONCE per process (not per rebuild) — a long-lived source
|
|
221
|
+
// shouldn't restart on every edit. Runs in listen() below.
|
|
222
|
+
const bootDef = await runtime.discoverBoot(resolve(root, 'boot.ts'), loader)
|
|
223
|
+
|
|
224
|
+
// Live reload: on a relevant source change, re-discover and rebuild the app,
|
|
225
|
+
// then tell the browser to reload. A template/route edit keeps the store (and
|
|
226
|
+
// your session — cart contents and all) intact; only a machine edit resets it,
|
|
227
|
+
// since route `reads:` bind to machine defs by identity and must re-bind as a
|
|
228
|
+
// set. Rebuilds are serialized so overlapping saves can't race.
|
|
229
|
+
const isAppFile = (file: string): boolean => {
|
|
230
|
+
if (!/\.(stator|ts|js)$/.test(file)) return false
|
|
231
|
+
if (file.includes('/node_modules/') || file.includes('/dist/')) return false
|
|
232
|
+
if (file.startsWith(machinesDir) || file.startsWith(routesDir)) return true
|
|
233
|
+
return (vite.moduleGraph.getModulesByFile(file)?.size ?? 0) > 0
|
|
234
|
+
}
|
|
235
|
+
let reloadChain: Promise<void> = Promise.resolve()
|
|
236
|
+
const onChange = (file: string): void => {
|
|
237
|
+
const abs = resolve(file)
|
|
238
|
+
if (!isAppFile(abs)) return
|
|
239
|
+
reloadChain = reloadChain.then(async () => {
|
|
240
|
+
invalidateModuleTree(vite, abs)
|
|
241
|
+
resultCache.delete(abs)
|
|
242
|
+
try {
|
|
243
|
+
let machinesChanged: string[] | undefined
|
|
244
|
+
if (touchesMachine(abs)) machinesChanged = await rebuildStore()
|
|
245
|
+
await rebuildRoutes()
|
|
246
|
+
await rebuildServer()
|
|
247
|
+
vite.ws.send({ type: 'full-reload' })
|
|
248
|
+
logger.info({ file: relative(root, abs) }, 'reloaded')
|
|
249
|
+
if (machinesChanged) {
|
|
250
|
+
logger.info(
|
|
251
|
+
{ machines: machinesChanged },
|
|
252
|
+
machinesChanged.length
|
|
253
|
+
? 'machine code changed — their sessions start fresh on the next request'
|
|
254
|
+
: 'machine files changed but no machine code changed — sessions carry over',
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
logger.error({ err: (err as Error).message, file: relative(root, abs) }, 'reload failed')
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
vite.watcher.on('change', onChange)
|
|
263
|
+
vite.watcher.on('add', onChange)
|
|
264
|
+
vite.watcher.on('unlink', onChange)
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
fetch: (request) => app.fetch(request),
|
|
268
|
+
get hono() {
|
|
269
|
+
return app
|
|
270
|
+
},
|
|
271
|
+
vite,
|
|
272
|
+
// `store` is read at call time, not captured — a machine-edit rebuild
|
|
273
|
+
// swaps it and the method follows.
|
|
274
|
+
dispatchToApp: (machine, event) => runtime.dispatchToApp(store, machine, event),
|
|
275
|
+
listen(port: number): Promise<void> {
|
|
276
|
+
// Vite's middlewares serve client modules + HMR to the browser; anything
|
|
277
|
+
// it doesn't handle (routes, /__events, /__sse) falls through to Hono.
|
|
278
|
+
// The arrow re-reads `app` each request so a rebuild swaps in seamlessly.
|
|
279
|
+
const honoListener = getRequestListener((req) => app.fetch(req))
|
|
280
|
+
const server = createHttpServer((req, res) => {
|
|
281
|
+
vite.middlewares(req, res, () => honoListener(req, res))
|
|
282
|
+
})
|
|
283
|
+
// Busy port? Shift up like every modern dev server (the requested
|
|
284
|
+
// port is someone's other project, not an error).
|
|
285
|
+
return findFreePort(port).then(
|
|
286
|
+
(freePort) =>
|
|
287
|
+
new Promise((resolveFn) => {
|
|
288
|
+
server.listen(freePort, async () => {
|
|
289
|
+
printDevBanner({
|
|
290
|
+
port: freePort,
|
|
291
|
+
requestedPort: port,
|
|
292
|
+
machines: machineCount,
|
|
293
|
+
routes: routes.length,
|
|
294
|
+
inspector: true,
|
|
295
|
+
})
|
|
296
|
+
// Boot runs once the dev server is up (store is read at call time,
|
|
297
|
+
// so a later rebuild's store is used). Teardown composes into the
|
|
298
|
+
// shutdown below.
|
|
299
|
+
const teardown = await runtime.runBoot(bootDef, {
|
|
300
|
+
dispatchToApp: (machine, event) => runtime.dispatchToApp(store, machine, event),
|
|
301
|
+
config: {
|
|
302
|
+
origin: resolved.origin,
|
|
303
|
+
trustedOrigins: resolved.trustedOrigins ?? [],
|
|
304
|
+
sameSite: resolved.sameSite ?? 'Lax',
|
|
305
|
+
cors: resolved.cors,
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
installGracefulShutdown(async () => {
|
|
309
|
+
if (teardown) await teardown()
|
|
310
|
+
await vite.close()
|
|
311
|
+
await new Promise<void>((done) => server.close(() => done()))
|
|
312
|
+
})
|
|
313
|
+
resolveFn()
|
|
314
|
+
})
|
|
315
|
+
}),
|
|
316
|
+
)
|
|
317
|
+
},
|
|
318
|
+
close: () => vite.close(),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Invalidate a changed file and everything that (transitively) imports it, so
|
|
323
|
+
* the next `ssrLoadModule` re-executes them with fresh code. */
|
|
324
|
+
function invalidateModuleTree(vite: ViteDevServer, file: string): void {
|
|
325
|
+
const seen = new Set<unknown>()
|
|
326
|
+
const stack: Array<{ importers: Set<unknown> }> = [
|
|
327
|
+
...((vite.moduleGraph.getModulesByFile(file) ?? []) as Set<never>),
|
|
328
|
+
]
|
|
329
|
+
while (stack.length) {
|
|
330
|
+
const mod = stack.pop()
|
|
331
|
+
if (!mod || seen.has(mod)) continue
|
|
332
|
+
seen.add(mod)
|
|
333
|
+
vite.moduleGraph.invalidateModule(mod as never)
|
|
334
|
+
for (const imp of mod.importers) stack.push(imp as never)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Walk the SSR module graph from a route's file and return every `.stator` file
|
|
340
|
+
* reachable from it (route page + the components it renders). The caller compiles
|
|
341
|
+
* each to collect scoped CSS and client-component module scripts.
|
|
342
|
+
*/
|
|
343
|
+
function reachableStatorFiles(vite: ViteDevServer, entryFile: string): string[] {
|
|
344
|
+
const seen = new Set<string>()
|
|
345
|
+
const statorFiles = new Set<string>()
|
|
346
|
+
|
|
347
|
+
const visit = (node: { id: string | null; importedModules: Set<unknown> } | undefined): void => {
|
|
348
|
+
if (!node) return
|
|
349
|
+
const id = node.id ?? ''
|
|
350
|
+
if (seen.has(id)) return
|
|
351
|
+
seen.add(id)
|
|
352
|
+
const file = id.split('?')[0]!
|
|
353
|
+
if (/\.stator$/.test(file)) statorFiles.add(file)
|
|
354
|
+
for (const dep of node.importedModules) visit(dep as never)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const node of vite.moduleGraph.getModulesByFile(entryFile) ?? []) {
|
|
358
|
+
visit(node as never)
|
|
359
|
+
}
|
|
360
|
+
return [...statorFiles]
|
|
361
|
+
}
|