@byline/cli 3.20.4 → 4.0.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/README.md +42 -2
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +10 -14
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/setup-checks.d.ts +9 -0
- package/dist/commands/setup-checks.d.ts.map +1 -1
- package/dist/commands/setup-checks.js +43 -18
- package/dist/commands/setup-checks.js.map +1 -1
- package/dist/commands/setup.js +1 -1
- package/dist/commands/setup.js.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -0
- package/dist/context.js.map +1 -1
- package/dist/lib/dependency-version.d.ts +12 -0
- package/dist/lib/dependency-version.d.ts.map +1 -0
- package/dist/lib/dependency-version.js +101 -0
- package/dist/lib/dependency-version.js.map +1 -0
- package/dist/lib/planned-writes.d.ts +8 -0
- package/dist/lib/planned-writes.d.ts.map +1 -0
- package/dist/lib/planned-writes.js +177 -0
- package/dist/lib/planned-writes.js.map +1 -0
- package/dist/lib/planned-writes.test.d.ts +2 -0
- package/dist/lib/planned-writes.test.d.ts.map +1 -0
- package/dist/lib/planned-writes.test.js +113 -0
- package/dist/lib/planned-writes.test.js.map +1 -0
- package/dist/lib/release-policy.d.ts +12 -0
- package/dist/lib/release-policy.d.ts.map +1 -0
- package/dist/lib/release-policy.js +20 -0
- package/dist/lib/release-policy.js.map +1 -0
- package/dist/lib/release-policy.test.d.ts +2 -0
- package/dist/lib/release-policy.test.d.ts.map +1 -0
- package/dist/lib/release-policy.test.js +36 -0
- package/dist/lib/release-policy.test.js.map +1 -0
- package/dist/lib/route-config.d.ts +40 -0
- package/dist/lib/route-config.d.ts.map +1 -0
- package/dist/lib/route-config.js +322 -0
- package/dist/lib/route-config.js.map +1 -0
- package/dist/lib/static-config.d.ts +20 -0
- package/dist/lib/static-config.d.ts.map +1 -0
- package/dist/lib/static-config.js +444 -0
- package/dist/lib/static-config.js.map +1 -0
- package/dist/lib/static-config.test.d.ts +2 -0
- package/dist/lib/static-config.test.d.ts.map +1 -0
- package/dist/lib/static-config.test.js +105 -0
- package/dist/lib/static-config.test.js.map +1 -0
- package/dist/lib/template-hook-boundary.test.node.d.ts +2 -0
- package/dist/lib/template-hook-boundary.test.node.d.ts.map +1 -0
- package/dist/lib/template-hook-boundary.test.node.js +30 -0
- package/dist/lib/template-hook-boundary.test.node.js.map +1 -0
- package/dist/lib/template-path.d.ts +5 -0
- package/dist/lib/template-path.d.ts.map +1 -0
- package/dist/lib/template-path.js +9 -0
- package/dist/lib/template-path.js.map +1 -0
- package/dist/lib/workspace-root.d.ts +28 -0
- package/dist/lib/workspace-root.d.ts.map +1 -0
- package/dist/lib/workspace-root.js +205 -0
- package/dist/lib/workspace-root.js.map +1 -0
- package/dist/manifest/deps.d.ts +1 -1
- package/dist/manifest/deps.d.ts.map +1 -1
- package/dist/manifest/deps.js +18 -8
- package/dist/manifest/deps.js.map +1 -1
- package/dist/phases/deps.d.ts +14 -1
- package/dist/phases/deps.d.ts.map +1 -1
- package/dist/phases/deps.js +312 -137
- package/dist/phases/deps.js.map +1 -1
- package/dist/phases/deps.test.d.ts +2 -0
- package/dist/phases/deps.test.d.ts.map +1 -0
- package/dist/phases/deps.test.js +343 -0
- package/dist/phases/deps.test.js.map +1 -0
- package/dist/phases/preflight.d.ts.map +1 -1
- package/dist/phases/preflight.js +59 -11
- package/dist/phases/preflight.js.map +1 -1
- package/dist/phases/prompts.d.ts +2 -0
- package/dist/phases/prompts.d.ts.map +1 -1
- package/dist/phases/prompts.js +51 -2
- package/dist/phases/prompts.js.map +1 -1
- package/dist/phases/routes-ui.test.d.ts +2 -0
- package/dist/phases/routes-ui.test.d.ts.map +1 -0
- package/dist/phases/routes-ui.test.js +766 -0
- package/dist/phases/routes-ui.test.js.map +1 -0
- package/dist/phases/routes.d.ts +4 -1
- package/dist/phases/routes.d.ts.map +1 -1
- package/dist/phases/routes.js +610 -122
- package/dist/phases/routes.js.map +1 -1
- package/dist/phases/scaffold-smoke.test.d.ts +2 -0
- package/dist/phases/scaffold-smoke.test.d.ts.map +1 -0
- package/dist/phases/scaffold-smoke.test.js +168 -0
- package/dist/phases/scaffold-smoke.test.js.map +1 -0
- package/dist/phases/scaffold.d.ts +4 -1
- package/dist/phases/scaffold.d.ts.map +1 -1
- package/dist/phases/scaffold.js +275 -72
- package/dist/phases/scaffold.js.map +1 -1
- package/dist/phases/scaffold.test.d.ts +2 -0
- package/dist/phases/scaffold.test.d.ts.map +1 -0
- package/dist/phases/scaffold.test.js +178 -0
- package/dist/phases/scaffold.test.js.map +1 -0
- package/dist/phases/ui.d.ts +4 -1
- package/dist/phases/ui.d.ts.map +1 -1
- package/dist/phases/ui.js +51 -59
- package/dist/phases/ui.js.map +1 -1
- package/dist/phases/wire/index.d.ts.map +1 -1
- package/dist/phases/wire/index.js +10 -6
- package/dist/phases/wire/index.js.map +1 -1
- package/dist/phases/wire/shared.d.ts +4 -1
- package/dist/phases/wire/shared.d.ts.map +1 -1
- package/dist/phases/wire/vite-config.d.ts.map +1 -1
- package/dist/phases/wire/vite-config.js +69 -20
- package/dist/phases/wire/vite-config.js.map +1 -1
- package/dist/phases/wire/vite-config.test.d.ts +2 -0
- package/dist/phases/wire/vite-config.test.d.ts.map +1 -0
- package/dist/phases/wire/vite-config.test.js +97 -0
- package/dist/phases/wire/vite-config.test.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +7 -2
- package/dist/prompts.js.map +1 -1
- package/dist/runner.js +1 -1
- package/dist/runner.js.map +1 -1
- package/dist/templates/byline/admin.config.ts +8 -7
- package/dist/templates/byline/clients.server.ts +77 -0
- package/dist/templates/byline/collection-types.contract.ts +67 -0
- package/dist/templates/byline/collections/index.ts +17 -0
- package/dist/templates/byline/collections/server-hooks.ts +7 -0
- package/dist/templates/byline/generated/collection-types.ts +11 -0
- package/dist/templates/byline/i18n.ts +3 -3
- package/dist/templates/byline/locales.ts +9 -9
- package/dist/templates/byline/public.ts +12 -0
- package/dist/templates/byline/routes.ts +7 -6
- package/dist/templates/byline/scripts/generate-types.ts +81 -0
- package/dist/templates/byline/server.config.ts +5 -5
- package/dist/templates/byline-examples/admin.config.ts +18 -21
- package/dist/templates/byline-examples/blocks/photo-block.ts +3 -2
- package/dist/templates/byline-examples/clients.server.ts +77 -0
- package/dist/templates/byline-examples/collection-types.contract.ts +67 -0
- package/dist/templates/byline-examples/collections/docs/hooks.ts +30 -20
- package/dist/templates/byline-examples/collections/docs/schema.ts +18 -18
- package/dist/templates/byline-examples/collections/index.ts +22 -0
- package/dist/templates/byline-examples/collections/media/components/media-list-view.tsx +11 -6
- package/dist/templates/byline-examples/collections/media/schema.ts +16 -0
- package/dist/templates/byline-examples/collections/news/schema.ts +17 -3
- package/dist/templates/byline-examples/collections/news-categories/schema.ts +13 -0
- package/dist/templates/byline-examples/collections/pages/schema.ts +13 -0
- package/dist/templates/byline-examples/collections/server-hooks.ts +8 -0
- package/dist/templates/byline-examples/fields/ai-text.ts +1 -1
- package/dist/templates/byline-examples/fields/ai-textarea.ts +1 -1
- package/dist/templates/byline-examples/fields/lexical-richtext-ai.tsx +3 -3
- package/dist/templates/byline-examples/generated/collection-types.ts +139 -0
- package/dist/templates/byline-examples/i18n.ts +3 -3
- package/dist/templates/byline-examples/locales.ts +9 -9
- package/dist/templates/byline-examples/public.ts +12 -0
- package/dist/templates/byline-examples/routes.ts +7 -6
- package/dist/templates/byline-examples/scripts/backfill-version-locales.ts +1 -1
- package/dist/templates/byline-examples/scripts/generate-types.ts +81 -0
- package/dist/templates/byline-examples/scripts/import-docs.ts +11 -12
- package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.test.node.ts +93 -233
- package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.ts +2 -2
- package/dist/templates/byline-examples/scripts/lib/strip-leading-h1.test.node.ts +0 -4
- package/dist/templates/byline-examples/scripts/re-anchor.ts +3 -3
- package/dist/templates/byline-examples/scripts/regenerate-media.ts +6 -5
- package/dist/templates/byline-examples/server.config.ts +6 -15
- package/dist/templates/host/vite.config.ts +2 -0
- package/dist/templates/routes/_byline/admin/collections/$collection/$id/api.tsx +1 -6
- package/dist/templates/routes/_byline/admin/collections/$collection/$id/history.tsx +1 -7
- package/dist/templates/routes/_byline/admin/collections/$collection/$id/index.tsx +1 -6
- package/dist/templates/routes/_byline/route.lazy.tsx +1 -2
- package/dist/templates/routes/_byline/route.tsx +1 -1
- package/dist/templates/ui-byline/blocks/photo-block/index.tsx +18 -23
- package/dist/templates/ui-byline/blocks/richtext-block/index.tsx +1 -1
- package/dist/templates/ui-byline/components/admonition/index.tsx +11 -8
- package/dist/templates/ui-byline/components/code/code.tsx +6 -4
- package/dist/templates/ui-byline/components/link/lang-link.tsx +4 -8
- package/dist/templates/ui-byline/components/link/link-field.tsx +3 -2
- package/dist/templates/ui-byline/components/link/link-lexical.tsx +9 -8
- package/dist/templates/ui-byline/render-blocks.tsx +38 -38
- package/dist/templates/ui-byline/types/content.ts +25 -0
- package/dist/templates/ui-byline/types/i18n.ts +4 -8
- package/dist/test-helpers.d.ts +5 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +52 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/types.d.ts +21 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui-template-contract.test.d.ts +2 -0
- package/dist/ui-template-contract.test.d.ts.map +1 -0
- package/dist/ui-template-contract.test.js +130 -0
- package/dist/ui-template-contract.test.js.map +1 -0
- package/dist/workspace-root.test.d.ts +2 -0
- package/dist/workspace-root.test.d.ts.map +1 -0
- package/dist/workspace-root.test.js +307 -0
- package/dist/workspace-root.test.js.map +1 -0
- package/package.json +9 -1
package/dist/phases/routes.js
CHANGED
|
@@ -1,159 +1,540 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, rmdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { applyPlannedWrites } from '../lib/planned-writes.js';
|
|
4
|
+
import { canonicalRoutesTemplatePath, DEFAULT_SIGN_IN_PATH, readStaticRoutesSource, recognizeGeneratedRoutesSource, renderGeneratedRoutesSource, routesSourceAligned, validateCanonicalRoutesSource, validateRoutePaths, } from '../lib/route-config.js';
|
|
5
|
+
import { normalizeTemplateSource, toPosixTemplatePath } from '../lib/template-path.js';
|
|
3
6
|
const TEMPLATE_DIR = 'routes/_byline';
|
|
4
7
|
const TARGET_GROUP_DIR = 'src/routes/_byline';
|
|
5
|
-
const
|
|
6
|
-
const
|
|
8
|
+
const RUNTIME_ROUTES = 'byline/routes.ts';
|
|
9
|
+
const SIGN_IN_TEMPLATE = 'sign-in.tsx';
|
|
7
10
|
export const routesPhase = {
|
|
8
11
|
id: 'routes',
|
|
9
|
-
title: 'Routes —
|
|
12
|
+
title: 'Routes — install the _byline pathless route group',
|
|
10
13
|
defaultMode: 'confirm',
|
|
11
14
|
async detect(ctx) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const answers = ctx.state.get().answers;
|
|
16
|
+
const validation = validateRoutePaths(ctx, answers.adminPath, answers.signInPath);
|
|
17
|
+
if (!validation.ok)
|
|
18
|
+
return 'pending';
|
|
19
|
+
return inspectRoutes(ctx, validation.value).complete ? 'done' : 'pending';
|
|
15
20
|
},
|
|
16
21
|
async plan(ctx) {
|
|
17
|
-
|
|
18
|
-
const slug = pathToSlug(adminPath);
|
|
19
|
-
if (slug === null) {
|
|
20
|
-
return {
|
|
21
|
-
writes: [],
|
|
22
|
-
commands: [],
|
|
23
|
-
notes: [
|
|
24
|
-
`invalid admin path "${adminPath}" — must be a single URL segment matching ${ADMIN_SLUG_REGEX}`,
|
|
25
|
-
],
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
const entries = collectEntries(ctx, slug);
|
|
29
|
-
const created = entries.filter((e) => e.status === 'create').length;
|
|
30
|
-
const skipped = entries.filter((e) => e.status === 'skip-already-exists').length;
|
|
31
|
-
const notes = [
|
|
32
|
-
`target: ${ctx.resolve(TARGET_GROUP_DIR)}/`,
|
|
33
|
-
`admin path: ${ctx.state.get().answers.adminPath === undefined ? `will prompt (default ${DEFAULT_ADMIN_PATH})` : adminPath}`,
|
|
34
|
-
slug !== 'admin'
|
|
35
|
-
? `pathless layout: _byline/${slug} (renamed from _byline/admin)`
|
|
36
|
-
: 'pathless layout: _byline/admin',
|
|
37
|
-
`${created} file(s) to create, ${skipped} already-existing skipped`,
|
|
38
|
-
];
|
|
39
|
-
return { writes: [], commands: [], notes };
|
|
22
|
+
return buildRoutesPlan(ctx);
|
|
40
23
|
},
|
|
41
|
-
async apply(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const slug = pathToSlug(adminPath);
|
|
52
|
-
if (!slug) {
|
|
53
|
-
ctx.logger.error(`invalid admin path "${adminPath}" — must be a single URL segment matching ${ADMIN_SLUG_REGEX}`);
|
|
24
|
+
async apply(plan, ctx) {
|
|
25
|
+
const answers = ctx.state.get().answers;
|
|
26
|
+
const validation = validateRoutePaths(ctx, answers.adminPath, answers.signInPath);
|
|
27
|
+
if (!validation.ok) {
|
|
28
|
+
ctx.logger.error(validation.error);
|
|
29
|
+
return { state: 'blocked' };
|
|
30
|
+
}
|
|
31
|
+
const canonical = canonicalRuntimeRoutesSource(ctx);
|
|
32
|
+
if (!canonical.ok) {
|
|
33
|
+
ctx.logger.error(canonical.error);
|
|
54
34
|
return { state: 'blocked' };
|
|
55
35
|
}
|
|
56
|
-
ctx.
|
|
57
|
-
|
|
58
|
-
if (entries.length === 0) {
|
|
59
|
-
ctx.logger.error('no route templates found — was the cli built with templates copied to dist?');
|
|
36
|
+
if (expectedRouteWrites(ctx, validation.value).length === 0) {
|
|
37
|
+
ctx.logger.error('no route templates found — was the CLI built with templates?');
|
|
60
38
|
return { state: 'blocked' };
|
|
61
39
|
}
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (entry.status === 'skip-already-exists') {
|
|
66
|
-
skipped++;
|
|
40
|
+
let stalePreconditionPath;
|
|
41
|
+
for (const precondition of plan.preconditions ?? []) {
|
|
42
|
+
if (precondition.type === 'value')
|
|
67
43
|
continue;
|
|
44
|
+
const stale = precondition.type === 'file'
|
|
45
|
+
? currentFileContents(precondition.path) !== precondition.contents
|
|
46
|
+
: !treeSnapshotsEqual(snapshotTree(precondition.path), precondition.entries);
|
|
47
|
+
if (stale) {
|
|
48
|
+
stalePreconditionPath = precondition.path;
|
|
49
|
+
break;
|
|
68
50
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
ctx.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'
|
|
84
|
-
''
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
'— `apps/webapp/src/routes/_byline/route.tsx` (admin) and',
|
|
96
|
-
' `apps/webapp/src/routes/{-$lng}/_public/route.tsx` (front-end) both',
|
|
97
|
-
'demonstrate this pattern.',
|
|
98
|
-
].join('\n'), 'Pathless layout + style isolation');
|
|
99
|
-
return { state: 'done' };
|
|
51
|
+
}
|
|
52
|
+
if (stalePreconditionPath) {
|
|
53
|
+
ctx.logger.warn(`${stalePreconditionPath} changed after preview; no route changes applied`);
|
|
54
|
+
return { state: 'partial' };
|
|
55
|
+
}
|
|
56
|
+
ctx.state.patchAnswers({
|
|
57
|
+
adminPath: validation.value.adminPath,
|
|
58
|
+
signInPath: validation.value.signInPath,
|
|
59
|
+
});
|
|
60
|
+
const result = applyPlannedWrites(plan.writes);
|
|
61
|
+
if (result.written.length > 0) {
|
|
62
|
+
ctx.logger.success(`wrote ${result.written.length} planned route file(s)`);
|
|
63
|
+
}
|
|
64
|
+
if (result.conflicts.length > 0) {
|
|
65
|
+
ctx.logger.warn('route files changed after preview and were left untouched');
|
|
66
|
+
return { state: 'partial' };
|
|
67
|
+
}
|
|
68
|
+
removeEmptyDeletedRouteDirectories(plan.writes, ctx.resolve(TARGET_GROUP_DIR));
|
|
69
|
+
const migrationResiduals = findMigrationResiduals(plan, ctx, validation.value);
|
|
70
|
+
for (const path of migrationResiduals) {
|
|
71
|
+
ctx.logger.warn(`${path}: manual — residual file remains in the old admin tree`);
|
|
72
|
+
}
|
|
73
|
+
const inspection = inspectRoutes(ctx, validation.value);
|
|
74
|
+
for (const note of inspection.manualNotes)
|
|
75
|
+
ctx.logger.warn(note);
|
|
76
|
+
return { state: inspection.complete && migrationResiduals.length === 0 ? 'done' : 'partial' };
|
|
100
77
|
},
|
|
101
78
|
};
|
|
102
|
-
function
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
79
|
+
export function buildRoutesPlan(ctx) {
|
|
80
|
+
const answers = ctx.state.get().answers;
|
|
81
|
+
const validation = validateRoutePaths(ctx, answers.adminPath, answers.signInPath);
|
|
82
|
+
if (!validation.ok)
|
|
83
|
+
return { writes: [], commands: [], notes: [validation.error] };
|
|
84
|
+
const routes = validation.value;
|
|
85
|
+
const canonical = canonicalRuntimeRoutesSource(ctx);
|
|
86
|
+
if (!canonical.ok)
|
|
87
|
+
return { writes: [], commands: [], notes: [canonical.error] };
|
|
88
|
+
if (expectedRouteWrites(ctx, routes).length === 0) {
|
|
89
|
+
return {
|
|
90
|
+
writes: [],
|
|
91
|
+
commands: [],
|
|
92
|
+
notes: ['no route templates found — was the CLI built with templates?'],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const routeFiles = planRouteFileWrites(ctx, routes);
|
|
96
|
+
if (routeFiles.blocked) {
|
|
97
|
+
return {
|
|
98
|
+
writes: [],
|
|
99
|
+
commands: [],
|
|
100
|
+
notes: [
|
|
101
|
+
`target: ${ctx.resolve(TARGET_GROUP_DIR)}/`,
|
|
102
|
+
`admin path: ${routes.adminPath}`,
|
|
103
|
+
`sign-in path: ${routes.signInPath}`,
|
|
104
|
+
'0 planned route/config change(s)',
|
|
105
|
+
...routeFiles.notes,
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const runtime = planRuntimeRoutesWrite(ctx, routes, canonical.source);
|
|
110
|
+
let writes = routeFiles.writes;
|
|
111
|
+
const notes = [...routeFiles.notes];
|
|
112
|
+
if (runtime.migrationBlocked) {
|
|
113
|
+
if (runtime.note)
|
|
114
|
+
notes.push(runtime.note);
|
|
115
|
+
return {
|
|
116
|
+
writes: [],
|
|
117
|
+
commands: [],
|
|
118
|
+
notes: [
|
|
119
|
+
`target: ${ctx.resolve(TARGET_GROUP_DIR)}/`,
|
|
120
|
+
`admin path: ${routes.adminPath}`,
|
|
121
|
+
`sign-in path: ${routes.signInPath}`,
|
|
122
|
+
'0 planned route/config change(s)',
|
|
123
|
+
...notes,
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const previous = runtime.previousRoutes;
|
|
128
|
+
const generatedMigration = previous &&
|
|
129
|
+
(previous.adminPath !== routes.adminPath || previous.signInPath !== routes.signInPath)
|
|
130
|
+
? planGeneratedRoutesMigration(ctx, previous, routes)
|
|
131
|
+
: undefined;
|
|
132
|
+
if (generatedMigration && (!generatedMigration.safe || runtime.write === undefined)) {
|
|
133
|
+
return {
|
|
134
|
+
writes: [],
|
|
135
|
+
commands: [],
|
|
136
|
+
notes: [
|
|
137
|
+
`target: ${ctx.resolve(TARGET_GROUP_DIR)}/`,
|
|
138
|
+
`admin path: ${routes.adminPath}`,
|
|
139
|
+
`sign-in path: ${routes.signInPath}`,
|
|
140
|
+
'0 planned route/config change(s)',
|
|
141
|
+
generatedMigration.note ??
|
|
142
|
+
`${ctx.resolve(RUNTIME_ROUTES)}: manual — route migration was left unchanged because every coupled file was not recognized as generated and safe`,
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (generatedMigration)
|
|
147
|
+
writes = generatedMigration.writes;
|
|
148
|
+
const defaultSignInPath = ctx.resolve(TARGET_GROUP_DIR, SIGN_IN_TEMPLATE);
|
|
149
|
+
if (!generatedMigration &&
|
|
150
|
+
routeFiles.signInPath !== defaultSignInPath &&
|
|
151
|
+
existsSync(defaultSignInPath)) {
|
|
152
|
+
notes.push(`${defaultSignInPath}: manual — existing sign-in route was preserved`);
|
|
153
|
+
}
|
|
154
|
+
if (runtime.write)
|
|
155
|
+
writes.push(runtime.write);
|
|
156
|
+
if (generatedMigration) {
|
|
157
|
+
const deletes = writes.filter((write) => write.mode === 'delete');
|
|
158
|
+
writes = [...writes.filter((write) => write.mode !== 'delete'), ...deletes];
|
|
159
|
+
}
|
|
160
|
+
if (runtime.note)
|
|
161
|
+
notes.push(runtime.note);
|
|
162
|
+
const planNotes = [
|
|
163
|
+
`target: ${ctx.resolve(TARGET_GROUP_DIR)}/`,
|
|
164
|
+
`admin path: ${routes.adminPath}`,
|
|
165
|
+
`sign-in path: ${routes.signInPath}`,
|
|
166
|
+
`${writes.length} planned route/config change(s)`,
|
|
167
|
+
...notes,
|
|
168
|
+
];
|
|
169
|
+
const preconditions = [
|
|
170
|
+
{ type: 'file', path: canonical.path, contents: canonical.source },
|
|
171
|
+
...(runtime.precondition ? [runtime.precondition] : []),
|
|
172
|
+
...(generatedMigration?.preconditions ?? []),
|
|
173
|
+
];
|
|
174
|
+
return { writes, commands: [], notes: planNotes, preconditions };
|
|
107
175
|
}
|
|
108
|
-
function
|
|
176
|
+
function expectedRouteWrites(ctx, routes) {
|
|
109
177
|
const templateRoot = join(ctx.templatesDir(), TEMPLATE_DIR);
|
|
110
178
|
if (!existsSync(templateRoot))
|
|
111
179
|
return [];
|
|
112
180
|
const targetRoot = ctx.resolve(TARGET_GROUP_DIR);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const renamed =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
rel
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
181
|
+
return walkFiles(templateRoot).map((abs) => {
|
|
182
|
+
const rel = toPosixTemplatePath(relative(templateRoot, abs));
|
|
183
|
+
const kind = rel === SIGN_IN_TEMPLATE ? 'signIn' : rel.startsWith('admin/') ? 'admin' : 'shared';
|
|
184
|
+
const renamed = rel === SIGN_IN_TEMPLATE
|
|
185
|
+
? `${routes.signInSegments.join('/')}.tsx`
|
|
186
|
+
: renameAdminSegment(rel, routes.adminSegments);
|
|
187
|
+
return {
|
|
188
|
+
rel,
|
|
189
|
+
kind,
|
|
190
|
+
path: join(targetRoot, renamed),
|
|
191
|
+
contents: rewriteRouteIds(readFileSync(abs, 'utf8'), routes),
|
|
192
|
+
mode: 'create',
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function planRouteFileWrites(ctx, routes) {
|
|
197
|
+
const writes = [];
|
|
198
|
+
const notes = [];
|
|
199
|
+
const signInPath = signInRouteFilePath(ctx, routes);
|
|
200
|
+
for (const expected of expectedRouteWrites(ctx, routes)) {
|
|
201
|
+
const invalidParent = firstInvalidParent(expected.path);
|
|
202
|
+
if (invalidParent) {
|
|
203
|
+
return {
|
|
204
|
+
writes: [],
|
|
205
|
+
notes: [
|
|
206
|
+
`${expected.path}: manual — expected route target has a non-directory parent at ${invalidParent}`,
|
|
207
|
+
],
|
|
208
|
+
signInPath,
|
|
209
|
+
blocked: true,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const target = lstatIfPresent(expected.path);
|
|
213
|
+
if (!target) {
|
|
214
|
+
writes.push(expected);
|
|
215
|
+
}
|
|
216
|
+
else if (!target.isFile()) {
|
|
217
|
+
return {
|
|
218
|
+
writes: [],
|
|
219
|
+
notes: [`${expected.path}: manual — expected route target is not a regular file`],
|
|
220
|
+
signInPath,
|
|
221
|
+
blocked: true,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
else if (normalizeTemplateSource(readFileSync(expected.path, 'utf8')) !==
|
|
225
|
+
normalizeTemplateSource(expected.contents)) {
|
|
226
|
+
notes.push(`${expected.path}: manual — existing route file differs from the template`);
|
|
227
|
+
}
|
|
124
228
|
}
|
|
125
|
-
return
|
|
229
|
+
return { writes, notes, signInPath, blocked: false };
|
|
230
|
+
}
|
|
231
|
+
function planRuntimeRoutesWrite(ctx, routes, canonical) {
|
|
232
|
+
const path = ctx.resolve(RUNTIME_ROUTES);
|
|
233
|
+
const desired = renderGeneratedRoutesSource(canonical, routes.adminPath, routes.apiPath, routes.signInPath);
|
|
234
|
+
const invalidParent = firstInvalidParent(path);
|
|
235
|
+
if (invalidParent) {
|
|
236
|
+
return {
|
|
237
|
+
note: `${path}: manual — routes.ts target has a non-directory parent at ${invalidParent}`,
|
|
238
|
+
migrationBlocked: true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const target = lstatIfPresent(path);
|
|
242
|
+
if (!target)
|
|
243
|
+
return { write: { path, contents: desired, mode: 'create' } };
|
|
244
|
+
if (!target.isFile()) {
|
|
245
|
+
return {
|
|
246
|
+
note: `${path}: manual — routes.ts target is not a regular file`,
|
|
247
|
+
migrationBlocked: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const before = readFileSync(path, 'utf8');
|
|
251
|
+
const staticRoutes = readStaticRoutesSource(before);
|
|
252
|
+
if (!staticRoutes.ok) {
|
|
253
|
+
return {
|
|
254
|
+
note: `${path}: manual — existing routes.ts cannot be resolved statically`,
|
|
255
|
+
migrationBlocked: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const previous = validateRoutePaths(ctx, staticRoutes.value.admin, staticRoutes.value.signIn, staticRoutes.value.api);
|
|
259
|
+
if (!previous.ok) {
|
|
260
|
+
return {
|
|
261
|
+
note: `${path}: manual — existing routes.ts has invalid route paths`,
|
|
262
|
+
migrationBlocked: true,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const generated = recognizeGeneratedRoutesSource(before, canonical);
|
|
266
|
+
if (generated) {
|
|
267
|
+
const generatedPrevious = validateRoutePaths(ctx, generated.adminPath, generated.signInPath, generated.apiPath);
|
|
268
|
+
if (!generatedPrevious.ok) {
|
|
269
|
+
return {
|
|
270
|
+
note: `${path}: manual — generated predecessor has invalid route paths`,
|
|
271
|
+
migrationBlocked: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (normalizeTemplateSource(before) === normalizeTemplateSource(desired)) {
|
|
275
|
+
return { precondition: { type: 'file', path, contents: before } };
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
write: { path, contents: desired, mode: 'patch', before },
|
|
279
|
+
previousRoutes: generatedPrevious.value,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (previous.value.adminPath === routes.adminPath &&
|
|
283
|
+
previous.value.apiPath === routes.apiPath &&
|
|
284
|
+
previous.value.signInPath === routes.signInPath) {
|
|
285
|
+
return { precondition: { type: 'file', path, contents: before } };
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
note: `${path}: manual — existing user-owned routes.ts does not match a generated predecessor`,
|
|
289
|
+
migrationBlocked: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function runtimeRoutesAligned(ctx, routes) {
|
|
293
|
+
const path = ctx.resolve(RUNTIME_ROUTES);
|
|
294
|
+
if (firstInvalidParent(path))
|
|
295
|
+
return false;
|
|
296
|
+
if (!lstatIfPresent(path)?.isFile())
|
|
297
|
+
return false;
|
|
298
|
+
return routesSourceAligned(readFileSync(path, 'utf8'), ctx, routes);
|
|
299
|
+
}
|
|
300
|
+
function canonicalRuntimeRoutesSource(ctx) {
|
|
301
|
+
const path = canonicalRoutesTemplatePath(ctx);
|
|
302
|
+
if (!existsSync(path)) {
|
|
303
|
+
return { ok: false, error: `${path}: manual — canonical routes.ts template is missing` };
|
|
304
|
+
}
|
|
305
|
+
const source = readFileSync(path, 'utf8');
|
|
306
|
+
const validation = validateCanonicalRoutesSource(source);
|
|
307
|
+
return validation.ok
|
|
308
|
+
? { ok: true, path, source }
|
|
309
|
+
: {
|
|
310
|
+
ok: false,
|
|
311
|
+
error: `${path}: manual — canonical routes.ts template is invalid (${validation.error})`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function inspectRoutes(ctx, routes) {
|
|
315
|
+
const expected = expectedRouteWrites(ctx, routes);
|
|
316
|
+
if (expected.length === 0) {
|
|
317
|
+
return { complete: false, manualNotes: ['no route templates found'] };
|
|
318
|
+
}
|
|
319
|
+
const manualNotes = [];
|
|
320
|
+
for (const route of expected) {
|
|
321
|
+
const invalidParent = firstInvalidParent(route.path);
|
|
322
|
+
if (invalidParent) {
|
|
323
|
+
manualNotes.push(`${route.path}: manual — expected route target has a non-directory parent at ${invalidParent}`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const target = lstatIfPresent(route.path);
|
|
327
|
+
if (!target) {
|
|
328
|
+
manualNotes.push(`${route.path}: missing generated route`);
|
|
329
|
+
}
|
|
330
|
+
else if (!target.isFile()) {
|
|
331
|
+
manualNotes.push(`${route.path}: manual — expected route target is not a regular file`);
|
|
332
|
+
}
|
|
333
|
+
else if (normalizeTemplateSource(readFileSync(route.path, 'utf8')) !==
|
|
334
|
+
normalizeTemplateSource(route.contents)) {
|
|
335
|
+
manualNotes.push(`${route.path}: manual — existing route file differs from the template`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const generated of findGeneratedSignInRoutes(ctx)) {
|
|
339
|
+
if (generated.routePath !== routes.signInPath) {
|
|
340
|
+
manualNotes.push(`${generated.filePath}: manual — old generated sign-in route still exists`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const generated of findGeneratedAdminRoutes(ctx, routes)) {
|
|
344
|
+
if (generated.adminPath !== routes.adminPath) {
|
|
345
|
+
manualNotes.push(`${generated.filePath}: manual — old generated admin tree still exists`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const defaultSignInPath = ctx.resolve(TARGET_GROUP_DIR, SIGN_IN_TEMPLATE);
|
|
349
|
+
if (routes.signInPath !== DEFAULT_SIGN_IN_PATH && existsSync(defaultSignInPath)) {
|
|
350
|
+
manualNotes.push(`${defaultSignInPath}: manual — existing sign-in route still exists`);
|
|
351
|
+
}
|
|
352
|
+
if (!runtimeRoutesAligned(ctx, routes)) {
|
|
353
|
+
manualNotes.push(`${ctx.resolve(RUNTIME_ROUTES)}: manual — routes.admin and routes.signIn must match ${routes.adminPath} and ${routes.signInPath}`);
|
|
354
|
+
}
|
|
355
|
+
return { complete: manualNotes.length === 0, manualNotes };
|
|
126
356
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
357
|
+
function planGeneratedRoutesMigration(ctx, previous, desired) {
|
|
358
|
+
const previousFiles = expectedRouteWrites(ctx, previous);
|
|
359
|
+
const desiredFiles = expectedRouteWrites(ctx, desired);
|
|
360
|
+
const previousByPath = new Map(previousFiles.map((file) => [file.path, file]));
|
|
361
|
+
const desiredByPath = new Map(desiredFiles.map((file) => [file.path, file]));
|
|
362
|
+
const writes = [];
|
|
363
|
+
const preconditions = [];
|
|
364
|
+
const oldAdminPaths = new Set(previousFiles.filter((file) => file.kind === 'admin').map((file) => file.path));
|
|
365
|
+
for (const old of previousFiles) {
|
|
366
|
+
const invalidParent = firstInvalidParent(old.path);
|
|
367
|
+
if (invalidParent) {
|
|
368
|
+
return migrationFailure(old.path, `old route has a non-directory parent at ${invalidParent}`);
|
|
369
|
+
}
|
|
370
|
+
const target = lstatIfPresent(old.path);
|
|
371
|
+
if (!target)
|
|
372
|
+
continue;
|
|
373
|
+
if (!target.isFile()) {
|
|
374
|
+
return migrationFailure(old.path, 'existing old route target is not a regular file');
|
|
375
|
+
}
|
|
376
|
+
const before = readFileSync(old.path, 'utf8');
|
|
377
|
+
const next = desiredByPath.get(old.path);
|
|
378
|
+
const matchesOld = sourcesEqual(before, old.contents);
|
|
379
|
+
const matchesDesired = next !== undefined && sourcesEqual(before, next.contents);
|
|
380
|
+
if (!matchesOld && !matchesDesired) {
|
|
381
|
+
const label = old.kind === 'signIn' ? 'sign-in route' : `${old.kind} route file`;
|
|
382
|
+
return migrationFailure(old.path, `existing old ${label} is user-owned`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const next of desiredFiles) {
|
|
386
|
+
const invalidParent = firstInvalidParent(next.path);
|
|
387
|
+
if (invalidParent) {
|
|
388
|
+
return migrationFailure(next.path, `migration destination has a non-directory parent at ${invalidParent}`);
|
|
389
|
+
}
|
|
390
|
+
const target = lstatIfPresent(next.path);
|
|
391
|
+
if (!target) {
|
|
392
|
+
writes.push(next);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (!target.isFile()) {
|
|
396
|
+
return migrationFailure(next.path, 'migration destination is not a regular file');
|
|
397
|
+
}
|
|
398
|
+
const before = readFileSync(next.path, 'utf8');
|
|
399
|
+
if (sourcesEqual(before, next.contents)) {
|
|
400
|
+
preconditions.push({ type: 'file', path: next.path, contents: before });
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const oldAtDestination = previousByPath.get(next.path);
|
|
404
|
+
if (oldAtDestination && sourcesEqual(before, oldAtDestination.contents)) {
|
|
405
|
+
writes.push({ path: next.path, contents: next.contents, mode: 'patch', before });
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
return migrationFailure(next.path, 'migration destination is user-owned');
|
|
409
|
+
}
|
|
410
|
+
for (const old of previousFiles) {
|
|
411
|
+
if (desiredByPath.has(old.path) || !existsSync(old.path))
|
|
412
|
+
continue;
|
|
413
|
+
const before = readFileSync(old.path, 'utf8');
|
|
414
|
+
writes.push({ path: old.path, contents: '', mode: 'delete', before });
|
|
415
|
+
}
|
|
416
|
+
if (previous.adminPath !== desired.adminPath) {
|
|
417
|
+
const oldRoot = ctx.resolve(TARGET_GROUP_DIR, ...previous.adminSegments);
|
|
418
|
+
const oldTree = snapshotTree(oldRoot);
|
|
419
|
+
if (oldTree.length > 0) {
|
|
420
|
+
if (oldTree[0]?.type !== 'directory') {
|
|
421
|
+
return migrationFailure(oldRoot, 'old admin tree root is user-owned');
|
|
422
|
+
}
|
|
423
|
+
const allowedPaths = new Set([
|
|
424
|
+
...oldAdminPaths,
|
|
425
|
+
...desiredFiles.filter((file) => file.kind === 'admin').map((file) => file.path),
|
|
426
|
+
]);
|
|
427
|
+
for (const entry of oldTree) {
|
|
428
|
+
if (entry.path === oldRoot)
|
|
429
|
+
continue;
|
|
430
|
+
if (entry.type === 'directory' && isManagedDirectory(entry.path, allowedPaths))
|
|
431
|
+
continue;
|
|
432
|
+
if (entry.type === 'file' && oldAdminPaths.has(entry.path))
|
|
433
|
+
continue;
|
|
434
|
+
const next = desiredByPath.get(entry.path);
|
|
435
|
+
if (entry.type === 'file' && next && sourcesEqual(entry.contents ?? '', next.contents)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
return migrationFailure(entry.path, 'old admin tree contains a user-owned file');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
preconditions.push({ type: 'tree', path: oldRoot, entries: oldTree });
|
|
442
|
+
}
|
|
443
|
+
return { safe: true, writes, preconditions };
|
|
444
|
+
}
|
|
445
|
+
function sourcesEqual(left, right) {
|
|
446
|
+
return normalizeTemplateSource(left) === normalizeTemplateSource(right);
|
|
447
|
+
}
|
|
448
|
+
function migrationFailure(path, reason) {
|
|
449
|
+
return {
|
|
450
|
+
safe: false,
|
|
451
|
+
writes: [],
|
|
452
|
+
preconditions: [],
|
|
453
|
+
note: `${path}: manual — ${reason}; no coupled route changes were planned`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function signInRouteFilePath(ctx, routes) {
|
|
457
|
+
return ctx.resolve(TARGET_GROUP_DIR, `${routes.signInSegments.join('/')}.tsx`);
|
|
458
|
+
}
|
|
459
|
+
function expectedSignInRouteSource(ctx, signInPath) {
|
|
460
|
+
const templatePath = join(ctx.templatesDir(), TEMPLATE_DIR, SIGN_IN_TEMPLATE);
|
|
461
|
+
if (!existsSync(templatePath))
|
|
462
|
+
return null;
|
|
463
|
+
return readFileSync(templatePath, 'utf8').replace(/(['"])\/_byline\/sign-in\b/g, `$1/_byline${signInPath}`);
|
|
464
|
+
}
|
|
465
|
+
function findGeneratedSignInRoutes(ctx) {
|
|
466
|
+
const root = ctx.resolve(TARGET_GROUP_DIR);
|
|
467
|
+
if (!existsSync(root))
|
|
468
|
+
return [];
|
|
469
|
+
const generated = [];
|
|
470
|
+
for (const filePath of walkFiles(root)) {
|
|
471
|
+
if (!filePath.endsWith('.tsx'))
|
|
472
|
+
continue;
|
|
473
|
+
const routePath = `/${toPosixTemplatePath(relative(root, filePath)).replace(/\.tsx$/, '')}`;
|
|
474
|
+
const expected = expectedSignInRouteSource(ctx, routePath);
|
|
475
|
+
if (expected !== null &&
|
|
476
|
+
normalizeTemplateSource(readFileSync(filePath, 'utf8')) === normalizeTemplateSource(expected)) {
|
|
477
|
+
generated.push({ filePath, routePath });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return generated;
|
|
481
|
+
}
|
|
482
|
+
function findGeneratedAdminRoutes(ctx, routes) {
|
|
483
|
+
const root = ctx.resolve(TARGET_GROUP_DIR);
|
|
484
|
+
const templateRoot = join(ctx.templatesDir(), TEMPLATE_DIR);
|
|
485
|
+
const adminTemplateRoot = join(templateRoot, 'admin');
|
|
486
|
+
if (!existsSync(root) || !existsSync(adminTemplateRoot))
|
|
487
|
+
return [];
|
|
488
|
+
const adminTemplates = walkFiles(adminTemplateRoot).map((templatePath) => ({
|
|
489
|
+
templatePath,
|
|
490
|
+
suffix: toPosixTemplatePath(relative(adminTemplateRoot, templatePath)),
|
|
491
|
+
}));
|
|
492
|
+
const generated = [];
|
|
493
|
+
for (const filePath of walkFiles(root)) {
|
|
494
|
+
const rel = toPosixTemplatePath(relative(root, filePath));
|
|
495
|
+
for (const template of adminTemplates) {
|
|
496
|
+
if (rel !== template.suffix && !rel.endsWith(`/${template.suffix}`))
|
|
497
|
+
continue;
|
|
498
|
+
const prefixLength = rel.length - template.suffix.length;
|
|
499
|
+
const adminRel = rel.slice(0, prefixLength).replace(/\/$/, '');
|
|
500
|
+
if (!adminRel)
|
|
501
|
+
continue;
|
|
502
|
+
const adminSegments = adminRel.split('/');
|
|
503
|
+
const candidate = {
|
|
504
|
+
...routes,
|
|
505
|
+
adminPath: `/${adminRel}`,
|
|
506
|
+
adminSegments,
|
|
507
|
+
};
|
|
508
|
+
const expected = rewriteRouteIds(readFileSync(template.templatePath, 'utf8'), candidate);
|
|
509
|
+
if (normalizeTemplateSource(readFileSync(filePath, 'utf8')) ===
|
|
510
|
+
normalizeTemplateSource(expected)) {
|
|
511
|
+
generated.push({ filePath, adminPath: candidate.adminPath });
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return generated;
|
|
517
|
+
}
|
|
518
|
+
export function renameAdminSegment(rel, segments) {
|
|
519
|
+
rel = toPosixTemplatePath(rel);
|
|
132
520
|
if (rel === 'admin' || rel.startsWith('admin/')) {
|
|
133
|
-
return `${
|
|
521
|
+
return `${segments.join('/')}${rel.slice('admin'.length)}`;
|
|
134
522
|
}
|
|
135
523
|
return rel;
|
|
136
524
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
*
|
|
142
|
-
* We match the literal `/_byline/admin` prefix only — we do not touch
|
|
143
|
-
* other occurrences of "admin" elsewhere in the file (comments, prop
|
|
144
|
-
* names, etc.).
|
|
145
|
-
*/
|
|
146
|
-
function rewriteRouteIds(source, slug) {
|
|
147
|
-
if (slug === 'admin')
|
|
148
|
-
return source;
|
|
149
|
-
return source.replace(/(['"])\/_byline\/admin\b/g, `$1/_byline/${slug}`);
|
|
525
|
+
function rewriteRouteIds(source, routes) {
|
|
526
|
+
return source
|
|
527
|
+
.replace(/(['"])\/_byline\/admin\b/g, `$1/_byline${routes.adminPath}`)
|
|
528
|
+
.replace(/(['"])\/_byline\/sign-in\b/g, `$1/_byline${routes.signInPath}`);
|
|
150
529
|
}
|
|
151
530
|
function walkFiles(root) {
|
|
152
531
|
const out = [];
|
|
532
|
+
if (!lstatIfPresent(root)?.isDirectory())
|
|
533
|
+
return out;
|
|
153
534
|
function walk(dir) {
|
|
154
|
-
for (const name of readdirSync(dir)) {
|
|
535
|
+
for (const name of readdirSync(dir).sort()) {
|
|
155
536
|
const abs = join(dir, name);
|
|
156
|
-
const st =
|
|
537
|
+
const st = lstatSync(abs);
|
|
157
538
|
if (st.isDirectory())
|
|
158
539
|
walk(abs);
|
|
159
540
|
else if (st.isFile())
|
|
@@ -163,4 +544,111 @@ function walkFiles(root) {
|
|
|
163
544
|
walk(root);
|
|
164
545
|
return out;
|
|
165
546
|
}
|
|
547
|
+
function snapshotTree(root) {
|
|
548
|
+
if (!lstatIfPresent(root))
|
|
549
|
+
return [];
|
|
550
|
+
const entries = [];
|
|
551
|
+
function walk(path) {
|
|
552
|
+
const stat = lstatSync(path);
|
|
553
|
+
if (stat.isDirectory()) {
|
|
554
|
+
entries.push({ path, type: 'directory' });
|
|
555
|
+
for (const name of readdirSync(path).sort())
|
|
556
|
+
walk(join(path, name));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (stat.isFile()) {
|
|
560
|
+
entries.push({ path, type: 'file', contents: readFileSync(path, 'utf8') });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
entries.push({ path, type: 'other' });
|
|
564
|
+
}
|
|
565
|
+
walk(root);
|
|
566
|
+
return entries;
|
|
567
|
+
}
|
|
568
|
+
function treeSnapshotsEqual(left, right) {
|
|
569
|
+
if (left.length !== right.length)
|
|
570
|
+
return false;
|
|
571
|
+
return left.every((entry, index) => {
|
|
572
|
+
const expected = right[index];
|
|
573
|
+
return (expected !== undefined &&
|
|
574
|
+
entry.path === expected.path &&
|
|
575
|
+
entry.type === expected.type &&
|
|
576
|
+
entry.contents === expected.contents);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
function currentFileContents(path) {
|
|
580
|
+
const target = lstatIfPresent(path);
|
|
581
|
+
if (!target)
|
|
582
|
+
return null;
|
|
583
|
+
return target.isFile() ? readFileSync(path, 'utf8') : undefined;
|
|
584
|
+
}
|
|
585
|
+
function lstatIfPresent(path) {
|
|
586
|
+
try {
|
|
587
|
+
return lstatSync(path);
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
const code = error.code;
|
|
591
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
592
|
+
return undefined;
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function firstInvalidParent(path) {
|
|
597
|
+
let parent = dirname(path);
|
|
598
|
+
while (true) {
|
|
599
|
+
const target = lstatIfPresent(parent);
|
|
600
|
+
if (target)
|
|
601
|
+
return target.isDirectory() ? undefined : parent;
|
|
602
|
+
const next = dirname(parent);
|
|
603
|
+
if (next === parent)
|
|
604
|
+
return parent;
|
|
605
|
+
parent = next;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function isManagedDirectory(path, managedFiles) {
|
|
609
|
+
return [...managedFiles].some((file) => isPathWithin(path, file));
|
|
610
|
+
}
|
|
611
|
+
function isPathWithin(parent, candidate) {
|
|
612
|
+
const rel = relative(parent, candidate);
|
|
613
|
+
return rel !== '' && rel !== '..' && !rel.startsWith('../') && !rel.startsWith('..\\');
|
|
614
|
+
}
|
|
615
|
+
function findMigrationResiduals(plan, ctx, routes) {
|
|
616
|
+
const oldTrees = (plan.preconditions ?? []).filter((precondition) => precondition.type === 'tree');
|
|
617
|
+
if (oldTrees.length === 0)
|
|
618
|
+
return [];
|
|
619
|
+
const desiredAdminPaths = new Set(expectedRouteWrites(ctx, routes)
|
|
620
|
+
.filter((file) => file.kind === 'admin')
|
|
621
|
+
.map((file) => file.path));
|
|
622
|
+
const residuals = [];
|
|
623
|
+
for (const tree of oldTrees) {
|
|
624
|
+
for (const entry of snapshotTree(tree.path)) {
|
|
625
|
+
if (entry.path === tree.path)
|
|
626
|
+
continue;
|
|
627
|
+
if (entry.type === 'directory' && isManagedDirectory(entry.path, desiredAdminPaths))
|
|
628
|
+
continue;
|
|
629
|
+
if (entry.type === 'file' && desiredAdminPaths.has(entry.path))
|
|
630
|
+
continue;
|
|
631
|
+
residuals.push(entry.path);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return residuals;
|
|
635
|
+
}
|
|
636
|
+
function removeEmptyDeletedRouteDirectories(writes, root) {
|
|
637
|
+
for (const write of writes) {
|
|
638
|
+
if (write.mode !== 'delete')
|
|
639
|
+
continue;
|
|
640
|
+
let directory = dirname(write.path);
|
|
641
|
+
while (directory !== root && directory.startsWith(`${root}/`) && existsSync(directory)) {
|
|
642
|
+
if (readdirSync(directory).length > 0)
|
|
643
|
+
break;
|
|
644
|
+
try {
|
|
645
|
+
rmdirSync(directory);
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
directory = dirname(directory);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
166
654
|
//# sourceMappingURL=routes.js.map
|