@luckystack/devkit 0.14.0 → 0.15.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/AGENTS.md CHANGED
@@ -6,4 +6,4 @@ Generated route contracts must preserve transport-safe types. A source file miss
6
6
 
7
7
  The one deliberate diagnostics exception — an absent optional sync side — must stay scoped to that side's own field and run after the thrown-extraction check (ADR 0057). Server-backed sync routes share the API side's auth/rate-limit extractors and schema converter so the test-runner can sweep them (ADR 0064). See `docs/type-map-generation.md`.
8
8
 
9
- Hot reload should not block serving live requests while building developer artifacts. Avoid invalidating the shared Program on initial population without cause; do invalidate when source changes. Consumer-owned template overrides intentionally outlive the installed devkit's bundled defaults; do not overwrite them on upgrade.
9
+ Hot reload does pause request handling while it builds the TypeScript Program and regenerates the type map: that work is synchronous, and deferring it only lets the current event finish. The pause is accepted for development; keep its duration logged and keep generation off the boot path so the server listens first. Do not describe it as non-blocking (see `docs/hot-reload.md`). Avoid invalidating the shared Program on initial population without cause; do invalidate when source changes. Consumer-owned template overrides intentionally outlive the installed devkit's bundled defaults; do not overwrite them on upgrade.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to `@luckystack/devkit` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.15.0] - 2026-10-19
9
+
10
+ ### Changed
11
+
12
+ - **Editing a file that a route imports restarts the supervised dev server** (`npm run server`), so the new code actually runs; connected clients reconnect (ADR 0079). Before, the route was re-imported but kept running the old dependency code. The supervisor now starts the dev server with an IPC channel for this (Node; Bun on Linux and macOS). Without the supervisor, a warning says a restart is needed. Editing a route file itself still reloads it in place.
13
+ - **Hot reload logs how long type-map regeneration and each route reload took.** The docs now say the dev server answers no requests during that time (ADR 0080).
14
+
15
+ ### Fixed
16
+
17
+ - **The dev server starts, and type generation runs, when a `.test.ts` or `.spec.ts` file sits next to a route.**
18
+ - **A project checked out under a folder named `_api` or `_sync` boots again.** Route naming is validated relative to the source folder.
19
+ - **The duplicate-route check reports a root sync** (`src/_sync/x_server_v1.ts`) that collides with `src/system/_sync/x_server_v1.ts`, as it already did for APIs.
20
+ - **Deleting a sync `_server` file moves the real `clientInput` type into the paired client** instead of an empty placeholder, and that client rewrite is no longer lost when another sync file is saved within the reload debounce window.
21
+ - **Helpers in a private folder under `_api`/`_sync`** (for example `_api/_lib/helpers.ts`) **and test files next to routes are no longer treated as misnamed routes:** a new empty helper gets no guidance comment written into it, there is no false "ignored by route loading" warning, and editing a helper regenerates types and reloads the routes that use it.
22
+ - **The paired-sync rewrite writes the correct `PagePath`** when the project lives under a folder named `src`, uses a different source folder, or uses a custom sync marker.
23
+ - **A fully commented-out route or page file that arrives by rename, move, `git checkout` or `stash pop` is no longer overwritten with the starter template.** Only empty files, or files that still hold only the injector's guidance line, receive a template.
24
+ - **Route reloads after a dependency change honour a custom source folder and custom route markers.**
25
+ - **Writing the generated Zod schema file no longer triggers a second full type-map generation.**
26
+ - **Saving a watched file while the supervisor waits to restart a crashed server no longer starts two server processes.**
27
+
8
28
  ## [0.14.0] - 2026-10-16
9
29
 
10
30
  ### Fixed
