@t09tanaka/stoneage 0.1.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +768 -0
  4. package/dist/agent-skill.d.ts +1 -0
  5. package/dist/agent-skill.js +140 -0
  6. package/dist/assets.d.ts +125 -0
  7. package/dist/assets.js +341 -0
  8. package/dist/cli.d.ts +236 -0
  9. package/dist/cli.js +3077 -0
  10. package/dist/core.d.ts +473 -0
  11. package/dist/core.js +2897 -0
  12. package/dist/data.d.ts +121 -0
  13. package/dist/data.js +358 -0
  14. package/dist/deploy.d.ts +34 -0
  15. package/dist/deploy.js +203 -0
  16. package/dist/dev.d.ts +36 -0
  17. package/dist/dev.js +313 -0
  18. package/dist/example.d.ts +134 -0
  19. package/dist/example.js +1272 -0
  20. package/dist/fragment/client.d.ts +13 -0
  21. package/dist/fragment/client.js +150 -0
  22. package/dist/fragment.d.ts +15 -0
  23. package/dist/fragment.js +27 -0
  24. package/dist/html.d.ts +57 -0
  25. package/dist/html.js +208 -0
  26. package/dist/images.d.ts +95 -0
  27. package/dist/images.js +292 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +1 -0
  30. package/dist/migration.d.ts +157 -0
  31. package/dist/migration.js +983 -0
  32. package/dist/optimize.d.ts +15 -0
  33. package/dist/optimize.js +215 -0
  34. package/dist/pagination.d.ts +26 -0
  35. package/dist/pagination.js +62 -0
  36. package/dist/paths.d.ts +1 -0
  37. package/dist/paths.js +10 -0
  38. package/dist/search.d.ts +32 -0
  39. package/dist/search.js +55 -0
  40. package/dist/testing.d.ts +66 -0
  41. package/dist/testing.js +97 -0
  42. package/dist/validate.d.ts +2 -0
  43. package/dist/validate.js +1 -0
  44. package/jsx-loader.mjs +52 -0
  45. package/jsx-register.mjs +7 -0
  46. package/package.json +135 -0
  47. package/tsconfig.base.json +16 -0
