toiljs 0.0.15 → 0.0.16
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/.babelrc +13 -13
- package/.gitattributes +2 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -90
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +43 -43
- package/.github/changelog-config.json +45 -45
- package/.github/dependabot.yml +27 -27
- package/.github/workflows/ci.yml +191 -191
- package/.prettierrc.json +11 -11
- package/.vscode/settings.json +9 -9
- package/CHANGELOG.md +5 -5
- package/LICENSE +187 -187
- package/README.md +339 -315
- package/as-pect.asconfig.json +34 -34
- package/as-pect.config.js +65 -65
- package/assets/logo.svg +36 -36
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +0 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.d.ts +6 -0
- package/build/client/dev/devtools.js +442 -0
- package/build/client/dev/error-overlay.d.ts +9 -0
- package/build/client/dev/error-overlay.js +19 -4
- package/build/client/navigation/prefetch.d.ts +1 -0
- package/build/client/navigation/prefetch.js +35 -0
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/hooks.js +6 -2
- package/build/client/routing/loader.d.ts +23 -0
- package/build/client/routing/loader.js +53 -7
- package/build/client/routing/mount.js +4 -3
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +16 -0
- package/build/compiler/config.js +7 -0
- package/build/compiler/docs.js +16 -16
- package/build/compiler/index.d.ts +2 -2
- package/build/compiler/index.js +1 -1
- package/build/compiler/plugin.js +156 -0
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +1 -1
- package/build/compiler/seo.d.ts +1 -1
- package/build/compiler/seo.js +5 -4
- package/build/compiler/ssg.js +32 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/eslint.config.js +48 -48
- package/examples/basic/client/404.tsx +11 -11
- package/examples/basic/client/components/.gitkeep +1 -1
- package/examples/basic/client/global-error.tsx +13 -13
- package/examples/basic/client/layout.tsx +25 -25
- package/examples/basic/client/public/images/.gitkeep +1 -1
- package/examples/basic/client/public/images/logo.svg +36 -36
- package/examples/basic/client/public/robots.txt +2 -2
- package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
- package/examples/basic/client/routes/features/error/error.tsx +16 -16
- package/examples/basic/client/routes/features/template/b.tsx +14 -14
- package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
- package/examples/basic/client/routes/gallery/layout.tsx +13 -13
- package/examples/basic/client/routes/io.tsx +24 -24
- package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
- package/examples/basic/client/routes/search.tsx +61 -61
- package/examples/basic/client/toil.tsx +5 -5
- package/package.json +155 -148
- package/presets/eslint.js +88 -88
- package/presets/no-uint8array-tostring.js +200 -200
- package/presets/prettier.json +18 -18
- package/presets/tsconfig.json +37 -37
- package/src/backend/index.ts +160 -160
- package/src/cli/proc.ts +50 -50
- package/src/cli/updates.ts +69 -69
- package/src/cli/validate.ts +31 -31
- package/src/client/channel/channel.ts +146 -146
- package/src/client/components/Form.tsx +65 -65
- package/src/client/components/Script.tsx +113 -113
- package/src/client/components/Slot.tsx +21 -21
- package/src/client/dev/devtools.tsx +973 -0
- package/src/client/dev/error-overlay.tsx +30 -4
- package/src/client/head/head.ts +167 -167
- package/src/client/head/metadata.ts +112 -112
- package/src/client/index.ts +89 -89
- package/src/client/navigation/NavLink.tsx +86 -86
- package/src/client/navigation/navigation.ts +235 -235
- package/src/client/navigation/prefetch.ts +169 -130
- package/src/client/navigation/scroll.ts +53 -53
- package/src/client/routing/Router.tsx +8 -2
- package/src/client/routing/action.ts +122 -122
- package/src/client/routing/error-boundary.tsx +43 -43
- package/src/client/routing/hooks.ts +21 -6
- package/src/client/routing/loader.ts +325 -235
- package/src/client/routing/match.ts +47 -47
- package/src/client/routing/mount.tsx +54 -52
- package/src/client/routing/params-context.ts +10 -10
- package/src/client/routing/slot-context.ts +7 -7
- package/src/client/search/search.ts +189 -189
- package/src/client/search/use-page-search.ts +73 -73
- package/src/client/types.ts +73 -73
- package/src/compiler/config.ts +219 -182
- package/src/compiler/docs.ts +228 -228
- package/src/compiler/generate.ts +394 -394
- package/src/compiler/index.ts +64 -57
- package/src/compiler/pages.ts +70 -70
- package/src/compiler/plugin.ts +170 -2
- package/src/compiler/prerender.ts +156 -156
- package/src/compiler/seo.ts +397 -390
- package/src/compiler/ssg.ts +162 -126
- package/src/io/BinaryReader.ts +340 -340
- package/src/io/BinaryWriter.ts +385 -385
- package/src/io/FastMap.ts +127 -127
- package/src/io/index.ts +11 -11
- package/src/io/lengths.ts +14 -14
- package/src/io/types.ts +18 -18
- package/src/logger/index.ts +22 -22
- package/src/server/index.ts +10 -10
- package/src/server/main.ts +13 -13
- package/src/server/tsconfig.json +4 -4
- package/src/shared/index.ts +10 -10
- package/std/client/index.d.ts +15 -15
- package/std/client/package.json +3 -3
- package/test/assembly/example.spec.ts +7 -7
- package/test/channel.test.ts +21 -21
- package/test/dom/Link.test.tsx +47 -47
- package/test/dom/NavLink.test.tsx +37 -37
- package/test/dom/error-overlay.test.tsx +44 -44
- package/test/dom/loader.test.tsx +121 -121
- package/test/dom/navigation.test.ts +59 -59
- package/test/dom/revalidate.test.tsx +38 -38
- package/test/dom/route-head.test.tsx +78 -78
- package/test/dom/router-loading.test.tsx +44 -44
- package/test/dom/scroll.test.ts +56 -56
- package/test/dom/use-metadata.test.tsx +58 -58
- package/test/io.test.ts +93 -93
- package/test/navlink.test.ts +28 -28
- package/test/placeholder.test.ts +9 -9
- package/test/routes.test.ts +76 -76
- package/test/seo.test.ts +175 -164
- package/test/slot-layouts.test.ts +69 -69
- package/test/ssg.test.ts +36 -36
- package/test/update.test.ts +44 -44
- package/test/validate.test.ts +42 -42
- package/toil-routes.d.ts +7 -0
- package/toilconfig.json +30 -30
- package/tsconfig.backend.json +13 -13
- package/tsconfig.base.json +35 -35
- package/tsconfig.cli.json +13 -13
- package/tsconfig.client.json +14 -14
- package/tsconfig.compiler.json +13 -13
- package/tsconfig.io.json +12 -12
- package/tsconfig.json +22 -22
- package/tsconfig.logger.json +12 -12
- package/tsconfig.server.json +10 -10
- package/tsconfig.shared.json +12 -12
- package/vitest.config.ts +26 -26
- package/.idea/codeStyles/Project.xml +0 -54
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -7
- package/.idea/toiljs.iml +0 -8
- package/.idea/vcs.xml +0 -6
- package/.toil/entry.tsx +0 -9
- package/.toil/index.html +0 -12
- package/.toil/routes.ts +0 -9
- package/build/cli/configure.d.ts +0 -16
- package/build/cli/configure.js +0 -272
- package/build/cli/create.d.ts +0 -16
- package/build/cli/create.js +0 -420
- package/build/cli/diagnostics.d.ts +0 -55
- package/build/cli/diagnostics.js +0 -333
- package/build/cli/doctor.d.ts +0 -6
- package/build/cli/doctor.js +0 -249
- package/build/cli/features.d.ts +0 -25
- package/build/cli/features.js +0 -107
- package/build/cli/index.d.ts +0 -2
- package/build/cli/proc.d.ts +0 -6
- package/build/cli/proc.js +0 -31
- package/build/cli/ui.d.ts +0 -9
- package/build/cli/ui.js +0 -75
- package/build/cli/update.d.ts +0 -7
- package/build/cli/update.js +0 -117
- package/build/cli/updates.d.ts +0 -10
- package/build/cli/updates.js +0 -45
- package/build/cli/validate.d.ts +0 -4
- package/build/cli/validate.js +0 -19
- package/build/client/Link.d.ts +0 -8
- package/build/client/Link.js +0 -44
- package/build/client/NavLink.d.ts +0 -14
- package/build/client/NavLink.js +0 -37
- package/build/client/Router.d.ts +0 -7
- package/build/client/Router.js +0 -55
- package/build/client/channel.d.ts +0 -23
- package/build/client/channel.js +0 -94
- package/build/client/error-boundary.d.ts +0 -16
- package/build/client/error-boundary.js +0 -19
- package/build/client/head.d.ts +0 -26
- package/build/client/head.js +0 -87
- package/build/client/hooks.d.ts +0 -17
- package/build/client/hooks.js +0 -48
- package/build/client/lazy.d.ts +0 -16
- package/build/client/lazy.js +0 -53
- package/build/client/match.d.ts +0 -2
- package/build/client/match.js +0 -32
- package/build/client/mount.d.ts +0 -2
- package/build/client/mount.js +0 -13
- package/build/client/navigation.d.ts +0 -13
- package/build/client/navigation.js +0 -97
- package/build/client/params-context.d.ts +0 -2
- package/build/client/params-context.js +0 -2
- package/build/client/prefetch.d.ts +0 -11
- package/build/client/prefetch.js +0 -100
- package/build/client/runtime.d.ts +0 -31
- package/build/client/runtime.js +0 -112
- package/build/client/scroll.d.ts +0 -8
- package/build/client/scroll.js +0 -36
- package/toil-env.d.ts +0 -16
package/src/compiler/generate.ts
CHANGED
|
@@ -1,394 +1,394 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { type ResolvedToilConfig } from './config.js';
|
|
5
|
-
import { writeDocs } from './docs.js';
|
|
6
|
-
import { buildPageIndex, pagesModuleSource } from './pages.js';
|
|
7
|
-
import { scanRoutes, type ScannedRoute } from './routes.js';
|
|
8
|
-
import { llmsTxt, robotsTxt, sitemapXml } from './seo.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Contents of the root `toil-env.d.ts`: ambient global types so `new BinaryWriter()` etc. resolve
|
|
12
|
-
* in the IDE without an import. Script-mode declaration (no top-level import/export → the
|
|
13
|
-
* `declare const`s are truly global, and it's not a module that could confuse ESLint's project
|
|
14
|
-
* service); the inline `import('toiljs/io')` type only needs the normal `toiljs/io` export.
|
|
15
|
-
* Lives at the project root because TypeScript's `include` globs skip dot-directories.
|
|
16
|
-
* Exported so `toiljs create` can write it during scaffolding, before the first dev/build.
|
|
17
|
-
*/
|
|
18
|
-
/** Side-effect style imports (e.g. `import './styles/main.css'`). */
|
|
19
|
-
const STYLE_EXTENSIONS = ['css', 'scss', 'sass', 'less', 'styl', 'stylus', 'pcss', 'sss'];
|
|
20
|
-
/** Asset imports whose default export is the resolved URL string (e.g. `import logo from './logo.svg'`). */
|
|
21
|
-
const ASSET_EXTENSIONS = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'ico', 'bmp', 'apng'];
|
|
22
|
-
|
|
23
|
-
const STYLE_MODULES = STYLE_EXTENSIONS.map((ext) => `declare module '*.${ext}' {}`).join('\n');
|
|
24
|
-
const ASSET_MODULES = ASSET_EXTENSIONS.map(
|
|
25
|
-
(ext) => `declare module '*.${ext}' {\n const src: string;\n export default src;\n}`,
|
|
26
|
-
).join('\n');
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Types for vite-imagetools query imports (e.g. `import src from './hero.png?w=800&as=srcset'`).
|
|
30
|
-
* Declared inline rather than via `/// <reference types="vite-imagetools/client" />` because
|
|
31
|
-
* vite-imagetools v10 ships no `client` types entry, and the package lives in toiljs's own
|
|
32
|
-
* node_modules (unresolvable from a symlinked consumer). One `*` wildcard each, matching any
|
|
33
|
-
* specifier ending in the directive.
|
|
34
|
-
*/
|
|
35
|
-
const IMAGETOOLS_MODULES = [
|
|
36
|
-
`declare module '*as=srcset' {\n const src: string;\n export default src;\n}`,
|
|
37
|
-
`declare module '*as=url' {\n const src: string;\n export default src;\n}`,
|
|
38
|
-
`declare module '*as=metadata' {\n const metadata: { src: string; width?: number; height?: number; format?: string }[];\n export default metadata;\n}`,
|
|
39
|
-
].join('\n');
|
|
40
|
-
|
|
41
|
-
export const TOIL_ENV_DTS =
|
|
42
|
-
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
43
|
-
`declare const Toil: typeof import('toiljs/client');\n` +
|
|
44
|
-
`declare namespace Toil {\n` +
|
|
45
|
-
` type LoaderArgs = import('toiljs/client').LoaderArgs;\n` +
|
|
46
|
-
` type LoaderFunction<T = unknown> = import('toiljs/client').LoaderFunction<T>;\n` +
|
|
47
|
-
` type Revalidate = import('toiljs/client').Revalidate;\n` +
|
|
48
|
-
` type Metadata = import('toiljs/client').Metadata;\n` +
|
|
49
|
-
` type GenerateMetadata<T = unknown> = import('toiljs/client').GenerateMetadata<T>;\n` +
|
|
50
|
-
` type GenerateStaticParams = import('toiljs/client').GenerateStaticParams;\n` +
|
|
51
|
-
` type StaticParams = import('toiljs/client').StaticParams;\n` +
|
|
52
|
-
` type RouteErrorProps = import('toiljs/client').RouteErrorProps;\n` +
|
|
53
|
-
` type Href = import('toiljs/client').Href;\n` +
|
|
54
|
-
` type RoutePath = import('toiljs/client').RoutePath;\n` +
|
|
55
|
-
` type PageMeta = import('toiljs/client').PageMeta;\n` +
|
|
56
|
-
` type SearchHints = import('toiljs/client').SearchHints;\n` +
|
|
57
|
-
`}\n` +
|
|
58
|
-
`declare const BinaryWriter: typeof import('toiljs/io').BinaryWriter;\n` +
|
|
59
|
-
`declare const BinaryReader: typeof import('toiljs/io').BinaryReader;\n` +
|
|
60
|
-
`declare const FastMap: typeof import('toiljs/io').FastMap;\n` +
|
|
61
|
-
`declare const FastSet: typeof import('toiljs/io').FastSet;\n` +
|
|
62
|
-
`\n` +
|
|
63
|
-
`${STYLE_MODULES}\n` +
|
|
64
|
-
`\n` +
|
|
65
|
-
`${ASSET_MODULES}\n` +
|
|
66
|
-
`\n` +
|
|
67
|
-
`${IMAGETOOLS_MODULES}\n` +
|
|
68
|
-
`\n` +
|
|
69
|
-
`declare module 'toiljs/routes' {\n` +
|
|
70
|
-
` export const routes: import('toiljs/client').RouteDef[];\n` +
|
|
71
|
-
` export const layout: import('toiljs/client').LayoutLoader;\n` +
|
|
72
|
-
` export const notFound: import('toiljs/client').NotFoundLoader;\n` +
|
|
73
|
-
` export const globalError: import('toiljs/client').ErrorComponentLoader;\n` +
|
|
74
|
-
` export const slots: Record<string, import('toiljs/client').RouteDef[]>;\n` +
|
|
75
|
-
` export const pages: import('toiljs/client').PageMeta[];\n` +
|
|
76
|
-
`}\n`;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Returns a `./`-prefixed, **extensionless** POSIX module specifier from `.toil` to `abs`, for use
|
|
80
|
-
* in generated `import(...)` calls. Extensionless so TypeScript doesn't demand
|
|
81
|
-
* `allowImportingTsExtensions` (TS5097) when the generated files are checked; Vite still resolves it.
|
|
82
|
-
*/
|
|
83
|
-
function relFromToil(cfg: ResolvedToilConfig, abs: string): string {
|
|
84
|
-
let rel = path
|
|
85
|
-
.relative(cfg.toilDir, abs)
|
|
86
|
-
.replace(/\\/g, '/')
|
|
87
|
-
.replace(/\.(tsx|jsx)$/, '');
|
|
88
|
-
if (!rel.startsWith('.')) rel = './' + rel;
|
|
89
|
-
return rel;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function findLayout(cfg: ResolvedToilConfig): string | undefined {
|
|
93
|
-
return ['layout.tsx', 'layout.jsx']
|
|
94
|
-
.map((name) => path.join(cfg.clientAbsDir, name))
|
|
95
|
-
.find((p) => fs.existsSync(p));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Finds an optional custom not-found page at `client/404.{tsx,jsx}`. */
|
|
99
|
-
function findNotFound(cfg: ResolvedToilConfig): string | undefined {
|
|
100
|
-
return ['404.tsx', '404.jsx']
|
|
101
|
-
.map((name) => path.join(cfg.clientAbsDir, name))
|
|
102
|
-
.find((p) => fs.existsSync(p));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Finds an optional root error boundary at `client/global-error.{tsx,jsx}`. */
|
|
106
|
-
function findGlobalError(cfg: ResolvedToilConfig): string | undefined {
|
|
107
|
-
return ['global-error.tsx', 'global-error.jsx']
|
|
108
|
-
.map((name) => path.join(cfg.clientAbsDir, name))
|
|
109
|
-
.find((p) => fs.existsSync(p));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Builds the `RoutePath` union for typed `Link`/`navigate` hrefs: static routes as string literals,
|
|
114
|
-
* dynamic/catch-all as `` `…/${string}` `` templates (optional catch-all also emits its bare prefix).
|
|
115
|
-
*/
|
|
116
|
-
function routePathUnion(routes: ScannedRoute[]): string {
|
|
117
|
-
const members = new Set<string>();
|
|
118
|
-
for (const route of routes) {
|
|
119
|
-
const segments = route.pattern.split('/').filter(Boolean);
|
|
120
|
-
const isDynamic = segments.some((s) => s.startsWith(':') || s.startsWith('*'));
|
|
121
|
-
if (!isDynamic) {
|
|
122
|
-
members.add(`'${route.pattern}'`);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
const parts = segments.map((s) =>
|
|
126
|
-
s.startsWith(':') || s.startsWith('*') ? '${string}' : s,
|
|
127
|
-
);
|
|
128
|
-
members.add('`/' + parts.join('/') + '`');
|
|
129
|
-
const optionalIdx = segments.findIndex((s) => s.startsWith('**'));
|
|
130
|
-
if (optionalIdx !== -1) {
|
|
131
|
-
const prefix = '/' + segments.slice(0, optionalIdx).join('/');
|
|
132
|
-
members.add(`'${prefix}'`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return members.size ? [...members].join(' | ') : 'string';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* The `toil-routes.d.ts` contents: a module augmentation registering the project's route paths so
|
|
140
|
-
* `Link`/`navigate`/`useRouter` hrefs are type-checked. Regenerated each dev/build.
|
|
141
|
-
*/
|
|
142
|
-
function routesDts(cfg: ResolvedToilConfig, routes: ScannedRoute[]): string {
|
|
143
|
-
// Type-only namespace import of every route module (erased at build) so editors don't flag a
|
|
144
|
-
// route's `loader` / `metadata` / `generateMetadata` / `revalidate` / `default` exports as unused,
|
|
145
|
-
// the compiler consumes them via dynamic `import()`, which editors don't count as a reference.
|
|
146
|
-
const refs = routes.map((route, i) => {
|
|
147
|
-
let rel = path
|
|
148
|
-
.relative(cfg.root, route.file)
|
|
149
|
-
.replace(/\\/g, '/')
|
|
150
|
-
.replace(/\.(tsx|jsx)$/, '');
|
|
151
|
-
if (!rel.startsWith('.')) rel = `./${rel}`;
|
|
152
|
-
return { name: `_toilRoute${String(i)}`, rel };
|
|
153
|
-
});
|
|
154
|
-
const imports = refs
|
|
155
|
-
.map((m) => `import type * as ${m.name} from ${JSON.stringify(m.rel)};\n`)
|
|
156
|
-
.join('');
|
|
157
|
-
const referenced = refs.length
|
|
158
|
-
? `export type _ToilRouteModules = [${refs.map((m) => `typeof ${m.name}`).join(', ')}];\n`
|
|
159
|
-
: `export {};\n`;
|
|
160
|
-
return (
|
|
161
|
-
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
162
|
-
imports +
|
|
163
|
-
referenced +
|
|
164
|
-
`declare module 'toiljs/client' {\n` +
|
|
165
|
-
` interface Register {\n` +
|
|
166
|
-
` routePath: ${routePathUnion(routes)};\n` +
|
|
167
|
-
` }\n` +
|
|
168
|
-
`}\n`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Finds the user-owned app entry at `client/toil.{tsx,jsx}` (where `mount` is called). */
|
|
173
|
-
function findEntry(cfg: ResolvedToilConfig): string | undefined {
|
|
174
|
-
return ['toil.tsx', 'toil.jsx']
|
|
175
|
-
.map((name) => path.join(cfg.clientAbsDir, name))
|
|
176
|
-
.find((p) => fs.existsSync(p));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** A `<base>.{tsx,jsx}` in `dir`, or undefined. */
|
|
180
|
-
function specialIn(dir: string, base: string): string | undefined {
|
|
181
|
-
return [`${base}.tsx`, `${base}.jsx`]
|
|
182
|
-
.map((name) => path.join(dir, name))
|
|
183
|
-
.find((p) => fs.existsSync(p));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Chain of `<base>.{tsx,jsx}` files wrapping a route, shallowest → deepest: the routes root and each
|
|
188
|
-
* ancestor directory down to the file's own. With `includeClientRoot`, `client/<base>` is prepended
|
|
189
|
-
* as the outermost (used by templates; the root `client/layout.tsx` is instead the top-level layout).
|
|
190
|
-
*/
|
|
191
|
-
function findSpecialChain(
|
|
192
|
-
cfg: ResolvedToilConfig,
|
|
193
|
-
routeFile: string,
|
|
194
|
-
base: string,
|
|
195
|
-
includeClientRoot: boolean,
|
|
196
|
-
): string[] {
|
|
197
|
-
const chain: string[] = [];
|
|
198
|
-
const relDir = path.dirname(path.relative(cfg.routesAbsDir, routeFile));
|
|
199
|
-
const segments = relDir === '.' ? [] : relDir.split(path.sep);
|
|
200
|
-
// A parallel-slot route (one under an `@slot` segment) is rendered INTO a parent layout's
|
|
201
|
-
// `<Slot>`. Its own layout/template chain must therefore start at the `@slot` directory, not the
|
|
202
|
-
// routes root: the parent segments' layouts already wrap the slot, so re-including them here
|
|
203
|
-
// would nest the slot inside itself and recurse. For non-slot routes this is the full chain.
|
|
204
|
-
const slotIdx = segments.findIndex((s) => s.startsWith('@'));
|
|
205
|
-
const startAt = slotIdx < 0 ? 0 : slotIdx + 1;
|
|
206
|
-
if (includeClientRoot && slotIdx < 0) {
|
|
207
|
-
const root = specialIn(cfg.clientAbsDir, base);
|
|
208
|
-
if (root) chain.push(root);
|
|
209
|
-
}
|
|
210
|
-
let dir = cfg.routesAbsDir;
|
|
211
|
-
for (let i = 0; i <= segments.length; i++) {
|
|
212
|
-
if (i > 0) dir = path.join(dir, segments[i - 1]);
|
|
213
|
-
if (i < startAt) continue;
|
|
214
|
-
const found = specialIn(dir, base);
|
|
215
|
-
if (found) chain.push(found);
|
|
216
|
-
}
|
|
217
|
-
return chain;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Nearest special file named `base` (e.g. `loading`/`error`) from the route's dir up to the routes root. */
|
|
221
|
-
function findNearest(cfg: ResolvedToilConfig, routeFile: string, base: string): string | undefined {
|
|
222
|
-
const root = path.resolve(cfg.routesAbsDir);
|
|
223
|
-
let dir = path.dirname(routeFile);
|
|
224
|
-
for (;;) {
|
|
225
|
-
const found = [`${base}.tsx`, `${base}.jsx`]
|
|
226
|
-
.map((name) => path.join(dir, name))
|
|
227
|
-
.find((p) => fs.existsSync(p));
|
|
228
|
-
if (found) return found;
|
|
229
|
-
if (path.resolve(dir) === root) return undefined;
|
|
230
|
-
const parent = path.dirname(dir);
|
|
231
|
-
if (parent === dir) return undefined;
|
|
232
|
-
dir = parent;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Generates the `.toil/` working dir (routes table, mount entry, the HTML entry built from the
|
|
238
|
-
* project's `public/index.html` template, and mirrored `public/` assets) and returns the scanned
|
|
239
|
-
* routes. Called before every dev/build and on route add/remove during dev.
|
|
240
|
-
*/
|
|
241
|
-
export function generate(cfg: ResolvedToilConfig): ScannedRoute[] {
|
|
242
|
-
const routes = scanRoutes(cfg.routesAbsDir);
|
|
243
|
-
fs.mkdirSync(cfg.toilDir, { recursive: true });
|
|
244
|
-
|
|
245
|
-
const layoutFile = findLayout(cfg);
|
|
246
|
-
const notFoundFile = findNotFound(cfg);
|
|
247
|
-
const globalErrorFile = findGlobalError(cfg);
|
|
248
|
-
const imp = (f: string): string => `() => import(${JSON.stringify(relFromToil(cfg, f))})`;
|
|
249
|
-
const routeObj = (r: ScannedRoute): string => {
|
|
250
|
-
const layouts = findSpecialChain(cfg, r.file, 'layout', false).map(imp).join(', ');
|
|
251
|
-
const templates = findSpecialChain(cfg, r.file, 'template', true).map(imp).join(', ');
|
|
252
|
-
const parts = [
|
|
253
|
-
`pattern: ${JSON.stringify(r.pattern)}`,
|
|
254
|
-
`load: ${imp(r.file)}`,
|
|
255
|
-
`layouts: [${layouts}]`,
|
|
256
|
-
];
|
|
257
|
-
if (templates) parts.push(`templates: [${templates}]`);
|
|
258
|
-
const loadingFile = findNearest(cfg, r.file, 'loading');
|
|
259
|
-
if (loadingFile) parts.push(`loading: ${imp(loadingFile)}`);
|
|
260
|
-
const errorFile = findNearest(cfg, r.file, 'error');
|
|
261
|
-
if (errorFile) parts.push(`errorComponent: ${imp(errorFile)}`);
|
|
262
|
-
if (r.intercept) parts.push(`intercept: true`);
|
|
263
|
-
return `{ ${parts.join(', ')} }`;
|
|
264
|
-
};
|
|
265
|
-
const mainRoutes = routes.filter((r) => r.slot === undefined);
|
|
266
|
-
const slotNames = [...new Set(routes.flatMap((r) => (r.slot ? [r.slot] : [])))];
|
|
267
|
-
const slotsBody = slotNames
|
|
268
|
-
.map((name) => {
|
|
269
|
-
const items = routes
|
|
270
|
-
.filter((r) => r.slot === name)
|
|
271
|
-
.map((r) => ` ${routeObj(r)},`)
|
|
272
|
-
.join('\n');
|
|
273
|
-
return ` ${JSON.stringify(name)}: [\n${items}\n ],`;
|
|
274
|
-
})
|
|
275
|
-
.join('\n');
|
|
276
|
-
const pages = buildPageIndex(cfg.root, routes);
|
|
277
|
-
const routesSrc =
|
|
278
|
-
`// @ts-nocheck\n` +
|
|
279
|
-
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
280
|
-
`import type { RouteDef, LayoutLoader, NotFoundLoader, PageMeta } from 'toiljs/client';\n\n` +
|
|
281
|
-
`export const routes: RouteDef[] = [\n${mainRoutes.map((r) => ` ${routeObj(r)},`).join('\n')}\n];\n\n` +
|
|
282
|
-
`export const slots: Record<string, RouteDef[]> = {\n${slotsBody}\n};\n\n` +
|
|
283
|
-
`export const layout: LayoutLoader = ${layoutFile ? `() => import(${JSON.stringify(relFromToil(cfg, layoutFile))})` : 'null'};\n` +
|
|
284
|
-
`export const notFound: NotFoundLoader = ${notFoundFile ? `() => import(${JSON.stringify(relFromToil(cfg, notFoundFile))})` : 'null'};\n` +
|
|
285
|
-
`export const globalError = ${globalErrorFile ? `() => import(${JSON.stringify(relFromToil(cfg, globalErrorFile))})` : 'null'};\n\n` +
|
|
286
|
-
pagesModuleSource(pages);
|
|
287
|
-
fs.writeFileSync(path.join(cfg.toilDir, 'routes.ts'), routesSrc);
|
|
288
|
-
|
|
289
|
-
const globalsSrc =
|
|
290
|
-
`// @ts-nocheck\n` +
|
|
291
|
-
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
292
|
-
`import * as Toil from 'toiljs/client';\n` +
|
|
293
|
-
`import { BinaryWriter, BinaryReader, FastMap, FastSet } from 'toiljs/io';\n` +
|
|
294
|
-
`import { pages } from './routes';\n\n` +
|
|
295
|
-
`Object.assign(globalThis, { Toil, BinaryWriter, BinaryReader, FastMap, FastSet });\n` +
|
|
296
|
-
`Toil.setViewTransitions(${String(cfg.viewTransitions)});\n` +
|
|
297
|
-
`Toil.setTransitions(${String(cfg.transitions)});\n` +
|
|
298
|
-
`Toil.registerPages(pages);\n`;
|
|
299
|
-
fs.writeFileSync(path.join(cfg.toilDir, 'globals.ts'), globalsSrc);
|
|
300
|
-
|
|
301
|
-
const entryFile = findEntry(cfg);
|
|
302
|
-
const entrySrc = entryFile
|
|
303
|
-
? `// @ts-nocheck\n` +
|
|
304
|
-
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
305
|
-
`import './globals';\n` +
|
|
306
|
-
`import ${JSON.stringify(relFromToil(cfg, entryFile))};\n`
|
|
307
|
-
: `// @ts-nocheck\n` +
|
|
308
|
-
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
309
|
-
`import './globals';\n` +
|
|
310
|
-
`import { mount } from 'toiljs/client';\n` +
|
|
311
|
-
`import { routes, layout, notFound, globalError, slots } from './routes';\n\n` +
|
|
312
|
-
`mount(routes, layout, notFound, globalError, slots);\n`;
|
|
313
|
-
fs.writeFileSync(path.join(cfg.toilDir, 'entry.tsx'), entrySrc);
|
|
314
|
-
|
|
315
|
-
fs.writeFileSync(path.join(cfg.root, 'toil-env.d.ts'), TOIL_ENV_DTS);
|
|
316
|
-
fs.writeFileSync(path.join(cfg.root, 'toil-routes.d.ts'), routesDts(cfg, routes));
|
|
317
|
-
|
|
318
|
-
fs.writeFileSync(path.join(cfg.toilDir, 'index.html'), buildHtml(cfg));
|
|
319
|
-
syncPublicAssets(cfg);
|
|
320
|
-
writeSeoFiles(cfg, routes);
|
|
321
|
-
writeDocs(cfg.toilDir);
|
|
322
|
-
|
|
323
|
-
return routes;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Writes the build-time SEO crawler files into `.toil/public` (Vite's publicDir, served in dev and
|
|
328
|
-
* copied to the output root in build): `robots.txt` (incl. AI-crawler directives), `sitemap.xml`
|
|
329
|
-
* (static routes), and `llms.txt` (AI guidance). No-op when SEO isn't configured.
|
|
330
|
-
*/
|
|
331
|
-
function writeSeoFiles(cfg: ResolvedToilConfig, routes: ScannedRoute[]): void {
|
|
332
|
-
if (!cfg.seo) return;
|
|
333
|
-
const dest = path.join(cfg.toilDir, 'public');
|
|
334
|
-
const files: [string, string][] = [
|
|
335
|
-
['robots.txt', robotsTxt(cfg.seo)],
|
|
336
|
-
['sitemap.xml', sitemapXml(cfg.seo, routes)],
|
|
337
|
-
['llms.txt', llmsTxt(cfg.seo, routes)],
|
|
338
|
-
];
|
|
339
|
-
const present = files.filter(([, content]) => content !== '');
|
|
340
|
-
if (present.length === 0) return;
|
|
341
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
342
|
-
for (const [name, content] of present) fs.writeFileSync(path.join(dest, name), content);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** Fallback HTML when the project has no `public/index.html` template. The entry script is added
|
|
346
|
-
* by {@link buildHtml}. */
|
|
347
|
-
const DEFAULT_HTML =
|
|
348
|
-
`<!doctype html>\n<html lang="en">\n <head>\n <meta charset="utf-8" />\n` +
|
|
349
|
-
` <meta name="viewport" content="width=device-width, initial-scale=1" />\n` +
|
|
350
|
-
` <meta name="description" content="" />\n` +
|
|
351
|
-
` <title>Toil App</title>\n </head>\n <body>\n <div id="root"></div>\n` +
|
|
352
|
-
` </body>\n</html>\n`;
|
|
353
|
-
|
|
354
|
-
/** The module entry that boots the app, injected into the HTML (resolved relative to `.toil`). */
|
|
355
|
-
const ENTRY_SCRIPT = `<script type="module" src="./entry.tsx"></script>`;
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Produces the `.toil/index.html` Vite entry from the project's `public/index.html` template (or
|
|
359
|
-
* the built-in default if absent), ensuring the generated module entry script is present. Users
|
|
360
|
-
* own the template, toil only guarantees the entry is wired, so it stays the SPA root.
|
|
361
|
-
*/
|
|
362
|
-
function buildHtml(cfg: ResolvedToilConfig): string {
|
|
363
|
-
const templatePath = path.join(cfg.publicDir, 'index.html');
|
|
364
|
-
let html = fs.existsSync(templatePath) ? fs.readFileSync(templatePath, 'utf8') : DEFAULT_HTML;
|
|
365
|
-
// Inject the entry only if the template doesn't already reference it as a module script
|
|
366
|
-
// (matching the literal filename anywhere in the file would be too eager).
|
|
367
|
-
if (!/src=["']\.\/entry\.tsx["']/.test(html)) {
|
|
368
|
-
html = html.includes('</body>')
|
|
369
|
-
? html.replace('</body>', ` ${ENTRY_SCRIPT}\n </body>`)
|
|
370
|
-
: `${html}\n${ENTRY_SCRIPT}\n`;
|
|
371
|
-
}
|
|
372
|
-
return html;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Mirrors the project's `public/` assets into `.toil/public/` (Vite's publicDir under the `.toil`
|
|
377
|
-
* root), excluding the `index.html` template, that is processed into the entry above, and copying
|
|
378
|
-
* it here would clobber the built, asset-hashed page. Cleared each run so deletions propagate.
|
|
379
|
-
*/
|
|
380
|
-
function syncPublicAssets(cfg: ResolvedToilConfig): void {
|
|
381
|
-
const dest = path.join(cfg.toilDir, 'public');
|
|
382
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
383
|
-
if (!fs.existsSync(cfg.publicDir)) return;
|
|
384
|
-
|
|
385
|
-
let copied = 0;
|
|
386
|
-
for (const entry of fs.readdirSync(cfg.publicDir, { withFileTypes: true })) {
|
|
387
|
-
if (entry.name === 'index.html') continue;
|
|
388
|
-
fs.cpSync(path.join(cfg.publicDir, entry.name), path.join(dest, entry.name), {
|
|
389
|
-
recursive: true,
|
|
390
|
-
});
|
|
391
|
-
copied++;
|
|
392
|
-
}
|
|
393
|
-
if (copied === 0) fs.rmSync(dest, { recursive: true, force: true });
|
|
394
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { type ResolvedToilConfig } from './config.js';
|
|
5
|
+
import { writeDocs } from './docs.js';
|
|
6
|
+
import { buildPageIndex, pagesModuleSource } from './pages.js';
|
|
7
|
+
import { scanRoutes, type ScannedRoute } from './routes.js';
|
|
8
|
+
import { llmsTxt, robotsTxt, sitemapXml } from './seo.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Contents of the root `toil-env.d.ts`: ambient global types so `new BinaryWriter()` etc. resolve
|
|
12
|
+
* in the IDE without an import. Script-mode declaration (no top-level import/export → the
|
|
13
|
+
* `declare const`s are truly global, and it's not a module that could confuse ESLint's project
|
|
14
|
+
* service); the inline `import('toiljs/io')` type only needs the normal `toiljs/io` export.
|
|
15
|
+
* Lives at the project root because TypeScript's `include` globs skip dot-directories.
|
|
16
|
+
* Exported so `toiljs create` can write it during scaffolding, before the first dev/build.
|
|
17
|
+
*/
|
|
18
|
+
/** Side-effect style imports (e.g. `import './styles/main.css'`). */
|
|
19
|
+
const STYLE_EXTENSIONS = ['css', 'scss', 'sass', 'less', 'styl', 'stylus', 'pcss', 'sss'];
|
|
20
|
+
/** Asset imports whose default export is the resolved URL string (e.g. `import logo from './logo.svg'`). */
|
|
21
|
+
const ASSET_EXTENSIONS = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'ico', 'bmp', 'apng'];
|
|
22
|
+
|
|
23
|
+
const STYLE_MODULES = STYLE_EXTENSIONS.map((ext) => `declare module '*.${ext}' {}`).join('\n');
|
|
24
|
+
const ASSET_MODULES = ASSET_EXTENSIONS.map(
|
|
25
|
+
(ext) => `declare module '*.${ext}' {\n const src: string;\n export default src;\n}`,
|
|
26
|
+
).join('\n');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Types for vite-imagetools query imports (e.g. `import src from './hero.png?w=800&as=srcset'`).
|
|
30
|
+
* Declared inline rather than via `/// <reference types="vite-imagetools/client" />` because
|
|
31
|
+
* vite-imagetools v10 ships no `client` types entry, and the package lives in toiljs's own
|
|
32
|
+
* node_modules (unresolvable from a symlinked consumer). One `*` wildcard each, matching any
|
|
33
|
+
* specifier ending in the directive.
|
|
34
|
+
*/
|
|
35
|
+
const IMAGETOOLS_MODULES = [
|
|
36
|
+
`declare module '*as=srcset' {\n const src: string;\n export default src;\n}`,
|
|
37
|
+
`declare module '*as=url' {\n const src: string;\n export default src;\n}`,
|
|
38
|
+
`declare module '*as=metadata' {\n const metadata: { src: string; width?: number; height?: number; format?: string }[];\n export default metadata;\n}`,
|
|
39
|
+
].join('\n');
|
|
40
|
+
|
|
41
|
+
export const TOIL_ENV_DTS =
|
|
42
|
+
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
43
|
+
`declare const Toil: typeof import('toiljs/client');\n` +
|
|
44
|
+
`declare namespace Toil {\n` +
|
|
45
|
+
` type LoaderArgs = import('toiljs/client').LoaderArgs;\n` +
|
|
46
|
+
` type LoaderFunction<T = unknown> = import('toiljs/client').LoaderFunction<T>;\n` +
|
|
47
|
+
` type Revalidate = import('toiljs/client').Revalidate;\n` +
|
|
48
|
+
` type Metadata = import('toiljs/client').Metadata;\n` +
|
|
49
|
+
` type GenerateMetadata<T = unknown> = import('toiljs/client').GenerateMetadata<T>;\n` +
|
|
50
|
+
` type GenerateStaticParams = import('toiljs/client').GenerateStaticParams;\n` +
|
|
51
|
+
` type StaticParams = import('toiljs/client').StaticParams;\n` +
|
|
52
|
+
` type RouteErrorProps = import('toiljs/client').RouteErrorProps;\n` +
|
|
53
|
+
` type Href = import('toiljs/client').Href;\n` +
|
|
54
|
+
` type RoutePath = import('toiljs/client').RoutePath;\n` +
|
|
55
|
+
` type PageMeta = import('toiljs/client').PageMeta;\n` +
|
|
56
|
+
` type SearchHints = import('toiljs/client').SearchHints;\n` +
|
|
57
|
+
`}\n` +
|
|
58
|
+
`declare const BinaryWriter: typeof import('toiljs/io').BinaryWriter;\n` +
|
|
59
|
+
`declare const BinaryReader: typeof import('toiljs/io').BinaryReader;\n` +
|
|
60
|
+
`declare const FastMap: typeof import('toiljs/io').FastMap;\n` +
|
|
61
|
+
`declare const FastSet: typeof import('toiljs/io').FastSet;\n` +
|
|
62
|
+
`\n` +
|
|
63
|
+
`${STYLE_MODULES}\n` +
|
|
64
|
+
`\n` +
|
|
65
|
+
`${ASSET_MODULES}\n` +
|
|
66
|
+
`\n` +
|
|
67
|
+
`${IMAGETOOLS_MODULES}\n` +
|
|
68
|
+
`\n` +
|
|
69
|
+
`declare module 'toiljs/routes' {\n` +
|
|
70
|
+
` export const routes: import('toiljs/client').RouteDef[];\n` +
|
|
71
|
+
` export const layout: import('toiljs/client').LayoutLoader;\n` +
|
|
72
|
+
` export const notFound: import('toiljs/client').NotFoundLoader;\n` +
|
|
73
|
+
` export const globalError: import('toiljs/client').ErrorComponentLoader;\n` +
|
|
74
|
+
` export const slots: Record<string, import('toiljs/client').RouteDef[]>;\n` +
|
|
75
|
+
` export const pages: import('toiljs/client').PageMeta[];\n` +
|
|
76
|
+
`}\n`;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns a `./`-prefixed, **extensionless** POSIX module specifier from `.toil` to `abs`, for use
|
|
80
|
+
* in generated `import(...)` calls. Extensionless so TypeScript doesn't demand
|
|
81
|
+
* `allowImportingTsExtensions` (TS5097) when the generated files are checked; Vite still resolves it.
|
|
82
|
+
*/
|
|
83
|
+
function relFromToil(cfg: ResolvedToilConfig, abs: string): string {
|
|
84
|
+
let rel = path
|
|
85
|
+
.relative(cfg.toilDir, abs)
|
|
86
|
+
.replace(/\\/g, '/')
|
|
87
|
+
.replace(/\.(tsx|jsx)$/, '');
|
|
88
|
+
if (!rel.startsWith('.')) rel = './' + rel;
|
|
89
|
+
return rel;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findLayout(cfg: ResolvedToilConfig): string | undefined {
|
|
93
|
+
return ['layout.tsx', 'layout.jsx']
|
|
94
|
+
.map((name) => path.join(cfg.clientAbsDir, name))
|
|
95
|
+
.find((p) => fs.existsSync(p));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Finds an optional custom not-found page at `client/404.{tsx,jsx}`. */
|
|
99
|
+
function findNotFound(cfg: ResolvedToilConfig): string | undefined {
|
|
100
|
+
return ['404.tsx', '404.jsx']
|
|
101
|
+
.map((name) => path.join(cfg.clientAbsDir, name))
|
|
102
|
+
.find((p) => fs.existsSync(p));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Finds an optional root error boundary at `client/global-error.{tsx,jsx}`. */
|
|
106
|
+
function findGlobalError(cfg: ResolvedToilConfig): string | undefined {
|
|
107
|
+
return ['global-error.tsx', 'global-error.jsx']
|
|
108
|
+
.map((name) => path.join(cfg.clientAbsDir, name))
|
|
109
|
+
.find((p) => fs.existsSync(p));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Builds the `RoutePath` union for typed `Link`/`navigate` hrefs: static routes as string literals,
|
|
114
|
+
* dynamic/catch-all as `` `…/${string}` `` templates (optional catch-all also emits its bare prefix).
|
|
115
|
+
*/
|
|
116
|
+
function routePathUnion(routes: ScannedRoute[]): string {
|
|
117
|
+
const members = new Set<string>();
|
|
118
|
+
for (const route of routes) {
|
|
119
|
+
const segments = route.pattern.split('/').filter(Boolean);
|
|
120
|
+
const isDynamic = segments.some((s) => s.startsWith(':') || s.startsWith('*'));
|
|
121
|
+
if (!isDynamic) {
|
|
122
|
+
members.add(`'${route.pattern}'`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const parts = segments.map((s) =>
|
|
126
|
+
s.startsWith(':') || s.startsWith('*') ? '${string}' : s,
|
|
127
|
+
);
|
|
128
|
+
members.add('`/' + parts.join('/') + '`');
|
|
129
|
+
const optionalIdx = segments.findIndex((s) => s.startsWith('**'));
|
|
130
|
+
if (optionalIdx !== -1) {
|
|
131
|
+
const prefix = '/' + segments.slice(0, optionalIdx).join('/');
|
|
132
|
+
members.add(`'${prefix}'`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return members.size ? [...members].join(' | ') : 'string';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The `toil-routes.d.ts` contents: a module augmentation registering the project's route paths so
|
|
140
|
+
* `Link`/`navigate`/`useRouter` hrefs are type-checked. Regenerated each dev/build.
|
|
141
|
+
*/
|
|
142
|
+
function routesDts(cfg: ResolvedToilConfig, routes: ScannedRoute[]): string {
|
|
143
|
+
// Type-only namespace import of every route module (erased at build) so editors don't flag a
|
|
144
|
+
// route's `loader` / `metadata` / `generateMetadata` / `revalidate` / `default` exports as unused,
|
|
145
|
+
// the compiler consumes them via dynamic `import()`, which editors don't count as a reference.
|
|
146
|
+
const refs = routes.map((route, i) => {
|
|
147
|
+
let rel = path
|
|
148
|
+
.relative(cfg.root, route.file)
|
|
149
|
+
.replace(/\\/g, '/')
|
|
150
|
+
.replace(/\.(tsx|jsx)$/, '');
|
|
151
|
+
if (!rel.startsWith('.')) rel = `./${rel}`;
|
|
152
|
+
return { name: `_toilRoute${String(i)}`, rel };
|
|
153
|
+
});
|
|
154
|
+
const imports = refs
|
|
155
|
+
.map((m) => `import type * as ${m.name} from ${JSON.stringify(m.rel)};\n`)
|
|
156
|
+
.join('');
|
|
157
|
+
const referenced = refs.length
|
|
158
|
+
? `export type _ToilRouteModules = [${refs.map((m) => `typeof ${m.name}`).join(', ')}];\n`
|
|
159
|
+
: `export {};\n`;
|
|
160
|
+
return (
|
|
161
|
+
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
162
|
+
imports +
|
|
163
|
+
referenced +
|
|
164
|
+
`declare module 'toiljs/client' {\n` +
|
|
165
|
+
` interface Register {\n` +
|
|
166
|
+
` routePath: ${routePathUnion(routes)};\n` +
|
|
167
|
+
` }\n` +
|
|
168
|
+
`}\n`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Finds the user-owned app entry at `client/toil.{tsx,jsx}` (where `mount` is called). */
|
|
173
|
+
function findEntry(cfg: ResolvedToilConfig): string | undefined {
|
|
174
|
+
return ['toil.tsx', 'toil.jsx']
|
|
175
|
+
.map((name) => path.join(cfg.clientAbsDir, name))
|
|
176
|
+
.find((p) => fs.existsSync(p));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A `<base>.{tsx,jsx}` in `dir`, or undefined. */
|
|
180
|
+
function specialIn(dir: string, base: string): string | undefined {
|
|
181
|
+
return [`${base}.tsx`, `${base}.jsx`]
|
|
182
|
+
.map((name) => path.join(dir, name))
|
|
183
|
+
.find((p) => fs.existsSync(p));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Chain of `<base>.{tsx,jsx}` files wrapping a route, shallowest → deepest: the routes root and each
|
|
188
|
+
* ancestor directory down to the file's own. With `includeClientRoot`, `client/<base>` is prepended
|
|
189
|
+
* as the outermost (used by templates; the root `client/layout.tsx` is instead the top-level layout).
|
|
190
|
+
*/
|
|
191
|
+
function findSpecialChain(
|
|
192
|
+
cfg: ResolvedToilConfig,
|
|
193
|
+
routeFile: string,
|
|
194
|
+
base: string,
|
|
195
|
+
includeClientRoot: boolean,
|
|
196
|
+
): string[] {
|
|
197
|
+
const chain: string[] = [];
|
|
198
|
+
const relDir = path.dirname(path.relative(cfg.routesAbsDir, routeFile));
|
|
199
|
+
const segments = relDir === '.' ? [] : relDir.split(path.sep);
|
|
200
|
+
// A parallel-slot route (one under an `@slot` segment) is rendered INTO a parent layout's
|
|
201
|
+
// `<Slot>`. Its own layout/template chain must therefore start at the `@slot` directory, not the
|
|
202
|
+
// routes root: the parent segments' layouts already wrap the slot, so re-including them here
|
|
203
|
+
// would nest the slot inside itself and recurse. For non-slot routes this is the full chain.
|
|
204
|
+
const slotIdx = segments.findIndex((s) => s.startsWith('@'));
|
|
205
|
+
const startAt = slotIdx < 0 ? 0 : slotIdx + 1;
|
|
206
|
+
if (includeClientRoot && slotIdx < 0) {
|
|
207
|
+
const root = specialIn(cfg.clientAbsDir, base);
|
|
208
|
+
if (root) chain.push(root);
|
|
209
|
+
}
|
|
210
|
+
let dir = cfg.routesAbsDir;
|
|
211
|
+
for (let i = 0; i <= segments.length; i++) {
|
|
212
|
+
if (i > 0) dir = path.join(dir, segments[i - 1]);
|
|
213
|
+
if (i < startAt) continue;
|
|
214
|
+
const found = specialIn(dir, base);
|
|
215
|
+
if (found) chain.push(found);
|
|
216
|
+
}
|
|
217
|
+
return chain;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Nearest special file named `base` (e.g. `loading`/`error`) from the route's dir up to the routes root. */
|
|
221
|
+
function findNearest(cfg: ResolvedToilConfig, routeFile: string, base: string): string | undefined {
|
|
222
|
+
const root = path.resolve(cfg.routesAbsDir);
|
|
223
|
+
let dir = path.dirname(routeFile);
|
|
224
|
+
for (;;) {
|
|
225
|
+
const found = [`${base}.tsx`, `${base}.jsx`]
|
|
226
|
+
.map((name) => path.join(dir, name))
|
|
227
|
+
.find((p) => fs.existsSync(p));
|
|
228
|
+
if (found) return found;
|
|
229
|
+
if (path.resolve(dir) === root) return undefined;
|
|
230
|
+
const parent = path.dirname(dir);
|
|
231
|
+
if (parent === dir) return undefined;
|
|
232
|
+
dir = parent;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Generates the `.toil/` working dir (routes table, mount entry, the HTML entry built from the
|
|
238
|
+
* project's `public/index.html` template, and mirrored `public/` assets) and returns the scanned
|
|
239
|
+
* routes. Called before every dev/build and on route add/remove during dev.
|
|
240
|
+
*/
|
|
241
|
+
export function generate(cfg: ResolvedToilConfig): ScannedRoute[] {
|
|
242
|
+
const routes = scanRoutes(cfg.routesAbsDir);
|
|
243
|
+
fs.mkdirSync(cfg.toilDir, { recursive: true });
|
|
244
|
+
|
|
245
|
+
const layoutFile = findLayout(cfg);
|
|
246
|
+
const notFoundFile = findNotFound(cfg);
|
|
247
|
+
const globalErrorFile = findGlobalError(cfg);
|
|
248
|
+
const imp = (f: string): string => `() => import(${JSON.stringify(relFromToil(cfg, f))})`;
|
|
249
|
+
const routeObj = (r: ScannedRoute): string => {
|
|
250
|
+
const layouts = findSpecialChain(cfg, r.file, 'layout', false).map(imp).join(', ');
|
|
251
|
+
const templates = findSpecialChain(cfg, r.file, 'template', true).map(imp).join(', ');
|
|
252
|
+
const parts = [
|
|
253
|
+
`pattern: ${JSON.stringify(r.pattern)}`,
|
|
254
|
+
`load: ${imp(r.file)}`,
|
|
255
|
+
`layouts: [${layouts}]`,
|
|
256
|
+
];
|
|
257
|
+
if (templates) parts.push(`templates: [${templates}]`);
|
|
258
|
+
const loadingFile = findNearest(cfg, r.file, 'loading');
|
|
259
|
+
if (loadingFile) parts.push(`loading: ${imp(loadingFile)}`);
|
|
260
|
+
const errorFile = findNearest(cfg, r.file, 'error');
|
|
261
|
+
if (errorFile) parts.push(`errorComponent: ${imp(errorFile)}`);
|
|
262
|
+
if (r.intercept) parts.push(`intercept: true`);
|
|
263
|
+
return `{ ${parts.join(', ')} }`;
|
|
264
|
+
};
|
|
265
|
+
const mainRoutes = routes.filter((r) => r.slot === undefined);
|
|
266
|
+
const slotNames = [...new Set(routes.flatMap((r) => (r.slot ? [r.slot] : [])))];
|
|
267
|
+
const slotsBody = slotNames
|
|
268
|
+
.map((name) => {
|
|
269
|
+
const items = routes
|
|
270
|
+
.filter((r) => r.slot === name)
|
|
271
|
+
.map((r) => ` ${routeObj(r)},`)
|
|
272
|
+
.join('\n');
|
|
273
|
+
return ` ${JSON.stringify(name)}: [\n${items}\n ],`;
|
|
274
|
+
})
|
|
275
|
+
.join('\n');
|
|
276
|
+
const pages = buildPageIndex(cfg.root, routes);
|
|
277
|
+
const routesSrc =
|
|
278
|
+
`// @ts-nocheck\n` +
|
|
279
|
+
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
280
|
+
`import type { RouteDef, LayoutLoader, NotFoundLoader, PageMeta } from 'toiljs/client';\n\n` +
|
|
281
|
+
`export const routes: RouteDef[] = [\n${mainRoutes.map((r) => ` ${routeObj(r)},`).join('\n')}\n];\n\n` +
|
|
282
|
+
`export const slots: Record<string, RouteDef[]> = {\n${slotsBody}\n};\n\n` +
|
|
283
|
+
`export const layout: LayoutLoader = ${layoutFile ? `() => import(${JSON.stringify(relFromToil(cfg, layoutFile))})` : 'null'};\n` +
|
|
284
|
+
`export const notFound: NotFoundLoader = ${notFoundFile ? `() => import(${JSON.stringify(relFromToil(cfg, notFoundFile))})` : 'null'};\n` +
|
|
285
|
+
`export const globalError = ${globalErrorFile ? `() => import(${JSON.stringify(relFromToil(cfg, globalErrorFile))})` : 'null'};\n\n` +
|
|
286
|
+
pagesModuleSource(pages);
|
|
287
|
+
fs.writeFileSync(path.join(cfg.toilDir, 'routes.ts'), routesSrc);
|
|
288
|
+
|
|
289
|
+
const globalsSrc =
|
|
290
|
+
`// @ts-nocheck\n` +
|
|
291
|
+
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
292
|
+
`import * as Toil from 'toiljs/client';\n` +
|
|
293
|
+
`import { BinaryWriter, BinaryReader, FastMap, FastSet } from 'toiljs/io';\n` +
|
|
294
|
+
`import { pages } from './routes';\n\n` +
|
|
295
|
+
`Object.assign(globalThis, { Toil, BinaryWriter, BinaryReader, FastMap, FastSet });\n` +
|
|
296
|
+
`Toil.setViewTransitions(${String(cfg.viewTransitions)});\n` +
|
|
297
|
+
`Toil.setTransitions(${String(cfg.transitions)});\n` +
|
|
298
|
+
`Toil.registerPages(pages);\n`;
|
|
299
|
+
fs.writeFileSync(path.join(cfg.toilDir, 'globals.ts'), globalsSrc);
|
|
300
|
+
|
|
301
|
+
const entryFile = findEntry(cfg);
|
|
302
|
+
const entrySrc = entryFile
|
|
303
|
+
? `// @ts-nocheck\n` +
|
|
304
|
+
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
305
|
+
`import './globals';\n` +
|
|
306
|
+
`import ${JSON.stringify(relFromToil(cfg, entryFile))};\n`
|
|
307
|
+
: `// @ts-nocheck\n` +
|
|
308
|
+
`// AUTO-GENERATED by toil, do not edit.\n` +
|
|
309
|
+
`import './globals';\n` +
|
|
310
|
+
`import { mount } from 'toiljs/client';\n` +
|
|
311
|
+
`import { routes, layout, notFound, globalError, slots } from './routes';\n\n` +
|
|
312
|
+
`mount(routes, layout, notFound, globalError, slots);\n`;
|
|
313
|
+
fs.writeFileSync(path.join(cfg.toilDir, 'entry.tsx'), entrySrc);
|
|
314
|
+
|
|
315
|
+
fs.writeFileSync(path.join(cfg.root, 'toil-env.d.ts'), TOIL_ENV_DTS);
|
|
316
|
+
fs.writeFileSync(path.join(cfg.root, 'toil-routes.d.ts'), routesDts(cfg, routes));
|
|
317
|
+
|
|
318
|
+
fs.writeFileSync(path.join(cfg.toilDir, 'index.html'), buildHtml(cfg));
|
|
319
|
+
syncPublicAssets(cfg);
|
|
320
|
+
writeSeoFiles(cfg, routes);
|
|
321
|
+
writeDocs(cfg.toilDir);
|
|
322
|
+
|
|
323
|
+
return routes;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Writes the build-time SEO crawler files into `.toil/public` (Vite's publicDir, served in dev and
|
|
328
|
+
* copied to the output root in build): `robots.txt` (incl. AI-crawler directives), `sitemap.xml`
|
|
329
|
+
* (static routes), and `llms.txt` (AI guidance). No-op when SEO isn't configured.
|
|
330
|
+
*/
|
|
331
|
+
function writeSeoFiles(cfg: ResolvedToilConfig, routes: ScannedRoute[]): void {
|
|
332
|
+
if (!cfg.seo) return;
|
|
333
|
+
const dest = path.join(cfg.toilDir, 'public');
|
|
334
|
+
const files: [string, string][] = [
|
|
335
|
+
['robots.txt', robotsTxt(cfg.seo)],
|
|
336
|
+
['sitemap.xml', sitemapXml(cfg.seo, routes)],
|
|
337
|
+
['llms.txt', llmsTxt(cfg.seo, routes)],
|
|
338
|
+
];
|
|
339
|
+
const present = files.filter(([, content]) => content !== '');
|
|
340
|
+
if (present.length === 0) return;
|
|
341
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
342
|
+
for (const [name, content] of present) fs.writeFileSync(path.join(dest, name), content);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Fallback HTML when the project has no `public/index.html` template. The entry script is added
|
|
346
|
+
* by {@link buildHtml}. */
|
|
347
|
+
const DEFAULT_HTML =
|
|
348
|
+
`<!doctype html>\n<html lang="en">\n <head>\n <meta charset="utf-8" />\n` +
|
|
349
|
+
` <meta name="viewport" content="width=device-width, initial-scale=1" />\n` +
|
|
350
|
+
` <meta name="description" content="" />\n` +
|
|
351
|
+
` <title>Toil App</title>\n </head>\n <body>\n <div id="root"></div>\n` +
|
|
352
|
+
` </body>\n</html>\n`;
|
|
353
|
+
|
|
354
|
+
/** The module entry that boots the app, injected into the HTML (resolved relative to `.toil`). */
|
|
355
|
+
const ENTRY_SCRIPT = `<script type="module" src="./entry.tsx"></script>`;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Produces the `.toil/index.html` Vite entry from the project's `public/index.html` template (or
|
|
359
|
+
* the built-in default if absent), ensuring the generated module entry script is present. Users
|
|
360
|
+
* own the template, toil only guarantees the entry is wired, so it stays the SPA root.
|
|
361
|
+
*/
|
|
362
|
+
function buildHtml(cfg: ResolvedToilConfig): string {
|
|
363
|
+
const templatePath = path.join(cfg.publicDir, 'index.html');
|
|
364
|
+
let html = fs.existsSync(templatePath) ? fs.readFileSync(templatePath, 'utf8') : DEFAULT_HTML;
|
|
365
|
+
// Inject the entry only if the template doesn't already reference it as a module script
|
|
366
|
+
// (matching the literal filename anywhere in the file would be too eager).
|
|
367
|
+
if (!/src=["']\.\/entry\.tsx["']/.test(html)) {
|
|
368
|
+
html = html.includes('</body>')
|
|
369
|
+
? html.replace('</body>', ` ${ENTRY_SCRIPT}\n </body>`)
|
|
370
|
+
: `${html}\n${ENTRY_SCRIPT}\n`;
|
|
371
|
+
}
|
|
372
|
+
return html;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Mirrors the project's `public/` assets into `.toil/public/` (Vite's publicDir under the `.toil`
|
|
377
|
+
* root), excluding the `index.html` template, that is processed into the entry above, and copying
|
|
378
|
+
* it here would clobber the built, asset-hashed page. Cleared each run so deletions propagate.
|
|
379
|
+
*/
|
|
380
|
+
function syncPublicAssets(cfg: ResolvedToilConfig): void {
|
|
381
|
+
const dest = path.join(cfg.toilDir, 'public');
|
|
382
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
383
|
+
if (!fs.existsSync(cfg.publicDir)) return;
|
|
384
|
+
|
|
385
|
+
let copied = 0;
|
|
386
|
+
for (const entry of fs.readdirSync(cfg.publicDir, { withFileTypes: true })) {
|
|
387
|
+
if (entry.name === 'index.html') continue;
|
|
388
|
+
fs.cpSync(path.join(cfg.publicDir, entry.name), path.join(dest, entry.name), {
|
|
389
|
+
recursive: true,
|
|
390
|
+
});
|
|
391
|
+
copied++;
|
|
392
|
+
}
|
|
393
|
+
if (copied === 0) fs.rmSync(dest, { recursive: true, force: true });
|
|
394
|
+
}
|