toiljs 0.0.10 → 0.0.11
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 +313 -1
- package/assets/logo.svg +37 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/create.js +2 -2
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +9 -3
- package/build/compiler/vite.js +7 -0
- package/examples/basic/client/components/Header.tsx +4 -1
- package/examples/basic/client/layout.tsx +4 -1
- package/examples/basic/client/public/index.html +1 -1
- package/examples/basic/client/routes/(legal)/privacy.tsx +19 -0
- package/examples/basic/client/routes/(legal)/terms.tsx +16 -0
- package/examples/basic/client/routes/about.tsx +8 -5
- package/examples/basic/client/routes/blog/[id].tsx +7 -1
- package/examples/basic/client/routes/features/actions.tsx +67 -0
- package/examples/basic/client/routes/features/error/error.tsx +16 -0
- package/examples/basic/client/routes/features/error/index.tsx +27 -0
- package/examples/basic/client/routes/features/head.tsx +38 -0
- package/examples/basic/client/routes/features/index.tsx +75 -0
- package/examples/basic/client/routes/features/realtime.tsx +32 -0
- package/examples/basic/client/routes/features/script.tsx +31 -0
- package/examples/basic/client/routes/features/seo.tsx +39 -0
- package/examples/basic/client/routes/features/template/b.tsx +14 -0
- package/examples/basic/client/routes/features/template/index.tsx +20 -0
- package/examples/basic/client/routes/features/template/template.tsx +18 -0
- package/examples/basic/client/routes/files/[[...slug]].tsx +21 -0
- package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -0
- package/examples/basic/client/routes/gallery/index.tsx +42 -0
- package/examples/basic/client/routes/gallery/layout.tsx +13 -0
- package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -0
- package/examples/basic/client/routes/index.tsx +11 -2
- package/examples/basic/client/routes/loader-demo/index.tsx +6 -4
- package/examples/basic/client/toil.tsx +2 -4
- package/package.json +3 -2
- package/src/cli/create.ts +2 -2
- package/src/compiler/generate.ts +12 -3
- package/src/compiler/vite.ts +15 -0
- package/test/dom/route-head.test.tsx +34 -0
- package/test/slot-layouts.test.ts +69 -0
package/src/compiler/generate.ts
CHANGED
|
@@ -47,6 +47,8 @@ export const TOIL_ENV_DTS =
|
|
|
47
47
|
` type Metadata = import('toiljs/client').Metadata;\n` +
|
|
48
48
|
` type GenerateMetadata<T = unknown> = import('toiljs/client').GenerateMetadata<T>;\n` +
|
|
49
49
|
` type RouteErrorProps = import('toiljs/client').RouteErrorProps;\n` +
|
|
50
|
+
` type Href = import('toiljs/client').Href;\n` +
|
|
51
|
+
` type RoutePath = import('toiljs/client').RoutePath;\n` +
|
|
50
52
|
`}\n` +
|
|
51
53
|
`declare const BinaryWriter: typeof import('toiljs/io').BinaryWriter;\n` +
|
|
52
54
|
`declare const BinaryReader: typeof import('toiljs/io').BinaryReader;\n` +
|
|
@@ -175,15 +177,22 @@ function findSpecialChain(
|
|
|
175
177
|
includeClientRoot: boolean,
|
|
176
178
|
): string[] {
|
|
177
179
|
const chain: string[] = [];
|
|
178
|
-
|
|
180
|
+
const relDir = path.dirname(path.relative(cfg.routesAbsDir, routeFile));
|
|
181
|
+
const segments = relDir === '.' ? [] : relDir.split(path.sep);
|
|
182
|
+
// A parallel-slot route (one under an `@slot` segment) is rendered INTO a parent layout's
|
|
183
|
+
// `<Slot>`. Its own layout/template chain must therefore start at the `@slot` directory, not the
|
|
184
|
+
// routes root: the parent segments' layouts already wrap the slot, so re-including them here
|
|
185
|
+
// would nest the slot inside itself and recurse. For non-slot routes this is the full chain.
|
|
186
|
+
const slotIdx = segments.findIndex((s) => s.startsWith('@'));
|
|
187
|
+
const startAt = slotIdx < 0 ? 0 : slotIdx + 1;
|
|
188
|
+
if (includeClientRoot && slotIdx < 0) {
|
|
179
189
|
const root = specialIn(cfg.clientAbsDir, base);
|
|
180
190
|
if (root) chain.push(root);
|
|
181
191
|
}
|
|
182
|
-
const relDir = path.dirname(path.relative(cfg.routesAbsDir, routeFile));
|
|
183
|
-
const segments = relDir === '.' ? [] : relDir.split(path.sep);
|
|
184
192
|
let dir = cfg.routesAbsDir;
|
|
185
193
|
for (let i = 0; i <= segments.length; i++) {
|
|
186
194
|
if (i > 0) dir = path.join(dir, segments[i - 1]);
|
|
195
|
+
if (i < startAt) continue;
|
|
187
196
|
const found = specialIn(dir, base);
|
|
188
197
|
if (found) chain.push(found);
|
|
189
198
|
}
|
package/src/compiler/vite.ts
CHANGED
|
@@ -13,6 +13,20 @@ import { imageReportPlugin } from './image-report.js';
|
|
|
13
13
|
import { toilPlugin } from './plugin.js';
|
|
14
14
|
import { prerenderPlugin } from './prerender.js';
|
|
15
15
|
|
|
16
|
+
// `vite-plugin-node-polyfills` rewrites react/react-dom to import its `vite-plugin-node-polyfills/
|
|
17
|
+
// shims/*` modules. When a consumer links toiljs by symlink (`file:`/workspace), that package lives
|
|
18
|
+
// only in toiljs's own node_modules, so resolving those bare specifiers from the consumer root
|
|
19
|
+
// fails ("Failed to resolve import vite-plugin-node-polyfills/shims/process"). Alias them to
|
|
20
|
+
// absolute paths resolved from toiljs's location so the build works however toiljs was installed.
|
|
21
|
+
const polyfillPkgRoot = path.dirname(
|
|
22
|
+
path.dirname(createRequire(import.meta.url).resolve('vite-plugin-node-polyfills')),
|
|
23
|
+
);
|
|
24
|
+
const polyfillShimAliases: Record<string, string> = {
|
|
25
|
+
'vite-plugin-node-polyfills/shims/buffer': path.join(polyfillPkgRoot, 'shims/buffer/dist/index.js'),
|
|
26
|
+
'vite-plugin-node-polyfills/shims/global': path.join(polyfillPkgRoot, 'shims/global/dist/index.js'),
|
|
27
|
+
'vite-plugin-node-polyfills/shims/process': path.join(polyfillPkgRoot, 'shims/process/dist/index.js'),
|
|
28
|
+
};
|
|
29
|
+
|
|
16
30
|
/** Image extensions routed to `images/` in the build output. */
|
|
17
31
|
const IMAGE_EXT = /^(png|jpe?g|svg|gif|tiff|bmp|ico|webp|avif)$/i;
|
|
18
32
|
/** Font extensions routed to `fonts/`. */
|
|
@@ -98,6 +112,7 @@ export async function createViteConfig(cfg: ResolvedToilConfig): Promise<InlineC
|
|
|
98
112
|
alias: {
|
|
99
113
|
'toiljs/client': cfg.runtimePath,
|
|
100
114
|
'toiljs/routes': path.join(cfg.toilDir, 'routes.ts'),
|
|
115
|
+
...polyfillShimAliases,
|
|
101
116
|
},
|
|
102
117
|
dedupe: ['react', 'react-dom'],
|
|
103
118
|
},
|
|
@@ -42,4 +42,38 @@ describe('route head (metadata baseline)', () => {
|
|
|
42
42
|
setRouteHead(resolveMetadata({ title: 'About' }));
|
|
43
43
|
expect(document.title).toBe('About · toiljs');
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
// Regression for the "metadata title doesn't update" report: a real layout (title + template)
|
|
47
|
+
// plus a route's full `metadata` (the exact shape users write) must land on the route's title,
|
|
48
|
+
// wrapped by the layout template, with the route's og:title applied too.
|
|
49
|
+
it('applies a full route metadata over a layout title + template', () => {
|
|
50
|
+
function LayoutDefaults() {
|
|
51
|
+
useHead({ titleTemplate: '%s | ToilJS', title: 'ToilJS' });
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
render(<LayoutDefaults />);
|
|
55
|
+
setRouteHead(
|
|
56
|
+
resolveMetadata({
|
|
57
|
+
title: 'useReducer | React Hooks',
|
|
58
|
+
description: 'Manage complex state with a reducer.',
|
|
59
|
+
openGraph: { title: 'useReducer | React Hooks', type: 'website' },
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
expect(document.title).toBe('useReducer | React Hooks | ToilJS');
|
|
63
|
+
expect(
|
|
64
|
+
document.head.querySelector('meta[property="og:title"]')?.getAttribute('content'),
|
|
65
|
+
).toBe('useReducer | React Hooks');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// A route can opt out of the layout's template by setting its own `titleTemplate: '%s'`, so the
|
|
69
|
+
// tab reads exactly the route title with no site suffix.
|
|
70
|
+
it("lets a route override the layout template with its own '%s'", () => {
|
|
71
|
+
function LayoutDefaults() {
|
|
72
|
+
useHead({ titleTemplate: '%s | ToilJS', title: 'ToilJS' });
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
render(<LayoutDefaults />);
|
|
76
|
+
setRouteHead(resolveMetadata({ title: 'useReducer | React Hooks', titleTemplate: '%s' }));
|
|
77
|
+
expect(document.title).toBe('useReducer | React Hooks');
|
|
78
|
+
});
|
|
45
79
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { loadConfig } from '../src/compiler/config';
|
|
8
|
+
import { generate } from '../src/compiler/generate';
|
|
9
|
+
|
|
10
|
+
const roots: string[] = [];
|
|
11
|
+
function project(files: Record<string, string>): string {
|
|
12
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'toil-gen-'));
|
|
13
|
+
roots.push(root);
|
|
14
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
15
|
+
const abs = path.join(root, rel);
|
|
16
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
17
|
+
fs.writeFileSync(abs, content);
|
|
18
|
+
}
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const COMP = `export default function C() { return null; }\n`;
|
|
26
|
+
const LAYOUT = `export default function L({ children }) { return children; }\n`;
|
|
27
|
+
const HTML = `<!doctype html><html><head></head><body><div id="root"></div></body></html>\n`;
|
|
28
|
+
|
|
29
|
+
describe('generate: parallel-slot layout chains', () => {
|
|
30
|
+
it('keeps the parent layout on the full-page route but drops it from the @slot route', async () => {
|
|
31
|
+
const root = project({
|
|
32
|
+
'client/public/index.html': HTML,
|
|
33
|
+
'client/routes/gallery/layout.tsx': LAYOUT,
|
|
34
|
+
'client/routes/gallery/index.tsx': COMP,
|
|
35
|
+
'client/routes/gallery/photo/[id].tsx': COMP,
|
|
36
|
+
'client/routes/gallery/@modal/(.)photo/[id].tsx': COMP,
|
|
37
|
+
});
|
|
38
|
+
const cfg = await loadConfig({ root });
|
|
39
|
+
generate(cfg);
|
|
40
|
+
const lines = fs.readFileSync(path.join(cfg.toilDir, 'routes.ts'), 'utf8').split('\n');
|
|
41
|
+
|
|
42
|
+
// The normal full-page route is wrapped by gallery/layout.
|
|
43
|
+
const mainLine = lines.find((l) => l.includes('photo/[id]') && !l.includes('@modal'));
|
|
44
|
+
expect(mainLine).toMatch(/gallery\/layout/);
|
|
45
|
+
|
|
46
|
+
// The intercepting @modal slot route is rendered INTO gallery/layout's <Slot>, so it must not
|
|
47
|
+
// re-include that layout (doing so recurses, the slot rendering itself forever).
|
|
48
|
+
const slotLine = lines.find((l) => l.includes('@modal'));
|
|
49
|
+
expect(slotLine).toContain('intercept: true');
|
|
50
|
+
expect(slotLine).toMatch(/layouts: \[\]/);
|
|
51
|
+
expect(slotLine).not.toMatch(/gallery\/layout/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('still applies a layout placed inside the @slot subtree', async () => {
|
|
55
|
+
const root = project({
|
|
56
|
+
'client/public/index.html': HTML,
|
|
57
|
+
'client/routes/gallery/layout.tsx': LAYOUT,
|
|
58
|
+
'client/routes/gallery/@modal/layout.tsx': LAYOUT,
|
|
59
|
+
'client/routes/gallery/@modal/(.)photo/[id].tsx': COMP,
|
|
60
|
+
});
|
|
61
|
+
const cfg = await loadConfig({ root });
|
|
62
|
+
generate(cfg);
|
|
63
|
+
const lines = fs.readFileSync(path.join(cfg.toilDir, 'routes.ts'), 'utf8').split('\n');
|
|
64
|
+
const slotLine = lines.find((l) => l.includes('@modal/(.)photo'));
|
|
65
|
+
// The slot's own layout (inside @modal) applies; the parent gallery layout does not.
|
|
66
|
+
expect(slotLine).toMatch(/@modal\/layout/);
|
|
67
|
+
expect(slotLine).not.toMatch(/gallery\/layout/);
|
|
68
|
+
});
|
|
69
|
+
});
|