@@ -0,0 +1,983 @@
1
+ import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { extname, join, relative, sep } from "node:path";
3
+ export async function auditSvelteKitPublicSurface(options) {
4
+ const paramMatchers = await readParamMatchers(options.paramsDir);
5
+ const files = (await walkFiles(options.routesDir)).filter(isSvelteKitPublicSurfaceFile);
6
+ const pageGroups = new Map();
7
+ const endpoints = [];
8
+ const inventoryFiles = [];
9
+ for (const absoluteFile of files) {
10
+ const relativeFile = toPosix(relative(options.routesDir, absoluteFile));
11
+ const source = await readFile(absoluteFile, "utf8");
12
+ const route = routeInfoForRelativeFile(relativeFile);
13
+ if (!route) {
14
+ continue;
15
+ }
16
+ const inventorySource = routeFileInventorySource(route, relativeFile, source);
17
+ inventoryFiles.push(inventorySource);
18
+ if (inventorySource.kind === "layout" ||
19
+ inventorySource.kind === "layoutServer" ||
20
+ inventorySource.kind === "layoutUniversal") {
21
+ continue;
22
+ }
23
+ if (isEndpointFile(relativeFile)) {
24
+ endpoints.push({
25
+ routePath: route.routePath,
26
+ stoneagePattern: route.stoneagePattern,
27
+ file: relativeFile,
28
+ params: route.params,
29
+ kind: endpointKind(route.routePath, source),
30
+ methods: endpointMethods(source),
31
+ usesJson: /\bjson\s*\(/.test(source),
32
+ usesResponse: /\bnew\s+Response\s*\(/.test(source),
33
+ usesEntries: exportsName(source, "entries"),
34
+ exportsPrerender: exportsName(source, "prerender"),
35
+ usesRedirect: /\bredirect\s*\(/.test(source),
36
+ usesError404: /\berror\s*\(\s*404\b/.test(source),
37
+ });
38
+ continue;
39
+ }
40
+ const group = pageGroups.get(route.routePath) ??
41
+ {
42
+ routePath: route.routePath,
43
+ stoneagePattern: route.stoneagePattern,
44
+ params: route.params,
45
+ files: [],
46
+ };
47
+ group.files.push({ relativeFile, source });
48
+ pageGroups.set(route.routePath, group);
49
+ }
50
+ const pages = Array.from(pageGroups.values())
51
+ .map((group) => pageAuditFromGroup(group))
52
+ .sort(compareByRoutePath);
53
+ const sortedEndpoints = endpoints.sort(compareByRoutePath);
54
+ const warnings = createAuditWarnings(pages, sortedEndpoints, paramMatchers).sort(compareWarnings);
55
+ const endpointKinds = summarizeEndpointKinds(sortedEndpoints);
56
+ const migrationInventory = createMigrationInventory(inventoryFiles);
57
+ const migrationPlan = options.migrationPlan
58
+ ? createMigrationPlan(pages, sortedEndpoints, paramMatchers, warnings, migrationInventory.layouts)
59
+ : undefined;
60
+ return {
61
+ version: 1,
62
+ routesDir: options.routesDir,
63
+ ...(options.paramsDir ? { paramsDir: options.paramsDir } : {}),
64
+ pages,
65
+ endpoints: sortedEndpoints,
66
+ params: Array.from(paramMatchers.values()).sort((left, right) => left.name.localeCompare(right.name)),
67
+ warnings,
68
+ migrationInventory,
69
+ ...(migrationPlan ? { migrationPlan } : {}),
70
+ summary: {
71
+ pages: pages.length,
72
+ endpoints: sortedEndpoints.length,
73
+ dynamicRoutes: [...pages, ...sortedEndpoints].filter((route) => route.params.length > 0).length,
74
+ endpointKinds,
75
+ warnings: warnings.length,
76
+ },
77
+ };
78
+ }
79
+ export function createSvelteKitMigrationStubs(plan) {
80
+ const matcherIdentifiers = uniqueIdentifierMap(plan.paramMatchers.map((matcher) => matcher.name), "Matcher");
81
+ return [
82
+ {
83
+ path: "data.ts",
84
+ contents: renderDataProducerStubs(),
85
+ },
86
+ {
87
+ path: "routes.ts",
88
+ contents: renderRouteStubs(plan.routeFamilies, matcherIdentifiers),
89
+ },
90
+ {
91
+ path: "artifacts.ts",
92
+ contents: renderArtifactStubs(plan.artifactFamilies, matcherIdentifiers),
93
+ },
94
+ {
95
+ path: "param-matchers.ts",
96
+ contents: renderParamMatcherStubs(plan.paramMatchers, matcherIdentifiers),
97
+ },
98
+ {
99
+ path: "build.ts",
100
+ contents: renderBuildEntrypointStub(),
101
+ },
102
+ {
103
+ path: "collision-tests.md",
104
+ contents: renderCollisionTestStubs(plan.collisionTests),
105
+ },
106
+ ];
107
+ }
108
+ export async function emitSvelteKitMigrationStubs(plan, outDir, options = {}) {
109
+ const stubs = createSvelteKitMigrationStubs(plan);
110
+ const stubTargets = stubs.map((stub) => ({
111
+ ...stub,
112
+ targetPath: join(outDir, stub.path),
113
+ }));
114
+ await mkdir(outDir, { recursive: true });
115
+ if (!options.overwrite) {
116
+ const conflictingPaths = await existingPaths(stubTargets.map((stub) => stub.targetPath));
117
+ if (conflictingPaths.length > 0) {
118
+ throw new Error([
119
+ "SvelteKit migration stub target file(s) already exist.",
120
+ "Pass { overwrite: true } to replace generated targets:",
121
+ ...conflictingPaths.map((path) => `- ${path}`),
122
+ ].join("\n"));
123
+ }
124
+ }
125
+ await Promise.all(stubTargets.map((stub) => writeFile(stub.targetPath, stub.contents, options.overwrite ? undefined : { flag: "wx" })));
126
+ return stubs;
127
+ }
128
+ async function existingPaths(paths) {
129
+ const results = await Promise.all(paths.map(async (path) => {
130
+ try {
131
+ await access(path);
132
+ return path;
133
+ }
134
+ catch (error) {
135
+ if (isErrorCode(error, "ENOENT")) {
136
+ return undefined;
137
+ }
138
+ throw error;
139
+ }
140
+ }));
141
+ return results.filter((path) => path !== undefined);
142
+ }
143
+ function isErrorCode(error, code) {
144
+ return (typeof error === "object" &&
145
+ error !== null &&
146
+ "code" in error &&
147
+ error.code === code);
148
+ }
149
+ async function readParamMatchers(paramsDir) {
150
+ const matchers = new Map();
151
+ if (!paramsDir) {
152
+ return matchers;
153
+ }
154
+ for (const file of await walkFiles(paramsDir, { missing: "empty" })) {
155
+ const extension = extname(file);
156
+ if (![".js", ".ts"].includes(extension) || file.endsWith(".d.ts")) {
157
+ continue;
158
+ }
159
+ const relativeFile = toPosix(relative(paramsDir, file));
160
+ const name = relativeFile.slice(0, -extension.length);
161
+ matchers.set(name, { name, file: relativeFile });
162
+ }
163
+ return matchers;
164
+ }
165
+ async function walkFiles(root, options = {}) {
166
+ let entries;
167
+ try {
168
+ entries = await readdir(root, { withFileTypes: true });
169
+ }
170
+ catch (error) {
171
+ if (options.missing === "empty" && isNodeError(error) && error.code === "ENOENT") {
172
+ return [];
173
+ }
174
+ throw error;
175
+ }
176
+ const files = await Promise.all(entries.map(async (entry) => {
177
+ const path = join(root, entry.name);
178
+ if (entry.isDirectory()) {
179
+ return walkFiles(path, options);
180
+ }
181
+ return entry.isFile() ? [path] : [];
182
+ }));
183
+ return files.flat().sort();
184
+ }
185
+ function isSvelteKitPublicSurfaceFile(path) {
186
+ const fileName = path.split(sep).at(-1) ?? path;
187
+ return svelteKitRouteFilePattern().test(fileName);
188
+ }
189
+ function routeInfoForRelativeFile(relativeFile) {
190
+ const sourceSegments = relativeFile.split("/");
191
+ const fileName = sourceSegments.at(-1);
192
+ if (!fileName) {
193
+ return undefined;
194
+ }
195
+ const routeSegments = sourceSegments.slice(0, -1).filter((segment) => !isRouteGroup(segment));
196
+ const isPageLike = fileName.startsWith("+page") || fileName.startsWith("+layout");
197
+ const routePath = formatRoutePath(routeSegments, isPageLike);
198
+ const parsed = parseRouteSegments(routeSegments, isPageLike);
199
+ return {
200
+ routePath,
201
+ stoneagePattern: parsed.stoneagePattern,
202
+ params: parsed.params,
203
+ };
204
+ }
205
+ function routeFileInventorySource(route, relativeFile, source) {
206
+ return {
207
+ routePath: route.routePath,
208
+ file: relativeFile,
209
+ kind: routeFileKind(relativeFile),
210
+ usesLoad: exportsName(source, "load"),
211
+ usesActions: exportsName(source, "actions"),
212
+ usesRedirect: /\bredirect\s*\(/.test(source),
213
+ usesError404: /\berror\s*\(\s*404\b/.test(source),
214
+ exportsPrerender: exportsName(source, "prerender"),
215
+ usesEntries: exportsName(source, "entries"),
216
+ ...(hasLayoutReset(relativeFile) ? { layoutReset: true } : {}),
217
+ imports: importSources(source),
218
+ components: componentNames(source),
219
+ };
220
+ }
221
+ function routeFileKind(relativeFile) {
222
+ if (isLayoutComponentFile(relativeFile)) {
223
+ return "layout";
224
+ }
225
+ if (relativeFile.endsWith("+layout.server.ts") || relativeFile.endsWith("+layout.server.js")) {
226
+ return "layoutServer";
227
+ }
228
+ if (relativeFile.endsWith("+layout.ts") || relativeFile.endsWith("+layout.js")) {
229
+ return "layoutUniversal";
230
+ }
231
+ if (isPageComponentFile(relativeFile)) {
232
+ return "page";
233
+ }
234
+ if (relativeFile.endsWith("+page.server.ts") || relativeFile.endsWith("+page.server.js")) {
235
+ return "pageServer";
236
+ }
237
+ if (relativeFile.endsWith("+page.ts") || relativeFile.endsWith("+page.js")) {
238
+ return "pageUniversal";
239
+ }
240
+ return "endpoint";
241
+ }
242
+ function svelteKitRouteFilePattern() {
243
+ return /^\+(?:layout(?:@[^.]*)?\.svelte|layout\.server\.[jt]s|layout\.[jt]s|page(?:@[^.]*)?\.svelte|page\.server\.[jt]s|page\.[jt]s|server\.[jt]s)$/;
244
+ }
245
+ function routeFileName(relativeFile) {
246
+ return relativeFile.split("/").at(-1) ?? relativeFile;
247
+ }
248
+ function isLayoutComponentFile(relativeFile) {
249
+ return /^\+layout(?:@[^.]*)?\.svelte$/.test(routeFileName(relativeFile));
250
+ }
251
+ function isPageComponentFile(relativeFile) {
252
+ return /^\+page(?:@[^.]*)?\.svelte$/.test(routeFileName(relativeFile));
253
+ }
254
+ function isEndpointFile(relativeFile) {
255
+ return routeFileName(relativeFile) === "+server.ts" || routeFileName(relativeFile) === "+server.js";
256
+ }
257
+ function hasLayoutReset(relativeFile) {
258
+ return /^\+(?:layout|page)@[^.]*\.svelte$/.test(routeFileName(relativeFile));
259
+ }
260
+ function isUniversalPageLoadFile(relativeFile) {
261
+ return relativeFile.endsWith("+page.ts") || relativeFile.endsWith("+page.js");
262
+ }
263
+ function exportsName(source, name) {
264
+ return new RegExp(`\\bexport\\s+(?:const|let|var|function|async\\s+function)\\s+${name}\\b`).test(source);
265
+ }
266
+ function importSources(source) {
267
+ const imports = Array.from(source.matchAll(/\bimport\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?["']([^"']+)["']/g))
268
+ .map((match) => match[1] ?? "")
269
+ .filter(Boolean);
270
+ return uniqueImportSources(imports);
271
+ }
272
+ function componentNames(source) {
273
+ const importedComponents = Array.from(source.matchAll(/\bimport\s+([A-Z][A-Za-z0-9_$]*)\s+from\s+["'][^"']+\.svelte["']/g)).map((match) => match[1] ?? "");
274
+ const taggedComponents = Array.from(source.matchAll(/<([A-Z][A-Za-z0-9_$]*(?:\.[A-Z][A-Za-z0-9_$]*)?)(?:\s|\/|>)/g))
275
+ .map((match) => match[1] ?? "");
276
+ return uniqueStringsFromPlans([...importedComponents, ...taggedComponents].filter(Boolean));
277
+ }
278
+ function createMigrationInventory(files) {
279
+ const sortedFiles = [...files].sort(compareByFile);
280
+ return {
281
+ layouts: sortedFiles
282
+ .filter((file) => file.kind === "layout" || file.kind === "layoutServer" || file.kind === "layoutUniversal")
283
+ .map((file) => ({
284
+ routePath: file.routePath,
285
+ file: file.file,
286
+ kind: file.kind,
287
+ imports: file.imports,
288
+ components: file.components,
289
+ ...(file.layoutReset ? { layoutReset: true } : {}),
290
+ })),
291
+ routeFiles: sortedFiles.map(({ imports: _imports, components: _components, ...file }) => file),
292
+ componentHints: sortedFiles
293
+ .filter((file) => file.imports.length > 0 || file.components.length > 0)
294
+ .map((file) => ({
295
+ file: file.file,
296
+ imports: file.imports,
297
+ components: file.components,
298
+ })),
299
+ checklist: migrationChecklist(),
300
+ };
301
+ }
302
+ function migrationChecklist() {
303
+ return [
304
+ {
305
+ category: "Data normalization",
306
+ items: [
307
+ "Move SvelteKit load and action data access into explicit normalized data producers.",
308
+ "Separate public data artifacts from HTML-only view models.",
309
+ ],
310
+ },
311
+ {
312
+ category: "Routing and entries",
313
+ items: [
314
+ "Convert dynamic routes and entries() exports into typed StoneAge route families.",
315
+ "Keep parameter matcher behavior explicit and add collision coverage for ambiguous routes.",
316
+ ],
317
+ },
318
+ {
319
+ category: "Rendering and layouts",
320
+ items: [
321
+ "Rebuild layout composition and Svelte component usage as static HTML render functions.",
322
+ "Preserve redirect and error handling as explicit generation decisions.",
323
+ ],
324
+ },
325
+ {
326
+ category: "Artifacts and metadata",
327
+ items: [
328
+ "Map +server outputs, status, headers, and content types into explicit artifact families.",
329
+ "Define metadata, sitemap entries, and export paths alongside route generation.",
330
+ ],
331
+ },
332
+ {
333
+ category: "Validation and tests",
334
+ items: [
335
+ "Add checks for route collisions, metadata completeness, sitemap output, and HTML size budgets.",
336
+ "Keep the migration inventory as static scaffold guidance before adding runtime behavior.",
337
+ ],
338
+ },
339
+ ];
340
+ }
341
+ function formatRoutePath(segments, isPage) {
342
+ if (segments.length === 0) {
343
+ return "/";
344
+ }
345
+ const path = `/${segments.join("/")}`;
346
+ return isPage ? `${path}/` : path;
347
+ }
348
+ function parseRouteSegments(segments, isPage) {
349
+ const params = [];
350
+ const stoneageSegments = segments.map((segment) => segment.replace(/\[\[([^\]=]+)(?:=([^\]]+))?\]\]|\[\.\.\.([^\]]+)\]|\[([^\]=]+)(?:=([^\]]+))?\]/g, (_match, optionalName, optionalMatcher, restName, dynamicName, dynamicMatcher) => {
351
+ if (optionalName) {
352
+ params.push({
353
+ name: optionalName,
354
+ ...(optionalMatcher ? { matcher: optionalMatcher } : {}),
355
+ optional: true,
356
+ });
357
+ return `:${optionalName}`;
358
+ }
359
+ if (restName) {
360
+ params.push({ name: restName, rest: true });
361
+ return `:${restName}`;
362
+ }
363
+ const name = dynamicName ?? "";
364
+ params.push({
365
+ name,
366
+ ...(dynamicMatcher ? { matcher: dynamicMatcher } : {}),
367
+ });
368
+ return `:${name}`;
369
+ }));
370
+ return {
371
+ params,
372
+ stoneagePattern: stoneageSegments.length === 0 ? "/" : `/${stoneageSegments.join("/")}${isPage ? "/" : ""}`,
373
+ };
374
+ }
375
+ function pageAuditFromGroup(group) {
376
+ const source = group.files.map((file) => file.source).join("\n");
377
+ const files = group.files.map((file) => file.relativeFile).sort();
378
+ return {
379
+ routePath: group.routePath,
380
+ stoneagePattern: group.stoneagePattern,
381
+ files,
382
+ params: group.params,
383
+ hasPage: files.some(isPageComponentFile),
384
+ hasPageServer: files.some((file) => file.endsWith("+page.server.ts") || file.endsWith("+page.server.js")),
385
+ hasPageUniversal: files.some(isUniversalPageLoadFile),
386
+ usesUniversalLoad: group.files.some((file) => isUniversalPageLoadFile(file.relativeFile) && exportsName(file.source, "load")),
387
+ usesActions: group.files.some((file) => exportsName(file.source, "actions")),
388
+ usesEntries: group.files.some((file) => exportsName(file.source, "entries")),
389
+ exportsPrerender: group.files.some((file) => exportsName(file.source, "prerender")),
390
+ usesRedirect: /\bredirect\s*\(/.test(source),
391
+ usesError404: /\berror\s*\(\s*404\b/.test(source),
392
+ };
393
+ }
394
+ function endpointMethods(source) {
395
+ const methodOrder = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
396
+ return methodOrder.filter((method) => exportsName(source, method));
397
+ }
398
+ function endpointKind(routePath, source) {
399
+ if (routePath.endsWith(".json") || /\bjson\s*\(/.test(source)) {
400
+ return "json";
401
+ }
402
+ if (routePath.endsWith(".csv") || /text\/csv|application\/csv/.test(source)) {
403
+ return "csv";
404
+ }
405
+ if (routePath.endsWith(".xml") || /application\/xml|text\/xml/.test(source)) {
406
+ return "xml";
407
+ }
408
+ if (routePath.endsWith(".txt") || /text\/plain/.test(source)) {
409
+ return "txt";
410
+ }
411
+ if (/application\/octet-stream/.test(source)) {
412
+ return "binary";
413
+ }
414
+ return "unknown";
415
+ }
416
+ function createAuditWarnings(pages, endpoints, matchers) {
417
+ const warnings = [];
418
+ const routes = [...pages, ...endpoints];
419
+ for (const route of routes) {
420
+ for (const param of route.params) {
421
+ if (param.rest) {
422
+ warnings.push({
423
+ code: "route.rest_param",
424
+ path: route.routePath,
425
+ message: `${route.routePath} uses a rest parameter and should be split into explicit StoneAge route families or custom paths.`,
426
+ });
427
+ }
428
+ if (param.optional) {
429
+ warnings.push({
430
+ code: "route.optional_param",
431
+ path: route.routePath,
432
+ message: `${route.routePath} uses an optional parameter and should be split into explicit StoneAge route families.`,
433
+ });
434
+ }
435
+ const matcherName = param.matcher;
436
+ if (!param.rest && matcherName && !matchers.has(matcherName)) {
437
+ warnings.push({
438
+ code: "param.matcher_missing",
439
+ path: route.routePath,
440
+ message: `${route.routePath} uses parameter ${param.name} without a matching src/params/${matcherName}.ts file.`,
441
+ });
442
+ }
443
+ }
444
+ }
445
+ for (const route of routes) {
446
+ if (hasSiblingDynamicWithoutMatcher(route.routePath, routes.map((candidate) => candidate.routePath))) {
447
+ warnings.push({
448
+ code: "route.sibling_dynamic_without_matcher",
449
+ path: route.routePath,
450
+ message: `${route.routePath} has a sibling dynamic route without complete matcher coverage.`,
451
+ });
452
+ }
453
+ }
454
+ for (const page of pages) {
455
+ if (page.usesActions) {
456
+ warnings.push({
457
+ code: "route.page_actions",
458
+ path: page.routePath,
459
+ message: `${page.routePath} exports SvelteKit page actions, which need an explicit static data migration or removal from the generated site.`,
460
+ });
461
+ }
462
+ const duplicateFiles = duplicatePageOutputFiles(page.files);
463
+ if (duplicateFiles.length > 0) {
464
+ warnings.push({
465
+ code: "route.duplicate_group_output",
466
+ path: page.routePath,
467
+ message: `${page.routePath} is produced by multiple SvelteKit page files after route groups are removed: ${duplicateFiles.join(", ")}.`,
468
+ });
469
+ }
470
+ }
471
+ for (const endpoint of endpoints) {
472
+ const nonStaticMethods = endpoint.methods.filter((method) => method !== "GET" && method !== "HEAD");
473
+ if (nonStaticMethods.length > 0) {
474
+ warnings.push({
475
+ code: "endpoint.non_static_method",
476
+ path: endpoint.routePath,
477
+ message: `${endpoint.routePath} exports non-static endpoint method(s): ${nonStaticMethods.join(", ")}. Migrate those handlers manually instead of treating them as generated artifacts.`,
478
+ });
479
+ }
480
+ }
481
+ const endpointCanonicalPaths = new Set(endpoints.map((endpoint) => canonicalRoutePath(endpoint.routePath)));
482
+ for (const page of pages) {
483
+ if (endpointCanonicalPaths.has(canonicalRoutePath(page.routePath))) {
484
+ warnings.push({
485
+ code: "route.page_artifact_same_path",
486
+ path: page.routePath,
487
+ message: `${page.routePath} has both a page and +server endpoint in the same public route directory; migrate them as separate StoneAge page and artifact outputs with explicit file paths.`,
488
+ });
489
+ }
490
+ }
491
+ for (const route of routes) {
492
+ if (hasStaticDynamicShadow(route.routePath, routes.map((candidate) => candidate.routePath))) {
493
+ warnings.push({
494
+ code: "route.static_dynamic_shadow",
495
+ path: route.routePath,
496
+ message: `${route.routePath} has a static sibling path that could also be emitted by this bare dynamic route. Add matcher constraints or route-entry collision tests.`,
497
+ });
498
+ }
499
+ }
500
+ return dedupeWarnings(warnings);
501
+ }
502
+ function createMigrationPlan(pages, endpoints, matchers, warnings, layouts) {
503
+ const routeFamilies = pages.map((page) => {
504
+ const layoutFiles = layoutFilesForRoute(page.routePath, layouts);
505
+ return {
506
+ name: migrationFamilyName(page.routePath, "Page"),
507
+ routePath: page.routePath,
508
+ pattern: page.stoneagePattern,
509
+ files: page.files,
510
+ params: page.params,
511
+ usesPageServer: page.hasPageServer,
512
+ usesUniversalLoad: page.usesUniversalLoad,
513
+ usesEntries: page.usesEntries,
514
+ ...(page.usesActions ? { usesActions: true } : {}),
515
+ ...(layoutFiles.length > 0 ? { layoutFiles } : {}),
516
+ paramMatchers: paramMatcherMap(page.params),
517
+ };
518
+ });
519
+ const artifactFamilies = endpoints.filter(canEmitStaticArtifact).map((endpoint) => {
520
+ const nonStaticMethods = endpoint.methods.filter((method) => method !== "GET" && method !== "HEAD");
521
+ return {
522
+ name: migrationFamilyName(endpoint.routePath, "Artifact"),
523
+ routePath: endpoint.routePath,
524
+ pattern: endpoint.stoneagePattern,
525
+ file: endpoint.file,
526
+ kind: endpoint.kind,
527
+ renderer: endpointRenderer(endpoint.kind),
528
+ params: endpoint.params,
529
+ paramMatchers: paramMatcherMap(endpoint.params),
530
+ usesResponseMetadata: endpoint.usesResponse,
531
+ ...(endpoint.usesEntries ? { usesEntries: true } : {}),
532
+ ...(nonStaticMethods.length > 0 ? { methods: endpoint.methods } : {}),
533
+ ...(nonStaticMethods.length > 0 ? { nonStaticMethods } : {}),
534
+ };
535
+ });
536
+ const routes = [...pages, ...endpoints];
537
+ return {
538
+ routeFamilies,
539
+ artifactFamilies,
540
+ paramMatchers: Array.from(matchers.values())
541
+ .map((matcher) => ({
542
+ name: matcher.name,
543
+ file: matcher.file,
544
+ usedBy: routes
545
+ .filter((route) => route.params.some((param) => param.matcher === matcher.name))
546
+ .map((route) => route.routePath)
547
+ .sort(),
548
+ }))
549
+ .sort((left, right) => left.name.localeCompare(right.name)),
550
+ collisionTests: warnings
551
+ .filter((warning) => [
552
+ "route.page_artifact_same_path",
553
+ "route.sibling_dynamic_without_matcher",
554
+ "route.static_dynamic_shadow",
555
+ ].includes(warning.code))
556
+ .map((warning) => ({
557
+ code: warning.code,
558
+ path: warning.path,
559
+ message: warning.message,
560
+ })),
561
+ };
562
+ }
563
+ function layoutFilesForRoute(routePath, layouts) {
564
+ return layouts
565
+ .filter((layout) => layout.routePath === "/" || routePath === layout.routePath || routePath.startsWith(layout.routePath))
566
+ .map((layout) => layout.file)
567
+ .sort();
568
+ }
569
+ function canEmitStaticArtifact(endpoint) {
570
+ return endpoint.methods.length === 0 || endpoint.methods.some((method) => method === "GET" || method === "HEAD");
571
+ }
572
+ function paramMatcherMap(params) {
573
+ return Object.fromEntries(params
574
+ .filter((param) => param.matcher)
575
+ .map((param) => [param.name, param.matcher])
576
+ .sort(([left], [right]) => left.localeCompare(right)));
577
+ }
578
+ function endpointRenderer(kind) {
579
+ if (kind === "json") {
580
+ return "jsonArtifact";
581
+ }
582
+ if (kind === "csv") {
583
+ return "csvArtifact";
584
+ }
585
+ if (kind === "xml") {
586
+ return "xmlArtifact";
587
+ }
588
+ if (kind === "txt") {
589
+ return "textArtifact";
590
+ }
591
+ return "artifactResponse";
592
+ }
593
+ function migrationFamilyName(routePath, suffix) {
594
+ const words = routePath
595
+ .split("/")
596
+ .filter(Boolean)
597
+ .flatMap((segment) => migrationSegmentWords(segment));
598
+ const base = words.length > 0 ? words.map(capitalizeIdentifier).join("") : "root";
599
+ return `${lowerFirst(base)}${suffix}`;
600
+ }
601
+ function migrationSegmentWords(segment) {
602
+ return segment
603
+ .replace(/\[\[([^\]=]+)(?:=([^\]]+))?\]\]|\[\.\.\.([^\]]+)\]|\[([^\]=]+)(?:=([^\]]+))?\]/g, (_match, optionalName, _optionalMatcher, restName, dynamicName) => optionalName ?? restName ?? dynamicName ?? "")
604
+ .split(/[^A-Za-z0-9]+/)
605
+ .filter(Boolean);
606
+ }
607
+ function capitalizeIdentifier(word) {
608
+ return `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`;
609
+ }
610
+ function lowerFirst(word) {
611
+ return `${word.slice(0, 1).toLowerCase()}${word.slice(1)}`;
612
+ }
613
+ function duplicatePageOutputFiles(files) {
614
+ return ["+page.svelte", "+page.server"].flatMap((kind) => {
615
+ const matches = files.filter((file) => {
616
+ if (kind === "+page.svelte") {
617
+ return isPageComponentFile(file);
618
+ }
619
+ return file.endsWith("+page.server.ts") || file.endsWith("+page.server.js");
620
+ });
621
+ return matches.length > 1 ? matches : [];
622
+ });
623
+ }
624
+ function hasSiblingDynamicWithoutMatcher(path, allPaths) {
625
+ const segments = path.split("/").filter(Boolean);
626
+ return bareDynamicSegmentIndexes(segments).some((dynamicIndex) => {
627
+ const dynamicSegment = segments[dynamicIndex] ?? "";
628
+ const parent = segments.slice(0, dynamicIndex).join("/");
629
+ const signature = dynamicSiblingSignature(dynamicSegment);
630
+ return allPaths.some((candidate) => {
631
+ if (candidate === path || canonicalRoutePath(candidate) === canonicalRoutePath(path)) {
632
+ return false;
633
+ }
634
+ const candidateSegments = candidate.split("/").filter(Boolean);
635
+ const candidateSegment = candidateSegments[dynamicIndex] ?? "";
636
+ return (candidateSegments.slice(0, dynamicIndex).join("/") === parent &&
637
+ candidateSegment !== dynamicSegment &&
638
+ anyDynamicParamPattern(candidateSegment) != null &&
639
+ dynamicSiblingSignature(candidateSegment) === signature);
640
+ });
641
+ });
642
+ }
643
+ function hasStaticDynamicShadow(path, allPaths) {
644
+ const segments = path.split("/").filter(Boolean);
645
+ return bareDynamicSegmentIndexes(segments).some((dynamicIndex) => {
646
+ return allPaths.some((candidate) => {
647
+ if (candidate === path || canonicalRoutePath(candidate) === canonicalRoutePath(path)) {
648
+ return false;
649
+ }
650
+ const candidateSegments = candidate.split("/").filter(Boolean);
651
+ return (candidateSegments.length === segments.length &&
652
+ candidateSegments.slice(0, dynamicIndex).join("/") === segments.slice(0, dynamicIndex).join("/") &&
653
+ candidateSegments.slice(dynamicIndex + 1).join("/") === segments.slice(dynamicIndex + 1).join("/") &&
654
+ isStaticRouteSegment(candidateSegments[dynamicIndex] ?? ""));
655
+ });
656
+ });
657
+ }
658
+ function bareDynamicSegmentIndexes(segments) {
659
+ return segments
660
+ .map((segment, index) => ({
661
+ index,
662
+ segment,
663
+ }))
664
+ .filter(({ segment }) => bareDynamicParamPattern(segment) != null && matcherDynamicParamPattern(segment) == null)
665
+ .map(({ index }) => index);
666
+ }
667
+ function canonicalRoutePath(path) {
668
+ if (path === "/") {
669
+ return path;
670
+ }
671
+ return path.endsWith("/") ? path.slice(0, -1) : path;
672
+ }
673
+ function isStaticRouteSegment(segment) {
674
+ return segment !== "" && !segment.includes("[");
675
+ }
676
+ function bareDynamicParamPattern(segment) {
677
+ if (segment.includes("[[") || segment.includes("[...")) {
678
+ return null;
679
+ }
680
+ return segment.match(/\[([^\]=.][^\]=]*)\]/);
681
+ }
682
+ function anyDynamicParamPattern(segment) {
683
+ if (segment.includes("[[") || segment.includes("[...")) {
684
+ return null;
685
+ }
686
+ return segment.match(/\[[^\]]+\]/);
687
+ }
688
+ function matcherDynamicParamPattern(segment) {
689
+ return segment.match(/\[[^\]=]+=([^\]]+)\]/);
690
+ }
691
+ function dynamicSiblingSignature(segment) {
692
+ return segment.replace(/\[[^\]]+\]/g, "[]");
693
+ }
694
+ function summarizeEndpointKinds(endpoints) {
695
+ const summary = {};
696
+ for (const endpoint of endpoints) {
697
+ summary[endpoint.kind] = (summary[endpoint.kind] ?? 0) + 1;
698
+ }
699
+ return summary;
700
+ }
701
+ function dedupeWarnings(warnings) {
702
+ return Array.from(new Map(warnings.map((warning) => [`${warning.code}:${warning.path}:${warning.message}`, warning])).values());
703
+ }
704
+ function renderDataProducerStubs() {
705
+ return [
706
+ "export type NormalizedData = {",
707
+ " records: unknown[];",
708
+ "};",
709
+ "",
710
+ "export async function loadNormalizedData(): Promise<NormalizedData> {",
711
+ " return {",
712
+ " records: [],",
713
+ " };",
714
+ "}",
715
+ "",
716
+ ].join("\n");
717
+ }
718
+ function renderRouteStubs(routeFamilies, matcherIdentifiers) {
719
+ const matcherNames = uniqueStringsFromPlans(routeFamilies.flatMap((family) => Object.values(family.paramMatchers)));
720
+ const imports = [
721
+ 'import { defineRouteFamily, type RouteFamily } from "@t09tanaka/stoneage";',
722
+ 'import type { NormalizedData } from "./data.js";',
723
+ ...(matcherNames.length > 0
724
+ ? [`import { ${matcherNames.map((name) => matcherIdentifiers.get(name) ?? identifierFor(name, "Matcher")).join(", ")} } from "./param-matchers.js";`]
725
+ : []),
726
+ ];
727
+ const body = routeFamilies.length === 0
728
+ ? "// No route families were found in the migration plan."
729
+ : routeFamilies
730
+ .map((family) => renderRouteFamilyStub(family, matcherIdentifiers))
731
+ .join("\n\n");
732
+ return `${imports.join("\n")}\n\n${body}\n\n${renderRouteFamilyFactory(routeFamilies)}\n`;
733
+ }
734
+ function renderRouteFamilyStub(family, matcherIdentifiers) {
735
+ const lines = [
736
+ `// Original route: ${family.routePath}`,
737
+ ...(family.layoutFiles ?? []).map((file) => `// Layout source: ${file}`),
738
+ ...family.files.map((file) => `// Source: ${file}`),
739
+ ...(family.usesUniversalLoad
740
+ ? ["// TODO: Move universal +page.ts/+page.js load data into normalized data producers."]
741
+ : []),
742
+ ...(family.usesActions
743
+ ? ["// TODO: Move SvelteKit page actions into explicit static data producers or remove them from the static output."]
744
+ : []),
745
+ family.usesPageServer
746
+ ? "// TODO: Move +page.server.ts loading into normalized data producers."
747
+ : "// TODO: Add normalized data entries for this page route.",
748
+ family.usesEntries
749
+ ? "// SvelteKit entries() was detected; use it only as a migration reference."
750
+ : "// TODO: Define explicit route entries from normalized data.",
751
+ `export const ${identifierFor(family.name, "routeFamily")} = defineRouteFamily({`,
752
+ ` name: ${stringLiteral(family.name)},`,
753
+ ` pattern: ${stringLiteral(family.pattern)},`,
754
+ ...renderParamMatchersProperty(family.paramMatchers, matcherIdentifiers, " "),
755
+ " entries: () => [",
756
+ " {",
757
+ ` params: ${renderParamsPlaceholder(family.params)},`,
758
+ " dependencies: [],",
759
+ " metadata: {",
760
+ ' title: "TODO",',
761
+ ' description: "TODO",',
762
+ " },",
763
+ " // TODO: Replace this placeholder with normalized data entries.",
764
+ ' render: () => "<main>TODO</main>",',
765
+ " },",
766
+ " ],",
767
+ "});",
768
+ ];
769
+ return lines.join("\n");
770
+ }
771
+ function renderRouteFamilyFactory(routeFamilies) {
772
+ const identifiers = routeFamilies.map((family) => identifierFor(family.name, "routeFamily"));
773
+ return [
774
+ "export function createRouteFamilies(_data: NormalizedData): RouteFamily[] {",
775
+ " return [",
776
+ ...identifiers.map((identifier) => ` ${identifier},`),
777
+ " ];",
778
+ "}",
779
+ ].join("\n");
780
+ }
781
+ function renderArtifactStubs(artifactFamilies, matcherIdentifiers) {
782
+ const renderers = uniqueStringsFromPlans(artifactFamilies.map((family) => family.renderer));
783
+ const matcherNames = uniqueStringsFromPlans(artifactFamilies.flatMap((family) => Object.values(family.paramMatchers)));
784
+ const stoneageImports = uniqueStringsFromPlans([...renderers, "defineArtifactFamily"]);
785
+ const imports = [
786
+ `import { ${stoneageImports.join(", ")} } from "@t09tanaka/stoneage";`,
787
+ 'import type { ArtifactFamily } from "@t09tanaka/stoneage";',
788
+ 'import type { NormalizedData } from "./data.js";',
789
+ ...(matcherNames.length > 0
790
+ ? [`import { ${matcherNames.map((name) => matcherIdentifiers.get(name) ?? identifierFor(name, "Matcher")).join(", ")} } from "./param-matchers.js";`]
791
+ : []),
792
+ ];
793
+ const body = artifactFamilies.length === 0
794
+ ? "// No artifact families were found in the migration plan."
795
+ : artifactFamilies
796
+ .map((family) => renderArtifactFamilyStub(family, matcherIdentifiers))
797
+ .join("\n\n");
798
+ return `${imports.join("\n")}\n\n${body}\n\n${renderArtifactFamilyFactory(artifactFamilies)}\n`;
799
+ }
800
+ function renderArtifactFamilyStub(family, matcherIdentifiers) {
801
+ const lines = [
802
+ `// Original route: ${family.routePath}`,
803
+ `// Source: ${family.file}`,
804
+ `// Renderer hint: ${family.renderer} for ${family.kind} endpoints.`,
805
+ ...(family.usesEntries
806
+ ? ["// SvelteKit entries() was detected; use it only as a migration reference."]
807
+ : []),
808
+ ...(family.nonStaticMethods && family.nonStaticMethods.length > 0
809
+ ? [`// TODO: Non-static endpoint methods require a manual migration path: ${family.nonStaticMethods.join(", ")}.`]
810
+ : []),
811
+ family.usesResponseMetadata
812
+ ? "// TODO: Preserve status, headers, and content type metadata from the SvelteKit Response."
813
+ : "// TODO: Add status, headers, or content type metadata if this endpoint needs them.",
814
+ `export const ${identifierFor(family.name, "artifactFamily")} = defineArtifactFamily({`,
815
+ ` name: ${stringLiteral(family.name)},`,
816
+ ` pattern: ${stringLiteral(family.pattern)},`,
817
+ ...renderParamMatchersProperty(family.paramMatchers, matcherIdentifiers, " "),
818
+ " entries: () => [",
819
+ " {",
820
+ ` params: ${renderParamsPlaceholder(family.params)},`,
821
+ " dependencies: [],",
822
+ " // TODO: Replace this placeholder with normalized data entries and render body.",
823
+ ` render: () => ${renderArtifactPlaceholder(family.renderer)},`,
824
+ " },",
825
+ " ],",
826
+ "});",
827
+ ];
828
+ return lines.join("\n");
829
+ }
830
+ function renderArtifactFamilyFactory(artifactFamilies) {
831
+ const identifiers = artifactFamilies.map((family) => identifierFor(family.name, "artifactFamily"));
832
+ return [
833
+ "export function createArtifactFamilies(_data: NormalizedData): ArtifactFamily[] {",
834
+ " return [",
835
+ ...identifiers.map((identifier) => ` ${identifier},`),
836
+ " ];",
837
+ "}",
838
+ ].join("\n");
839
+ }
840
+ function renderParamMatcherStubs(paramMatchers, matcherIdentifiers) {
841
+ const body = paramMatchers.length === 0
842
+ ? "// No param matchers were found in the migration plan."
843
+ : paramMatchers
844
+ .map((matcher) => {
845
+ const usedBy = matcher.usedBy.length > 0 ? matcher.usedBy.join(", ") : "none";
846
+ return [
847
+ `// Source: ${matcher.file}`,
848
+ `// Used by: ${usedBy}`,
849
+ `// TODO: Port the original SvelteKit matcher logic into this StoneAge matcher.`,
850
+ `export const ${matcherIdentifiers.get(matcher.name) ?? identifierFor(matcher.name, "Matcher")} = patternParamMatcher(/^TODO$/);`,
851
+ ].join("\n");
852
+ })
853
+ .join("\n\n");
854
+ return `import { patternParamMatcher } from "@t09tanaka/stoneage";\n\n${body}\n`;
855
+ }
856
+ function renderCollisionTestStubs(collisionTests) {
857
+ if (collisionTests.length === 0) {
858
+ return "# Collision Test Candidates\n\nNo collision test candidates were found.\n";
859
+ }
860
+ const items = collisionTests.map((candidate) => [
861
+ `- \`${candidate.code}\` \`${candidate.path}\``,
862
+ ` - ${candidate.message}`,
863
+ " - TODO: Add a StoneAge route-entry test that proves this path cannot collide.",
864
+ ].join("\n"));
865
+ return `# Collision Test Candidates\n\n${items.join("\n")}\n`;
866
+ }
867
+ function renderBuildEntrypointStub() {
868
+ return [
869
+ 'import { buildSite } from "@t09tanaka/stoneage";',
870
+ 'import { createArtifactFamilies } from "./artifacts.js";',
871
+ 'import { loadNormalizedData } from "./data.js";',
872
+ 'import { createRouteFamilies } from "./routes.js";',
873
+ "",
874
+ "const data = await loadNormalizedData();",
875
+ "",
876
+ "await buildSite({",
877
+ ' outDir: "dist",',
878
+ " site: {",
879
+ ' baseUrl: "https://example.com",',
880
+ ' title: "TODO",',
881
+ ' description: "TODO",',
882
+ " },",
883
+ " routes: createRouteFamilies(data),",
884
+ " artifacts: createArtifactFamilies(data),",
885
+ "});",
886
+ "",
887
+ ].join("\n");
888
+ }
889
+ function renderParamMatchersProperty(paramMatchers, matcherIdentifiers, indent) {
890
+ const entries = Object.entries(paramMatchers).sort(([left], [right]) => left.localeCompare(right));
891
+ if (entries.length === 0) {
892
+ return [];
893
+ }
894
+ return [
895
+ `${indent}paramMatchers: {`,
896
+ ...entries.map(([paramName, matcherName]) => {
897
+ const identifier = matcherIdentifiers.get(matcherName) ?? identifierFor(matcherName, "Matcher");
898
+ return `${indent} ${objectKey(paramName)}: ${identifier},`;
899
+ }),
900
+ `${indent}},`,
901
+ ];
902
+ }
903
+ function renderParamsPlaceholder(params) {
904
+ if (params.length === 0) {
905
+ return "{}";
906
+ }
907
+ const entries = params
908
+ .map((param) => `${objectKey(param.name)}: "TODO"`)
909
+ .join(", ");
910
+ return `{ ${entries} }`;
911
+ }
912
+ function renderArtifactPlaceholder(renderer) {
913
+ if (renderer === "jsonArtifact") {
914
+ return "jsonArtifact({ todo: true })";
915
+ }
916
+ if (renderer === "csvArtifact") {
917
+ return 'csvArtifact("TODO\\n")';
918
+ }
919
+ if (renderer === "xmlArtifact") {
920
+ return 'xmlArtifact("<root />")';
921
+ }
922
+ if (renderer === "textArtifact") {
923
+ return 'textArtifact("TODO")';
924
+ }
925
+ return 'artifactResponse("TODO")';
926
+ }
927
+ function uniqueIdentifierMap(names, fallback) {
928
+ const identifiers = new Map();
929
+ const used = new Set();
930
+ for (const name of uniqueStringsFromPlans(names)) {
931
+ const base = identifierFor(name, fallback);
932
+ let identifier = base;
933
+ let suffix = 2;
934
+ while (used.has(identifier)) {
935
+ identifier = `${base}${suffix}`;
936
+ suffix += 1;
937
+ }
938
+ used.add(identifier);
939
+ identifiers.set(name, identifier);
940
+ }
941
+ return identifiers;
942
+ }
943
+ function identifierFor(value, fallback) {
944
+ const words = value.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
945
+ const candidate = words.length === 0
946
+ ? fallback
947
+ : `${words[0]}${words.slice(1).map(capitalizeIdentifier).join("")}`;
948
+ const identifier = candidate.replace(/^[^A-Za-z_$]+/, "");
949
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier) ? identifier : fallback;
950
+ }
951
+ function objectKey(value) {
952
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value) ? value : stringLiteral(value);
953
+ }
954
+ function stringLiteral(value) {
955
+ return JSON.stringify(value);
956
+ }
957
+ function uniqueStringsFromPlans(values) {
958
+ return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
959
+ }
960
+ function uniqueImportSources(values) {
961
+ return Array.from(new Set(values)).sort((left, right) => {
962
+ const relativeOrder = Number(left.startsWith(".")) - Number(right.startsWith("."));
963
+ return relativeOrder || left.localeCompare(right);
964
+ });
965
+ }
966
+ function compareByRoutePath(left, right) {
967
+ return left.routePath.localeCompare(right.routePath);
968
+ }
969
+ function compareByFile(left, right) {
970
+ return left.file.localeCompare(right.file);
971
+ }
972
+ function compareWarnings(left, right) {
973
+ return left.path.localeCompare(right.path) || left.code.localeCompare(right.code);
974
+ }
975
+ function isRouteGroup(segment) {
976
+ return segment.startsWith("(") && segment.endsWith(")");
977
+ }
978
+ function toPosix(path) {
979
+ return path.split(sep).join("/");
980
+ }
981
+ function isNodeError(error) {
982
+ return typeof error === "object" && error !== null && "code" in error;
983
+ }