@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/dist/hot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hot.js","sourceRoot":"","sources":["../src/hot.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"hot.js","sourceRoot":"","sources":["../src/hot.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -121,5 +121,5 @@ export { BaseEvent, Emitter, type EmitterResolver, type ListenerClass, } from '.
|
|
|
121
121
|
export { type BufferedEvent, type BufferedEventName, type EventFinder, EventsBuffer, } from './events/EventsBuffer.js';
|
|
122
122
|
export type { ErrorReporter, LockBackend, LockBackendFactory, LockRedisClient, LockRedisResolver, RunTaskOutcome, ScheduleEvent, ScheduleEventSink, ScheduleInvocation, ScheduleMetadata, ScheduleProviderOptions, SchedulerConfig, SchedulerOptions, ScheduleTaskCompletedEvent, ScheduleTaskFailedEvent, ScheduleTaskSkippedEvent, ScheduleTaskStartedEvent, TaskInfo, TaskStats, } from './scheduler/index.js';
|
|
123
123
|
export { defineSchedulerConfig, getScheduleMetadata, locks, MemoryLockBackend, RedisLockBackend, SCHEDULE_METADATA_KEY, Schedule, ScheduleProvider, Scheduler, StatsTracker, } from './scheduler/index.js';
|
|
124
|
-
export declare const VERSION = "0.2.
|
|
124
|
+
export declare const VERSION = "0.2.24";
|
|
125
125
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -92,5 +92,5 @@ export { defineConfig as defineEventsConfig } from './events/config.js';
|
|
|
92
92
|
export { BaseEvent, Emitter, } from './events/Emitter.js';
|
|
93
93
|
export { EventsBuffer, } from './events/EventsBuffer.js';
|
|
94
94
|
export { defineSchedulerConfig, getScheduleMetadata, locks, MemoryLockBackend, RedisLockBackend, SCHEDULE_METADATA_KEY, Schedule, ScheduleProvider, Scheduler, StatsTracker, } from './scheduler/index.js';
|
|
95
|
-
export const VERSION = '0.2.
|
|
95
|
+
export const VERSION = '0.2.24';
|
|
96
96
|
//# sourceMappingURL=index.js.map
|
package/events.darwin-arm64.node
CHANGED
|
Binary file
|
package/events.darwin-x64.node
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.darwin-arm64.node
CHANGED
|
Binary file
|
package/index.darwin-x64.node
CHANGED
|
Binary file
|
|
Binary file
|
package/index.linux-x64-gnu.node
CHANGED
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/ream",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"description": "Ream — Rust-powered Node.js application framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -233,8 +233,7 @@
|
|
|
233
233
|
},
|
|
234
234
|
"peerDependencies": {
|
|
235
235
|
"@c9up/comet": "^0.1.0",
|
|
236
|
-
"@c9up/quasar": "^0.1.0 || ^0.2.0"
|
|
237
|
-
"hot-hook": "^1.0.0"
|
|
236
|
+
"@c9up/quasar": "^0.1.0 || ^0.2.0"
|
|
238
237
|
},
|
|
239
238
|
"peerDependenciesMeta": {
|
|
240
239
|
"@c9up/comet": {
|
|
@@ -242,9 +241,6 @@
|
|
|
242
241
|
},
|
|
243
242
|
"@c9up/quasar": {
|
|
244
243
|
"optional": true
|
|
245
|
-
},
|
|
246
|
-
"hot-hook": {
|
|
247
|
-
"optional": true
|
|
248
244
|
}
|
|
249
245
|
},
|
|
250
246
|
"devDependencies": {
|
|
@@ -257,8 +253,9 @@
|
|
|
257
253
|
"typescript": "^6.0.3",
|
|
258
254
|
"vitest": "4.1.11"
|
|
259
255
|
},
|
|
256
|
+
"//engines": "Node 24, not 22, because AsyncLocalStorage sits on the request hot path — the container resolves through it, HttpContext is it, and the emitter reads it on every event. Node 24 backs it with AsyncContextFrame by default, and measured on that exact pattern (one context per request, six async boundaries, several reads) it costs 0.61 us per request instead of 1.55 us: 2.5x, on every request, for free. Before 24 the same mechanism existed only behind an experimental flag, and a framework cannot base its performance on a flag the application has to remember to pass. This is NOT 'track the current LTS' — security fixes still land in 22 until 2027, npm 11 is irrelevant under pnpm, and node:sqlite is not what atlas uses.",
|
|
260
257
|
"engines": {
|
|
261
|
-
"node": ">=
|
|
258
|
+
"node": ">=24.0.0"
|
|
262
259
|
},
|
|
263
260
|
"publishConfig": {
|
|
264
261
|
"access": "public"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/HttpKernel.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { Container } from './container/Container.js'
|
|
14
14
|
import { debugHttp } from './debug.js'
|
|
15
|
+
import { hotReloadCount } from './dev/hmr.js'
|
|
15
16
|
import type { Emitter } from './events/Emitter.js'
|
|
16
17
|
import { E_ROUTE_NOT_FOUND, ExceptionHandler } from './http/Exception.js'
|
|
17
18
|
import { type ChildLoggerSource, HttpContext } from './http/HttpContext.js'
|
|
@@ -117,7 +118,16 @@ export function createHttpKernel(
|
|
|
117
118
|
// router.clear() drops the route objects, their chains become unreachable
|
|
118
119
|
// and are GC'd — no need to monkey-patch router.clear (which leaked a
|
|
119
120
|
// wrapper chain when createHttpKernel ran more than once on one router).
|
|
120
|
-
|
|
121
|
+
// `hotAt` is what makes hot reloading reach the router at all. A lazy
|
|
122
|
+
// controller is promoted to a concrete one on first request and the whole
|
|
123
|
+
// pipeline is cached per route — so once hot-hook swapped a module, both
|
|
124
|
+
// still pointed at the class from before, the process kept serving it, and
|
|
125
|
+
// the swap looked like it had not happened. Remembering when an entry was
|
|
126
|
+
// built lets a later swap throw it away.
|
|
127
|
+
//
|
|
128
|
+
// In production this is a number compared against a constant zero: nothing
|
|
129
|
+
// bumps the counter when `@c9up/ream/hot` was never loaded.
|
|
130
|
+
const pipelineCache = new WeakMap<RouteDefinition, { chain: MiddlewareFunction; hotAt: number }>()
|
|
121
131
|
|
|
122
132
|
// Resolve the events emitter once, only when the app registered
|
|
123
133
|
// `EventsProvider`. Lazy + cached: the binding may not exist yet at kernel
|
|
@@ -245,7 +255,17 @@ export function createHttpKernel(
|
|
|
245
255
|
throw new E_ROUTE_NOT_FOUND(reqData.method, reqData.path)
|
|
246
256
|
}
|
|
247
257
|
|
|
258
|
+
const hotAt = hotReloadCount()
|
|
248
259
|
let cached = pipelineCache.get(match.route)
|
|
260
|
+
if (cached !== undefined && cached.hotAt !== hotAt) {
|
|
261
|
+
// A module was swapped since this was built. Drop the pipeline AND
|
|
262
|
+
// the promoted controller, so the loader runs again and `import()`
|
|
263
|
+
// hands back what hot-hook just invalidated rather than the copy in
|
|
264
|
+
// the ESM cache.
|
|
265
|
+
pipelineCache.delete(match.route)
|
|
266
|
+
if (match.route.lazyController) match.route.controller = undefined
|
|
267
|
+
cached = undefined
|
|
268
|
+
}
|
|
249
269
|
|
|
250
270
|
if (!cached) {
|
|
251
271
|
// Promote a lazy/string controller to a concrete controller tuple on
|
|
@@ -314,7 +334,7 @@ export function createHttpKernel(
|
|
|
314
334
|
{ guards, roles, permissions, validators },
|
|
315
335
|
)
|
|
316
336
|
|
|
317
|
-
cached = { chain }
|
|
337
|
+
cached = { chain, hotAt }
|
|
318
338
|
pipelineCache.set(match.route, cached)
|
|
319
339
|
}
|
|
320
340
|
|
package/src/Ignitor.ts
CHANGED
|
@@ -63,6 +63,18 @@ import type { AppEnvironment } from './Application.js'
|
|
|
63
63
|
/**
|
|
64
64
|
* Reamrc config — like AdonisJS adonisrc.ts with defineConfig().
|
|
65
65
|
*/
|
|
66
|
+
/** A command the CLI runs on the application's behalf. */
|
|
67
|
+
export interface AssetsCommand {
|
|
68
|
+
command: string
|
|
69
|
+
args?: string[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What `assets` declares: a watcher for `dev`, a one-shot for `build`. */
|
|
73
|
+
export interface AssetsConfig {
|
|
74
|
+
devServer?: AssetsCommand
|
|
75
|
+
build?: AssetsCommand
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
export interface ReamrcConfig {
|
|
67
79
|
providers?: Array<
|
|
68
80
|
| (() => Promise<{ default: new (app: AppContext) => ProviderContract }>)
|
|
@@ -113,6 +125,18 @@ export interface ReamrcConfig {
|
|
|
113
125
|
}
|
|
114
126
|
/** Test suites and runner settings — the `tests` block of adonisrc.ts. */
|
|
115
127
|
tests?: TestsConfig
|
|
128
|
+
/**
|
|
129
|
+
* The asset pipeline `ream dev` and `ream build` drive alongside the app.
|
|
130
|
+
*
|
|
131
|
+
* `devServer` is started beside the development server and killed with it;
|
|
132
|
+
* `build` runs once, before the TypeScript build, and a non-zero exit stops
|
|
133
|
+
* the build rather than shipping a bundle nobody rebuilt.
|
|
134
|
+
*
|
|
135
|
+
* Declared here because the CLI already reads it: without the type an app
|
|
136
|
+
* that configures its asset build gets an excess-property error from
|
|
137
|
+
* `defineConfig`, which is a strange way to learn that the option exists.
|
|
138
|
+
*/
|
|
139
|
+
assets?: AssetsConfig
|
|
116
140
|
}
|
|
117
141
|
|
|
118
142
|
/** One test suite, as declared in the rc file (AdonisJS `tests.suites[]`). */
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saying why the process is about to restart.
|
|
3
|
+
*
|
|
4
|
+
* A change the loader cannot swap in place becomes an exit code the dev server
|
|
5
|
+
* restarts on. From the terminal the only trace of it was the boot banner
|
|
6
|
+
* printing itself a second time — which reads as a server rebooting on its own
|
|
7
|
+
* rather than as a file that fell outside `hotHook.boundaries`. Upstream's dev
|
|
8
|
+
* server logs the file it reloaded for; this is that line.
|
|
9
|
+
*
|
|
10
|
+
* Kept apart from the loader entry so it can be tested: importing that entry
|
|
11
|
+
* registers module hooks, which a test cannot undo.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Why a change could not be swapped in place. */
|
|
15
|
+
export type ReloadReason = 'outside-boundaries' | 'restart-list'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The line to print before restarting.
|
|
19
|
+
*
|
|
20
|
+
* @param file - absolute path of the file that changed.
|
|
21
|
+
* @param relativise - turns it into the path the editor shows, so the line can
|
|
22
|
+
* be read at a glance.
|
|
23
|
+
*/
|
|
24
|
+
export function fullReloadNotice(
|
|
25
|
+
file: string,
|
|
26
|
+
reason: ReloadReason,
|
|
27
|
+
relativise: (file: string) => string,
|
|
28
|
+
): string {
|
|
29
|
+
const why =
|
|
30
|
+
reason === 'restart-list'
|
|
31
|
+
? ' (it is on `hotHook.restart`, which always restarts)'
|
|
32
|
+
: ' (nothing importing it is inside a `hotHook.boundaries` entry, so it could not be swapped)'
|
|
33
|
+
return `[ream] full reload — ${relativise(file)}${why}`
|
|
34
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matching the globs an application declares under `hotHook.boundaries`.
|
|
3
|
+
*
|
|
4
|
+
* A dependency for this would be `picomatch` plus `fast-glob`, which is two
|
|
5
|
+
* packages and their trees for the three wildcards these patterns actually use.
|
|
6
|
+
* The patterns are written by hand in a `package.json` and read once at boot;
|
|
7
|
+
* what they need is to be right, not fast.
|
|
8
|
+
*
|
|
9
|
+
* The dialect is the one those files already use:
|
|
10
|
+
* * any run of characters inside one path segment
|
|
11
|
+
* ** any number of segments, including none
|
|
12
|
+
* ? exactly one character inside a segment
|
|
13
|
+
*
|
|
14
|
+
* A leading `./` is optional, and matching is done on POSIX-shaped paths
|
|
15
|
+
* relative to the project root, so a Windows checkout and a Linux one read the
|
|
16
|
+
* same pattern the same way.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Turn one glob into an anchored regular expression. */
|
|
20
|
+
export function globToRegExp(pattern: string): RegExp {
|
|
21
|
+
const cleaned = pattern.replace(/^\.\//, '')
|
|
22
|
+
let out = ''
|
|
23
|
+
for (let i = 0; i < cleaned.length; i += 1) {
|
|
24
|
+
const char = cleaned[i]
|
|
25
|
+
if (char === '*') {
|
|
26
|
+
if (cleaned[i + 1] === '*') {
|
|
27
|
+
// `**/` swallows the separator so that `a/**/b` also matches `a/b`;
|
|
28
|
+
// a trailing `**` simply matches the rest.
|
|
29
|
+
i += 1
|
|
30
|
+
if (cleaned[i + 1] === '/') {
|
|
31
|
+
i += 1
|
|
32
|
+
out += '(?:[^/]+/)*'
|
|
33
|
+
} else {
|
|
34
|
+
out += '.*'
|
|
35
|
+
}
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
out += '[^/]*'
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
if (char === '?') {
|
|
42
|
+
out += '[^/]'
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
// Everything else is literal, including the characters a regular
|
|
46
|
+
// expression would otherwise read as syntax.
|
|
47
|
+
out += (char ?? '').replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
48
|
+
}
|
|
49
|
+
return new RegExp(`^${out}$`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A matcher over several globs.
|
|
54
|
+
*
|
|
55
|
+
* Returns a predicate rather than a class: the graph takes a function, and a
|
|
56
|
+
* project with no boundaries gets one that is honestly always false instead of
|
|
57
|
+
* an empty object that has to be checked for.
|
|
58
|
+
*/
|
|
59
|
+
export function globMatcher(patterns: readonly string[]): (path: string) => boolean {
|
|
60
|
+
const expressions = patterns.map(globToRegExp)
|
|
61
|
+
if (expressions.length === 0) return () => false
|
|
62
|
+
return (path: string) => {
|
|
63
|
+
const normalised = path.replace(/\\/g, '/').replace(/^\.\//, '')
|
|
64
|
+
return expressions.some((expression) => expression.test(normalised))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import graph a hot reload reasons over, and what it decides.
|
|
3
|
+
*
|
|
4
|
+
* Owned here rather than taken from `hot-hook`, which used to do this job. The
|
|
5
|
+
* package is 655 lines and four npm dependencies, and it had to be installed by
|
|
6
|
+
* every application because a dev-only loader has no business in a production
|
|
7
|
+
* install of the framework — so a project created before that convention simply
|
|
8
|
+
* lost hot reloading and was told nothing. The part that genuinely needs a
|
|
9
|
+
* library is the part Node now provides itself: `module.registerHooks` for the
|
|
10
|
+
* graph, `fs.watch` for the changes.
|
|
11
|
+
*
|
|
12
|
+
* NO node_modules dependency, deliberately: everything below is arithmetic on
|
|
13
|
+
* paths.
|
|
14
|
+
*
|
|
15
|
+
* The graph is built by the resolver as the program runs, never by reading
|
|
16
|
+
* files. A `resolve` hook is handed the importer and the imported, which is
|
|
17
|
+
* exactly an edge, and it sees dynamic imports as they happen — a static parse
|
|
18
|
+
* of the source could only guess at those.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** A module's place in the graph: who imports it, and which version is live. */
|
|
22
|
+
interface Node {
|
|
23
|
+
/** Modules that imported this one. The graph is walked upward, so this is the direction stored. */
|
|
24
|
+
readonly importers: Set<string>
|
|
25
|
+
/** Bumped on invalidation; appended to the URL so the loader fetches the file again. */
|
|
26
|
+
version: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What a change to one file means. */
|
|
30
|
+
export type Decision =
|
|
31
|
+
| { kind: 'swap'; invalidated: string[] }
|
|
32
|
+
| { kind: 'reload'; file: string; reason: 'outside-boundaries' | 'restart-list' }
|
|
33
|
+
| { kind: 'ignore' }
|
|
34
|
+
|
|
35
|
+
export class HotGraph {
|
|
36
|
+
readonly #nodes = new Map<string, Node>()
|
|
37
|
+
readonly #isBoundary: (file: string) => boolean
|
|
38
|
+
readonly #isRestart: (file: string) => boolean
|
|
39
|
+
|
|
40
|
+
constructor(options: {
|
|
41
|
+
isBoundary: (file: string) => boolean
|
|
42
|
+
isRestart: (file: string) => boolean
|
|
43
|
+
}) {
|
|
44
|
+
this.#isBoundary = options.isBoundary
|
|
45
|
+
this.#isRestart = options.isRestart
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#node(file: string): Node {
|
|
49
|
+
let node = this.#nodes.get(file)
|
|
50
|
+
if (node === undefined) {
|
|
51
|
+
node = { importers: new Set(), version: 0 }
|
|
52
|
+
this.#nodes.set(file, node)
|
|
53
|
+
}
|
|
54
|
+
return node
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Record that `importer` imported `file`. Called from the resolve hook. */
|
|
58
|
+
link(file: string, importer: string | undefined): void {
|
|
59
|
+
const node = this.#node(file)
|
|
60
|
+
if (importer !== undefined && importer !== file) node.importers.add(importer)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The version to hang off the URL, or 0 while the module has never been invalidated. */
|
|
64
|
+
version(file: string): number {
|
|
65
|
+
return this.#nodes.get(file)?.version ?? 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether the resolver has ever seen this file. */
|
|
69
|
+
knows(file: string): boolean {
|
|
70
|
+
return this.#nodes.has(file)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What to do about `file` having changed.
|
|
75
|
+
*
|
|
76
|
+
* Walks up from the changed file collecting what has to be re-imported, and
|
|
77
|
+
* stops at each boundary: a boundary is imported dynamically, so the next
|
|
78
|
+
* import of it re-resolves, and with it everything below that was collected
|
|
79
|
+
* on the way. A path that reaches a module nobody imports without crossing a
|
|
80
|
+
* boundary cannot be re-imported by anyone, and that is a full reload.
|
|
81
|
+
*
|
|
82
|
+
* Deciding before invalidating matters: a decision that turns out to be a
|
|
83
|
+
* full reload must leave no version bumped behind, or a later swap would
|
|
84
|
+
* hand the caller a URL for a module the process never re-imported.
|
|
85
|
+
*/
|
|
86
|
+
decide(file: string): Decision {
|
|
87
|
+
if (this.#isRestart(file)) return { kind: 'reload', file, reason: 'restart-list' }
|
|
88
|
+
if (!this.#nodes.has(file)) return { kind: 'ignore' }
|
|
89
|
+
|
|
90
|
+
const collected = new Set<string>()
|
|
91
|
+
const queue = [file]
|
|
92
|
+
while (queue.length > 0) {
|
|
93
|
+
const current = queue.pop()
|
|
94
|
+
if (current === undefined || collected.has(current)) continue
|
|
95
|
+
collected.add(current)
|
|
96
|
+
// A boundary is where the walk stops: whoever imports it does so
|
|
97
|
+
// dynamically and will ask for it again.
|
|
98
|
+
if (this.#isBoundary(current)) continue
|
|
99
|
+
const importers = this.#nodes.get(current)?.importers
|
|
100
|
+
if (importers === undefined || importers.size === 0) {
|
|
101
|
+
return { kind: 'reload', file, reason: 'outside-boundaries' }
|
|
102
|
+
}
|
|
103
|
+
for (const importer of importers) queue.push(importer)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const path of collected) this.#node(path).version += 1
|
|
107
|
+
return { kind: 'swap', invalidated: [...collected] }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watching the project for the changes a hot reload reacts to.
|
|
3
|
+
*
|
|
4
|
+
* `fs.watch` with `recursive: true`, which Node implements on Linux, macOS and
|
|
5
|
+
* Windows alike — the reason a watcher used to be a dependency here, and is not
|
|
6
|
+
* any more.
|
|
7
|
+
*
|
|
8
|
+
* Two behaviours are worth stating because they are not obvious from the API:
|
|
9
|
+
*
|
|
10
|
+
* A save is rarely one event. Editors write, rename and touch, and a single
|
|
11
|
+
* Ctrl-S can arrive as several notifications for the same file within a few
|
|
12
|
+
* milliseconds. Each one would walk the graph and bump a version, so they are
|
|
13
|
+
* coalesced.
|
|
14
|
+
*
|
|
15
|
+
* `ream dev` also watches from the outside, in Rust. That is not a duplicate:
|
|
16
|
+
* this watcher dies with the process it lives in, so a server that crashed on a
|
|
17
|
+
* syntax error has nothing left to notice the fix. The outer one covers exactly
|
|
18
|
+
* that window, and hands back over by restarting the process.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { type FSWatcher, watch } from 'node:fs'
|
|
22
|
+
import { join, resolve } from 'node:path'
|
|
23
|
+
|
|
24
|
+
/** How long to wait for a save to finish arriving before acting on it. */
|
|
25
|
+
const SETTLE_MS = 40
|
|
26
|
+
|
|
27
|
+
export interface WatcherOptions {
|
|
28
|
+
/** Project root; every reported path is absolute, resolved against it. */
|
|
29
|
+
root: string
|
|
30
|
+
/** Directories under the root to watch, when they exist. */
|
|
31
|
+
directories: readonly string[]
|
|
32
|
+
/** Individual files to watch — env files sit at the root, so no directory covers them. */
|
|
33
|
+
files: readonly string[]
|
|
34
|
+
/** Called once per settled change, with an absolute path. */
|
|
35
|
+
onChange: (file: string) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Watcher {
|
|
39
|
+
close: () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function watchProject(options: WatcherOptions): Watcher {
|
|
43
|
+
const watchers: FSWatcher[] = []
|
|
44
|
+
const pending = new Map<string, NodeJS.Timeout>()
|
|
45
|
+
|
|
46
|
+
const report = (file: string): void => {
|
|
47
|
+
const existing = pending.get(file)
|
|
48
|
+
if (existing !== undefined) clearTimeout(existing)
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
pending.delete(file)
|
|
51
|
+
options.onChange(file)
|
|
52
|
+
}, SETTLE_MS)
|
|
53
|
+
// The dev server must still be able to exit while a save is settling.
|
|
54
|
+
timer.unref?.()
|
|
55
|
+
pending.set(file, timer)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const directory of options.directories) {
|
|
59
|
+
const absolute = resolve(options.root, directory)
|
|
60
|
+
try {
|
|
61
|
+
watchers.push(
|
|
62
|
+
watch(absolute, { recursive: true }, (_event, name) => {
|
|
63
|
+
if (typeof name !== 'string' || name === '') return
|
|
64
|
+
report(join(absolute, name))
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
} catch {
|
|
68
|
+
// A directory this project does not have is not an error: the list is
|
|
69
|
+
// the framework's conventional layout, not a requirement.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const file of options.files) {
|
|
74
|
+
const absolute = resolve(options.root, file)
|
|
75
|
+
try {
|
|
76
|
+
watchers.push(watch(absolute, () => report(absolute)))
|
|
77
|
+
} catch {
|
|
78
|
+
// Same: a `.env` that does not exist is simply not watched.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
close: () => {
|
|
84
|
+
for (const timer of pending.values()) clearTimeout(timer)
|
|
85
|
+
pending.clear()
|
|
86
|
+
for (const watcher of watchers) watcher.close()
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/env/loadEnvFiles.ts
CHANGED
|
@@ -1,41 +1,82 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
3
3
|
import { parseEnv } from 'node:util'
|
|
4
4
|
import { interpolate } from './interpolate.js'
|
|
5
5
|
import { normalizeNodeEnv } from './nodeEnv.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* Ignitor (HTTP/console boot) and `Env.create()` (config/test flow), mirroring
|
|
10
|
-
* AdonisJS which loads env in every flow.
|
|
8
|
+
* The `.env*` files that will be read, most-specific first.
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Exported because `@c9up/ream/hot` has to WATCH exactly this list: nothing
|
|
11
|
+
* imports an env file, so hot-hook's dependency tree never sees one and an
|
|
12
|
+
* edit to `.env` changed nothing at all. A second copy of the list in the
|
|
13
|
+
* loader entry would drift from this one the first time the naming changes.
|
|
15
14
|
*
|
|
16
|
-
* @param
|
|
17
|
-
* @param options.skipEnvLocal Skip `.env.local` (the test flow does, so a
|
|
18
|
-
* developer's local overrides don't leak into tests).
|
|
15
|
+
* @param options.skipEnvLocal Skip `.env.local` — see {@link loadEnvFiles}.
|
|
19
16
|
*/
|
|
20
|
-
export function
|
|
17
|
+
export function envFileNames(options: { skipEnvLocal?: boolean } = {}): string[] {
|
|
21
18
|
// Normalised, so `NODE_ENV=prod` loads `.env.production` — the file the
|
|
22
19
|
// deployment actually wrote — instead of looking for `.env.prod`.
|
|
23
20
|
const raw = normalizeNodeEnv(process.env.NODE_ENV)
|
|
24
21
|
const nodeEnv = raw === 'unknown' ? undefined : raw
|
|
25
|
-
|
|
22
|
+
return [
|
|
26
23
|
nodeEnv ? `.env.${nodeEnv}.local` : null,
|
|
27
24
|
options.skipEnvLocal ? null : '.env.local',
|
|
28
25
|
nodeEnv ? `.env.${nodeEnv}` : null,
|
|
29
26
|
'.env',
|
|
30
27
|
].filter((name): name is string => name !== null)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Where the `.env*` files are read from.
|
|
32
|
+
*
|
|
33
|
+
* Normally beside the application, which is what `appRoot` names. A BUILT
|
|
34
|
+
* application moves: `start/env.js` sits in `dist/`, so `new URL('../',
|
|
35
|
+
* import.meta.url)` resolves there and the files at the project root become
|
|
36
|
+
* invisible — `ream start` died on a `.env` it was standing next to.
|
|
37
|
+
*
|
|
38
|
+
* `ENV_PATH` is AdonisJS's own variable for this, named and shaped the same
|
|
39
|
+
* way: a DIRECTORY to read the files from, as in
|
|
40
|
+
* `ENV_PATH=/etc/secrets node server.js`. An application moving over keeps
|
|
41
|
+
* whatever it already sets.
|
|
42
|
+
*
|
|
43
|
+
* It is also why the ordering rule stays here rather than in the CLI: node's
|
|
44
|
+
* `--env-file` applies files last-wins while this loader applies them
|
|
45
|
+
* most-specific-first, and a second copy of that would be one to keep in step
|
|
46
|
+
* forever.
|
|
47
|
+
*/
|
|
48
|
+
function envDirectory(appRoot: URL): URL {
|
|
49
|
+
const override = process.env.ENV_PATH
|
|
50
|
+
if (override === undefined || override.trim() === '') return appRoot
|
|
51
|
+
// A directory URL, so `new URL('.env', directory)` lands inside it rather
|
|
52
|
+
// than beside it.
|
|
53
|
+
return pathToFileURL(override.endsWith('/') ? override : `${override}/`)
|
|
54
|
+
}
|
|
31
55
|
|
|
32
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Load `.env` files into `process.env` — the shared primitive behind both the
|
|
58
|
+
* Ignitor (HTTP/console boot) and `Env.create()` (config/test flow), mirroring
|
|
59
|
+
* AdonisJS which loads env in every flow.
|
|
60
|
+
*
|
|
61
|
+
* Order is most-specific-first and "already-present wins", so the shell / CI
|
|
62
|
+
* always overrides the files. Missing files are skipped silently. Uses Node's
|
|
63
|
+
* built-in parser — no dependency.
|
|
64
|
+
*
|
|
65
|
+
* @param appRoot Directory URL the `.env*` files are resolved against.
|
|
66
|
+
* @param options.skipEnvLocal Skip `.env.local` (the test flow does, so a
|
|
67
|
+
* developer's local overrides don't leak into tests).
|
|
68
|
+
*/
|
|
69
|
+
export function loadEnvFiles(appRoot: URL, options: { skipEnvLocal?: boolean } = {}): void {
|
|
70
|
+
const directory = envDirectory(appRoot)
|
|
71
|
+
let found = false
|
|
72
|
+
for (const name of envFileNames(options)) {
|
|
33
73
|
let contents: string
|
|
34
74
|
try {
|
|
35
|
-
contents = readFileSync(fileURLToPath(new URL(name,
|
|
75
|
+
contents = readFileSync(fileURLToPath(new URL(name, directory)), 'utf8')
|
|
36
76
|
} catch {
|
|
37
77
|
continue // file absent — nothing to load
|
|
38
78
|
}
|
|
79
|
+
found = true
|
|
39
80
|
const parsed = parseEnv(contents)
|
|
40
81
|
for (const [key, value] of Object.entries(parsed)) {
|
|
41
82
|
if (typeof value !== 'string' || process.env[key] !== undefined) continue
|
|
@@ -49,4 +90,18 @@ export function loadEnvFiles(appRoot: URL, options: { skipEnvLocal?: boolean } =
|
|
|
49
90
|
})
|
|
50
91
|
}
|
|
51
92
|
}
|
|
93
|
+
|
|
94
|
+
// Upstream raises when `ENV_PATH` names a place with no env file in it, and
|
|
95
|
+
// for the reason silence would be wrong: the variable is somebody saying
|
|
96
|
+
// where the file IS. A typo in the path would otherwise start a server on
|
|
97
|
+
// whatever defaults happened to be around, which is how a deployment reads
|
|
98
|
+
// its staging config in production and nobody finds out until later.
|
|
99
|
+
//
|
|
100
|
+
// Without `ENV_PATH` a missing file stays silent: an application may have
|
|
101
|
+
// none at all, and every value may come from the environment.
|
|
102
|
+
if (!found && process.env.ENV_PATH !== undefined && process.env.ENV_PATH.trim() !== '') {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`ENV_PATH points at ${process.env.ENV_PATH}, which holds none of ${envFileNames(options).join(', ')}`,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
52
107
|
}
|