waku 0.20.2-alpha.1 → 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/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
- type SetElements = (updater: Elements | ((prev: Elements) => Elements)) => void;
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 = async ()=>{
21
- const nextElements = {
22
- ...await a,
23
- ...await b
24
- };
25
- delete nextElements._value;
26
- return nextElements;
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
- throw e;
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) {
@@ -94,11 +94,7 @@ export const devServer = (options)=>{
94
94
  },
95
95
  ssr: {
96
96
  external: [
97
- 'waku',
98
- 'waku/client',
99
- 'waku/server',
100
- 'waku/router/client',
101
- 'waku/router/server'
97
+ 'waku'
102
98
  ]
103
99
  },
104
100
  server: {
@@ -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 === id) {
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
- isDev ? opts.loadServerModule(SERVER_MODULE_MAP['waku-server']) : loadModule('waku-server')
23
+ loadServerModule('waku-server')
24
24
  ]);
25
25
  const bundlerConfig = new Proxy({}, {
26
26
  get (_target, encodedId) {
@@ -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;
@@ -193,7 +193,21 @@ const equalRouteProps = (a, b)=>{
193
193
  }
194
194
  return true;
195
195
  };
196
- function InnerRouter({ routerData }) {
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(Slot, {
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));
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-alpha.1",
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",
@@ -86,9 +83,9 @@
86
83
  "vitest": "^1.6.0"
87
84
  },
88
85
  "peerDependencies": {
89
- "react": "19.0.0-beta-4508873393-20240430",
90
- "react-dom": "19.0.0-beta-4508873393-20240430",
91
- "react-server-dom-webpack": "19.0.0-beta-4508873393-20240430"
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/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
- a: Elements,
49
- b: Elements | Awaited<Elements>,
50
- ): Elements => {
51
- const getResult = async () => {
52
- const nextElements = { ...(await a), ...(await b) };
53
- delete nextElements._value;
54
- return nextElements;
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: Elements | ((prev: Elements) => Elements)) => void;
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
- throw e;
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) {
@@ -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
  });
@@ -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 === id) {
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
- (isDev
89
- ? opts.loadServerModule(SERVER_MODULE_MAP['waku-server'])
90
- : loadModule('waku-server')) as Promise<{
91
- runWithRenderStore: typeof runWithRenderStoreType;
92
- }>,
87
+ loadServerModule<{ runWithRenderStore: typeof runWithRenderStoreType }>(
88
+ 'waku-server',
89
+ ),
93
90
  ]);
94
91
 
95
92
  const bundlerConfig = new Proxy(
@@ -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;
@@ -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
- function InnerRouter({ routerData }: { routerData: RouterData }) {
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) => createElement(Slot, { id, fallback: acc }, acc),
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 = [