@c9up/ream 0.2.22 → 0.2.24
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/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +22 -1
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +22 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js.map +1 -1
- package/dist/dev/fullReload.d.ts +23 -0
- package/dist/dev/fullReload.d.ts.map +1 -0
- package/dist/dev/fullReload.js +26 -0
- package/dist/dev/fullReload.js.map +1 -0
- package/dist/dev/hotGlob.d.ts +28 -0
- package/dist/dev/hotGlob.d.ts.map +1 -0
- package/dist/dev/hotGlob.js +67 -0
- package/dist/dev/hotGlob.js.map +1 -0
- package/dist/dev/hotGraph.d.ts +58 -0
- package/dist/dev/hotGraph.d.ts.map +1 -0
- package/dist/dev/hotGraph.js +91 -0
- package/dist/dev/hotGraph.js.map +1 -0
- package/dist/dev/hotWatcher.d.ts +34 -0
- package/dist/dev/hotWatcher.d.ts.map +1 -0
- package/dist/dev/hotWatcher.js +72 -0
- package/dist/dev/hotWatcher.js.map +1 -0
- package/dist/env/loadEnvFiles.d.ts +13 -0
- package/dist/env/loadEnvFiles.d.ts.map +1 -1
- package/dist/env/loadEnvFiles.js +65 -13
- package/dist/env/loadEnvFiles.js.map +1 -1
- package/dist/hot.d.ts +13 -16
- package/dist/hot.d.ts.map +1 -1
- package/dist/hot.js +110 -86
- package/dist/hot.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +4 -7
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/HttpKernel.ts +22 -2
- package/src/Ignitor.ts +24 -0
- package/src/dev/fullReload.ts +34 -0
- package/src/dev/hotGlob.ts +66 -0
- package/src/dev/hotGraph.ts +109 -0
- package/src/dev/hotWatcher.ts +89 -0
- package/src/env/loadEnvFiles.ts +69 -14
- package/src/hot.ts +116 -105
- package/src/index.ts +1 -1
package/src/hot.ts
CHANGED
|
@@ -5,132 +5,143 @@
|
|
|
5
5
|
*
|
|
6
6
|
* node --import @swc-node/register/esm-register --import @c9up/ream/hot bin/server.ts
|
|
7
7
|
*
|
|
8
|
-
* It
|
|
9
|
-
*
|
|
10
|
-
* boundary changes it invalidates just that module instead of the process. The
|
|
11
|
-
* `hotHook` key in the application's `package.json` configures it, exactly as
|
|
12
|
-
* upstream reads it.
|
|
8
|
+
* It replaces a module that changed instead of the process that was serving it,
|
|
9
|
+
* so the container, the pools and the connections survive a save.
|
|
13
10
|
*
|
|
14
|
-
* NAMED DEVIATION —
|
|
11
|
+
* NAMED DEVIATION — this is ream's own, where AdonisJS uses `hot-hook`.
|
|
15
12
|
*
|
|
16
|
-
* Upstream
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
13
|
+
* Upstream ships that package in its starter kit, which means every application
|
|
14
|
+
* installs it, and one created before the convention existed silently loses hot
|
|
15
|
+
* reloading. The parts that once justified a library are now in Node:
|
|
16
|
+
* `module.registerHooks` gives the import graph, `fs.watch` gives the changes.
|
|
17
|
+
* What is left is the decision of what to invalidate, which is this file and
|
|
18
|
+
* its three neighbours — and no entry in anyone's `package.json`.
|
|
22
19
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* The graph comes from the resolver rather than from reading files: a resolve
|
|
21
|
+
* hook is handed the importer and the imported, which is exactly an edge, and
|
|
22
|
+
* it sees a dynamic import at the moment it happens rather than guessing at it
|
|
23
|
+
* from the source.
|
|
27
24
|
*/
|
|
28
25
|
|
|
26
|
+
import { existsSync, writeSync } from 'node:fs'
|
|
27
|
+
import { readFile } from 'node:fs/promises'
|
|
28
|
+
import { registerHooks } from 'node:module'
|
|
29
|
+
import { dirname, relative, resolve as resolvePath } from 'node:path'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
31
|
+
import { fullReloadNotice } from './dev/fullReload.js'
|
|
29
32
|
import { hotReloadHappened } from './dev/hmr.js'
|
|
30
|
-
import {
|
|
33
|
+
import { readHotHookConfig } from './dev/hotConfig.js'
|
|
34
|
+
import { globMatcher } from './dev/hotGlob.js'
|
|
35
|
+
import { HotGraph } from './dev/hotGraph.js'
|
|
36
|
+
import { watchProject } from './dev/hotWatcher.js'
|
|
37
|
+
import { envFileNames } from './env/loadEnvFiles.js'
|
|
31
38
|
|
|
32
39
|
/** Exit code meaning "restart me" — see `EXIT_RESTART` in the CLI's dev module. */
|
|
33
40
|
const FULL_RELOAD_EXIT_CODE = 75
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
restart?: string[]
|
|
41
|
-
ignore?: string[]
|
|
42
|
-
throwWhenBoundariesAreNotDynamicallyImported?: boolean
|
|
43
|
-
onFullReloadAsked?: () => void
|
|
44
|
-
}): Promise<void>
|
|
45
|
-
}
|
|
42
|
+
/** The query parameter that makes the loader fetch a module again. */
|
|
43
|
+
const VERSION_PARAM = 'ream_hot'
|
|
44
|
+
|
|
45
|
+
/** Directories a project keeps its own code in — the CLI watches the same list. */
|
|
46
|
+
const WATCH_DIRS = ['app', 'bin', 'config', 'start', 'database', 'providers', 'commands']
|
|
46
47
|
|
|
47
|
-
const
|
|
48
|
-
const
|
|
48
|
+
const packageJsonPath = resolvePath(process.cwd(), 'package.json')
|
|
49
|
+
const root = dirname(packageJsonPath)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
let config: HotHookConfig = {}
|
|
51
|
+
let config = {}
|
|
52
52
|
try {
|
|
53
53
|
config = readHotHookConfig(JSON.parse(await readFile(packageJsonPath, 'utf8')))
|
|
54
54
|
} catch {
|
|
55
|
-
// No package.json, or unreadable:
|
|
56
|
-
//
|
|
55
|
+
// No package.json, or unreadable: a project with no boundaries declared gets
|
|
56
|
+
// a working server that restarts on change, which is the old behaviour.
|
|
57
57
|
}
|
|
58
|
+
const { boundaries = [], restart = [] } = config as { boundaries?: string[]; restart?: string[] }
|
|
58
59
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
// Env files are not modules: nothing imports one, so the graph never sees them
|
|
61
|
+
// and an edit to `.env` did nothing at all — the values are read once, at boot.
|
|
62
|
+
// They always restart.
|
|
63
|
+
const restartFiles = new Set(
|
|
64
|
+
envFileNames()
|
|
65
|
+
.map((name) => resolvePath(root, name))
|
|
66
|
+
.filter((file) => existsSync(file)),
|
|
67
|
+
)
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// a bare `import("hot-hook")` here looks inside @c9up/ream and finds nothing.
|
|
72
|
-
// It is the same reason upstream passes `hot-hook/register` from the app's own
|
|
73
|
-
// cwd instead of re-exporting it from the framework.
|
|
74
|
-
const { createRequire } = await import('node:module')
|
|
75
|
-
const { pathToFileURL } = await import('node:url')
|
|
69
|
+
const relativeToRoot = (file: string): string => relative(root, file).replace(/\\/g, '/')
|
|
70
|
+
const matchesBoundary = globMatcher(boundaries)
|
|
71
|
+
const matchesRestartGlob = globMatcher(restart)
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const candidate =
|
|
82
|
-
typeof module === 'object' && module !== null ? Reflect.get(module, 'hot') : undefined
|
|
83
|
-
if (!isHotHook(candidate)) {
|
|
84
|
-
throw new Error('hot-hook did not expose a `hot` object with init()')
|
|
85
|
-
}
|
|
86
|
-
hot = candidate
|
|
87
|
-
} catch (error) {
|
|
88
|
-
// Loud, and it names the fix. Silence here would start a perfectly healthy
|
|
89
|
-
// server that never picks up an edit, which reads as "HMR is broken" rather
|
|
90
|
-
// than "HMR is not installed".
|
|
91
|
-
//
|
|
92
|
-
// It does NOT throw: a dev server that refuses to start is worse than one
|
|
93
|
-
// without hot reloading, and the CLI only loads this file when it has
|
|
94
|
-
// already seen hot-hook in the project — so reaching here means it went
|
|
95
|
-
// missing under a running session.
|
|
96
|
-
console.error(
|
|
97
|
-
`[ream] hot module replacement is unavailable: ${error instanceof Error ? error.message : String(error)}\n` +
|
|
98
|
-
'[ream] Install it with: pnpm add -D hot-hook, then restart `ream dev`.\n' +
|
|
99
|
-
'[ream] Until then the server runs, but an edit will not be picked up.',
|
|
100
|
-
)
|
|
101
|
-
}
|
|
73
|
+
const graph = new HotGraph({
|
|
74
|
+
isBoundary: (file) => matchesBoundary(relativeToRoot(file)),
|
|
75
|
+
isRestart: (file) => restartFiles.has(file) || matchesRestartGlob(relativeToRoot(file)),
|
|
76
|
+
})
|
|
102
77
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Track every edge the resolver reports, and hand back a versioned URL for a
|
|
80
|
+
* module that has been invalidated.
|
|
81
|
+
*
|
|
82
|
+
* The version travels in the URL because that is the only handle V8 offers: a
|
|
83
|
+
* module is keyed by its URL, so a different one is a different module. The
|
|
84
|
+
* parameter is stripped off the importer before delegating, or a relative
|
|
85
|
+
* specifier would be resolved against a URL that does not name a directory.
|
|
86
|
+
*/
|
|
87
|
+
registerHooks({
|
|
88
|
+
resolve(specifier, context, nextResolve) {
|
|
89
|
+
let parentURL = context.parentURL
|
|
90
|
+
let parentPath: string | undefined
|
|
91
|
+
if (typeof parentURL === 'string' && parentURL.startsWith('file:')) {
|
|
92
|
+
const url = new URL(parentURL)
|
|
93
|
+
if (url.searchParams.has(VERSION_PARAM)) {
|
|
94
|
+
url.searchParams.delete(VERSION_PARAM)
|
|
95
|
+
parentURL = url.href
|
|
96
|
+
}
|
|
97
|
+
parentPath = fileURLToPath(url)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = nextResolve(specifier, { ...context, parentURL })
|
|
101
|
+
if (!result.url.startsWith('file:')) return result
|
|
102
|
+
|
|
103
|
+
const resolved = new URL(result.url)
|
|
104
|
+
resolved.searchParams.delete(VERSION_PARAM)
|
|
105
|
+
const file = fileURLToPath(resolved)
|
|
106
|
+
graph.link(file, parentPath)
|
|
107
|
+
|
|
108
|
+
const version = graph.version(file)
|
|
109
|
+
if (version === 0) return result
|
|
110
|
+
resolved.searchParams.set(VERSION_PARAM, String(version))
|
|
111
|
+
return { ...result, url: resolved.href }
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/** Print a line that must survive the exit on the very next statement. */
|
|
116
|
+
function sayNow(line: string): void {
|
|
117
|
+
// Synchronously: a piped stdout — what `ream dev` hands this process when it
|
|
118
|
+
// runs an asset watcher alongside — is asynchronous, and the line would be
|
|
119
|
+
// dropped on the way out.
|
|
120
|
+
try {
|
|
121
|
+
writeSync(1, `${line}\n`)
|
|
122
|
+
} catch {
|
|
123
|
+
// The parent can close the pipe first on Ctrl-C. Losing the line is fine.
|
|
125
124
|
}
|
|
126
|
-
return true
|
|
127
125
|
}
|
|
128
126
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
127
|
+
watchProject({
|
|
128
|
+
root,
|
|
129
|
+
directories: WATCH_DIRS,
|
|
130
|
+
files: [...restartFiles].map((file) => relative(root, file)),
|
|
131
|
+
onChange: (file) => {
|
|
132
|
+
const decision = graph.decide(file)
|
|
133
|
+
if (decision.kind === 'ignore') return
|
|
134
|
+
if (decision.kind === 'reload') {
|
|
135
|
+
sayNow(fullReloadNotice(decision.file, decision.reason, relativeToRoot))
|
|
136
|
+
// Exiting on a known code is how the Rust parent learns it must restart —
|
|
137
|
+
// see `EXIT_RESTART` in the CLI's dev module.
|
|
138
|
+
process.exit(FULL_RELOAD_EXIT_CODE)
|
|
139
|
+
}
|
|
140
|
+
// The router caches a promoted controller per route, and the kernel drops
|
|
141
|
+
// that cache when this counter moves. Without it the swap happens and the
|
|
142
|
+
// process keeps serving the class it promoted: a stable PID and stale
|
|
143
|
+
// output, which is exactly how it was first reported.
|
|
144
|
+
hotReloadHappened()
|
|
145
|
+
sayNow(`[ream] hot swap — ${relativeToRoot(file)}`)
|
|
146
|
+
},
|
|
136
147
|
})
|
package/src/index.ts
CHANGED