@qwik.dev/router 2.0.0-beta.29 → 2.0.0-beta.30

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.
@@ -68,9 +68,9 @@ function createQwikRouter(opts) {
68
68
  };
69
69
  const handledResponse = await requestHandler(serverRequestEv, opts);
70
70
  if (handledResponse) {
71
- handledResponse.completion.then((v) => {
72
- if (v) {
73
- console.error(v);
71
+ handledResponse.completion.then((completion) => {
72
+ if (completion) {
73
+ console.error(completion);
74
74
  }
75
75
  });
76
76
  const response = await handledResponse.response;
@@ -75,8 +75,8 @@ export declare interface SsgRenderOptions extends RenderOptions {
75
75
  * root of the `outDir`. Setting to `null` will prevent the sitemap from being created.
76
76
  */
77
77
  sitemapOutFile?: string | null;
78
- /** Log level. */
79
- log?: 'debug';
78
+ /** Log level. `'quiet'` suppresses per-page output, `'debug'` enables verbose logging. */
79
+ log?: 'debug' | 'quiet';
80
80
  /**
81
81
  * Set to `false` if the generated static HTML files should not be written to disk. Setting to
82
82
  * `false` is useful if the SSG should only write the `q-data.json` files to disk. Defaults to
package/lib/ssg/index.mjs CHANGED
@@ -78,6 +78,7 @@ async function mainThread(sys) {
78
78
  const log = await sys.createLogger();
79
79
  log.info("\n" + bold(green("Starting Qwik Router SSG...")));
80
80
  const qwikRouterConfig = opts.qwikRouterConfig;
81
+ const renderTimeout = 3e4;
81
82
  const queue = [];
82
83
  const active = /* @__PURE__ */ new Set();
83
84
  const routes = qwikRouterConfig.routes;
@@ -143,11 +144,16 @@ ${green("SSG results")}`);
143
144
  const render = async (staticRoute) => {
144
145
  try {
145
146
  active.add(staticRoute.pathname);
146
- const result = await main.render({
147
- type: "render",
148
- ...staticRoute
149
- });
147
+ log.debug(`render start: ${staticRoute.pathname}`);
148
+ const result = await Promise.race([
149
+ main.render({
150
+ type: "render",
151
+ ...staticRoute
152
+ }),
153
+ new Promise((_, reject2) => setTimeout(() => reject2(new Error(`SSG render timed out after ${renderTimeout}ms`)), renderTimeout))
154
+ ]);
150
155
  active.delete(staticRoute.pathname);
156
+ log.debug(`render done: ${staticRoute.pathname}`);
151
157
  if (result.error) {
152
158
  const err = new Error(result.error.message);
153
159
  err.stack = result.error.stack;
@@ -216,26 +222,28 @@ ${bold(red(`!!! ${result.pathname}: Error during SSG`))}`);
216
222
  const joinedParts = pathParts.join("/");
217
223
  const originalPathname = basePathname + (joinedParts ? joinedParts + "/" : "");
218
224
  const paramNames = pathParts.filter((p) => p.startsWith("[") && p.endsWith("]")).map((p) => p.startsWith("[...") ? p.slice(4, -1) : p.slice(1, -1));
219
- const modules = await Promise.all(pageLoaders.filter((l) => typeof l === "function").map((l) => l()));
220
- const pageModule = modules[modules.length - 1];
221
- if (paramNames.length > 0) {
222
- if (typeof pageModule.onStaticGenerate === "function") {
223
- const staticGenerate = await pageModule.onStaticGenerate({
224
- env: {
225
- get(key) {
226
- return sys.getEnv(key);
225
+ if (paramNames.length === 0) {
226
+ addToQueue(originalPathname, void 0);
227
+ } else {
228
+ const pageLoader = pageLoaders[pageLoaders.length - 1];
229
+ if (typeof pageLoader === "function") {
230
+ const pageModule = await pageLoader();
231
+ if (typeof pageModule.onStaticGenerate === "function") {
232
+ const staticGenerate = await pageModule.onStaticGenerate({
233
+ env: {
234
+ get(key) {
235
+ return sys.getEnv(key);
236
+ }
237
+ }
238
+ });
239
+ if (Array.isArray(staticGenerate.params)) {
240
+ for (const params of staticGenerate.params) {
241
+ const pathname = getPathnameForDynamicRoute(originalPathname, paramNames, params);
242
+ addToQueue(pathname, params);
227
243
  }
228
- }
229
- });
230
- if (Array.isArray(staticGenerate.params)) {
231
- for (const params of staticGenerate.params) {
232
- const pathname = getPathnameForDynamicRoute(originalPathname, paramNames, params);
233
- addToQueue(pathname, params);
234
244
  }
235
245
  }
236
246
  }
237
- } else {
238
- addToQueue(originalPathname, void 0);
239
247
  }
240
248
  }
241
249
  if (node._M) {
@@ -273,8 +281,9 @@ ${bold(red(`!!! ${result.pathname}: Error during SSG`))}`);
273
281
  }
274
282
  };
275
283
  const loadStaticRoutes = async () => {
276
- const basePathname = opts.basePathname || "/";
277
- await traverseRouteTree(routes, [], basePathname, []);
284
+ log.debug("traversing route tree...");
285
+ await traverseRouteTree(routes, [], "/", []);
286
+ log.debug(`route tree traversed, ${queue.length} routes queued, ${active.size} active`);
278
287
  isRoutesLoaded = true;
279
288
  flushQueue();
280
289
  };
@@ -319,7 +328,7 @@ async function startWorker(opts) {
319
328
  qwikRouterConfig: opts.qwikRouterConfig
320
329
  };
321
330
  const { createSystem } = await import('../chunks/system.mjs');
322
- const { workerThread } = await import('../chunks/worker-thread.mjs');
331
+ const { workerThread } = await import('../chunks/worker-thread.qwik.mjs');
323
332
  const sys = await createSystem(mergedOpts, workerData?.threadId);
324
333
  await workerThread(sys);
325
334
  }
@@ -638,9 +638,97 @@ function rewriteRoutes(ctx, routes) {
638
638
  });
639
639
  return translatedRoutes.sort(routeSortCompare);
640
640
  }
641
+ function applyRewriteRoutes(root, rewriteConfigs) {
642
+ const routables = [];
643
+ function walk(node, steps) {
644
+ const hasRoute = node._files.some((f) => f.type === "route" && f.extlessName !== "error" && f.extlessName !== "404");
645
+ if (hasRoute) {
646
+ routables.push({
647
+ steps: [
648
+ ...steps
649
+ ],
650
+ node
651
+ });
652
+ }
653
+ for (const [key, child] of node.children) {
654
+ const isGroup = key.startsWith("(") && key.endsWith(")");
655
+ if (isGroup) {
656
+ walk(child, steps);
657
+ } else {
658
+ walk(child, [
659
+ ...steps,
660
+ {
661
+ key,
662
+ paramName: child._P,
663
+ prefix: child._0,
664
+ suffix: child._9
665
+ }
666
+ ]);
667
+ }
668
+ }
669
+ }
670
+ walk(root, []);
671
+ for (const config of rewriteConfigs) {
672
+ const translations = config.paths || {};
673
+ const translatable = new Set(Object.keys(translations).map((k) => k.toLowerCase()));
674
+ for (const { steps } of routables) {
675
+ const hasTranslatable = steps.some((s) => translatable.has(s.key));
676
+ if (!hasTranslatable && !config.prefix) {
677
+ continue;
678
+ }
679
+ if (steps.length === 0 && !config.prefix) {
680
+ continue;
681
+ }
682
+ const originalKeyPath = steps.map((s) => s.key).join("/");
683
+ const translatedSteps = [];
684
+ if (config.prefix) {
685
+ translatedSteps.push({
686
+ key: config.prefix.toLowerCase()
687
+ });
688
+ }
689
+ for (const step of steps) {
690
+ const translated = translations[step.key];
691
+ translatedSteps.push({
692
+ ...step,
693
+ key: translated ? translated.toLowerCase() : step.key
694
+ });
695
+ }
696
+ const translatedKeyPath = translatedSteps.map((s) => s.key).join("/");
697
+ if (translatedKeyPath === originalKeyPath) {
698
+ continue;
699
+ }
700
+ let current = root;
701
+ for (const step of translatedSteps) {
702
+ let child = current.children.get(step.key);
703
+ if (!child) {
704
+ child = {
705
+ _files: [],
706
+ _dirPath: "",
707
+ children: /* @__PURE__ */ new Map()
708
+ };
709
+ if (step.paramName) {
710
+ child._P = step.paramName;
711
+ }
712
+ if (step.prefix) {
713
+ child._0 = step.prefix;
714
+ }
715
+ if (step.suffix) {
716
+ child._9 = step.suffix;
717
+ }
718
+ current.children.set(step.key, child);
719
+ }
720
+ current = child;
721
+ }
722
+ current._G = originalKeyPath;
723
+ }
724
+ }
725
+ }
641
726
  async function _updateRoutingContext(ctx) {
642
727
  const serverPlugins = await walkServerPlugins(ctx.opts);
643
728
  const routeTrie = await walkRoutes(ctx.opts.routesDir);
729
+ if (ctx.opts.rewriteRoutes) {
730
+ applyRewriteRoutes(routeTrie, ctx.opts.rewriteRoutes);
731
+ }
644
732
  ctx.routeTrie = routeTrie;
645
733
  ctx.serverPlugins = serverPlugins;
646
734
  const derived = deriveFromTrie(ctx.opts, routeTrie);
@@ -1486,6 +1574,9 @@ function generateQwikRouterConfig(ctx, qwikPlugin, isSSR) {
1486
1574
  /** Qwik Router Config */`);
1487
1575
  c.push(`
1488
1576
  import { isDev } from '@qwik.dev/core/build';`);
1577
+ if (isSSR) {
1578
+ esmImports.push(`import 'virtual:qwik-router-server-fns';`);
1579
+ }
1489
1580
  createServerPlugins(ctx, qwikPlugin, c, esmImports, isSSR);
1490
1581
  createRoutes(ctx, qwikPlugin, c, esmImports, isSSR);
1491
1582
  createEntries(ctx, c);
@@ -1736,7 +1827,7 @@ function imagePlugin(userOpts) {
1736
1827
  ".tiff"
1737
1828
  ];
1738
1829
  return [
1739
- import('vite-imagetools').then(({ imagetools }) => imagetools({
1830
+ import('vite-imagetools').then((_rawProps) => _rawProps.imagetools({
1740
1831
  exclude: [],
1741
1832
  extendOutputFormats(builtins) {
1742
1833
  const jsx = () => (metadatas) => {
@@ -2176,6 +2267,7 @@ const QWIK_ROUTER_CONFIG_ID = "@qwik-router-config";
2176
2267
  const QWIK_ROUTER_ENTRIES_ID = "@qwik-router-entries";
2177
2268
  const QWIK_ROUTER = "@qwik.dev/router";
2178
2269
  const QWIK_ROUTER_SW_REGISTER = "@qwik-router-sw-register";
2270
+ const VIRTUAL_SERVER_FNS = "virtual:qwik-router-server-fns";
2179
2271
  async function generateServerPackageJson(outDir) {
2180
2272
  await fs.promises.mkdir(outDir, {
2181
2273
  recursive: true
@@ -2381,7 +2473,7 @@ function qwikRouterPlugin(userOpts) {
2381
2473
  }
2382
2474
  return null;
2383
2475
  },
2384
- async load(id, opts) {
2476
+ async load(id) {
2385
2477
  if (ctx) {
2386
2478
  if (id.endsWith(QWIK_ROUTER_ENTRIES_ID)) {
2387
2479
  return generateQwikRouterEntries(ctx);
@@ -2486,9 +2578,77 @@ function qwikRouterPlugin(userOpts) {
2486
2578
  };
2487
2579
  return plugin;
2488
2580
  }
2581
+ function serverFnsPlugin() {
2582
+ const RESOLVED_ID = "\0" + VIRTUAL_SERVER_FNS;
2583
+ const serverFnModules = /* @__PURE__ */ new Set();
2584
+ let pendingModules = 0;
2585
+ let resolveServerFns = null;
2586
+ let serverFnsReady;
2587
+ function reset() {
2588
+ serverFnModules.clear();
2589
+ pendingModules = 0;
2590
+ serverFnsReady = new Promise((r) => {
2591
+ resolveServerFns = r;
2592
+ });
2593
+ }
2594
+ reset();
2595
+ return {
2596
+ name: "vite-plugin-qwik-router-server-fns",
2597
+ buildStart() {
2598
+ reset();
2599
+ },
2600
+ resolveId(id) {
2601
+ if (id === VIRTUAL_SERVER_FNS) {
2602
+ return {
2603
+ id: RESOLVED_ID,
2604
+ moduleSideEffects: "no-treeshake"
2605
+ };
2606
+ }
2607
+ },
2608
+ load: {
2609
+ order: "pre",
2610
+ async handler(id) {
2611
+ const isServerBuild = this.environment.config.consumer === "server" && this.environment.mode === "build";
2612
+ if (id === RESOLVED_ID) {
2613
+ if (isServerBuild) {
2614
+ await serverFnsReady;
2615
+ }
2616
+ if (!isServerBuild || serverFnModules.size === 0) {
2617
+ return "// No server$ functions";
2618
+ }
2619
+ return [
2620
+ ...serverFnModules
2621
+ ].map((mod) => `import ${JSON.stringify(mod)};`).join("\n");
2622
+ }
2623
+ if (isServerBuild && id !== RESOLVED_ID) {
2624
+ pendingModules++;
2625
+ this.load({
2626
+ id
2627
+ }).then((result) => {
2628
+ if (typeof result.code === "string" && result.code.includes("serverQrl(")) {
2629
+ serverFnModules.add(id);
2630
+ }
2631
+ }).finally(() => {
2632
+ pendingModules--;
2633
+ if (pendingModules <= 0 && resolveServerFns) {
2634
+ setTimeout(() => {
2635
+ if (pendingModules <= 0 && resolveServerFns) {
2636
+ resolveServerFns();
2637
+ resolveServerFns = null;
2638
+ }
2639
+ }, 50);
2640
+ }
2641
+ });
2642
+ }
2643
+ return null;
2644
+ }
2645
+ }
2646
+ };
2647
+ }
2489
2648
  function qwikRouter(userOpts) {
2490
2649
  return [
2491
2650
  qwikRouterPlugin(userOpts),
2651
+ serverFnsPlugin(),
2492
2652
  ...imagePlugin(userOpts)
2493
2653
  ];
2494
2654
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@qwik.dev/router",
3
3
  "description": "The router for Qwik.",
4
- "version": "2.0.0-beta.29",
4
+ "version": "2.0.0-beta.30",
5
5
  "bugs": "https://github.com/QwikDev/qwik/issues",
6
6
  "dependencies": {
7
7
  "@azure/functions": "3.5.1",
8
8
  "@mdx-js/mdx": "^3.1.1",
9
- "@netlify/edge-functions": "^2.17.0",
9
+ "@netlify/edge-functions": "^3.0.6",
10
10
  "@types/mdx": "^2.0.13",
11
11
  "estree-util-value-to-estree": "^3.5.0",
12
12
  "github-slugger": "^2.0.0",
@@ -40,7 +40,7 @@
40
40
  "tsm": "2.3.0",
41
41
  "typescript": "5.9.3",
42
42
  "uvu": "0.5.6",
43
- "@qwik.dev/core": "2.0.0-beta.29"
43
+ "@qwik.dev/core": "2.0.0-beta.30"
44
44
  },
45
45
  "engines": {
46
46
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@@ -169,7 +169,7 @@
169
169
  "main": "./lib/index.qwik.mjs",
170
170
  "peerDependencies": {
171
171
  "vite": ">=6 <9",
172
- "@qwik.dev/core": "^2.0.0-beta.29"
172
+ "@qwik.dev/core": "^2.0.0-beta.30"
173
173
  },
174
174
  "publishConfig": {
175
175
  "access": "public"
@@ -1,271 +0,0 @@
1
- import { _serialize } from '@qwik.dev/core/internal';
2
- import { RequestEvShareQData, requestHandler, RedirectMessage } from '@qwik.dev/router/middleware/request-handler';
3
- import { parentPort } from 'node:worker_threads';
4
-
5
- const createNoopWritableStream = () => new WritableStream();
6
- async function workerRender(sys, opts, staticRoute, pendingPromises, callback) {
7
- const url = new URL(staticRoute.pathname, opts.origin);
8
- const result = {
9
- type: "render",
10
- pathname: staticRoute.pathname,
11
- url: url.href,
12
- ok: false,
13
- error: null,
14
- filePath: null,
15
- contentType: null,
16
- resourceType: null
17
- };
18
- try {
19
- let routeWriter = null;
20
- let closeResolved;
21
- const closePromise = new Promise((closePromiseResolve) => {
22
- closeResolved = closePromiseResolve;
23
- });
24
- const request = new Request(url);
25
- const requestCtx = {
26
- mode: "static",
27
- locale: void 0,
28
- url,
29
- request,
30
- env: {
31
- get(key) {
32
- return sys.getEnv(key);
33
- }
34
- },
35
- platform: sys.platform,
36
- getClientConn: () => {
37
- return {};
38
- },
39
- getWritableStream: (status, headers, _, _r, requestEv) => {
40
- result.ok = status >= 200 && status < 300;
41
- if (!result.ok) {
42
- return createNoopWritableStream();
43
- }
44
- result.contentType = (headers.get("Content-Type") || "").toLowerCase();
45
- const isHtml = result.contentType.includes("text/html");
46
- const is404ErrorPage = url.pathname.endsWith("/404.html");
47
- const routeFilePath = sys.getRouteFilePath(url.pathname, isHtml);
48
- if (is404ErrorPage) {
49
- result.resourceType = "404";
50
- } else if (isHtml) {
51
- result.resourceType = "page";
52
- }
53
- const hasRouteWriter = isHtml ? opts.emitHtml !== false : true;
54
- const writeQDataEnabled = isHtml && opts.emitData !== false;
55
- const stream = new WritableStream({
56
- async start() {
57
- try {
58
- if (hasRouteWriter || writeQDataEnabled) {
59
- await sys.ensureDir(routeFilePath);
60
- }
61
- if (hasRouteWriter) {
62
- routeWriter = sys.createWriteStream(routeFilePath);
63
- routeWriter.on("error", (e) => {
64
- console.error(e);
65
- routeWriter = null;
66
- result.error = {
67
- message: e.message,
68
- stack: e.stack
69
- };
70
- });
71
- }
72
- } catch (e) {
73
- console.error("Error during stream start", staticRoute.pathname, e);
74
- routeWriter = null;
75
- result.error = {
76
- message: String(e),
77
- stack: e.stack || ""
78
- };
79
- }
80
- },
81
- write(chunk) {
82
- try {
83
- if (routeWriter) {
84
- routeWriter.write(Buffer.from(chunk.buffer));
85
- }
86
- } catch (e) {
87
- console.error("Error during stream write", staticRoute.pathname, e);
88
- routeWriter = null;
89
- result.error = {
90
- message: String(e),
91
- stack: e.stack || ""
92
- };
93
- }
94
- },
95
- async close() {
96
- const writePromises = [];
97
- try {
98
- if (writeQDataEnabled) {
99
- const qData = requestEv.sharedMap.get(RequestEvShareQData);
100
- if (qData && !is404ErrorPage) {
101
- const qDataFilePath = sys.getDataFilePath(url.pathname);
102
- const dataWriter = sys.createWriteStream(qDataFilePath);
103
- dataWriter.on("error", (e) => {
104
- console.error(e);
105
- result.error = {
106
- message: e.message,
107
- stack: e.stack
108
- };
109
- });
110
- const serialized = await _serialize(qData);
111
- dataWriter.write(serialized);
112
- writePromises.push(new Promise((resolve) => {
113
- result.filePath = routeFilePath;
114
- dataWriter.end(resolve);
115
- }));
116
- }
117
- }
118
- if (routeWriter) {
119
- writePromises.push(new Promise((resolve) => {
120
- result.filePath = routeFilePath;
121
- routeWriter.end(resolve);
122
- }).finally(closeResolved));
123
- }
124
- if (writePromises.length > 0) {
125
- await Promise.all(writePromises);
126
- }
127
- } catch (e) {
128
- console.error("Error during stream close", staticRoute.pathname, e);
129
- routeWriter = null;
130
- result.error = {
131
- message: String(e),
132
- stack: e.stack || ""
133
- };
134
- }
135
- }
136
- });
137
- return stream;
138
- }
139
- };
140
- const promise = requestHandler(requestCtx, opts).then((rsp) => {
141
- if (rsp != null) {
142
- return rsp.completion.then((r) => {
143
- if (routeWriter) {
144
- return closePromise.then(() => r);
145
- }
146
- return r;
147
- });
148
- }
149
- }).then((e) => {
150
- if (e !== void 0) {
151
- if (e instanceof RedirectMessage) {
152
- return;
153
- }
154
- if (e instanceof Error) {
155
- result.error = {
156
- message: e.message,
157
- stack: e.stack
158
- };
159
- } else {
160
- result.error = {
161
- message: String(e),
162
- stack: void 0
163
- };
164
- }
165
- console.error("Error during request handling", staticRoute.pathname, e);
166
- }
167
- }).catch((e) => {
168
- console.error("Unhandled error during request handling", staticRoute.pathname, e);
169
- result.error = {
170
- message: String(e),
171
- stack: e.stack || ""
172
- };
173
- }).finally(() => {
174
- pendingPromises.delete(promise);
175
- callback(result);
176
- });
177
- pendingPromises.add(promise);
178
- } catch (e) {
179
- console.error("Error during render", staticRoute.pathname, e);
180
- if (e instanceof Error) {
181
- result.error = {
182
- message: e.message,
183
- stack: e.stack
184
- };
185
- } else {
186
- result.error = {
187
- message: String(e),
188
- stack: void 0
189
- };
190
- }
191
- callback(result);
192
- }
193
- }
194
- async function workerThread(sys) {
195
- delete globalThis.__qwik;
196
- const opts = sys.getOptions();
197
- const pendingPromises = /* @__PURE__ */ new Set();
198
- const log = await sys.createLogger();
199
- process.on("uncaughtException", (e) => {
200
- console.error("Worker uncaught exception (suppressed):", e.message);
201
- });
202
- process.on("unhandledRejection", (e) => {
203
- console.error("Worker unhandled rejection (suppressed):", e instanceof Error ? e.message : e);
204
- });
205
- const onMessage = async (msg) => {
206
- switch (msg.type) {
207
- case "render": {
208
- log.debug(`Worker thread rendering: ${msg.pathname}`);
209
- return new Promise((resolve) => {
210
- workerRender(sys, opts, msg, pendingPromises, resolve).catch((e) => {
211
- console.error("Error during render", msg.pathname, e);
212
- resolve({
213
- type: "render",
214
- pathname: msg.pathname,
215
- url: "",
216
- ok: false,
217
- error: {
218
- message: e instanceof Error ? e.message : String(e),
219
- stack: e instanceof Error ? e.stack : void 0
220
- },
221
- filePath: null,
222
- contentType: null,
223
- resourceType: null
224
- });
225
- });
226
- });
227
- }
228
- case "close": {
229
- if (pendingPromises.size) {
230
- log.debug(`Worker thread closing, waiting for ${pendingPromises.size} pending renders`);
231
- const promises = Array.from(pendingPromises);
232
- pendingPromises.clear();
233
- await Promise.all(promises);
234
- }
235
- log.debug(`Worker thread closed`);
236
- return {
237
- type: "close"
238
- };
239
- }
240
- }
241
- };
242
- parentPort?.on("message", async (msg) => {
243
- try {
244
- parentPort?.postMessage(await onMessage(msg));
245
- } catch (e) {
246
- if (msg.type === "render") {
247
- const error = e instanceof Error ? e : new Error(String(e));
248
- parentPort?.postMessage({
249
- type: "render",
250
- pathname: msg.pathname,
251
- url: "",
252
- ok: false,
253
- error: {
254
- message: error.message,
255
- stack: error.stack
256
- },
257
- filePath: null,
258
- contentType: null,
259
- resourceType: null
260
- });
261
- } else {
262
- console.error("Worker message handler error", e);
263
- }
264
- }
265
- if (msg.type === "close") {
266
- parentPort?.close();
267
- }
268
- });
269
- }
270
-
271
- export { workerThread };