@@ -0,0 +1,17 @@
1
+ // src/devServerRestart.ts
2
+ var DEV_RESTART_MESSAGE_TYPE = "luckystack:dev-restart";
3
+ var isDevRestartMessage = (value) => typeof value === "object" && value !== null && "type" in value && value.type === DEV_RESTART_MESSAGE_TYPE && "reason" in value && typeof value.reason === "string";
4
+ var requestDevServerRestart = (reason) => {
5
+ if (process.env.LUCKYSTACK_CORE_SUPERVISED !== "true") return false;
6
+ if (typeof process.send !== "function" || !process.connected) return false;
7
+ const message = { type: DEV_RESTART_MESSAGE_TYPE, reason };
8
+ return process.send(message, void 0, void 0, (error) => {
9
+ if (error) console.log(`[HotReload] Could not ask the supervisor to restart: ${String(error)}`, "red");
10
+ });
11
+ };
12
+
13
+ export {
14
+ isDevRestartMessage,
15
+ requestDevServerRestart
16
+ };
17
+ //# sourceMappingURL=chunk-FWE6YJ3G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/devServerRestart.ts"],"sourcesContent":["//? Restart request from the dev server (child) to the supervisor (parent).\n//?\n//? Why a restart and not a reload: hot reload re-imports a route with a `?v=`\n//? query, but the ESM loader caches the route's own imports by their plain URL.\n//? After a helper the route imports changes, the re-imported route still links\n//? to the cached OLD helper, so only a fresh process runs the new code. No\n//? loader hook can carry the query down the import graph under both tsx and\n//? Bun, so the supervisor restarts the child, the same way it does for a\n//? `config.ts` edit.\n//?\n//? This module is bundled into the supervisor entry, so it must import nothing\n//? from `@luckystack/core` (see the env invariant in `supervisor.ts`).\n\nexport const DEV_RESTART_MESSAGE_TYPE = 'luckystack:dev-restart';\n\nexport interface DevRestartMessage {\n type: typeof DEV_RESTART_MESSAGE_TYPE;\n reason: string;\n}\n\nexport const isDevRestartMessage = (value: unknown): value is DevRestartMessage =>\n typeof value === 'object'\n && value !== null\n && 'type' in value\n && value.type === DEV_RESTART_MESSAGE_TYPE\n && 'reason' in value\n && typeof value.reason === 'string';\n\n/**\n * Ask the supervisor to restart this dev server. Returns false when nothing\n * can restart it (started without the supervisor, or no IPC channel), so the\n * caller can tell the developer to restart by hand.\n */\nexport const requestDevServerRestart = (reason: string): boolean => {\n if (process.env.LUCKYSTACK_CORE_SUPERVISED !== 'true') return false;\n if (typeof process.send !== 'function' || !process.connected) return false;\n\n const message: DevRestartMessage = { type: DEV_RESTART_MESSAGE_TYPE, reason };\n //? The callback keeps a failed send from surfacing as an unhandled 'error'.\n return process.send(message, undefined, undefined, (error: Error | null) => {\n if (error) console.log(`[HotReload] Could not ask the supervisor to restart: ${String(error)}`, 'red');\n });\n};\n"],"mappings":";AAaO,IAAM,2BAA2B;AAOjC,IAAM,sBAAsB,CAAC,UAClC,OAAO,UAAU,YACd,UAAU,QACV,UAAU,SACV,MAAM,SAAS,4BACf,YAAY,SACZ,OAAO,MAAM,WAAW;AAOtB,IAAM,0BAA0B,CAAC,WAA4B;AAClE,MAAI,QAAQ,IAAI,+BAA+B,OAAQ,QAAO;AAC9D,MAAI,OAAO,QAAQ,SAAS,cAAc,CAAC,QAAQ,UAAW,QAAO;AAErE,QAAM,UAA6B,EAAE,MAAM,0BAA0B,OAAO;AAE5E,SAAO,QAAQ,KAAK,SAAS,QAAW,QAAW,CAAC,UAAwB;AAC1E,QAAI,MAAO,SAAQ,IAAI,wDAAwD,OAAO,KAAK,CAAC,IAAI,KAAK;AAAA,EACvG,CAAC;AACH;","names":[]}
@@ -0,0 +1,12 @@
1
+ export declare const DEV_RESTART_MESSAGE_TYPE = "luckystack:dev-restart";
2
+ export interface DevRestartMessage {
3
+ type: typeof DEV_RESTART_MESSAGE_TYPE;
4
+ reason: string;
5
+ }
6
+ export declare const isDevRestartMessage: (value: unknown) => value is DevRestartMessage;
7
+ /**
8
+ * Ask the supervisor to restart this dev server. Returns false when nothing
9
+ * can restart it (started without the supervisor, or no IPC channel), so the
10
+ * caller can tell the developer to restart by hand.
11
+ */
12
+ export declare const requestDevServerRestart: (reason: string) => boolean;
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import {
2
+ requestDevServerRestart
3
+ } from "./chunk-FWE6YJ3G.js";
1
4
  import {
2
5
  validateDeploy
3
6
  } from "./chunk-BJN4XIUX.js";
@@ -64,9 +67,6 @@ var isSyncFileName = (fileName) => {
64
67
  const stem = fileName.slice(0, -3);
65
68
  return getRoutingRules().syncVersionRegex.test(stem);
66
69
  };
67
- var isRouteTestFile = (fileNameOrPath) => {
68
- return fileNameOrPath.endsWith(".tests.ts");
69
- };
70
70
  var isNonRouteDirectory = (directoryName) => directoryName.startsWith(getRoutingRules().privateFolderPrefix) || isTestDirectory(directoryName);
