@c9up/ream 0.2.24 → 0.2.25

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.
@@ -7,9 +7,16 @@
7
7
  * what they need is to be right, not fast.
8
8
  *
9
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
10
+ * * any run of characters inside one path segment
11
+ * ** any number of segments, including none
12
+ * ? exactly one character inside a segment
13
+ * {a,b} either alternative
14
+ *
15
+ * The brace form is the one a `metaFiles` entry uses —
16
+ * `resources/lang/**\/*.{json,yaml,yml}` — and it has to be understood on this
17
+ * side too: the CLI matches the same pattern when it copies those files into
18
+ * the build, and a dialect that differed between the two would let an entry
19
+ * copy at build time and never fire in development, or the reverse.
13
20
  *
14
21
  * A leading `./` is optional, and matching is done on POSIX-shaped paths
15
22
  * relative to the project root, so a Windows checkout and a Linux one read the
@@ -42,6 +49,24 @@ export function globToRegExp(pattern) {
42
49
  out += '[^/]';
43
50
  continue;
44
51
  }
52
+ if (char === '{') {
53
+ const close = cleaned.indexOf('}', i);
54
+ if (close !== -1) {
55
+ // Alternatives are literal text, never nested globs: that is the whole
56
+ // of the form these patterns use, and a recursive reading would accept
57
+ // shapes the CLI's matcher does not.
58
+ const options = cleaned
59
+ .slice(i + 1, close)
60
+ .split(',')
61
+ .map((option) => option.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
62
+ out += `(?:${options.join('|')})`;
63
+ i = close;
64
+ continue;
65
+ }
66
+ // Unclosed: read literally rather than throwing. A pattern is a string in
67
+ // a config file, and a malformed one should match nothing, not take the
68
+ // dev server down at boot.
69
+ }
45
70
  // Everything else is literal, including the characters a regular
46
71
  // expression would otherwise read as syntax.
47
72
  out += (char ?? '').replace(/[.+^${}()|[\]\\]/g, '\\$&');
@@ -1 +1 @@
1
- {"version":3,"file":"hotGlob.js","sourceRoot":"","sources":["../../src/dev/hotGlob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,yDAAyD;AACzD,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IAC5C,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;QACvB,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC3B,oEAAoE;gBACpE,2CAA2C;gBAC3C,CAAC,IAAI,CAAC,CAAA;gBACN,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC3B,CAAC,IAAI,CAAC,CAAA;oBACN,GAAG,IAAI,aAAa,CAAA;gBACtB,CAAC;qBAAM,CAAC;oBACN,GAAG,IAAI,IAAI,CAAA;gBACb,CAAC;gBACD,SAAQ;YACV,CAAC;YACD,GAAG,IAAI,OAAO,CAAA;YACd,SAAQ;QACV,CAAC;QACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,GAAG,IAAI,MAAM,CAAA;YACb,SAAQ;QACV,CAAC;QACD,iEAAiE;QACjE,6CAA6C;QAC7C,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAA;IAC1D,CAAC;IACD,OAAO,IAAI,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,CAAA;AAC/B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,QAA2B;IACrD,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAC9C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,EAAE,CAAC,KAAK,CAAA;IAChD,OAAO,CAAC,IAAY,EAAE,EAAE;QACtB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;QAChE,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAA;IACtE,CAAC,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"hotGlob.js","sourceRoot":"","sources":["../../src/dev/hotGlob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,yDAAyD;AACzD,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IAC5C,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;QACvB,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC3B,oEAAoE;gBACpE,2CAA2C;gBAC3C,CAAC,IAAI,CAAC,CAAA;gBACN,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC3B,CAAC,IAAI,CAAC,CAAA;oBACN,GAAG,IAAI,aAAa,CAAA;gBACtB,CAAC;qBAAM,CAAC;oBACN,GAAG,IAAI,IAAI,CAAA;gBACb,CAAC;gBACD,SAAQ;YACV,CAAC;YACD,GAAG,IAAI,OAAO,CAAA;YACd,SAAQ;QACV,CAAC;QACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,GAAG,IAAI,MAAM,CAAA;YACb,SAAQ;QACV,CAAC;QACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;YACrC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;gBACjB,uEAAuE;gBACvE,uEAAuE;gBACvE,qCAAqC;gBACrC,MAAM,OAAO,GAAG,OAAO;qBACpB,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;qBACnB,KAAK,CAAC,GAAG,CAAC;qBACV,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAA;gBACjE,GAAG,IAAI,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;gBACjC,CAAC,GAAG,KAAK,CAAA;gBACT,SAAQ;YACV,CAAC;YACD,0EAA0E;YAC1E,wEAAwE;YACxE,2BAA2B;QAC7B,CAAC;QACD,iEAAiE;QACjE,6CAA6C;QAC7C,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAA;IAC1D,CAAC;IACD,OAAO,IAAI,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,CAAA;AAC/B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,QAA2B;IACrD,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAC9C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,EAAE,CAAC,KAAK,CAAA;IAChD,OAAO,CAAC,IAAY,EAAE,EAAE;QACtB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;QAChE,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAA;IACtE,CAAC,CAAA;AACH,CAAC"}
package/dist/hot.js CHANGED
@@ -26,7 +26,7 @@ import { existsSync, writeSync } from 'node:fs';
26
26
  import { readFile } from 'node:fs/promises';
27
27
  import { registerHooks } from 'node:module';
28
28
  import { dirname, relative, resolve as resolvePath } from 'node:path';
29
- import { fileURLToPath } from 'node:url';
29
+ import { fileURLToPath, pathToFileURL } from 'node:url';
30
30
  import { fullReloadNotice } from './dev/fullReload.js';
31
31
  import { hotReloadHappened } from './dev/hmr.js';
32
32
  import { readHotHookConfig } from './dev/hotConfig.js';
@@ -57,9 +57,57 @@ const { boundaries = [], restart = [] } = config;
57
57
  const restartFiles = new Set(envFileNames()
58
58
  .map((name) => resolvePath(root, name))
59
59
  .filter((file) => existsSync(file)));
60
+ /**
61
+ * `metaFiles` entries that asked for a restart.
62
+ *
63
+ * Non-module files the application owns — translations, view templates — live
64
+ * in `reamrc.ts` because the BUILD has to copy them. A change to one is
65
+ * invisible to the import graph, so the only way it can do anything in
66
+ * development is by being named here.
67
+ *
68
+ * `reloadServer: false` is the interesting default, and the one upstream picks
69
+ * for translations: the entry exists so the file ships, and an edit does NOT
70
+ * restart. NAMED DEVIATION — upstream still logs such a change, because its
71
+ * watcher covers the whole project anyway. This one watches a fixed list of
72
+ * directories on purpose (`resources/` belongs to the asset watcher, and a
73
+ * stylesheet edit must not restart the server), so a `false` entry is simply
74
+ * not watched. The observable behaviour is the same: nothing happens.
75
+ */
76
+ async function readReloadPatterns() {
77
+ try {
78
+ const rc = (await import(pathToFileURL(resolvePath(root, 'reamrc.ts')).href));
79
+ return (rc.default?.metaFiles ?? [])
80
+ .filter((entry) => entry.reloadServer === true && typeof entry.pattern === 'string')
81
+ .map((entry) => String(entry.pattern));
82
+ }
83
+ catch {
84
+ // No rc file, or one that does not load. The server itself reports a broken
85
+ // rc far more clearly than the loader could, and a dev server that refuses
86
+ // to start because of a watch list helps nobody.
87
+ return [];
88
+ }
89
+ }
90
+ const reloadPatterns = await readReloadPatterns();
91
+ /**
92
+ * The directory a pattern lives under — everything before its first wildcard.
93
+ *
94
+ * `resources/lang/**\/*.json` is watched as `resources/lang`. Watching the
95
+ * project root instead would pull in `node_modules` and the build output.
96
+ */
97
+ function watchRootOf(pattern) {
98
+ const segments = pattern.replace(/^\.\//, '').split('/');
99
+ const fixed = [];
100
+ for (const segment of segments) {
101
+ if (/[*?{[]/.test(segment))
102
+ break;
103
+ fixed.push(segment);
104
+ }
105
+ // A pattern that is a bare glob has no directory to narrow to.
106
+ return fixed.slice(0, -1).join('/') || fixed.join('/');
107
+ }
60
108
  const relativeToRoot = (file) => relative(root, file).replace(/\\/g, '/');
61
109
  const matchesBoundary = globMatcher(boundaries);
62
- const matchesRestartGlob = globMatcher(restart);
110
+ const matchesRestartGlob = globMatcher([...restart, ...reloadPatterns]);
63
111
  const graph = new HotGraph({
64
112
  isBoundary: (file) => matchesBoundary(relativeToRoot(file)),
65
113
  isRestart: (file) => restartFiles.has(file) || matchesRestartGlob(relativeToRoot(file)),
@@ -113,7 +161,12 @@ function sayNow(line) {
113
161
  }
114
162
  watchProject({
115
163
  root,
116
- directories: WATCH_DIRS,
164
+ directories: [
165
+ ...WATCH_DIRS,
166
+ // Whatever a `reloadServer: true` entry named, and nothing else: the
167
+ // directories above are the ones the graph can reason about.
168
+ ...reloadPatterns.map(watchRootOf).filter((dir) => dir !== ''),
169
+ ],
117
170
  files: [...restartFiles].map((file) => relative(root, file)),
118
171
  onChange: (file) => {
119
172
  const decision = graph.decide(file);
package/dist/hot.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hot.js","sourceRoot":"","sources":["../src/hot.ts"],"names":[],"mappings":"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"}
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.24";
124
+ export declare const VERSION = "0.2.25";
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.24';
95
+ export const VERSION = '0.2.25';
96
96
  //# sourceMappingURL=index.js.map
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/ream",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "Ream — Rust-powered Node.js application framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
Binary file
package/src/Codemods.ts CHANGED
@@ -3,6 +3,7 @@ import * as path from 'node:path'
3
3
 
4
4
  export interface Codemods {
5
5
  addProvider(importPath: string): Promise<void>
6
+ addMetaFile(pattern: string, reloadServer?: boolean): Promise<void>
6
7
  addEnvVars(vars: Record<string, string>): Promise<void>
7
8
  writeFile(filePath: string, content: string, options?: { force?: boolean }): Promise<void>
8
9
  registerCommand(importPath: string): Promise<void>
@@ -120,6 +121,58 @@ export function createCodemods(options?: { force?: boolean; cwd?: string }): Cod
120
121
  fs.writeFileSync(rcPath, content)
121
122
  },
122
123
 
124
+ /**
125
+ * Record a non-module file the application owns.
126
+ *
127
+ * A package calls this from `configure()` so its files reach the build:
128
+ * `addMetaFile('resources/lang/**\/*.json', false)`. The second argument
129
+ * is DELIBERATELY explicit at every call site — whether an edit costs a
130
+ * development restart is the decision the entry exists to record.
131
+ *
132
+ * Same regex codemod as `addProvider`, and the same limits: a `metaFiles`
133
+ * assembled by a spread or extracted into a `const` above the config is not
134
+ * rewritten. The block is created when absent, which `addProvider` cannot
135
+ * do — `providers` is always there, `metaFiles` usually is not.
136
+ */
137
+ async addMetaFile(pattern: string, reloadServer = false): Promise<void> {
138
+ const rcPath = path.join(root, 'reamrc.ts')
139
+ if (!fs.existsSync(rcPath)) {
140
+ throw new Error(
141
+ `[configure] reamrc.ts not found — cannot register meta file ${pattern}. Run 'ream new' first.`,
142
+ )
143
+ }
144
+ assertCanonicallyInside(root, rcPath, 'reamrc.ts')
145
+
146
+ let content = fs.readFileSync(rcPath, 'utf8')
147
+ // Dedup on the PATTERN alone: the same glob registered twice with
148
+ // different flags is a contradiction, and the first one wins rather than
149
+ // both being written.
150
+ if (content.includes(`'${pattern}'`) || content.includes(`"${pattern}"`)) return
151
+
152
+ const eol = detectLineEnding(content)
153
+ const entry = ` { pattern: '${pattern}', reloadServer: ${reloadServer} },`
154
+
155
+ const existing = /metaFiles\s*:\s*(?:\/\*[\s\S]*?\*\/\s*)?\[/.exec(content)
156
+ if (existing !== null) {
157
+ const insertAt = existing.index + existing[0].length
158
+ content = `${content.slice(0, insertAt)}${eol}${entry}${content.slice(insertAt)}`
159
+ fs.writeFileSync(rcPath, content)
160
+ return
161
+ }
162
+
163
+ // No block yet — open one beside `providers`, which every rc file has.
164
+ const providers = /providers\s*:\s*(?:\/\*[\s\S]*?\*\/\s*)?\[/.exec(content)
165
+ if (providers === null) {
166
+ throw new Error(
167
+ `[configure] Could not find 'providers: [' in reamrc.ts — meta file ${pattern} not added. Check your reamrc.ts format.`,
168
+ )
169
+ }
170
+ const blockAt = providers.index
171
+ const block = `metaFiles: [${eol}${entry}${eol} ],${eol} `
172
+ content = `${content.slice(0, blockAt)}${block}${content.slice(blockAt)}`
173
+ fs.writeFileSync(rcPath, content)
174
+ },
175
+
123
176
  async addEnvVars(vars: Record<string, string>): Promise<void> {
124
177
  const envPath = path.join(root, '.env')
125
178
  // A symlinked .env must not redirect the write outside the root.
package/src/Ignitor.ts CHANGED
@@ -17,7 +17,7 @@ import { Application } from './Application.js'
17
17
  import type { Console } from './console/Console.js'
18
18
  import type { CommandLoader, Kernel as ConsoleKernelInstance } from './console/Kernel.js'
19
19
  import { type CommandClass, isCommandClass } from './console/types.js'
20
- import { HMR_PATH, hmrClientScript, hmrToken } from './dev/hmr.js'
20
+ import { hmrClientScript, hmrEndpoint } from './dev/hmr.js'
21
21
  import type { DirectoriesNode } from './directories.js'
22
22
  import type { ErrorEvent } from './ErrorBoundary.js'
23
23
  import { ErrorBoundary } from './ErrorBoundary.js'
@@ -137,6 +137,35 @@ export interface ReamrcConfig {
137
137
  * `defineConfig`, which is a strange way to learn that the option exists.
138
138
  */
139
139
  assets?: AssetsConfig
140
+ /**
141
+ * Files that are not modules but belong to the application.
142
+ *
143
+ * Translations, view templates, a mail signature: the build has no reason to
144
+ * know about them, and without being told it ships a `dist/` that boots and
145
+ * then cannot find them. Every pattern here is copied into the build output,
146
+ * keeping its path relative to the project root.
147
+ *
148
+ * `reloadServer` decides what a change does in DEVELOPMENT, and the default
149
+ * is the interesting half. Upstream registers translations with `false` —
150
+ * the files are watched so the build copies them, and an edit does NOT
151
+ * restart the server. A package that genuinely needs a restart asks for one
152
+ * by passing `true`.
153
+ */
154
+ metaFiles?: MetaFileConfig[]
155
+ }
156
+
157
+ /**
158
+ * One `metaFiles` entry.
159
+ *
160
+ * `reloadServer` is required rather than defaulted: whether an edit costs a
161
+ * restart is the whole decision this entry exists to record, and a default
162
+ * would let it be made by accident.
163
+ */
164
+ export interface MetaFileConfig {
165
+ /** Glob, relative to the project root — `resources/lang/**\/*.json`. */
166
+ pattern: string
167
+ /** Restart the development server when a matching file changes. */
168
+ reloadServer: boolean
140
169
  }
141
170
 
142
171
  /** One test suite, as declared in the rc file (AdonisJS `tests.suites[]`). */
@@ -709,7 +738,6 @@ export class Ignitor {
709
738
  // anything. Registered in `ready()` it came up after the server was already
710
739
  // serving, so a page loaded in that window polled a route that did not
711
740
  // exist yet.
712
- this.#registerDevReloadRoute()
713
741
 
714
742
  // Providers start BEFORE the preload files, matching upstream's warm-up
715
743
  // order (`providers.start()` → `starting` hooks → preloads). A preload is
@@ -797,7 +825,10 @@ export class Ignitor {
797
825
  container: this.#app.container,
798
826
  exceptionHandler:
799
827
  this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
800
- serverMiddleware: this.#server.getServerMiddleware(),
828
+ serverMiddleware: [
829
+ ...this.#frameworkServerMiddleware(),
830
+ ...this.#server.getServerMiddleware(),
831
+ ],
801
832
  routerMiddleware: this.#router.getRouterMiddleware(),
802
833
  devReloadScript: this.#devReloadScript(),
803
834
  onError: (error, ctx) => {
@@ -1288,19 +1319,18 @@ export class Ignitor {
1288
1319
  }
1289
1320
 
1290
1321
  /**
1291
- * Serve the token the injected script polls.
1322
+ * The server middleware the framework itself contributes, ahead of the
1323
+ * application's.
1292
1324
  *
1293
- * Registered on the router in development, next to the application's own
1294
- * routes, so it goes through the same server as everything else. Plain text
1295
- * and uncached: it is read once a second and its whole content is the answer.
1325
+ * Only the hot-reload token, and only in development. It answers first
1326
+ * because the poll arrives once a second per open tab and its content
1327
+ * depends on nothing — no user, no session, no body. Behind an application's
1328
+ * middleware it paid for the whole stack each time, which for anything that
1329
+ * resolves a user from a cookie meant a `SELECT` per second for a string
1330
+ * holding a process id and a counter.
1296
1331
  */
1297
- #registerDevReloadRoute(): void {
1298
- if (!this.isDevMode()) return
1299
- this.#router.get(HMR_PATH, ({ response }) => {
1300
- response.header('cache-control', 'no-store')
1301
- response.header('content-type', 'text/plain; charset=utf-8')
1302
- response.send(hmrToken())
1303
- })
1332
+ #frameworkServerMiddleware(): MiddlewareFunction[] {
1333
+ return this.isDevMode() ? [hmrEndpoint()] : []
1304
1334
  }
1305
1335
 
1306
1336
  getPhase(): string {
package/src/dev/hmr.ts CHANGED
@@ -45,6 +45,90 @@ function holder(): CounterHolder {
45
45
  export function hotReloadHappened(): void {
46
46
  const store = holder()
47
47
  store[COUNTER] = (store[COUNTER] ?? 0) + 1
48
+ notifyClients()
49
+ }
50
+
51
+ /**
52
+ * The dev clients currently listening.
53
+ *
54
+ * A `Set` and not a count: each entry is pushed to on a swap, and dropped when
55
+ * the tab closes. `unknown`-typed on purpose — this module must not import the
56
+ * HTTP layer, which imports it back.
57
+ */
58
+ interface DevClient {
59
+ send: (event: string, data: unknown) => Promise<boolean>
60
+ onClose: (callback: () => void) => void
61
+ }
62
+
63
+ const clients = new Set<DevClient>()
64
+
65
+ /** The event name the injected script listens for. */
66
+ const HMR_EVENT = 'ream:hmr'
67
+
68
+ /**
69
+ * Answer the dev client BEFORE any application middleware runs, and PUSH.
70
+ *
71
+ * Two problems, one change.
72
+ *
73
+ * It used to be an ordinary route polled once a second, per open tab, and a
74
+ * route runs the whole middleware stack. An application that resolves a user
75
+ * from a session cookie therefore ran `SELECT * FROM users` once a second for
76
+ * a response whose entire content is a process id and a counter, and which
77
+ * depends on no user, no session and no body.
78
+ *
79
+ * It is a stream now, so the cost is paid ONCE per tab instead of once a
80
+ * second, and a change reaches the browser immediately rather than up to a
81
+ * second later. Upstream gets the same thing from Vite's websocket; this needs
82
+ * no bundler, because the server already speaks SSE.
83
+ *
84
+ * Registered FIRST rather than merely outside the router: middleware that
85
+ * authenticates lives at either tier, and an application is free to put it in
86
+ * `server.use([...])` — many do, because it must run before routing.
87
+ *
88
+ * Development only. In production nothing injects the script and nothing
89
+ * registers this.
90
+ */
91
+ export function hmrEndpoint(): (
92
+ ctx: {
93
+ request: { url: () => string }
94
+ response: { sse: () => Promise<DevClient> }
95
+ },
96
+ next: () => Promise<void>,
97
+ ) => Promise<void> {
98
+ return async (ctx, next) => {
99
+ // `url()` carries the query string; a client may append a cache-buster.
100
+ const path = ctx.request.url().split('?')[0]
101
+ if (path !== HMR_PATH) {
102
+ await next()
103
+ return
104
+ }
105
+
106
+ let stream: DevClient
107
+ try {
108
+ stream = await ctx.response.sse()
109
+ } catch {
110
+ // A host without the streaming NAPI — a mock server in a test, say.
111
+ // Losing the reload is fine; failing the request is not.
112
+ await next()
113
+ return
114
+ }
115
+
116
+ clients.add(stream)
117
+ stream.onClose(() => clients.delete(stream))
118
+ // Immediately, so a tab that connects after a restart compares against the
119
+ // CURRENT token rather than waiting for the next swap to learn anything.
120
+ await stream.send(HMR_EVENT, hmrToken())
121
+ }
122
+ }
123
+
124
+ /** Tell every open tab the token moved. */
125
+ function notifyClients(): void {
126
+ const token = hmrToken()
127
+ for (const client of clients) {
128
+ // Detached: a swap must not wait on a socket, and a dead one is dropped by
129
+ // its own close handler.
130
+ void client.send(HMR_EVENT, token).catch(() => clients.delete(client))
131
+ }
48
132
  }
49
133
 
50
134
  /** How many hot swaps this process has seen. */
@@ -63,10 +147,15 @@ export function hmrToken(): string {
63
147
  /**
64
148
  * The script injected into HTML pages in dev.
65
149
  *
66
- * It reloads when the token changes, NOT when the request fails: a server that
67
- * is restarting is unreachable for a moment, and reloading then would only show
68
- * the browser's error page. It waits for an answer, and the answer carries a
69
- * new boot id.
150
+ * An `EventSource`, not a poll. The server pushes the token, so a change
151
+ * reaches the page immediately and an idle tab costs one open connection
152
+ * rather than a request per second through the whole middleware stack.
153
+ *
154
+ * It reloads when the token CHANGES, never when the connection fails: a
155
+ * restarting server is unreachable for a moment, and reloading then would only
156
+ * show the browser's error page. `EventSource` reconnects on its own, and the
157
+ * server greets the new connection with its token — which after a restart is a
158
+ * different boot id, so the reload happens then, once the server can serve it.
70
159
  */
71
160
  export function hmrClientScript(nonce?: string): string {
72
161
  // A nonce-based CSP — which the scaffold ships, via `@c9up/blackhole` —
@@ -75,9 +164,13 @@ export function hmrClientScript(nonce?: string): string {
75
164
  // are escaped because a nonce is generated, not user input, but a broken
76
165
  // attribute would silently disable the tag rather than fail loudly.
77
166
  const attribute = nonce === undefined ? '' : ` nonce="${nonce.replace(/"/g, '&quot;')}"`
78
- // A failed poll RETRIES. Returning on `!r.ok` stopped the loop for good, so a
79
- // page opened in the window before the endpoint is mounted — or during any
80
- // blip — never reloaded again for the life of that tab, which looks exactly
81
- // like the feature not working.
82
- return `<script${attribute}>(()=>{let t=null;const p=${JSON.stringify(HMR_PATH)};const tick=async()=>{try{const r=await fetch(p,{cache:'no-store'});if(r.ok){const n=(await r.text()).trim();if(t===null){t=n}else if(n!==t){location.reload();return}}}catch(e){}setTimeout(tick,1000)};tick()})()</script>`
167
+ // `addEventListener(HMR_EVENT)`, NOT `onmessage`: the stream names its
168
+ // events, and `onmessage` fires only for unnamed ones — a client wired that
169
+ // way receives nothing at all, silently, which is a mistake this codebase
170
+ // has already made once with relay.
171
+ return `<script${attribute}>(()=>{let t=null;const s=new EventSource(${JSON.stringify(
172
+ HMR_PATH,
173
+ )});s.addEventListener(${JSON.stringify(
174
+ HMR_EVENT,
175
+ )},e=>{let n;try{n=JSON.parse(e.data)}catch(_){n=e.data}if(t===null){t=n}else if(n!==t){s.close();location.reload()}})})()</script>`
83
176
  }
@@ -7,9 +7,16 @@
7
7
  * what they need is to be right, not fast.
8
8
  *
9
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
10
+ * * any run of characters inside one path segment
11
+ * ** any number of segments, including none
12
+ * ? exactly one character inside a segment
13
+ * {a,b} either alternative
14
+ *
15
+ * The brace form is the one a `metaFiles` entry uses —
16
+ * `resources/lang/**\/*.{json,yaml,yml}` — and it has to be understood on this
17
+ * side too: the CLI matches the same pattern when it copies those files into
18
+ * the build, and a dialect that differed between the two would let an entry
19
+ * copy at build time and never fire in development, or the reverse.
13
20
  *
14
21
  * A leading `./` is optional, and matching is done on POSIX-shaped paths
15
22
  * relative to the project root, so a Windows checkout and a Linux one read the
@@ -42,6 +49,24 @@ export function globToRegExp(pattern: string): RegExp {
42
49
  out += '[^/]'
43
50
  continue
44
51
  }
52
+ if (char === '{') {
53
+ const close = cleaned.indexOf('}', i)
54
+ if (close !== -1) {
55
+ // Alternatives are literal text, never nested globs: that is the whole
56
+ // of the form these patterns use, and a recursive reading would accept
57
+ // shapes the CLI's matcher does not.
58
+ const options = cleaned
59
+ .slice(i + 1, close)
60
+ .split(',')
61
+ .map((option) => option.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
62
+ out += `(?:${options.join('|')})`
63
+ i = close
64
+ continue
65
+ }
66
+ // Unclosed: read literally rather than throwing. A pattern is a string in
67
+ // a config file, and a malformed one should match nothing, not take the
68
+ // dev server down at boot.
69
+ }
45
70
  // Everything else is literal, including the characters a regular
46
71
  // expression would otherwise read as syntax.
47
72
  out += (char ?? '').replace(/[.+^${}()|[\]\\]/g, '\\$&')
package/src/hot.ts CHANGED
@@ -27,7 +27,7 @@ import { existsSync, writeSync } from 'node:fs'
27
27
  import { readFile } from 'node:fs/promises'
28
28
  import { registerHooks } from 'node:module'
29
29
  import { dirname, relative, resolve as resolvePath } from 'node:path'
30
- import { fileURLToPath } from 'node:url'
30
+ import { fileURLToPath, pathToFileURL } from 'node:url'
31
31
  import { fullReloadNotice } from './dev/fullReload.js'
32
32
  import { hotReloadHappened } from './dev/hmr.js'
33
33
  import { readHotHookConfig } from './dev/hotConfig.js'
@@ -66,9 +66,60 @@ const restartFiles = new Set(
66
66
  .filter((file) => existsSync(file)),
67
67
  )
68
68
 
69
+ /**
70
+ * `metaFiles` entries that asked for a restart.
71
+ *
72
+ * Non-module files the application owns — translations, view templates — live
73
+ * in `reamrc.ts` because the BUILD has to copy them. A change to one is
74
+ * invisible to the import graph, so the only way it can do anything in
75
+ * development is by being named here.
76
+ *
77
+ * `reloadServer: false` is the interesting default, and the one upstream picks
78
+ * for translations: the entry exists so the file ships, and an edit does NOT
79
+ * restart. NAMED DEVIATION — upstream still logs such a change, because its
80
+ * watcher covers the whole project anyway. This one watches a fixed list of
81
+ * directories on purpose (`resources/` belongs to the asset watcher, and a
82
+ * stylesheet edit must not restart the server), so a `false` entry is simply
83
+ * not watched. The observable behaviour is the same: nothing happens.
84
+ */
85
+ async function readReloadPatterns(): Promise<string[]> {
86
+ try {
87
+ const rc = (await import(pathToFileURL(resolvePath(root, 'reamrc.ts')).href)) as {
88
+ default?: { metaFiles?: Array<{ pattern?: unknown; reloadServer?: unknown }> }
89
+ }
90
+ return (rc.default?.metaFiles ?? [])
91
+ .filter((entry) => entry.reloadServer === true && typeof entry.pattern === 'string')
92
+ .map((entry) => String(entry.pattern))
93
+ } catch {
94
+ // No rc file, or one that does not load. The server itself reports a broken
95
+ // rc far more clearly than the loader could, and a dev server that refuses
96
+ // to start because of a watch list helps nobody.
97
+ return []
98
+ }
99
+ }
100
+
101
+ const reloadPatterns = await readReloadPatterns()
102
+
103
+ /**
104
+ * The directory a pattern lives under — everything before its first wildcard.
105
+ *
106
+ * `resources/lang/**\/*.json` is watched as `resources/lang`. Watching the
107
+ * project root instead would pull in `node_modules` and the build output.
108
+ */
109
+ function watchRootOf(pattern: string): string {
110
+ const segments = pattern.replace(/^\.\//, '').split('/')
111
+ const fixed: string[] = []
112
+ for (const segment of segments) {
113
+ if (/[*?{[]/.test(segment)) break
114
+ fixed.push(segment)
115
+ }
116
+ // A pattern that is a bare glob has no directory to narrow to.
117
+ return fixed.slice(0, -1).join('/') || fixed.join('/')
118
+ }
119
+
69
120
  const relativeToRoot = (file: string): string => relative(root, file).replace(/\\/g, '/')
70
121
  const matchesBoundary = globMatcher(boundaries)
71
- const matchesRestartGlob = globMatcher(restart)
122
+ const matchesRestartGlob = globMatcher([...restart, ...reloadPatterns])
72
123
 
73
124
  const graph = new HotGraph({
74
125
  isBoundary: (file) => matchesBoundary(relativeToRoot(file)),
@@ -126,7 +177,12 @@ function sayNow(line: string): void {
126
177
 
127
178
  watchProject({
128
179
  root,
129
- directories: WATCH_DIRS,
180
+ directories: [
181
+ ...WATCH_DIRS,
182
+ // Whatever a `reloadServer: true` entry named, and nothing else: the
183
+ // directories above are the ones the graph can reason about.
184
+ ...reloadPatterns.map(watchRootOf).filter((dir) => dir !== ''),
185
+ ],
130
186
  files: [...restartFiles].map((file) => relative(root, file)),
131
187
  onChange: (file) => {
132
188
  const decision = graph.decide(file)
package/src/index.ts CHANGED
@@ -385,4 +385,4 @@ export {
385
385
  StatsTracker,
386
386
  } from './scheduler/index.js'
387
387
 
388
- export const VERSION = '0.2.24'
388
+ export const VERSION = '0.2.25'