@agjs/tsforge 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.
- package/bin/tsforge.js +2 -0
- package/package.json +35 -0
- package/src/agent/agent.constants.ts +382 -0
- package/src/agent/agent.types.ts +34 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/model-agent.ts +297 -0
- package/src/agent/tool-repair.ts +194 -0
- package/src/agent/tools.ts +190 -0
- package/src/browser/checks.ts +96 -0
- package/src/browser/index.ts +8 -0
- package/src/browser/oracle.ts +303 -0
- package/src/classify.ts +48 -0
- package/src/cli.ts +1333 -0
- package/src/config/config.constants.ts +9 -0
- package/src/config/flags.ts +32 -0
- package/src/config/index.ts +8 -0
- package/src/config/tsforge-config.ts +301 -0
- package/src/constitution/baseline.ts +257 -0
- package/src/detect-gate.ts +498 -0
- package/src/eval/eval.types.ts +36 -0
- package/src/eval/index.ts +3 -0
- package/src/eval/judge.ts +62 -0
- package/src/eval/score.ts +39 -0
- package/src/files/create.ts +22 -0
- package/src/files/edit.ts +193 -0
- package/src/files/files.constants.ts +11 -0
- package/src/files/files.types.ts +81 -0
- package/src/files/hashline-format.ts +110 -0
- package/src/files/hashline.ts +689 -0
- package/src/files/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/inference/index.ts +6 -0
- package/src/inference/inference.constants.ts +34 -0
- package/src/inference/inference.types.ts +123 -0
- package/src/inference/openai-compatible.ts +113 -0
- package/src/inference/stream-guard.ts +161 -0
- package/src/inference/stream.ts +370 -0
- package/src/inference/transport.ts +78 -0
- package/src/inference/wire.ts +0 -0
- package/src/lib/fs/fs.ts +126 -0
- package/src/lib/fs/fs.types.ts +5 -0
- package/src/lib/fs/index.ts +3 -0
- package/src/lib/fs/process.ts +146 -0
- package/src/lib/guards/guards.ts +9 -0
- package/src/lib/guards/index.ts +1 -0
- package/src/lib/json/index.ts +1 -0
- package/src/lib/json/json.ts +12 -0
- package/src/lib/scope/index.ts +2 -0
- package/src/lib/scope/scope.constants.ts +3 -0
- package/src/lib/scope/scope.ts +40 -0
- package/src/loop/astgrep-fix.ts +228 -0
- package/src/loop/feedback/feedback.ts +138 -0
- package/src/loop/feedback/index.ts +8 -0
- package/src/loop/feedback/meta-rule-docs.ts +41 -0
- package/src/loop/feedback/meta-rule-feedback.ts +61 -0
- package/src/loop/feedback/rule-docs.generated.json +112 -0
- package/src/loop/feedback/rule-docs.ts +342 -0
- package/src/loop/index.ts +19 -0
- package/src/loop/loop.constants.ts +68 -0
- package/src/loop/loop.types.ts +99 -0
- package/src/loop/prompt/index.ts +2 -0
- package/src/loop/prompt/project-map.ts +69 -0
- package/src/loop/prompt/prompt.ts +107 -0
- package/src/loop/quality.ts +174 -0
- package/src/loop/rule-docs.generated.json +367 -0
- package/src/loop/run-spec.ts +88 -0
- package/src/loop/run.ts +400 -0
- package/src/loop/session.ts +1410 -0
- package/src/loop/tools/add-dependency.ts +71 -0
- package/src/loop/tools/condense.ts +498 -0
- package/src/loop/tools/edit-hashline.ts +80 -0
- package/src/loop/tools/execute-tool.ts +80 -0
- package/src/loop/tools/file-ops.ts +323 -0
- package/src/loop/tools/index.ts +2 -0
- package/src/loop/tools/lsp-ops.ts +222 -0
- package/src/loop/tools/scaffold-routes.ts +68 -0
- package/src/loop/tools/scaffold-ui.ts +62 -0
- package/src/loop/tools/scaffold-web.ts +35 -0
- package/src/loop/tools/tool-context.ts +126 -0
- package/src/loop/ttsr-defaults.ts +53 -0
- package/src/loop/ttsr.ts +322 -0
- package/src/loop/turn.ts +856 -0
- package/src/lsp/index.ts +2 -0
- package/src/lsp/lsp.types.ts +56 -0
- package/src/lsp/service.ts +500 -0
- package/src/meta-rules/context.ts +195 -0
- package/src/meta-rules/index.ts +9 -0
- package/src/meta-rules/meta-rules.types.ts +47 -0
- package/src/meta-rules/parsers/package-json-parser.ts +51 -0
- package/src/meta-rules/registry.ts +37 -0
- package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
- package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
- package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
- package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
- package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
- package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
- package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
- package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
- package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
- package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
- package/src/meta-rules/runner.ts +64 -0
- package/src/models-config.ts +196 -0
- package/src/render/ansi.ts +289 -0
- package/src/render/banner.ts +113 -0
- package/src/render/box.ts +134 -0
- package/src/render/index.ts +7 -0
- package/src/render/markdown.ts +123 -0
- package/src/render/render.types.ts +21 -0
- package/src/render/stream-markdown.ts +128 -0
- package/src/render/style.ts +26 -0
- package/src/rule-packs/bullmq/index.ts +39 -0
- package/src/rule-packs/bullmq/rules/index.ts +7 -0
- package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
- package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
- package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
- package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
- package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
- package/src/rule-packs/bullmq/utils.ts +334 -0
- package/src/rule-packs/code-flow/index.ts +25 -0
- package/src/rule-packs/code-flow/rules/index.ts +3 -0
- package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
- package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
- package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
- package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
- package/src/rule-packs/comment-hygiene/index.ts +25 -0
- package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
- package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
- package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
- package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
- package/src/rule-packs/create-rule.ts +9 -0
- package/src/rule-packs/drizzle/index.ts +41 -0
- package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
- package/src/rule-packs/drizzle/rules/index.ts +8 -0
- package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
- package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
- package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
- package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
- package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
- package/src/rule-packs/drizzle/utils.ts +115 -0
- package/src/rule-packs/elysia/index.ts +43 -0
- package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
- package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
- package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
- package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
- package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
- package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
- package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
- package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
- package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
- package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
- package/src/rule-packs/env-access/index.ts +23 -0
- package/src/rule-packs/env-access/rules/index.ts +2 -0
- package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
- package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
- package/src/rule-packs/i18n-keys/index.ts +19 -0
- package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
- package/src/rule-packs/index.ts +139 -0
- package/src/rule-packs/jwt-cookies/index.ts +25 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
- package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
- package/src/rule-packs/jwt-cookies/utils.ts +188 -0
- package/src/rule-packs/oauth-security/index.ts +25 -0
- package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
- package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
- package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
- package/src/rule-packs/oauth-security/utils.ts +127 -0
- package/src/rule-packs/react-component-architecture/index.ts +35 -0
- package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
- package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
- package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
- package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
- package/src/rule-packs/react-component-architecture/utils.ts +47 -0
- package/src/rule-packs/rule-packs.types.ts +18 -0
- package/src/rule-packs/structured-logging/index.ts +26 -0
- package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
- package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
- package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
- package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
- package/src/rule-packs/tanstack-query/index.ts +20 -0
- package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
- package/src/rule-packs/test-conventions/index.ts +23 -0
- package/src/rule-packs/test-conventions/rules/index.ts +2 -0
- package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
- package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
- package/src/rule-packs/utils.ts +142 -0
- package/src/session-store.ts +359 -0
- package/src/spec/generate-tests.ts +213 -0
- package/src/spec/index.ts +5 -0
- package/src/spec/parse.ts +152 -0
- package/src/spec/review-tests.ts +162 -0
- package/src/spec/spec.constants.ts +13 -0
- package/src/spec/spec.types.ts +79 -0
- package/src/stack-detection/detect.ts +246 -0
- package/src/stack-detection/index.ts +3 -0
- package/src/stack-detection/packs.ts +174 -0
- package/src/stack-detection/stack-detection.types.ts +47 -0
- package/src/validate/accept.ts +49 -0
- package/src/validate/errors.ts +35 -0
- package/src/validate/index.ts +12 -0
- package/src/validate/parse.ts +148 -0
- package/src/validate/run-tests.ts +59 -0
- package/src/validate/validate.ts +40 -0
- package/src/validate/validate.types.ts +52 -0
- package/src/web-components.ts +638 -0
- package/src/web-coverage.ts +89 -0
- package/src/web-routes.ts +151 -0
- package/src/web-templates.ts +1011 -0
- package/strict.eslint.config.mjs +84 -0
- package/strict.web.eslint.config.mjs +185 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Entity-coverage check — the completeness oracle for catalog builds. The gate
|
|
6
|
+
* proves the app COMPILES and RENDERS, but not that the spec's entities actually
|
|
7
|
+
* got built: a run greened with 4 of 8 entities (organization/user/note/tag) as
|
|
8
|
+
* types-only — no components, no routes, no create button anywhere — because
|
|
9
|
+
* nothing held the model to the declared entity list. This turns that list into
|
|
10
|
+
* an enforced contract: every declared entity must have real UI (a feature folder
|
|
11
|
+
* with ≥1 .tsx component), else the gate fails listing what's missing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The normalized core noun of a declared entity: "Organization (tenant)" →
|
|
15
|
+
* "organization", "StockMovement (receipt | …)" → "stockmovement". Strips the
|
|
16
|
+
* parenthetical, lowercases, drops non-alphanumerics (so casing/separators in a
|
|
17
|
+
* feature-folder name don't matter). */
|
|
18
|
+
export function entityNoun(raw: string): string {
|
|
19
|
+
const before = raw.split("(")[0] ?? raw;
|
|
20
|
+
|
|
21
|
+
return before.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Lowercased basenames of every `.tsx` under `src/routes` + `src/features` — the
|
|
25
|
+
* two places an entity's UI lives (a route PAGE like `routes/accounts.tsx`, or a
|
|
26
|
+
* feature component like `features/account/AccountCard.tsx`). We scan BOTH because
|
|
27
|
+
* the layout varies: some builds put pages in routes/, others in features/. We do
|
|
28
|
+
* NOT scan `src/components` (shared chrome) so a `UserMenu` avatar doesn't "cover"
|
|
29
|
+
* the User entity. */
|
|
30
|
+
async function uiBasenames(dir: string): Promise<string[]> {
|
|
31
|
+
const out: string[] = [];
|
|
32
|
+
|
|
33
|
+
const readEntries = async (d: string) => {
|
|
34
|
+
try {
|
|
35
|
+
return await readdir(d, { withFileTypes: true });
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const walk = async (d: string): Promise<void> => {
|
|
42
|
+
const entries = await readEntries(d);
|
|
43
|
+
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const p = join(d, entry.name);
|
|
46
|
+
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
await walk(p);
|
|
49
|
+
} else if (entry.name.endsWith(".tsx")) {
|
|
50
|
+
out.push(entry.name.toLowerCase());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
await walk(join(dir, "src", "routes"));
|
|
56
|
+
await walk(join(dir, "src", "features"));
|
|
57
|
+
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Does any UI basename belong to this entity? Matches the noun as a PREFIX
|
|
62
|
+
* (so "accounts.tsx"/"AccountsListPage.tsx" cover "account") and handles the
|
|
63
|
+
* common plurals (s, y→ies) — but requires the FULL noun, so "stage"/"table"
|
|
64
|
+
* never falsely cover "tag". */
|
|
65
|
+
function isCovered(basenames: readonly string[], noun: string): boolean {
|
|
66
|
+
const stems = [noun, `${noun}s`];
|
|
67
|
+
|
|
68
|
+
if (noun.endsWith("y")) {
|
|
69
|
+
stems.push(`${noun.slice(0, -1)}ies`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return basenames.some((name) => {
|
|
73
|
+
const letters = name.replace(/[^a-z]/g, "");
|
|
74
|
+
|
|
75
|
+
return stems.some((stem) => letters.startsWith(stem));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Declared entities with NO built UI anywhere (no route page and no feature
|
|
80
|
+
* component) — the "types only, never built" gap. Returns the raw entity
|
|
81
|
+
* strings, in order. */
|
|
82
|
+
export async function uncoveredEntities(
|
|
83
|
+
dir: string,
|
|
84
|
+
entities: readonly string[]
|
|
85
|
+
): Promise<string[]> {
|
|
86
|
+
const basenames = await uiBasenames(dir);
|
|
87
|
+
|
|
88
|
+
return entities.filter((raw) => !isCovered(basenames, entityNoun(raw)));
|
|
89
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route scaffolding — the `scaffold_routes` tool's materializer. The MODEL
|
|
3
|
+
* declares which pages the app needs (entirely app-specific, from the user's
|
|
4
|
+
* prompt); this turns each path into a mechanically-correct TanStack file-based
|
|
5
|
+
* route STUB so the model never hand-writes `createFileRoute` boilerplate, never
|
|
6
|
+
* mis-maps a `$param` filename, and — crucially — every route exists at once, so
|
|
7
|
+
* the typed route union is complete and no `<Link to>` can forward-reference a
|
|
8
|
+
* not-yet-created route. Same division as scaffold_ui: model picks WHAT, the
|
|
9
|
+
* harness does HOW. Stubs render a placeholder; the model fills each component in.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Normalize a declared path to a leading-slash, no-trailing-slash form. */
|
|
13
|
+
export function normalizeRoutePath(path: string): string {
|
|
14
|
+
const trimmed = path.trim().replace(/\/+$/, "");
|
|
15
|
+
|
|
16
|
+
if (trimmed.length === 0 || trimmed === "/") {
|
|
17
|
+
return "/";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Map a route path to its TanStack file-based filename:
|
|
24
|
+
* "/" → src/routes/index.tsx, "/accounts/$accountId" →
|
|
25
|
+
* src/routes/accounts.$accountId.tsx, "/settings/profile" →
|
|
26
|
+
* src/routes/settings.profile.tsx (segments joined with "."). */
|
|
27
|
+
export function routeFileName(path: string): string {
|
|
28
|
+
const body = normalizeRoutePath(path).replace(/^\//, "");
|
|
29
|
+
const stem = body.length === 0 ? "index" : body.split("/").join(".");
|
|
30
|
+
|
|
31
|
+
return `src/routes/${stem}.tsx`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A valid, unique-ish PascalCase component name for a path's stub. "/" →
|
|
35
|
+
* IndexPage, "/accounts/$accountId" → AccountsAccountIdPage. */
|
|
36
|
+
export function routeComponentName(path: string): string {
|
|
37
|
+
const body = normalizeRoutePath(path).replace(/^\//, "");
|
|
38
|
+
|
|
39
|
+
if (body.length === 0) {
|
|
40
|
+
return "IndexPage";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pascal = body
|
|
44
|
+
.split("/")
|
|
45
|
+
.map((seg) =>
|
|
46
|
+
seg
|
|
47
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
48
|
+
.trim()
|
|
49
|
+
.split(" ")
|
|
50
|
+
.filter((w) => w.length > 0)
|
|
51
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
52
|
+
.join("")
|
|
53
|
+
)
|
|
54
|
+
.join("");
|
|
55
|
+
|
|
56
|
+
return `${pascal}Page`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A minimal, gate-passing route stub — same idiom as the scaffolded index route
|
|
60
|
+
* (no comments, prettier-clean), rendering the path as a placeholder. */
|
|
61
|
+
export function routeStub(path: string): string {
|
|
62
|
+
const route = normalizeRoutePath(path);
|
|
63
|
+
const name = routeComponentName(route);
|
|
64
|
+
|
|
65
|
+
// The placeholder carries `data-tsforge-stub` — a sentinel the gate greps for so
|
|
66
|
+
// an UNFILLED stub fails the build (coverage "file exists" + the render smoke
|
|
67
|
+
// "not blank" both miss an empty route). The model must REPLACE this component
|
|
68
|
+
// with the real page; doing so removes the marker.
|
|
69
|
+
return (
|
|
70
|
+
`import { createFileRoute } from "@tanstack/react-router";\n\n` +
|
|
71
|
+
`export const Route = createFileRoute("${route}")({\n` +
|
|
72
|
+
` component: ${name},\n` +
|
|
73
|
+
`});\n\n` +
|
|
74
|
+
`function ${name}() {\n` +
|
|
75
|
+
` return (\n` +
|
|
76
|
+
` <div data-tsforge-stub className="p-8">\n` +
|
|
77
|
+
` TODO: build the ${route} page\n` +
|
|
78
|
+
` </div>\n` +
|
|
79
|
+
` );\n` +
|
|
80
|
+
`}\n`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Materialize stub files for every declared path → { relPath: content }.
|
|
85
|
+
* Deduped by filename so two spellings of one path don't clobber. */
|
|
86
|
+
export function materializeRoutes(
|
|
87
|
+
paths: readonly string[]
|
|
88
|
+
): Record<string, string> {
|
|
89
|
+
const out: Record<string, string> = {};
|
|
90
|
+
|
|
91
|
+
for (const path of paths) {
|
|
92
|
+
out[routeFileName(path)] = routeStub(path);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The URL path a route FILE serves, for crawling: "index.tsx" → "/",
|
|
99
|
+
* "accounts.create.tsx" → "/accounts/create", "settings.profile.tsx" →
|
|
100
|
+
* "/settings/profile". Returns null for things we can't visit statically:
|
|
101
|
+
* the root layout (__root) and any DYNAMIC route (a `$param` segment — we have
|
|
102
|
+
* no real id to fill). */
|
|
103
|
+
export function routePathFromFile(filename: string): string | null {
|
|
104
|
+
const stem = filename.replace(/\.tsx$/, "");
|
|
105
|
+
|
|
106
|
+
if (stem === "__root") {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const segments = stem.split(".");
|
|
111
|
+
|
|
112
|
+
if (segments.some((s) => s.startsWith("$"))) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (stem === "index") {
|
|
117
|
+
return "/";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `/${segments.join("/")}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The set of statically-crawlable URL paths from a list of route filenames
|
|
124
|
+
* (skips the root layout + dynamic `$param` routes), deduped. */
|
|
125
|
+
export function crawlableRoutePaths(routeFiles: readonly string[]): string[] {
|
|
126
|
+
const paths = new Set<string>();
|
|
127
|
+
|
|
128
|
+
for (const file of routeFiles) {
|
|
129
|
+
const path = routePathFromFile(file);
|
|
130
|
+
|
|
131
|
+
if (path !== null) {
|
|
132
|
+
paths.add(path);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return [...paths];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Validate + normalize the tool's `routes` arg: a non-empty array of path
|
|
140
|
+
* strings. Returns [] (caller rejects) when the shape is wrong. */
|
|
141
|
+
export function asRoutePaths(value: unknown): string[] {
|
|
142
|
+
if (!Array.isArray(value)) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const paths = value.filter(
|
|
147
|
+
(v): v is string => typeof v === "string" && v.trim().length > 0
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return paths.map(normalizeRoutePath);
|
|
151
|
+
}
|