71
71
  var validatePagePath = (srcRelativePath) => {
72
72
  const rules2 = getRoutingRules();
@@ -129,54 +129,56 @@ var isInsidePrivateRouteSubfolder = (fullPath) => {
129
129
  return segments.slice(markerIndex + 1).some((segment) => segment.startsWith(privateFolderPrefix));
130
130
  };
131
131
  var isRouteSurfaceFile = (fullPath) => !isInsidePrivateRouteSubfolder(fullPath) && !isTestFile(fullPath);
132
- var walkRouteFiles = (dir, results = []) => {
132
+ var toSrcMarkerPath = (srcDir, filePath) => `/${normalizePath(path.relative(srcDir, filePath))}`;
133
+ var walkRouteFiles = (srcDir, dir = srcDir, results = []) => {
133
134
  const entries = fs.readdirSync(dir, { withFileTypes: true });
134
135
  const apiSeg = apiMarkerSegment();
135
136
  const syncSeg = syncMarkerSegment();
136
137
  const { ignore } = getRoutingRules();
137
138
  for (const entry of entries) {
138
139
  const fullPath = path.join(dir, entry.name);
139
- const normalizedFullPath = normalizePath(fullPath);
140
+ const srcMarkerPath = toSrcMarkerPath(srcDir, fullPath);
140
141
  if (ignore(toRel(fullPath))) continue;
141
- if (isInsidePrivateRouteSubfolder(fullPath)) continue;
142
+ if (isInsidePrivateRouteSubfolder(srcMarkerPath)) continue;
142
143
  if (entry.isDirectory()) {
143
144
  if (entry.name.startsWith(".") || entry.name === "node_modules") {
144
145
  continue;
145
146
  }
146
- walkRouteFiles(fullPath, results);
147
+ walkRouteFiles(srcDir, fullPath, results);
147
148
  continue;
148
149
  }
149
150
  if (!entry.isFile() || !entry.name.endsWith(".ts")) {
150
151
  continue;
151
152
  }
152
- if (isRouteTestFile(entry.name)) {
153
+ if (!isRouteSurfaceFile(srcMarkerPath)) {
153
154
  continue;
154
155
  }
155
- if (normalizedFullPath.includes(apiSeg) || normalizedFullPath.includes(syncSeg)) {
156
+ if (srcMarkerPath.includes(apiSeg) || srcMarkerPath.includes(syncSeg)) {
156
157
  results.push(fullPath);
157
158
  }
158
159
  }
159
160
  return results;
160
161
  };
161
162
  var getFileRouteToken = ({
162
- normalizedFilePath,
163
+ srcMarkerPath,
163
164
  marker
164
165
  }) => {
165
- const markerIndex = normalizedFilePath.indexOf(marker);
166
+ const markerIndex = srcMarkerPath.indexOf(marker);
166
167
  if (markerIndex === -1) {
167
168
  return "";
168
169
  }
169
170
  const tokenStart = markerIndex + marker.length;
170
- return normalizedFilePath.slice(tokenStart, normalizedFilePath.length - ".ts".length);
171
+ return srcMarkerPath.slice(tokenStart, srcMarkerPath.length - ".ts".length);
171
172
  };
172
- var validateRouteFilePath = (filePath) => {
173
+ var validateRouteFilePath = (srcDir, filePath) => {
173
174
  const issues = [];
174
175
  const normalizedFilePath = normalizePath(path.resolve(filePath));
176
+ const srcMarkerPath = toSrcMarkerPath(srcDir, filePath);
175
177
  const fileName = path.basename(filePath);
176
178
  const apiSeg = apiMarkerSegment();
177
179
  const syncSeg = syncMarkerSegment();
178
- if (normalizedFilePath.includes(apiSeg)) {
179
- const apiRouteToken = getFileRouteToken({ normalizedFilePath, marker: apiSeg });
180
+ if (srcMarkerPath.includes(apiSeg)) {
181
+ const apiRouteToken = getFileRouteToken({ srcMarkerPath, marker: apiSeg });
180
182
  if (apiRouteToken.includes("/")) {
181
183
  issues.push({
182
184
  kind: "api",
@@ -194,8 +196,8 @@ var validateRouteFilePath = (filePath) => {
194
196
  });
195
197
  }
196
198
  }
197
- if (normalizedFilePath.includes(syncSeg)) {
198
- const syncRouteToken = getFileRouteToken({ normalizedFilePath, marker: syncSeg });
199
+ if (srcMarkerPath.includes(syncSeg)) {
200
+ const syncRouteToken = getFileRouteToken({ srcMarkerPath, marker: syncSeg });
199
201
  if (syncRouteToken.includes("/")) {
200
202
  issues.push({
201
203
  kind: "sync",
@@ -259,14 +261,14 @@ var resolveSyncRouteKey = ({
259
261
  const kind = syncMatch[1];
260
262
  const version = `v${syncMatch[2]}`;
261
263
  const syncName = rawSyncName.replace(rules2.syncVersionRegex, "");
262
- const routeBaseKey = pageLocation ? `sync/${pageLocation}/${syncName}/${version}` : `sync/${syncName}/${version}`;
264
+ const routeBaseKey = `sync/${pageLocation || "system"}/${syncName}/${version}`;
263
265
  return `${routeBaseKey}_${kind}`;
264
266
  };
265
267
  var collectInvalidRouteNamingIssues = (srcDir) => {
266
268
  const allRouteFiles = walkRouteFiles(srcDir);
267
269
  const issues = [];
268
270
  for (const filePath of allRouteFiles) {
269
- issues.push(...validateRouteFilePath(filePath));
271
+ issues.push(...validateRouteFilePath(srcDir, filePath));
270
272
  }
271
273
  return issues.toSorted((a, b) => a.filePath.localeCompare(b.filePath));
272
274
  };
@@ -278,7 +280,8 @@ var collectDuplicateNormalizedRouteKeyIssues = (srcDir) => {
278
280
  const syncSeg = syncMarkerSegment();
279
281
  for (const filePath of allRouteFiles) {
280
282
  const normalizedFilePath = normalizePath(path.resolve(filePath));
281
- if (normalizedFilePath.includes(apiSeg)) {
283
+ const srcMarkerPath = toSrcMarkerPath(srcDir, filePath);
284
+ if (srcMarkerPath.includes(apiSeg)) {
282
285
  const routeKey = resolveApiRouteKey({ srcDir, filePath });
283
286
  if (!routeKey) {
284
287
  continue;
@@ -287,7 +290,7 @@ var collectDuplicateNormalizedRouteKeyIssues = (srcDir) => {
287
290
  routeKeyKinds.set(routeKey, "api");
288
291
  continue;
289
292
  }
290
- if (normalizedFilePath.includes(syncSeg)) {
293
+ if (srcMarkerPath.includes(syncSeg)) {
291
294
  const routeKey = resolveSyncRouteKey({ srcDir, filePath });
292
295
  if (!routeKey) {
293
296
  continue;
@@ -4079,11 +4082,15 @@ import path15 from "path";
4079
4082
  import fs9 from "fs";
4080
4083
  import path12 from "path";
4081
4084
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
4082
- import { ScriptKind as ScriptKind3 } from "typescript-7-api/ast";
4085
+ import {
4086
+ ScriptKind as ScriptKind3
4087
+ } from "typescript-7-api/ast";
4083
4088
  import {
4084
4089
  isIdentifier as isIdentifier8,
4085
4090
  isInterfaceDeclaration as isInterfaceDeclaration6,
4086
4091
  isPropertySignatureDeclaration as isPropertySignatureDeclaration4,
4092
+ isStringLiteral as isStringLiteral7,
4093
+ isTypeAliasDeclaration as isTypeAliasDeclaration6,
4087
4094
  isTypeLiteralNode as isTypeLiteralNode3
4088
4095
  } from "typescript-7-api/ast/is";
4089
4096
  import { ROOT_DIR as ROOT_DIR7, getGeneratedSocketTypesPath as getGeneratedSocketTypesPath6, getSrcDir as getSrcDir4 } from "@luckystack/core";
@@ -4156,23 +4163,22 @@ var isEmptyFile = (filePath) => {
4156
4163
  return false;
4157
4164
  }
4158
4165
  };
4159
- var isCommentOnlyFile = (filePath) => {
4166
+ var INJECTED_GUIDANCE_PATTERN = /^\/\/ Invalid (?:API|sync) filename: [^\r\n]*$/;
4167
+ var isInjectedGuidanceOnlyFile = (filePath) => {
4160
4168
  try {
4161
4169
  const content = fs9.readFileSync(filePath, "utf8");
4162
- const withoutBlockComments = content.replaceAll(/\/\*[\s\S]*?\*\//g, "");
4163
- const withoutLineComments = withoutBlockComments.replaceAll(/(^|\s)\/\/.*$/gm, "$1");
4164
- return withoutLineComments.trim().length === 0;
4170
+ return INJECTED_GUIDANCE_PATTERN.test(content.trim());
4165
4171
  } catch {
4166
4172
  return false;
4167
4173
  }
4168
4174
  };
4169
4175
  var isInApiFolder = (filePath) => {
4170
4176
  const normalized = filePath.replaceAll("\\", "/");
4171
- return normalized.includes(apiMarkerSegment()) && filePath.endsWith(".ts") && !isRouteTestFile(filePath);
4177
+ return normalized.includes(apiMarkerSegment()) && filePath.endsWith(".ts") && isRouteSurfaceFile(normalized);
4172
4178
  };
4173
4179
  var isInSyncFolder = (filePath) => {
4174
4180
  const normalized = filePath.replaceAll("\\", "/");
4175
- return normalized.includes(syncMarkerSegment()) && filePath.endsWith(".ts") && !isRouteTestFile(filePath);
4181
+ return normalized.includes(syncMarkerSegment()) && filePath.endsWith(".ts") && isRouteSurfaceFile(normalized);
4176
4182
  };
4177
4183
  var isPageFile = (filePath) => {
4178
4184
  const normalized = filePath.replaceAll("\\", "/");
@@ -4318,22 +4324,10 @@ var hasPairedFile = (filePath) => {
4318
4324
  if (!pairedPath) return false;
4319
4325
  return fs9.existsSync(pairedPath);
4320
4326
  };
4321
- var extractSyncPagePath2 = (filePath) => {
4322
- const normalized = filePath.replaceAll("\\", "/");
4323
- const match = /src\/(.+?)\/_sync\//.exec(normalized);
4324
- if (match?.[1]) return match[1];
4325
- if (/(?:^|\/)src\/_sync\//.test(normalized)) return "system";
4326
- return "";
4327
- };
4328
- var extractSyncName2 = (filePath) => {
4329
- const normalized = filePath.replaceAll("\\", "/");
4330
- const match = /_sync\/(.+)\.ts$/.exec(normalized);
4331
- if (!match) {
4332
- const basename = path12.basename(filePath, ".ts");
4333
- return basename.replace(/_server_v\d+$/, "").replace(/_client_v\d+$/, "");
4334
- }
4335
- return (match[1] ?? "").replace(/_server_v\d+$/, "").replace(/_client_v\d+$/, "");
4336
- };
4327
+ var toSrcRelativeOrSelf = (filePath) => computeSrcRelativePath(filePath) ?? filePath.replaceAll("\\", "/");
4328
+ var extractSyncPagePath2 = (filePath) => extractSyncPagePath(path12.resolve(filePath).replaceAll("\\", "/"));
4329
+ var extractSyncName2 = (filePath) => extractSyncName(toSrcRelativeOrSelf(filePath));
4330
+ var extractSyncVersion2 = (filePath) => extractSyncVersion(toSrcRelativeOrSelf(filePath));
4337
4331
  var extractClientInputFromFile = (filePath) => {
4338
4332
  try {
4339
4333
  const content = fs9.readFileSync(filePath, "utf8");
@@ -4364,43 +4358,32 @@ var extractClientInputFromFile = (filePath) => {
4364
4358
  return null;
4365
4359
  }
4366
4360
  };
4367
- var extractClientInputFromGeneratedTypes = (pagePath, syncName) => {
4361
+ var propertyKeyText = (name) => isIdentifier8(name) || isStringLiteral7(name) ? name.text : null;
4362
+ var findTypeLiteralMemberType = (typeNode, key) => {
4363
+ if (!typeNode || !isTypeLiteralNode3(typeNode)) return void 0;
4364
+ for (const member of typeNode.members) {
4365
+ if (isPropertySignatureDeclaration4(member) && propertyKeyText(member.name) === key) {
4366
+ return member.type;
4367
+ }
4368
+ }
4369
+ return void 0;
4370
+ };
4371
+ var extractClientInputFromGeneratedTypes = (pagePath, syncName, version) => {
4368
4372
  try {
4369
4373
  const generatedTypesPath = getGeneratedSocketTypesPath6();
4370
4374
  const content = fs9.readFileSync(generatedTypesPath, "utf8");
4371
- const escapeRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
4372
- const escapedPagePath = escapeRegex(pagePath);
4373
- const escapedSyncName = escapeRegex(syncName);
4374
- const pageBlockRegex = new RegExp(String.raw`'${escapedPagePath}'\s*:\s*\{([\s\S]*?)\n\s{2}\};`, "m");
4375
- const pageBlockMatch = content.match(pageBlockRegex);
4376
- if (!pageBlockMatch?.[1]) {
4377
- console.log(`[TemplateInjector] Could not find page block for ${pagePath}`);
4378
- return null;
4379
- }
4380
- const pageBlock = pageBlockMatch[1];
4381
- const syncEntryPattern = new RegExp(String.raw`'${escapedSyncName}':\s*\{\s*clientInput:\s*`);
4382
- const match = pageBlock.match(syncEntryPattern);
4383
- if (!match || typeof match.index !== "number") {
4384
- console.log(`[TemplateInjector] Could not find sync entry for ${pagePath}/${syncName}`);
4375
+ const sourceFile = parseSourceText(path12.basename(generatedTypesPath), content, ScriptKind3.TS);
4376
+ const syncTypeMap = sourceFile.statements.find(
4377
+ (statement) => isTypeAliasDeclaration6(statement) && statement.name.text === "_ProjectSyncTypeMap"
4378
+ );
4379
+ const pageType = findTypeLiteralMemberType(syncTypeMap?.type, pagePath);
4380
+ const versionType = findTypeLiteralMemberType(findTypeLiteralMemberType(pageType, syncName), version);
4381
+ const clientInputType = findTypeLiteralMemberType(versionType, "clientInput");
4382
+ if (!clientInputType) {
4383
+ console.log(`[TemplateInjector] Could not find sync entry for ${pagePath}/${syncName}/${version}`);
4385
4384
  return null;
4386
4385
  }
4387
- const pageStart = content.indexOf(pageBlock);
4388
- const globalMatchIndex = pageStart + match.index;
4389
- const searchStart = globalMatchIndex + match[0].length;
4390
- const braceStart = content.indexOf("{", searchStart - 1);
4391
- if (braceStart === -1) return null;
4392
- let depth = 0;
4393
- let endIndex = -1;
4394
- for (let i = braceStart; i < content.length; i++) {
4395
- if (content[i] === "{") depth++;
4396
- else if (content[i] === "}") depth--;
4397
- if (depth === 0) {
4398
- endIndex = i;
4399
- break;
4400
- }
4401
- }
4402
- if (endIndex === -1) return null;
4403
- const extracted = content.slice(braceStart, endIndex + 1);
4386
+ const extracted = content.slice(clientInputType.getStart(sourceFile), clientInputType.getEnd());
4404
4387
  console.log(`[TemplateInjector] Extracted clientInput types: ${extracted}`);
4405
4388
  return extracted;
4406
4389
  } catch (error) {
@@ -4524,7 +4507,7 @@ var shouldInjectTemplate = (filePath, options = {}) => {
4524
4507
  return false;
4525
4508
  }
4526
4509
  if (isEmptyFile(filePath)) return true;
4527
- if (options.isNewFile) return isCommentOnlyFile(filePath);
4510
+ if (options.isNewFile) return isInjectedGuidanceOnlyFile(filePath);
4528
4511
  return false;
4529
4512
  };
4530
4513
  var ensureImportedTypes = (content, clientFilePath) => {
@@ -4719,6 +4702,7 @@ var injectServerTemplateWithClientInput = async (serverFilePath, clientInputType
4719
4702
  import fs10 from "fs";
4720
4703
  import path13 from "path";
4721
4704
  import { ROOT_DIR as ROOT_DIR8, getServerFunctionDirs as getServerFunctionDirs2, getSharedDir, getSrcDir as getSrcDir5 } from "@luckystack/core";
4705
+ var srcDirFromRoot = () => path13.relative(ROOT_DIR8, getSrcDir5()).replaceAll("\\", "/");
4722
4706
  var SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
4723
4707
  var normalizePath4 = (value) => path13.resolve(value).replaceAll("\\", "/");
4724
4708
  var isSupportedScriptFile = (value) => {
@@ -4836,9 +4820,12 @@ var resolveImportToFile = (importerPath, specifier) => {
4836
4820
  const relativeBase = path13.resolve(path13.dirname(importerPath), specifier);
4837
4821
  return tryResolveWithExtensions(relativeBase);
4838
4822
  }
4839
- if (specifier.startsWith("src/") || specifier.startsWith("@/")) {
4840
- const sourcePath = specifier.startsWith("@/") ? specifier.slice(2) : specifier.slice(4);
4841
- return tryResolveWithExtensions(path13.join(getSrcDir5(), sourcePath));
4823
+ const srcFromRoot = srcDirFromRoot();
4824
+ const srcAliases = ["src/", "@/"];
4825
+ if (srcFromRoot && !srcFromRoot.startsWith("..")) srcAliases.push(`${srcFromRoot}/`);
4826
+ const srcAlias = srcAliases.find((alias) => specifier.startsWith(alias));
4827
+ if (srcAlias) {
4828
+ return tryResolveWithExtensions(path13.join(getSrcDir5(), specifier.slice(srcAlias.length)));
4842
4829
  }
4843
4830
  if (specifier.startsWith("shared/")) {
4844
4831
  return tryResolveWithExtensions(path13.join(getSharedDir(), specifier.slice(7)));
@@ -4846,7 +4833,10 @@ var resolveImportToFile = (importerPath, specifier) => {
4846
4833
  return null;
4847
4834
  };
4848
4835
  var isRouteFile = (filePath) => {
4849
- return filePath.includes("/src/") && (filePath.includes("/_api/") || filePath.includes("/_sync/"));
4836
+ const relative = path13.relative(getSrcDir5(), filePath).replaceAll("\\", "/");
4837
+ if (relative.startsWith("..") || path13.isAbsolute(relative)) return false;
4838
+ const markerPath = `/${relative}`;
4839
+ return (markerPath.includes(apiMarkerSegment()) || markerPath.includes(syncMarkerSegment())) && isRouteSurfaceFile(markerPath);
4850
4840
  };
4851
4841
  var collectScopedFiles = () => {
4852
4842
  const now = Date.now();
@@ -4939,26 +4929,36 @@ var runPrismaGenerate = async () => tryCatch2(
4939
4929
  );
4940
4930
 
4941
4931
  // src/hotReload.ts
4942
- import { tryCatch as tryCatch3, getProjectConfig, getLocaleReloader, isProductionRuntime } from "@luckystack/core";
4932
+ import {
4933
+ tryCatch as tryCatch3,
4934
+ getProjectConfig,
4935
+ getLocaleReloader,
4936
+ isProductionRuntime,
4937
+ getGeneratedSocketTypesPath as getGeneratedSocketTypesPath7,
4938
+ getGeneratedApiSchemasPath as getGeneratedApiSchemasPath2,
4939
+ getGeneratedApiDocsPath as getGeneratedApiDocsPath2
4940
+ } from "@luckystack/core";
4943
4941
  var normalizeFsPath = (value) => path15.resolve(value).replaceAll("\\", "/");
4944
4942
  var isGeneratedPath = (normalizedPath) => {
4945
- return normalizedPath.includes("apiTypes.generated.ts") || normalizedPath.includes("apiDocs.generated.json");
4943
+ if (normalizedPath.includes(".generated.")) return true;
4944
+ return [getGeneratedSocketTypesPath7(), getGeneratedApiSchemasPath2(), getGeneratedApiDocsPath2()].some((generatedPath) => normalizeFsPath(generatedPath) === normalizedPath);
4946
4945
  };
4947
4946
  var createTypeMapQueue = () => {
4948
4947
  const queue = { pending: false, running: false };
4949
4948
  const run = () => {
4950
4949
  queue.running = true;
4951
4950
  queue.pending = false;
4952
- const startedAt = Date.now();
4953
4951
  setImmediate(() => {
4954
4952
  void (async () => {
4953
+ const startedAt = Date.now();
4955
4954
  const [err] = await tryCatch3(() => {
4956
4955
  generateTypeMapFile({ quiet: true });
4957
4956
  });
4957
+ const durationMs = Date.now() - startedAt;
4958
4958
  if (err) {
4959
- console.log(`[HotReload] type map regeneration failed: ${String(err)}`, "red");
4959
+ console.log(`[HotReload] type map regeneration failed after ${durationMs}ms: ${String(err)}`, "red");
4960
4960
  } else {
4961
- console.log(`[HotReload] type map ready in ${Date.now() - startedAt}ms`, "green");
4961
+ console.log(`[HotReload] type map regenerated in ${durationMs}ms (the server answered no requests meanwhile)`, "green");
4962
4962
  }
4963
4963
  queue.running = false;
4964
4964
  if (queue.pending) {
@@ -4980,7 +4980,12 @@ var createPendingChangeSets = () => ({
4980
4980
  apiUpserts: /* @__PURE__ */ new Set(),
4981
4981
  apiDeletes: /* @__PURE__ */ new Set(),
4982
4982
  syncUpserts: /* @__PURE__ */ new Set(),
4983
- syncDeletes: /* @__PURE__ */ new Set()
4983
+ syncDeletes: /* @__PURE__ */ new Set(),
4984
+ //? Deleted sync SERVER files whose paired client still needs its types
4985
+ //? inlined. A set the sync task drains, not a closure handed to the
4986
+ //? scheduler: any later sync event in the debounce window replaces the
4987
+ //? scheduled task, and a closure would be dropped with it.
4988
+ deletedSyncServers: /* @__PURE__ */ new Set()
4984
4989
  });
4985
4990
  var buildPathSegments = () => {
4986
4991
  const rules2 = getRoutingRules();
@@ -5001,10 +5006,11 @@ var buildPathSegments = () => {
5001
5006
  };
5002
5007
  };
5003
5008
  var makeIsInServerFunctionsDir = (segments) => (normalizedPath) => segments.serverFunctionsSegments.some((seg) => normalizedPath.includes(seg));
5004
- var makeIsRouteDependencyFile = (segments) => (normalizedPath) => {
5009
+ var makeIsRouteFile = (segments) => (normalizedPath) => (normalizedPath.includes(segments.apiMarkerSlash) || normalizedPath.includes(segments.syncMarkerSlash)) && isRouteSurfaceFile(normalizedPath);
5010
+ var makeIsRouteDependencyFile = (segments, isRouteFile2) => (normalizedPath) => {
5005
5011
  if (!normalizedPath.endsWith(".ts") && !normalizedPath.endsWith(".tsx")) return false;
5006
5012
  if (!normalizedPath.includes(segments.srcSegment)) return false;
5007
- if (normalizedPath.includes(segments.apiMarkerSlash) || normalizedPath.includes(segments.syncMarkerSlash)) return false;
5013
+ if (isRouteFile2(normalizedPath)) return false;
5008
5014
  if (isGeneratedPath(normalizedPath)) return false;
5009
5015
  return true;
5010
5016
  };
@@ -5013,10 +5019,10 @@ var makeIsSharedDependencyFile = (segments, isInServerFunctionsDir) => (normaliz
5013
5019
  if (normalizedPath.includes(segments.sharedSegment)) return true;
5014
5020
  return isInServerFunctionsDir(normalizedPath);
5015
5021
  };
5016
- var makeIsTypeMapRelevantFile = (segments) => (normalizedPath) => {
5022
+ var makeIsTypeMapRelevantFile = (segments, isRouteFile2) => (normalizedPath) => {
5017
5023
  if (isGeneratedPath(normalizedPath)) return false;
5018
5024
  if (!(normalizedPath.endsWith(".ts") || normalizedPath.endsWith(".tsx"))) return false;
5019
- if (normalizedPath.includes(segments.apiMarkerSlash) || normalizedPath.includes(segments.syncMarkerSlash)) return false;
5025
+ if (isRouteFile2(normalizedPath)) return false;
5020
5026
  return normalizedPath.includes(segments.srcSegment) || normalizedPath.endsWith("/config.ts") || normalizedPath.includes(segments.sharedSegment);
5021
5027
  };
5022
5028
  var makeIsLocaleFile = (segments) => (normalizedPath) => normalizedPath.includes(segments.localesSegment) && normalizedPath.endsWith(".json");
@@ -5054,9 +5060,10 @@ var setupWatchers = () => {
5054
5060
  if (!isDevMode) return;
5055
5061
  const segments = buildPathSegments();
5056
5062
  const isInServerFunctionsDir = makeIsInServerFunctionsDir(segments);
5057
- const isRouteDependencyFile = makeIsRouteDependencyFile(segments);
5063
+ const isRouteFile2 = makeIsRouteFile(segments);
5064
+ const isRouteDependencyFile = makeIsRouteDependencyFile(segments, isRouteFile2);
5058
5065
  const isSharedDependencyFile = makeIsSharedDependencyFile(segments, isInServerFunctionsDir);
5059
- const isTypeMapRelevantFile = makeIsTypeMapRelevantFile(segments);
5066
+ const isTypeMapRelevantFile = makeIsTypeMapRelevantFile(segments, isRouteFile2);
5060
5067
  const isLocaleFile = makeIsLocaleFile(segments);
5061
5068
  const typeMap = createTypeMapQueue();
5062
5069
  const pending = createPendingChangeSets();
@@ -5075,11 +5082,31 @@ var setupWatchers = () => {
5075
5082
  console.log(`[HotReload] API removed: ${deletePath}`, "yellow");
5076
5083
  }
5077
5084
  for (const upsertPath of upsertPaths) {
5085
+ const startedAt = Date.now();
5078
5086
  await upsertApiFromFile(upsertPath);
5079
- console.log(`[HotReload] API reloaded: ${upsertPath}`, "green");
5087
+ console.log(`[HotReload] API reloaded in ${Date.now() - startedAt}ms: ${upsertPath}`, "green");
5088
+ }
5089
+ };
5090
+ const migrateClientsOfDeletedSyncServers = async () => {
5091
+ const serverPaths = [...pending.deletedSyncServers];
5092
+ pending.deletedSyncServers.clear();
5093
+ for (const serverPath of serverPaths) {
5094
+ if (fs12.existsSync(serverPath)) continue;
5095
+ const clientPath = getPairedSyncFile(serverPath);
5096
+ if (!clientPath || !fs12.existsSync(clientPath)) continue;
5097
+ const clientInputTypes = extractClientInputFromGeneratedTypes(
5098
+ extractSyncPagePath2(serverPath),
5099
+ extractSyncName2(serverPath),
5100
+ extractSyncVersion2(serverPath)
5101
+ );
5102
+ await updateClientFileForDeletedServer(
5103
+ clientPath,
5104
+ clientInputTypes ?? "{\n // Types were in _server.ts - please add them here\n }"
5105
+ );
5080
5106
  }
5081
5107
  };
5082
5108
  const processPendingSyncChanges = async ({ regenerateTypeMap = false } = {}) => {
5109
+ await migrateClientsOfDeletedSyncServers();
5083
5110
  const deletePaths = [...pending.syncDeletes];
5084
5111
  const upsertPaths = [...pending.syncUpserts];
5085
5112
  pending.syncDeletes.clear();
@@ -5093,16 +5120,28 @@ var setupWatchers = () => {
5093
5120
  console.log(`[HotReload] Sync removed: ${deletePath}`, "yellow");
5094
5121
  }
5095
5122
  for (const upsertPath of upsertPaths) {
5123
+ const startedAt = Date.now();
5096
5124
  await upsertSyncFromFile(upsertPath);
5097
- console.log(`[HotReload] Sync reloaded: ${upsertPath}`, "green");
5125
+ console.log(`[HotReload] Sync reloaded in ${Date.now() - startedAt}ms: ${upsertPath}`, "green");
5098
5126
  }
5099
5127
  };
5100
5128
  const enqueueAffectedRoutesFromDependency = (changedPath) => {
5101
5129
  const affectedRoutes = findDependentRouteFiles(changedPath);
5102
5130
  if (affectedRoutes.size === 0) {
5103
5131
  console.log(`[HotReload] No API/Sync routes depend on: ${changedPath}`, "yellow");
5104
- return;
5132
+ return false;
5133
+ }
5134
+ if (requestDevServerRestart(changedPath)) {
5135
+ console.log(
5136
+ `[HotReload] Dependency changed: ${changedPath} -> restarting the dev server (${affectedRoutes.size} route(s) import it)`,
5137
+ "blue"
5138
+ );
5139
+ return true;
5105
5140
  }
5141
+ console.log(
5142
+ `[HotReload] ${changedPath} changed, but routes that import it keep running its old code until you restart the dev server (the supervised \`npm run server\` restarts automatically)`,
5143
+ "yellow"
5144
+ );
5106
5145
  let queuedApiCount = 0;
5107
5146
  let queuedSyncCount = 0;
5108
5147
  for (const routePath of affectedRoutes) {
@@ -5130,6 +5169,7 @@ var setupWatchers = () => {
5130
5169
  await processPendingSyncChanges();
5131
5170
  });
5132
5171
  }
5172
+ return false;
5133
5173
  };
5134
5174
  const handleAdd = async (filePath) => {
5135
5175
  const normalizedPath = normalizeFsPath(filePath);
@@ -5169,7 +5209,8 @@ var setupWatchers = () => {
5169
5209
  return;
5170
5210
  }
5171
5211
  }
5172
- if (normalizedPath.includes(segments.apiMarkerNoLead)) {
5212
+ const isRoute = isRouteFile2(normalizedPath);
5213
+ if (isRoute && normalizedPath.includes(segments.apiMarkerNoLead)) {
5173
5214
  pending.apiDeletes.delete(normalizedPath);
5174
5215
  pending.apiUpserts.add(normalizedPath);
5175
5216
  scheduleReload("api", async () => {
@@ -5177,7 +5218,7 @@ var setupWatchers = () => {
5177
5218
  });
5178
5219
  return;
5179
5220
  }
5180
- if (normalizedPath.includes(segments.syncMarkerNoLead)) {
5221
+ if (isRoute && normalizedPath.includes(segments.syncMarkerNoLead)) {
5181
5222
  pending.syncDeletes.delete(normalizedPath);
5182
5223
  pending.syncUpserts.add(normalizedPath);
5183
5224
  scheduleReload("sync", async () => {
@@ -5219,11 +5260,11 @@ var setupWatchers = () => {
5219
5260
  return;
5220
5261
  }
5221
5262
  if (isRouteDependencyFile(normalizedPath)) {
5263
+ if (enqueueAffectedRoutesFromDependency(normalizedPath)) return;
5222
5264
  scheduleReload("typemap", () => {
5223
5265
  console.log(`[HotReload] Route dependency changed, scheduling type map regeneration`, "blue");
5224
5266
  typeMap.request();
5225
5267
  });
5226
- enqueueAffectedRoutesFromDependency(normalizedPath);
5227
5268
  return;
5228
5269
  }
5229
5270
  if (isTypeMapRelevantFile(normalizedPath)) {
@@ -5257,13 +5298,13 @@ var setupWatchers = () => {
5257
5298
  const handleFunctionChange = (changedPath) => {
5258
5299
  const normalizedPath = normalizeFsPath(changedPath);
5259
5300
  invalidateGraphForFile(normalizedPath);
5301
+ if (isSharedDependencyFile(normalizedPath) && enqueueAffectedRoutesFromDependency(normalizedPath)) {
5302
+ return;
5303
+ }
5260
5304
  scheduleReload("functions", async () => {
5261
5305
  typeMap.request();
5262
5306
  await initializeFunctions();
5263
5307
  });
5264
- if (isSharedDependencyFile(normalizedPath)) {
5265
- enqueueAffectedRoutesFromDependency(normalizedPath);
5266
- }
5267
5308
  };
5268
5309
  const handleDelete = (filePath) => {
5269
5310
  const normalizedPath = normalizeFsPath(filePath);
@@ -5278,11 +5319,11 @@ var setupWatchers = () => {
5278
5319
  return;
5279
5320
  }
5280
5321
  if (isRouteDependencyFile(normalizedPath)) {
5322
+ if (enqueueAffectedRoutesFromDependency(normalizedPath)) return;
5281
5323
  scheduleReload("typemap", () => {
5282
5324
  console.log(`[HotReload] Route dependency deleted, scheduling type map regeneration`, "blue");
5283
5325
  typeMap.request();
5284
5326
  });
5285
- enqueueAffectedRoutesFromDependency(normalizedPath);
5286
5327
  return;
5287
5328
  }
5288
5329
  if (isTypeMapRelevantFile(normalizedPath)) {
@@ -5308,19 +5349,10 @@ var setupWatchers = () => {
5308
5349
  });
5309
5350
  pending.syncUpserts.delete(normalizedPath);
5310
5351
  pending.syncDeletes.add(normalizedPath);
5352
+ if (isSyncServerFile(normalizedPath)) {
5353
+ pending.deletedSyncServers.add(normalizedPath);
5354
+ }
5311
5355
  scheduleReload("sync", async () => {
5312
- if (isSyncServerFile(normalizedPath)) {
5313
- const clientPath = getPairedSyncFile(normalizedPath);
5314
- if (clientPath && fs12.existsSync(clientPath)) {
5315
- const pagePath = extractSyncPagePath2(normalizedPath);
5316
- const syncName = extractSyncName2(normalizedPath);
5317
- const clientInputTypes = extractClientInputFromGeneratedTypes(pagePath, syncName);
5318
- await updateClientFileForDeletedServer(
5319
- clientPath,
5320
- clientInputTypes ?? "{\n // Types were in _server.ts - please add them here\n }"
5321
- );
5322
- }
5323
- }
5324
5356
  await processPendingSyncChanges();
5325
5357
  });
5326
5358
  }