waku 0.20.2-alpha.0 → 0.20.2
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/dist/cli.js +4 -0
- package/dist/client.d.ts +6 -3
- package/dist/client.js +31 -10
- package/dist/lib/builder/build.d.ts +1 -0
- package/dist/lib/builder/build.js +18 -7
- package/dist/lib/middleware/dev-server.js +1 -5
- package/dist/lib/plugins/vite-plugin-rsc-index.js +1 -9
- package/dist/lib/plugins/vite-plugin-rsc-serve.js +2 -1
- package/dist/lib/renderers/dev-worker-impl.js +4 -11
- package/dist/lib/renderers/rsc-renderer.d.ts +0 -1
- package/dist/lib/renderers/rsc-renderer.js +1 -1
- package/dist/lib/utils/path.js +1 -1
- package/dist/router/client.js +20 -3
- package/dist/router/create-pages.d.ts +1 -1
- package/dist/router/create-pages.js +18 -10
- package/package.json +11 -14
- package/src/cli.ts +4 -0
- package/src/client.ts +36 -12
- package/src/lib/builder/build.ts +22 -1
- package/src/lib/middleware/dev-server.ts +1 -7
- package/src/lib/plugins/vite-plugin-rsc-index.ts +1 -4
- package/src/lib/plugins/vite-plugin-rsc-serve.ts +9 -2
- package/src/lib/renderers/dev-worker-impl.ts +6 -18
- package/src/lib/renderers/rsc-renderer.ts +3 -6
- package/src/lib/utils/path.ts +1 -1
- package/src/router/client.ts +36 -3
- package/src/router/create-pages.ts +20 -12
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,9 @@ const { values, positionals } = parseArgs({
|
|
|
46
46
|
'with-aws-lambda': {
|
|
47
47
|
type: 'boolean'
|
|
48
48
|
},
|
|
49
|
+
'experimental-partial': {
|
|
50
|
+
type: 'boolean'
|
|
51
|
+
},
|
|
49
52
|
port: {
|
|
50
53
|
type: 'string',
|
|
51
54
|
short: 'p'
|
|
@@ -102,6 +105,7 @@ async function runBuild() {
|
|
|
102
105
|
await build({
|
|
103
106
|
config,
|
|
104
107
|
env: process.env,
|
|
108
|
+
partial: !!values['experimental-partial'],
|
|
105
109
|
deploy: (values['with-vercel'] ?? !!process.env.VERCEL ? values['with-vercel-static'] ? 'vercel-static' : 'vercel-serverless' : undefined) || (values['with-netlify'] ?? !!process.env.NETLIFY ? values['with-netlify-static'] ? 'netlify-static' : 'netlify-functions' : undefined) || (values['with-cloudflare'] ? 'cloudflare' : undefined) || (values['with-partykit'] ? 'partykit' : undefined) || (values['with-deno'] ? 'deno' : undefined) || (values['with-aws-lambda'] ? 'aws-lambda' : undefined)
|
|
106
110
|
});
|
|
107
111
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ declare global {
|
|
|
5
5
|
readonly env: Record<string, string>;
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
|
-
type Elements = Promise<Record<string, ReactNode
|
|
9
|
-
|
|
8
|
+
type Elements = Promise<Record<string, ReactNode>> & {
|
|
9
|
+
prev?: Record<string, ReactNode> | undefined;
|
|
10
|
+
};
|
|
11
|
+
type SetElements = (updater: (prev: Elements) => Elements) => void;
|
|
10
12
|
type CacheEntry = [
|
|
11
13
|
input: string,
|
|
12
14
|
searchParamsString: string,
|
|
@@ -24,10 +26,11 @@ export declare const Root: ({ initialInput, initialSearchParamsString, cache, un
|
|
|
24
26
|
children: ReactNode;
|
|
25
27
|
}) => import("react").FunctionComponentElement<import("react").ProviderProps<(input: string, searchParams?: URLSearchParams) => void>>;
|
|
26
28
|
export declare const useRefetch: () => (input: string, searchParams?: URLSearchParams) => void;
|
|
27
|
-
export declare const Slot: ({ id, children, fallback, }: {
|
|
29
|
+
export declare const Slot: ({ id, children, fallback, unstable_shouldRenderPrev, }: {
|
|
28
30
|
id: string;
|
|
29
31
|
children?: ReactNode;
|
|
30
32
|
fallback?: ReactNode;
|
|
33
|
+
unstable_shouldRenderPrev?: (err: unknown) => boolean;
|
|
31
34
|
}) => string | number | bigint | true | import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | Promise<import("react").AwaitedReactNode> | import("react").FunctionComponentElement<import("react").ProviderProps<ReactNode>>;
|
|
32
35
|
export declare const Children: () => ReactNode;
|
|
33
36
|
/**
|
package/dist/client.js
CHANGED
|
@@ -17,13 +17,30 @@ const checkStatus = async (responsePromise)=>{
|
|
|
17
17
|
const getCached = (c, m, k)=>(m.has(k) ? m : m.set(k, c())).get(k);
|
|
18
18
|
const cache1 = new WeakMap();
|
|
19
19
|
const mergeElements = (a, b)=>{
|
|
20
|
-
const getResult =
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
const getResult = ()=>{
|
|
21
|
+
const promise = new Promise((resolve, reject)=>{
|
|
22
|
+
Promise.all([
|
|
23
|
+
a,
|
|
24
|
+
b
|
|
25
|
+
]).then(([a, b])=>{
|
|
26
|
+
const nextElements = {
|
|
27
|
+
...a,
|
|
28
|
+
...b
|
|
29
|
+
};
|
|
30
|
+
delete nextElements._value;
|
|
31
|
+
promise.prev = a;
|
|
32
|
+
resolve(nextElements);
|
|
33
|
+
}).catch((e)=>{
|
|
34
|
+
a.then((a)=>{
|
|
35
|
+
promise.prev = a;
|
|
36
|
+
reject(e);
|
|
37
|
+
}, ()=>{
|
|
38
|
+
promise.prev = a.prev;
|
|
39
|
+
reject(e);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return promise;
|
|
27
44
|
};
|
|
28
45
|
const cache2 = getCached(()=>new WeakMap(), cache1, a);
|
|
29
46
|
return getCached(getResult, cache2, b);
|
|
@@ -96,7 +113,7 @@ export const Root = ({ initialInput, initialSearchParamsString, cache, unstable_
|
|
|
96
113
|
export const useRefetch = ()=>use(RefetchContext);
|
|
97
114
|
const ChildrenContext = createContext(undefined);
|
|
98
115
|
const ChildrenContextProvider = memo(ChildrenContext.Provider);
|
|
99
|
-
export const Slot = ({ id, children, fallback })=>{
|
|
116
|
+
export const Slot = ({ id, children, fallback, unstable_shouldRenderPrev })=>{
|
|
100
117
|
const elementsPromise = use(ElementsContext);
|
|
101
118
|
if (!elementsPromise) {
|
|
102
119
|
throw new Error('Missing Root component');
|
|
@@ -105,12 +122,16 @@ export const Slot = ({ id, children, fallback })=>{
|
|
|
105
122
|
try {
|
|
106
123
|
elements = use(elementsPromise);
|
|
107
124
|
} catch (e) {
|
|
108
|
-
if (e instanceof Error) {
|
|
125
|
+
if (e instanceof Error && !('statusCode' in e)) {
|
|
109
126
|
// HACK we assume any error as Not Found,
|
|
110
127
|
// probably caused by history api fallback
|
|
111
128
|
e.statusCode = 404;
|
|
112
129
|
}
|
|
113
|
-
|
|
130
|
+
if (unstable_shouldRenderPrev?.(e) && elementsPromise.prev) {
|
|
131
|
+
elements = elementsPromise.prev;
|
|
132
|
+
} else {
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
114
135
|
}
|
|
115
136
|
if (!(id in elements)) {
|
|
116
137
|
if (fallback) {
|
|
@@ -2,5 +2,6 @@ import type { Config } from '../../config.js';
|
|
|
2
2
|
export declare function build(options: {
|
|
3
3
|
config: Config;
|
|
4
4
|
env?: Record<string, string>;
|
|
5
|
+
partial?: boolean;
|
|
5
6
|
deploy?: 'vercel-static' | 'vercel-serverless' | 'netlify-static' | 'netlify-functions' | 'cloudflare' | 'partykit' | 'deno' | 'aws-lambda' | undefined;
|
|
6
7
|
}): Promise<void>;
|
|
@@ -105,7 +105,7 @@ const analyzeEntries = async (rootDir, config)=>{
|
|
|
105
105
|
};
|
|
106
106
|
};
|
|
107
107
|
// For RSC
|
|
108
|
-
const buildServerBundle = async (rootDir, config, clientEntryFiles, serverEntryFiles, serverModuleFiles, serve, isNodeCompatible)=>{
|
|
108
|
+
const buildServerBundle = async (rootDir, config, clientEntryFiles, serverEntryFiles, serverModuleFiles, serve, isNodeCompatible, partial)=>{
|
|
109
109
|
const serverBuildOutput = await buildVite({
|
|
110
110
|
plugins: [
|
|
111
111
|
nonjsResolvePlugin(),
|
|
@@ -189,6 +189,7 @@ const buildServerBundle = async (rootDir, config, clientEntryFiles, serverEntryF
|
|
|
189
189
|
},
|
|
190
190
|
publicDir: false,
|
|
191
191
|
build: {
|
|
192
|
+
emptyOutDir: !partial,
|
|
192
193
|
ssr: true,
|
|
193
194
|
ssrEmitAssets: true,
|
|
194
195
|
target: 'node18',
|
|
@@ -210,7 +211,7 @@ const buildServerBundle = async (rootDir, config, clientEntryFiles, serverEntryF
|
|
|
210
211
|
return serverBuildOutput;
|
|
211
212
|
};
|
|
212
213
|
// For SSR (render client components on server to generate HTML)
|
|
213
|
-
const buildSsrBundle = async (rootDir, config, clientEntryFiles, serverBuildOutput, isNodeCompatible)=>{
|
|
214
|
+
const buildSsrBundle = async (rootDir, config, clientEntryFiles, serverBuildOutput, isNodeCompatible, partial)=>{
|
|
214
215
|
const cssAssets = serverBuildOutput.output.flatMap(({ type, fileName })=>type === 'asset' && fileName.endsWith('.css') ? [
|
|
215
216
|
fileName
|
|
216
217
|
] : []);
|
|
@@ -252,6 +253,7 @@ const buildSsrBundle = async (rootDir, config, clientEntryFiles, serverBuildOutp
|
|
|
252
253
|
},
|
|
253
254
|
publicDir: false,
|
|
254
255
|
build: {
|
|
256
|
+
emptyOutDir: !partial,
|
|
255
257
|
ssr: true,
|
|
256
258
|
target: 'node18',
|
|
257
259
|
outDir: joinPath(rootDir, config.distDir, DIST_SSR),
|
|
@@ -274,7 +276,7 @@ const buildSsrBundle = async (rootDir, config, clientEntryFiles, serverBuildOutp
|
|
|
274
276
|
});
|
|
275
277
|
};
|
|
276
278
|
// For Browsers
|
|
277
|
-
const buildClientBundle = async (rootDir, config, clientEntryFiles, serverBuildOutput)=>{
|
|
279
|
+
const buildClientBundle = async (rootDir, config, clientEntryFiles, serverBuildOutput, partial)=>{
|
|
278
280
|
const nonJsAssets = serverBuildOutput.output.flatMap(({ type, fileName })=>type === 'asset' && !fileName.endsWith('.js') ? [
|
|
279
281
|
fileName
|
|
280
282
|
] : []);
|
|
@@ -297,6 +299,7 @@ const buildClientBundle = async (rootDir, config, clientEntryFiles, serverBuildO
|
|
|
297
299
|
})
|
|
298
300
|
],
|
|
299
301
|
build: {
|
|
302
|
+
emptyOutDir: !partial,
|
|
300
303
|
outDir: joinPath(rootDir, config.distDir, DIST_PUBLIC),
|
|
301
304
|
rollupOptions: {
|
|
302
305
|
onwarn,
|
|
@@ -349,6 +352,10 @@ const emitRscFiles = async (rootDir, config, distEntries, buildConfig)=>{
|
|
|
349
352
|
}
|
|
350
353
|
staticInputSet.add(input);
|
|
351
354
|
const destRscFile = joinPath(rootDir, config.distDir, DIST_PUBLIC, config.rscPath, encodeInput(input));
|
|
355
|
+
// Skip if the file already exists.
|
|
356
|
+
if (existsSync(destRscFile)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
352
359
|
await mkdir(joinPath(destRscFile, '..'), {
|
|
353
360
|
recursive: true
|
|
354
361
|
});
|
|
@@ -398,7 +405,7 @@ const emitHtmlFiles = async (rootDir, config, distEntriesFile, distEntries, buil
|
|
|
398
405
|
let htmlStr = publicIndexHtml;
|
|
399
406
|
let htmlHead = publicIndexHtmlHead;
|
|
400
407
|
if (cssAssets.length) {
|
|
401
|
-
const cssStr = cssAssets.map((asset)=>`<link rel="stylesheet" href="${asset}">`).join('\n');
|
|
408
|
+
const cssStr = cssAssets.map((asset)=>`<link rel="stylesheet" href="${config.basePath}${asset}">`).join('\n');
|
|
402
409
|
// HACK is this too naive to inject style code?
|
|
403
410
|
htmlStr = htmlStr.replace(/<\/head>/, cssStr);
|
|
404
411
|
htmlHead += cssStr;
|
|
@@ -426,6 +433,10 @@ const emitHtmlFiles = async (rootDir, config, distEntriesFile, distEntries, buil
|
|
|
426
433
|
pathname = pathSpec2pathname(pathSpec);
|
|
427
434
|
const destHtmlFile = joinPath(rootDir, config.distDir, DIST_PUBLIC, extname(pathname) ? pathname : pathname === '/404' ? '404.html' // HACK special treatment for 404, better way?
|
|
428
435
|
: pathname + '/index.html');
|
|
436
|
+
// In partial mode, skip if the file already exists.
|
|
437
|
+
if (existsSync(destHtmlFile)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
429
440
|
const htmlReadable = await renderHtml({
|
|
430
441
|
config,
|
|
431
442
|
pathname,
|
|
@@ -475,9 +486,9 @@ export async function build(options) {
|
|
|
475
486
|
const distEntriesFile = joinPath(rootDir, config.distDir, DIST_ENTRIES_JS);
|
|
476
487
|
const isNodeCompatible = options.deploy !== 'cloudflare' && options.deploy !== 'partykit' && options.deploy !== 'deno';
|
|
477
488
|
const { clientEntryFiles, serverEntryFiles, serverModuleFiles } = await analyzeEntries(rootDir, config);
|
|
478
|
-
const serverBuildOutput = await buildServerBundle(rootDir, config, clientEntryFiles, serverEntryFiles, serverModuleFiles, (options.deploy === 'vercel-serverless' ? 'vercel' : false) || (options.deploy === 'netlify-functions' ? 'netlify' : false) || (options.deploy === 'cloudflare' ? 'cloudflare' : false) || (options.deploy === 'partykit' ? 'partykit' : false) || (options.deploy === 'deno' ? 'deno' : false) || (options.deploy === 'aws-lambda' ? 'aws-lambda' : false), isNodeCompatible);
|
|
479
|
-
await buildSsrBundle(rootDir, config, clientEntryFiles, serverBuildOutput, isNodeCompatible);
|
|
480
|
-
const clientBuildOutput = await buildClientBundle(rootDir, config, clientEntryFiles, serverBuildOutput);
|
|
489
|
+
const serverBuildOutput = await buildServerBundle(rootDir, config, clientEntryFiles, serverEntryFiles, serverModuleFiles, (options.deploy === 'vercel-serverless' ? 'vercel' : false) || (options.deploy === 'netlify-functions' ? 'netlify' : false) || (options.deploy === 'cloudflare' ? 'cloudflare' : false) || (options.deploy === 'partykit' ? 'partykit' : false) || (options.deploy === 'deno' ? 'deno' : false) || (options.deploy === 'aws-lambda' ? 'aws-lambda' : false), isNodeCompatible, !!options.partial);
|
|
490
|
+
await buildSsrBundle(rootDir, config, clientEntryFiles, serverBuildOutput, isNodeCompatible, !!options.partial);
|
|
491
|
+
const clientBuildOutput = await buildClientBundle(rootDir, config, clientEntryFiles, serverBuildOutput, !!options.partial);
|
|
481
492
|
const distEntries = await import(filePathToFileURL(distEntriesFile));
|
|
482
493
|
// TODO: Add progress indication for static builds.
|
|
483
494
|
const buildConfig = await getBuildConfig({
|
|
@@ -71,14 +71,6 @@ ${opts.htmlHead}
|
|
|
71
71
|
},
|
|
72
72
|
transformIndexHtml () {
|
|
73
73
|
return [
|
|
74
|
-
// HACK without <base>, some relative assets don't work.
|
|
75
|
-
// FIXME ideally, we should avoid this.
|
|
76
|
-
{
|
|
77
|
-
tag: 'base',
|
|
78
|
-
attrs: {
|
|
79
|
-
href: opts.basePath
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
74
|
{
|
|
83
75
|
tag: 'script',
|
|
84
76
|
attrs: {
|
|
@@ -91,7 +83,7 @@ ${opts.htmlHead}
|
|
|
91
83
|
tag: 'link',
|
|
92
84
|
attrs: {
|
|
93
85
|
rel: 'stylesheet',
|
|
94
|
-
href
|
|
86
|
+
href: `${opts.basePath}${href}`
|
|
95
87
|
},
|
|
96
88
|
injectTo: 'head'
|
|
97
89
|
}))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { normalizePath } from 'vite';
|
|
3
4
|
// HACK: Depending on a different plugin isn't ideal.
|
|
4
5
|
// Maybe we could put in vite config object?
|
|
5
6
|
import { SRC_ENTRIES } from './vite-plugin-rsc-managed.js';
|
|
@@ -19,7 +20,7 @@ export function rscServePlugin(opts) {
|
|
|
19
20
|
name: 'rsc-serve-plugin',
|
|
20
21
|
config (viteConfig) {
|
|
21
22
|
// FIXME This seems too hacky (The use of viteConfig.root, '.', path.resolve and resolveFileName)
|
|
22
|
-
const entriesFile = resolveFileName(path.resolve(viteConfig.root || '.', opts.srcDir, SRC_ENTRIES + '.
|
|
23
|
+
const entriesFile = normalizePath(resolveFileName(path.resolve(viteConfig.root || '.', opts.srcDir, SRC_ENTRIES + '.jsx')));
|
|
23
24
|
const { input } = viteConfig.build?.rollupOptions ?? {};
|
|
24
25
|
if (input && !(typeof input === 'string') && !(input instanceof Array)) {
|
|
25
26
|
input[opts.distServeJs.replace(/\.js$/, '')] = opts.srcServeFile;
|
|
@@ -5,7 +5,7 @@ import { Server } from 'node:http';
|
|
|
5
5
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
6
|
import { createServer as createViteServer } from 'vite';
|
|
7
7
|
import viteReact from '@vitejs/plugin-react';
|
|
8
|
-
import { joinPath, fileURLToFilePath, encodeFilePathToAbsolute } from '../utils/path.js';
|
|
8
|
+
import { joinPath, fileURLToFilePath, encodeFilePathToAbsolute, decodeFilePathFromAbsolute } from '../utils/path.js';
|
|
9
9
|
import { deepFreeze, hasStatusCode } from './utils.js';
|
|
10
10
|
import { renderRsc, getSsrConfig } from './rsc-renderer.js';
|
|
11
11
|
import { nonjsResolvePlugin } from '../plugins/vite-plugin-nonjs-resolve.js';
|
|
@@ -29,12 +29,12 @@ const configSrcDir = getEnvironmentData('CONFIG_SRC_DIR');
|
|
|
29
29
|
const configEntries = getEnvironmentData('CONFIG_ENTRIES');
|
|
30
30
|
const configPrivateDir = getEnvironmentData('CONFIG_PRIVATE_DIR');
|
|
31
31
|
const resolveClientEntryForDev = (id, config, initialModules)=>{
|
|
32
|
+
let file = id.startsWith('file://') ? decodeFilePathFromAbsolute(fileURLToFilePath(id)) : id;
|
|
32
33
|
for (const moduleNode of initialModules){
|
|
33
|
-
if (moduleNode.file ===
|
|
34
|
+
if (moduleNode.file === file) {
|
|
34
35
|
return moduleNode.url;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
let file = id.startsWith('file://') ? fileURLToFilePath(id) : id;
|
|
38
38
|
if (file.startsWith(config.rootDir)) {
|
|
39
39
|
file = file.slice(config.rootDir.length + 1); // '+ 1' to remove '/'
|
|
40
40
|
} else {
|
|
@@ -69,7 +69,6 @@ const handleRender = async (mesg)=>{
|
|
|
69
69
|
}, {
|
|
70
70
|
isDev: true,
|
|
71
71
|
loadServerFile,
|
|
72
|
-
loadServerModule,
|
|
73
72
|
resolveClientEntry: (id)=>resolveClientEntryForDev(id, {
|
|
74
73
|
rootDir: vite.config.root,
|
|
75
74
|
basePath: rest.config.basePath
|
|
@@ -191,9 +190,7 @@ const mergedViteConfig = await mergeUserViteConfig({
|
|
|
191
190
|
'workerd'
|
|
192
191
|
]
|
|
193
192
|
},
|
|
194
|
-
external: [
|
|
195
|
-
// FIXME We want to externalize waku, but it fails on windows.
|
|
196
|
-
noExternal: [
|
|
193
|
+
external: [
|
|
197
194
|
'waku'
|
|
198
195
|
]
|
|
199
196
|
},
|
|
@@ -214,10 +211,6 @@ const loadServerFile = async (fileURL)=>{
|
|
|
214
211
|
const vite = await vitePromise;
|
|
215
212
|
return vite.ssrLoadModule(fileURLToFilePath(fileURL));
|
|
216
213
|
};
|
|
217
|
-
const loadServerModule = async (id)=>{
|
|
218
|
-
const vite = await vitePromise;
|
|
219
|
-
return vite.ssrLoadModule(id);
|
|
220
|
-
};
|
|
221
214
|
const loadEntries = async (config)=>{
|
|
222
215
|
const vite = await vitePromise;
|
|
223
216
|
const filePath = joinPath(vite.config.root, config.srcDir, configEntries);
|
|
@@ -21,7 +21,6 @@ type RenderRscOpts = {
|
|
|
21
21
|
isDev: true;
|
|
22
22
|
entries: EntriesDev;
|
|
23
23
|
loadServerFile: (fileURL: string) => Promise<unknown>;
|
|
24
|
-
loadServerModule: (id: string) => Promise<unknown>;
|
|
25
24
|
resolveClientEntry: (id: string) => string;
|
|
26
25
|
};
|
|
27
26
|
export declare function renderRsc(args: RenderRscArgs, opts: RenderRscOpts): Promise<ReadableStream>;
|
|
@@ -20,7 +20,7 @@ export async function renderRsc(args, opts) {
|
|
|
20
20
|
const loadServerModule = (key)=>isDev ? import(/* @vite-ignore */ SERVER_MODULE_MAP[key]) : loadModule(key);
|
|
21
21
|
const [{ default: { renderToReadableStream, decodeReply } }, { runWithRenderStore }] = await Promise.all([
|
|
22
22
|
loadServerModule('rsdw-server'),
|
|
23
|
-
|
|
23
|
+
loadServerModule('waku-server')
|
|
24
24
|
]);
|
|
25
25
|
const bundlerConfig = new Proxy({}, {
|
|
26
26
|
get (_target, encodedId) {
|
package/dist/lib/utils/path.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const ABSOLUTE_WIN32_PATH_REGEXP = /^\/[a-zA-Z]:\//;
|
|
9
9
|
export const encodeFilePathToAbsolute = (filePath)=>{
|
|
10
10
|
if (ABSOLUTE_WIN32_PATH_REGEXP.test(filePath)) {
|
|
11
|
-
throw new Error('Unsupported absolute file path');
|
|
11
|
+
throw new Error('Unsupported absolute file path: ' + filePath);
|
|
12
12
|
}
|
|
13
13
|
if (filePath.startsWith('/')) {
|
|
14
14
|
return filePath;
|
package/dist/router/client.js
CHANGED
|
@@ -193,7 +193,21 @@ const equalRouteProps = (a, b)=>{
|
|
|
193
193
|
}
|
|
194
194
|
return true;
|
|
195
195
|
};
|
|
196
|
-
|
|
196
|
+
const RouterSlot = ({ route, routerData, cachedRef, id, fallback, children })=>{
|
|
197
|
+
const unstable_shouldRenderPrev = (_err)=>{
|
|
198
|
+
const shouldSkip = routerData[0];
|
|
199
|
+
const skip = getSkipList(shouldSkip, [
|
|
200
|
+
id
|
|
201
|
+
], route, cachedRef.current);
|
|
202
|
+
return skip.length > 0;
|
|
203
|
+
};
|
|
204
|
+
return createElement(Slot, {
|
|
205
|
+
id,
|
|
206
|
+
fallback,
|
|
207
|
+
unstable_shouldRenderPrev
|
|
208
|
+
}, children);
|
|
209
|
+
};
|
|
210
|
+
const InnerRouter = ({ routerData })=>{
|
|
197
211
|
const refetch = useRefetch();
|
|
198
212
|
const [route, setRoute] = useState(()=>parseRoute(new URL(window.location.href)));
|
|
199
213
|
const componentIds = getComponentIds(route.path);
|
|
@@ -314,7 +328,10 @@ function InnerRouter({ routerData }) {
|
|
|
314
328
|
behavior: state?.waku_new_path ? 'instant' : 'auto'
|
|
315
329
|
});
|
|
316
330
|
});
|
|
317
|
-
const children = componentIds.reduceRight((acc, id)=>createElement(
|
|
331
|
+
const children = componentIds.reduceRight((acc, id)=>createElement(RouterSlot, {
|
|
332
|
+
route,
|
|
333
|
+
routerData,
|
|
334
|
+
cachedRef,
|
|
318
335
|
id,
|
|
319
336
|
fallback: acc
|
|
320
337
|
}, acc), null);
|
|
@@ -325,7 +342,7 @@ function InnerRouter({ routerData }) {
|
|
|
325
342
|
prefetchRoute
|
|
326
343
|
}
|
|
327
344
|
}, children);
|
|
328
|
-
}
|
|
345
|
+
};
|
|
329
346
|
const DEFAULT_ROUTER_DATA = [];
|
|
330
347
|
export function Router({ routerData = DEFAULT_ROUTER_DATA }) {
|
|
331
348
|
const route = parseRoute(new URL(window.location.href));
|
|
@@ -13,7 +13,7 @@ export type CreatePage = <Path extends string, SlugKey extends string, WildSlugK
|
|
|
13
13
|
component: FunctionComponent<RouteProps>;
|
|
14
14
|
} | {
|
|
15
15
|
render: 'static';
|
|
16
|
-
path:
|
|
16
|
+
path: PathWithWildcard<Path, SlugKey, WildSlugKey>;
|
|
17
17
|
staticPaths: string[] | string[][];
|
|
18
18
|
component: FunctionComponent<RouteProps & Record<SlugKey, string>>;
|
|
19
19
|
} | {
|
|
@@ -42,25 +42,33 @@ export function createPages(fn) {
|
|
|
42
42
|
]);
|
|
43
43
|
const id = joinPath(page.path, 'page').replace(/^\//, '');
|
|
44
44
|
registerStaticComponent(id, page.component);
|
|
45
|
-
} else if (page.render === 'static' && numSlugs > 0
|
|
45
|
+
} else if (page.render === 'static' && numSlugs > 0) {
|
|
46
46
|
const staticPaths = page.staticPaths.map((item)=>Array.isArray(item) ? item : [
|
|
47
47
|
item
|
|
48
48
|
]);
|
|
49
49
|
for (const staticPath of staticPaths){
|
|
50
|
-
if (staticPath.length !== numSlugs) {
|
|
50
|
+
if (staticPath.length !== numSlugs && numWildcards === 0) {
|
|
51
51
|
throw new Error('staticPaths does not match with slug pattern');
|
|
52
52
|
}
|
|
53
53
|
const mapping = {};
|
|
54
54
|
let slugIndex = 0;
|
|
55
|
-
const pathItems =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
const pathItems = [];
|
|
56
|
+
pathSpec.forEach(({ type, name })=>{
|
|
57
|
+
switch(type){
|
|
58
|
+
case 'literal':
|
|
59
|
+
pathItems.push(name);
|
|
60
|
+
break;
|
|
61
|
+
case 'wildcard':
|
|
62
|
+
mapping[name] = staticPath.slice(slugIndex);
|
|
63
|
+
staticPath.slice(slugIndex++).forEach((slug)=>{
|
|
64
|
+
pathItems.push(slug);
|
|
65
|
+
});
|
|
66
|
+
break;
|
|
67
|
+
case 'group':
|
|
68
|
+
pathItems.push(staticPath[slugIndex++]);
|
|
69
|
+
mapping[name] = pathItems[pathItems.length - 1];
|
|
70
|
+
break;
|
|
62
71
|
}
|
|
63
|
-
return name;
|
|
64
72
|
});
|
|
65
73
|
staticPathSet.add([
|
|
66
74
|
page.path,
|
package/package.json
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waku",
|
|
3
3
|
"description": "⛩️ The minimal React framework",
|
|
4
|
-
"version": "0.20.2
|
|
5
|
-
"publishConfig": {
|
|
6
|
-
"tag": "next"
|
|
7
|
-
},
|
|
4
|
+
"version": "0.20.2",
|
|
8
5
|
"type": "module",
|
|
9
6
|
"author": "Daishi Kato",
|
|
10
7
|
"homepage": "https://waku.gg",
|
|
@@ -70,25 +67,25 @@
|
|
|
70
67
|
"node": "^20.8.0 || ^18.17.0"
|
|
71
68
|
},
|
|
72
69
|
"dependencies": {
|
|
73
|
-
"@hono/node-server": "1.
|
|
74
|
-
"@swc/core": "1.4.
|
|
70
|
+
"@hono/node-server": "1.11.1",
|
|
71
|
+
"@swc/core": "1.4.17",
|
|
75
72
|
"@vitejs/plugin-react": "4.2.1",
|
|
76
73
|
"dotenv": "16.4.5",
|
|
77
|
-
"hono": "4.
|
|
74
|
+
"hono": "4.3.2",
|
|
78
75
|
"rsc-html-stream": "0.0.3",
|
|
79
|
-
"vite": "5.2.
|
|
76
|
+
"vite": "5.2.11"
|
|
80
77
|
},
|
|
81
78
|
"devDependencies": {
|
|
82
|
-
"@netlify/functions": "^2.6.
|
|
79
|
+
"@netlify/functions": "^2.6.3",
|
|
83
80
|
"@swc/cli": "^0.3.12",
|
|
84
|
-
"rollup": "^4.
|
|
81
|
+
"rollup": "^4.17.2",
|
|
85
82
|
"ts-expect": "^1.3.0",
|
|
86
|
-
"vitest": "^1.
|
|
83
|
+
"vitest": "^1.6.0"
|
|
87
84
|
},
|
|
88
85
|
"peerDependencies": {
|
|
89
|
-
"react": "19.0.0-beta-
|
|
90
|
-
"react-dom": "19.0.0-beta-
|
|
91
|
-
"react-server-dom-webpack": "19.0.0-beta-
|
|
86
|
+
"react": "19.0.0-beta-e7d213dfb0-20240507",
|
|
87
|
+
"react-dom": "19.0.0-beta-e7d213dfb0-20240507",
|
|
88
|
+
"react-server-dom-webpack": "19.0.0-beta-e7d213dfb0-20240507"
|
|
92
89
|
},
|
|
93
90
|
"scripts": {
|
|
94
91
|
"dev": "swc src -d dist -w --strip-leading-paths",
|
package/src/cli.ts
CHANGED
|
@@ -47,6 +47,9 @@ const { values, positionals } = parseArgs({
|
|
|
47
47
|
'with-aws-lambda': {
|
|
48
48
|
type: 'boolean',
|
|
49
49
|
},
|
|
50
|
+
'experimental-partial': {
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
},
|
|
50
53
|
port: {
|
|
51
54
|
type: 'string',
|
|
52
55
|
short: 'p',
|
|
@@ -103,6 +106,7 @@ async function runBuild() {
|
|
|
103
106
|
await build({
|
|
104
107
|
config,
|
|
105
108
|
env: process.env as any,
|
|
109
|
+
partial: !!values['experimental-partial'],
|
|
106
110
|
deploy:
|
|
107
111
|
(values['with-vercel'] ?? !!process.env.VERCEL
|
|
108
112
|
? values['with-vercel-static']
|
package/src/client.ts
CHANGED
|
@@ -39,25 +39,43 @@ const checkStatus = async (
|
|
|
39
39
|
return response;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
type Elements = Promise<Record<string, ReactNode
|
|
42
|
+
type Elements = Promise<Record<string, ReactNode>> & {
|
|
43
|
+
prev?: Record<string, ReactNode> | undefined;
|
|
44
|
+
};
|
|
43
45
|
|
|
44
46
|
const getCached = <T>(c: () => T, m: WeakMap<object, T>, k: object): T =>
|
|
45
47
|
(m.has(k) ? m : m.set(k, c())).get(k) as T;
|
|
46
48
|
const cache1 = new WeakMap();
|
|
47
|
-
const mergeElements = (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
const mergeElements = (a: Elements, b: Elements): Elements => {
|
|
50
|
+
const getResult = () => {
|
|
51
|
+
const promise: Elements = new Promise((resolve, reject) => {
|
|
52
|
+
Promise.all([a, b])
|
|
53
|
+
.then(([a, b]) => {
|
|
54
|
+
const nextElements = { ...a, ...b };
|
|
55
|
+
delete nextElements._value;
|
|
56
|
+
promise.prev = a;
|
|
57
|
+
resolve(nextElements);
|
|
58
|
+
})
|
|
59
|
+
.catch((e) => {
|
|
60
|
+
a.then(
|
|
61
|
+
(a) => {
|
|
62
|
+
promise.prev = a;
|
|
63
|
+
reject(e);
|
|
64
|
+
},
|
|
65
|
+
() => {
|
|
66
|
+
promise.prev = a.prev;
|
|
67
|
+
reject(e);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return promise;
|
|
55
73
|
};
|
|
56
74
|
const cache2 = getCached(() => new WeakMap(), cache1, a);
|
|
57
75
|
return getCached(getResult, cache2, b);
|
|
58
76
|
};
|
|
59
77
|
|
|
60
|
-
type SetElements = (updater:
|
|
78
|
+
type SetElements = (updater: (prev: Elements) => Elements) => void;
|
|
61
79
|
type CacheEntry = [
|
|
62
80
|
input: string,
|
|
63
81
|
searchParamsString: string,
|
|
@@ -191,10 +209,12 @@ export const Slot = ({
|
|
|
191
209
|
id,
|
|
192
210
|
children,
|
|
193
211
|
fallback,
|
|
212
|
+
unstable_shouldRenderPrev,
|
|
194
213
|
}: {
|
|
195
214
|
id: string;
|
|
196
215
|
children?: ReactNode;
|
|
197
216
|
fallback?: ReactNode;
|
|
217
|
+
unstable_shouldRenderPrev?: (err: unknown) => boolean;
|
|
198
218
|
}) => {
|
|
199
219
|
const elementsPromise = use(ElementsContext);
|
|
200
220
|
if (!elementsPromise) {
|
|
@@ -204,12 +224,16 @@ export const Slot = ({
|
|
|
204
224
|
try {
|
|
205
225
|
elements = use(elementsPromise);
|
|
206
226
|
} catch (e) {
|
|
207
|
-
if (e instanceof Error) {
|
|
227
|
+
if (e instanceof Error && !('statusCode' in e)) {
|
|
208
228
|
// HACK we assume any error as Not Found,
|
|
209
229
|
// probably caused by history api fallback
|
|
210
230
|
(e as any).statusCode = 404;
|
|
211
231
|
}
|
|
212
|
-
|
|
232
|
+
if (unstable_shouldRenderPrev?.(e) && elementsPromise.prev) {
|
|
233
|
+
elements = elementsPromise.prev;
|
|
234
|
+
} else {
|
|
235
|
+
throw e;
|
|
236
|
+
}
|
|
213
237
|
}
|
|
214
238
|
if (!(id in elements)) {
|
|
215
239
|
if (fallback) {
|
package/src/lib/builder/build.ts
CHANGED
|
@@ -165,6 +165,7 @@ const buildServerBundle = async (
|
|
|
165
165
|
| 'aws-lambda'
|
|
166
166
|
| false,
|
|
167
167
|
isNodeCompatible: boolean,
|
|
168
|
+
partial: boolean,
|
|
168
169
|
) => {
|
|
169
170
|
const serverBuildOutput = await buildVite({
|
|
170
171
|
plugins: [
|
|
@@ -247,6 +248,7 @@ const buildServerBundle = async (
|
|
|
247
248
|
},
|
|
248
249
|
publicDir: false,
|
|
249
250
|
build: {
|
|
251
|
+
emptyOutDir: !partial,
|
|
250
252
|
ssr: true,
|
|
251
253
|
ssrEmitAssets: true,
|
|
252
254
|
target: 'node18',
|
|
@@ -275,6 +277,7 @@ const buildSsrBundle = async (
|
|
|
275
277
|
clientEntryFiles: Record<string, string>,
|
|
276
278
|
serverBuildOutput: Awaited<ReturnType<typeof buildServerBundle>>,
|
|
277
279
|
isNodeCompatible: boolean,
|
|
280
|
+
partial: boolean,
|
|
278
281
|
) => {
|
|
279
282
|
const cssAssets = serverBuildOutput.output.flatMap(({ type, fileName }) =>
|
|
280
283
|
type === 'asset' && fileName.endsWith('.css') ? [fileName] : [],
|
|
@@ -310,6 +313,7 @@ const buildSsrBundle = async (
|
|
|
310
313
|
},
|
|
311
314
|
publicDir: false,
|
|
312
315
|
build: {
|
|
316
|
+
emptyOutDir: !partial,
|
|
313
317
|
ssr: true,
|
|
314
318
|
target: 'node18',
|
|
315
319
|
outDir: joinPath(rootDir, config.distDir, DIST_SSR),
|
|
@@ -343,6 +347,7 @@ const buildClientBundle = async (
|
|
|
343
347
|
config: ResolvedConfig,
|
|
344
348
|
clientEntryFiles: Record<string, string>,
|
|
345
349
|
serverBuildOutput: Awaited<ReturnType<typeof buildServerBundle>>,
|
|
350
|
+
partial: boolean,
|
|
346
351
|
) => {
|
|
347
352
|
const nonJsAssets = serverBuildOutput.output.flatMap(({ type, fileName }) =>
|
|
348
353
|
type === 'asset' && !fileName.endsWith('.js') ? [fileName] : [],
|
|
@@ -361,6 +366,7 @@ const buildClientBundle = async (
|
|
|
361
366
|
rscManagedPlugin({ ...config, addMainToInput: true }),
|
|
362
367
|
],
|
|
363
368
|
build: {
|
|
369
|
+
emptyOutDir: !partial,
|
|
364
370
|
outDir: joinPath(rootDir, config.distDir, DIST_PUBLIC),
|
|
365
371
|
rollupOptions: {
|
|
366
372
|
onwarn,
|
|
@@ -426,6 +432,10 @@ const emitRscFiles = async (
|
|
|
426
432
|
config.rscPath,
|
|
427
433
|
encodeInput(input),
|
|
428
434
|
);
|
|
435
|
+
// Skip if the file already exists.
|
|
436
|
+
if (existsSync(destRscFile)) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
429
439
|
await mkdir(joinPath(destRscFile, '..'), { recursive: true });
|
|
430
440
|
const readable = await renderRsc(
|
|
431
441
|
{
|
|
@@ -504,7 +514,10 @@ const emitHtmlFiles = async (
|
|
|
504
514
|
let htmlHead = publicIndexHtmlHead;
|
|
505
515
|
if (cssAssets.length) {
|
|
506
516
|
const cssStr = cssAssets
|
|
507
|
-
.map(
|
|
517
|
+
.map(
|
|
518
|
+
(asset) =>
|
|
519
|
+
`<link rel="stylesheet" href="${config.basePath}${asset}">`,
|
|
520
|
+
)
|
|
508
521
|
.join('\n');
|
|
509
522
|
// HACK is this too naive to inject style code?
|
|
510
523
|
htmlStr = htmlStr.replace(/<\/head>/, cssStr);
|
|
@@ -549,6 +562,10 @@ const emitHtmlFiles = async (
|
|
|
549
562
|
? '404.html' // HACK special treatment for 404, better way?
|
|
550
563
|
: pathname + '/index.html',
|
|
551
564
|
);
|
|
565
|
+
// In partial mode, skip if the file already exists.
|
|
566
|
+
if (existsSync(destHtmlFile)) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
552
569
|
const htmlReadable = await renderHtml({
|
|
553
570
|
config,
|
|
554
571
|
pathname,
|
|
@@ -606,6 +623,7 @@ export const publicIndexHtml = ${JSON.stringify(publicIndexHtml)};
|
|
|
606
623
|
export async function build(options: {
|
|
607
624
|
config: Config;
|
|
608
625
|
env?: Record<string, string>;
|
|
626
|
+
partial?: boolean;
|
|
609
627
|
deploy?:
|
|
610
628
|
| 'vercel-static'
|
|
611
629
|
| 'vercel-serverless'
|
|
@@ -643,6 +661,7 @@ export async function build(options: {
|
|
|
643
661
|
(options.deploy === 'deno' ? 'deno' : false) ||
|
|
644
662
|
(options.deploy === 'aws-lambda' ? 'aws-lambda' : false),
|
|
645
663
|
isNodeCompatible,
|
|
664
|
+
!!options.partial,
|
|
646
665
|
);
|
|
647
666
|
await buildSsrBundle(
|
|
648
667
|
rootDir,
|
|
@@ -650,12 +669,14 @@ export async function build(options: {
|
|
|
650
669
|
clientEntryFiles,
|
|
651
670
|
serverBuildOutput,
|
|
652
671
|
isNodeCompatible,
|
|
672
|
+
!!options.partial,
|
|
653
673
|
);
|
|
654
674
|
const clientBuildOutput = await buildClientBundle(
|
|
655
675
|
rootDir,
|
|
656
676
|
config,
|
|
657
677
|
clientEntryFiles,
|
|
658
678
|
serverBuildOutput,
|
|
679
|
+
!!options.partial,
|
|
659
680
|
);
|
|
660
681
|
|
|
661
682
|
const distEntries = await import(filePathToFileURL(distEntriesFile));
|
|
@@ -97,13 +97,7 @@ export const devServer: Middleware = (options) => {
|
|
|
97
97
|
],
|
|
98
98
|
},
|
|
99
99
|
ssr: {
|
|
100
|
-
external: [
|
|
101
|
-
'waku',
|
|
102
|
-
'waku/client',
|
|
103
|
-
'waku/server',
|
|
104
|
-
'waku/router/client',
|
|
105
|
-
'waku/router/server',
|
|
106
|
-
],
|
|
100
|
+
external: ['waku'],
|
|
107
101
|
},
|
|
108
102
|
server: { middlewareMode: true },
|
|
109
103
|
});
|
|
@@ -79,9 +79,6 @@ ${opts.htmlHead}
|
|
|
79
79
|
},
|
|
80
80
|
transformIndexHtml() {
|
|
81
81
|
return [
|
|
82
|
-
// HACK without <base>, some relative assets don't work.
|
|
83
|
-
// FIXME ideally, we should avoid this.
|
|
84
|
-
{ tag: 'base', attrs: { href: opts.basePath } },
|
|
85
82
|
{
|
|
86
83
|
tag: 'script',
|
|
87
84
|
attrs: { type: 'module', async: true },
|
|
@@ -89,7 +86,7 @@ ${opts.htmlHead}
|
|
|
89
86
|
},
|
|
90
87
|
...(opts.cssAssets || []).map((href) => ({
|
|
91
88
|
tag: 'link',
|
|
92
|
-
attrs: { rel: 'stylesheet', href },
|
|
89
|
+
attrs: { rel: 'stylesheet', href: `${opts.basePath}${href}` },
|
|
93
90
|
injectTo: 'head' as const,
|
|
94
91
|
})),
|
|
95
92
|
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { normalizePath } from 'vite';
|
|
3
4
|
import type { Plugin } from 'vite';
|
|
4
5
|
|
|
5
6
|
// HACK: Depending on a different plugin isn't ideal.
|
|
@@ -37,8 +38,14 @@ export function rscServePlugin(opts: {
|
|
|
37
38
|
name: 'rsc-serve-plugin',
|
|
38
39
|
config(viteConfig) {
|
|
39
40
|
// FIXME This seems too hacky (The use of viteConfig.root, '.', path.resolve and resolveFileName)
|
|
40
|
-
const entriesFile =
|
|
41
|
-
|
|
41
|
+
const entriesFile = normalizePath(
|
|
42
|
+
resolveFileName(
|
|
43
|
+
path.resolve(
|
|
44
|
+
viteConfig.root || '.',
|
|
45
|
+
opts.srcDir,
|
|
46
|
+
SRC_ENTRIES + '.jsx',
|
|
47
|
+
),
|
|
48
|
+
),
|
|
42
49
|
);
|
|
43
50
|
const { input } = viteConfig.build?.rollupOptions ?? {};
|
|
44
51
|
if (input && !(typeof input === 'string') && !(input instanceof Array)) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
joinPath,
|
|
14
14
|
fileURLToFilePath,
|
|
15
15
|
encodeFilePathToAbsolute,
|
|
16
|
+
decodeFilePathFromAbsolute,
|
|
16
17
|
} from '../utils/path.js';
|
|
17
18
|
import { deepFreeze, hasStatusCode } from './utils.js';
|
|
18
19
|
import type { MessageReq, MessageRes } from './dev-worker-api.js';
|
|
@@ -50,12 +51,14 @@ const resolveClientEntryForDev = (
|
|
|
50
51
|
config: { rootDir: string; basePath: string },
|
|
51
52
|
initialModules: ClonableModuleNode[],
|
|
52
53
|
) => {
|
|
54
|
+
let file = id.startsWith('file://')
|
|
55
|
+
? decodeFilePathFromAbsolute(fileURLToFilePath(id))
|
|
56
|
+
: id;
|
|
53
57
|
for (const moduleNode of initialModules) {
|
|
54
|
-
if (moduleNode.file ===
|
|
58
|
+
if (moduleNode.file === file) {
|
|
55
59
|
return moduleNode.url;
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
|
-
let file = id.startsWith('file://') ? fileURLToFilePath(id) : id;
|
|
59
62
|
if (file.startsWith(config.rootDir)) {
|
|
60
63
|
file = file.slice(config.rootDir.length + 1); // '+ 1' to remove '/'
|
|
61
64
|
} else {
|
|
@@ -95,7 +98,6 @@ const handleRender = async (mesg: MessageReq & { type: 'render' }) => {
|
|
|
95
98
|
{
|
|
96
99
|
isDev: true,
|
|
97
100
|
loadServerFile,
|
|
98
|
-
loadServerModule,
|
|
99
101
|
resolveClientEntry: (id: string) =>
|
|
100
102
|
resolveClientEntryForDev(
|
|
101
103
|
id,
|
|
@@ -201,16 +203,7 @@ const mergedViteConfig = await mergeUserViteConfig({
|
|
|
201
203
|
conditions: ['react-server', 'workerd'],
|
|
202
204
|
externalConditions: ['react-server', 'workerd'],
|
|
203
205
|
},
|
|
204
|
-
external: [
|
|
205
|
-
// FIXME We want to externalize waku, but it fails on windows.
|
|
206
|
-
// 'waku',
|
|
207
|
-
// 'waku/client',
|
|
208
|
-
// 'waku/server',
|
|
209
|
-
// 'waku/router/client',
|
|
210
|
-
// 'waku/router/server',
|
|
211
|
-
],
|
|
212
|
-
// FIXME We want to externalize waku, but it fails on windows.
|
|
213
|
-
noExternal: ['waku'],
|
|
206
|
+
external: ['waku'],
|
|
214
207
|
},
|
|
215
208
|
appType: 'custom',
|
|
216
209
|
server: { middlewareMode: true, hmr: { server: dummyServer } },
|
|
@@ -227,11 +220,6 @@ const loadServerFile = async (fileURL: string) => {
|
|
|
227
220
|
return vite.ssrLoadModule(fileURLToFilePath(fileURL));
|
|
228
221
|
};
|
|
229
222
|
|
|
230
|
-
const loadServerModule = async (id: string) => {
|
|
231
|
-
const vite = await vitePromise;
|
|
232
|
-
return vite.ssrLoadModule(id);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
223
|
const loadEntries = async (config: { srcDir: string }) => {
|
|
236
224
|
const vite = await vitePromise;
|
|
237
225
|
const filePath = joinPath(vite.config.root, config.srcDir, configEntries);
|
|
@@ -41,7 +41,6 @@ type RenderRscOpts =
|
|
|
41
41
|
isDev: true;
|
|
42
42
|
entries: EntriesDev;
|
|
43
43
|
loadServerFile: (fileURL: string) => Promise<unknown>;
|
|
44
|
-
loadServerModule: (id: string) => Promise<unknown>;
|
|
45
44
|
resolveClientEntry: (id: string) => string;
|
|
46
45
|
};
|
|
47
46
|
|
|
@@ -85,11 +84,9 @@ export async function renderRsc(
|
|
|
85
84
|
{ runWithRenderStore },
|
|
86
85
|
] = await Promise.all([
|
|
87
86
|
loadServerModule<{ default: typeof RSDWServerType }>('rsdw-server'),
|
|
88
|
-
(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
runWithRenderStore: typeof runWithRenderStoreType;
|
|
92
|
-
}>,
|
|
87
|
+
loadServerModule<{ runWithRenderStore: typeof runWithRenderStoreType }>(
|
|
88
|
+
'waku-server',
|
|
89
|
+
),
|
|
93
90
|
]);
|
|
94
91
|
|
|
95
92
|
const bundlerConfig = new Proxy(
|
package/src/lib/utils/path.ts
CHANGED
|
@@ -10,7 +10,7 @@ const ABSOLUTE_WIN32_PATH_REGEXP = /^\/[a-zA-Z]:\//;
|
|
|
10
10
|
|
|
11
11
|
export const encodeFilePathToAbsolute = (filePath: string) => {
|
|
12
12
|
if (ABSOLUTE_WIN32_PATH_REGEXP.test(filePath)) {
|
|
13
|
-
throw new Error('Unsupported absolute file path');
|
|
13
|
+
throw new Error('Unsupported absolute file path: ' + filePath);
|
|
14
14
|
}
|
|
15
15
|
if (filePath.startsWith('/')) {
|
|
16
16
|
return filePath;
|
package/src/router/client.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import type {
|
|
16
16
|
ComponentProps,
|
|
17
17
|
FunctionComponent,
|
|
18
|
+
MutableRefObject,
|
|
18
19
|
ReactNode,
|
|
19
20
|
AnchorHTMLAttributes,
|
|
20
21
|
ReactElement,
|
|
@@ -286,7 +287,34 @@ const equalRouteProps = (a: RouteProps, b: RouteProps) => {
|
|
|
286
287
|
return true;
|
|
287
288
|
};
|
|
288
289
|
|
|
289
|
-
|
|
290
|
+
const RouterSlot = ({
|
|
291
|
+
route,
|
|
292
|
+
routerData,
|
|
293
|
+
cachedRef,
|
|
294
|
+
id,
|
|
295
|
+
fallback,
|
|
296
|
+
children,
|
|
297
|
+
}: {
|
|
298
|
+
route: RouteProps;
|
|
299
|
+
routerData: RouterData;
|
|
300
|
+
cachedRef: MutableRefObject<Record<string, RouteProps>>;
|
|
301
|
+
id: string;
|
|
302
|
+
fallback?: ReactNode;
|
|
303
|
+
children?: ReactNode;
|
|
304
|
+
}) => {
|
|
305
|
+
const unstable_shouldRenderPrev = (_err: unknown) => {
|
|
306
|
+
const shouldSkip = routerData[0];
|
|
307
|
+
const skip = getSkipList(shouldSkip, [id], route, cachedRef.current);
|
|
308
|
+
return skip.length > 0;
|
|
309
|
+
};
|
|
310
|
+
return createElement(
|
|
311
|
+
Slot,
|
|
312
|
+
{ id, fallback, unstable_shouldRenderPrev },
|
|
313
|
+
children,
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const InnerRouter = ({ routerData }: { routerData: RouterData }) => {
|
|
290
318
|
const refetch = useRefetch();
|
|
291
319
|
|
|
292
320
|
const [route, setRoute] = useState(() =>
|
|
@@ -418,7 +446,12 @@ function InnerRouter({ routerData }: { routerData: RouterData }) {
|
|
|
418
446
|
});
|
|
419
447
|
|
|
420
448
|
const children = componentIds.reduceRight(
|
|
421
|
-
(acc: ReactNode, id) =>
|
|
449
|
+
(acc: ReactNode, id) =>
|
|
450
|
+
createElement(
|
|
451
|
+
RouterSlot,
|
|
452
|
+
{ route, routerData, cachedRef, id, fallback: acc },
|
|
453
|
+
acc,
|
|
454
|
+
),
|
|
422
455
|
null,
|
|
423
456
|
);
|
|
424
457
|
|
|
@@ -427,7 +460,7 @@ function InnerRouter({ routerData }: { routerData: RouterData }) {
|
|
|
427
460
|
{ value: { route, changeRoute, prefetchRoute } },
|
|
428
461
|
children,
|
|
429
462
|
);
|
|
430
|
-
}
|
|
463
|
+
};
|
|
431
464
|
|
|
432
465
|
// Note: The router data must be a stable mutable object (array).
|
|
433
466
|
type RouterData = [
|
|
@@ -82,7 +82,7 @@ export type CreatePage = <
|
|
|
82
82
|
}
|
|
83
83
|
| {
|
|
84
84
|
render: 'static';
|
|
85
|
-
path:
|
|
85
|
+
path: PathWithWildcard<Path, SlugKey, WildSlugKey>;
|
|
86
86
|
staticPaths: string[] | string[][];
|
|
87
87
|
component: FunctionComponent<RouteProps & Record<SlugKey, string>>;
|
|
88
88
|
}
|
|
@@ -170,27 +170,35 @@ export function createPages(
|
|
|
170
170
|
staticPathSet.add([page.path, pathSpec]);
|
|
171
171
|
const id = joinPath(page.path, 'page').replace(/^\//, '');
|
|
172
172
|
registerStaticComponent(id, page.component);
|
|
173
|
-
} else if (page.render === 'static' && numSlugs > 0
|
|
173
|
+
} else if (page.render === 'static' && numSlugs > 0) {
|
|
174
174
|
const staticPaths = (
|
|
175
175
|
page as {
|
|
176
176
|
staticPaths: string[] | string[][];
|
|
177
177
|
}
|
|
178
178
|
).staticPaths.map((item) => (Array.isArray(item) ? item : [item]));
|
|
179
179
|
for (const staticPath of staticPaths) {
|
|
180
|
-
if (staticPath.length !== numSlugs) {
|
|
180
|
+
if (staticPath.length !== numSlugs && numWildcards === 0) {
|
|
181
181
|
throw new Error('staticPaths does not match with slug pattern');
|
|
182
182
|
}
|
|
183
|
-
const mapping: Record<string, string> = {};
|
|
183
|
+
const mapping: Record<string, string | string[]> = {};
|
|
184
184
|
let slugIndex = 0;
|
|
185
|
-
const pathItems =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
185
|
+
const pathItems = [] as string[];
|
|
186
|
+
pathSpec.forEach(({ type, name }) => {
|
|
187
|
+
switch (type) {
|
|
188
|
+
case 'literal':
|
|
189
|
+
pathItems.push(name!);
|
|
190
|
+
break;
|
|
191
|
+
case 'wildcard':
|
|
192
|
+
mapping[name!] = staticPath.slice(slugIndex);
|
|
193
|
+
staticPath.slice(slugIndex++).forEach((slug) => {
|
|
194
|
+
pathItems.push(slug);
|
|
195
|
+
});
|
|
196
|
+
break;
|
|
197
|
+
case 'group':
|
|
198
|
+
pathItems.push(staticPath[slugIndex++]!);
|
|
199
|
+
mapping[name!] = pathItems[pathItems.length - 1]!;
|
|
200
|
+
break;
|
|
192
201
|
}
|
|
193
|
-
return name;
|
|
194
202
|
});
|
|
195
203
|
staticPathSet.add([
|
|
196
204
|
page.path,
|