@blinkk/root 4.0.1 → 4.1.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.
@@ -19,6 +19,19 @@ export declare class Renderer {
19
19
  walkRoutes(cb: (urlPathFormat: string, route: Route) => void | Promise<void>): Promise<void>;
20
20
  handle(req: Request, res: Response, next: NextFunction): Promise<void>;
21
21
  private renderComponent;
22
+ /**
23
+ * Runs a synchronous render function with a hook installed that auto-injects
24
+ * the CSP nonce into `<script>`, `<style>` and `<link rel="stylesheet">`
25
+ * vnodes.
26
+ *
27
+ * NOTE: `preactOptions.vnode` is a global, so the hook must be installed and
28
+ * restored within a single synchronous block. Awaiting inside the swapped
29
+ * window allows concurrent renders to interleave, leaking a stale hook (and
30
+ * its nonce) into other requests.
31
+ *
32
+ * https://preactjs.com/guide/v10/options/
33
+ */
34
+ private withNonceHooks;
22
35
  /** SSG renders a route. */
23
36
  renderRoute(route: Route, options: {
24
37
  routeParams: Record<string, string>;
package/dist/render.js CHANGED
@@ -17,7 +17,9 @@ import {
17
17
  import {
18
18
  renderJsxToString
19
19
  } from "./chunk-5OPFFJU7.js";
20
- import "./chunk-3Z5TNZEP.js";
20
+ import {
21
+ options
22
+ } from "./chunk-3Z5TNZEP.js";
21
23
  import {
22
24
  getSecurityConfig,
23
25
  setSecurityHeaders
@@ -13071,9 +13073,9 @@ var Router = class {
13071
13073
  return urlPaths;
13072
13074
  }
13073
13075
  };
13074
- function normalizeUrlPath(urlPath, options) {
13076
+ function normalizeUrlPath(urlPath, options2) {
13075
13077
  urlPath = urlPath.replace(/\/+/g, "/");
13076
- if (testPathHasFileExt(urlPath) || options?.trailingSlash === false && urlPath !== "/" && urlPath.endsWith("/")) {
13078
+ if (testPathHasFileExt(urlPath) || options2?.trailingSlash === false && urlPath !== "/" && urlPath.endsWith("/")) {
13077
13079
  urlPath = removeTrailingSlash(urlPath);
13078
13080
  }
13079
13081
  if (urlPath.endsWith("/index")) {
@@ -13082,7 +13084,7 @@ function normalizeUrlPath(urlPath, options) {
13082
13084
  if (!urlPath.startsWith("/")) {
13083
13085
  urlPath = `/${urlPath}`;
13084
13086
  }
13085
- if (options?.trailingSlash && !testPathHasFileExt(urlPath) && !urlPath.endsWith("/")) {
13087
+ if (options2?.trailingSlash && !testPathHasFileExt(urlPath) && !urlPath.endsWith("/")) {
13086
13088
  urlPath = `${urlPath}/`;
13087
13089
  }
13088
13090
  return urlPath;
@@ -13142,10 +13144,10 @@ var Renderer = class {
13142
13144
  assetMap;
13143
13145
  elementGraph;
13144
13146
  router;
13145
- constructor(rootConfig, options) {
13147
+ constructor(rootConfig, options2) {
13146
13148
  this.rootConfig = rootConfig;
13147
- this.assetMap = options.assetMap;
13148
- this.elementGraph = options.elementGraph;
13149
+ this.assetMap = options2.assetMap;
13150
+ this.elementGraph = options2.elementGraph;
13149
13151
  this.router = new Router(rootConfig);
13150
13152
  }
13151
13153
  /** Returns a route from the router. */
@@ -13187,12 +13189,12 @@ var Renderer = class {
13187
13189
  };
13188
13190
  await nextRouteHandler();
13189
13191
  }
13190
- async renderComponent(Component, props, options) {
13191
- const { currentPath, route, routeParams, nonce } = options;
13192
- const locale = options.locale;
13192
+ async renderComponent(Component, props, options2) {
13193
+ const { currentPath, route, routeParams, nonce } = options2;
13194
+ const locale = options2.locale;
13193
13195
  const translations = {
13194
13196
  ...getTranslations(locale),
13195
- ...options.translations || {}
13197
+ ...options2.translations || {}
13196
13198
  };
13197
13199
  const ctx = {
13198
13200
  currentPath,
@@ -13211,29 +13213,10 @@ var Renderer = class {
13211
13213
  scriptDeps: [],
13212
13214
  styleDeps: []
13213
13215
  };
13214
- const vdom = /* @__PURE__ */ jsx4(ASSET_MAP_CONTEXT.Provider, { value: this.assetMap, children: /* @__PURE__ */ jsx4(REQUEST_CONTEXT.Provider, { value: ctx, children: /* @__PURE__ */ jsx4(I18N_CONTEXT.Provider, { value: { locale, translations }, children: /* @__PURE__ */ jsx4(HTML_CONTEXT.Provider, { value: htmlContext, children: /* @__PURE__ */ jsx4(Component, { ...props }) }) }) }) });
13215
- const renderJsxFn = await this.getJsxRenderFn({ mode: options.renderMode });
13216
- const preactHook = preactOptions.vnode;
13217
- let mainHtml;
13218
- try {
13219
- preactOptions.vnode = (vnode) => {
13220
- if (vnode && vnode.type === "script") {
13221
- vnode.props.nonce = nonce;
13222
- }
13223
- if (vnode && vnode.type === "style") {
13224
- vnode.props.nonce = nonce;
13225
- }
13226
- if (vnode && vnode.type === "link" && vnode.props.rel === "stylesheet") {
13227
- vnode.props.nonce = nonce;
13228
- }
13229
- if (preactHook) {
13230
- preactHook(vnode);
13231
- }
13232
- };
13233
- mainHtml = renderJsxFn(vdom);
13234
- } finally {
13235
- preactOptions.vnode = preactHook;
13236
- }
13216
+ const withProviders = (children) => /* @__PURE__ */ jsx4(ASSET_MAP_CONTEXT.Provider, { value: this.assetMap, children: /* @__PURE__ */ jsx4(REQUEST_CONTEXT.Provider, { value: ctx, children: /* @__PURE__ */ jsx4(I18N_CONTEXT.Provider, { value: { locale, translations }, children: /* @__PURE__ */ jsx4(HTML_CONTEXT.Provider, { value: htmlContext, children }) }) }) });
13217
+ const vdom = withProviders(/* @__PURE__ */ jsx4(Component, { ...props }));
13218
+ const renderJsxFn = await this.getJsxRenderFn({ mode: options2.renderMode });
13219
+ const mainHtml = this.withNonceHooks(nonce, () => renderJsxFn(vdom));
13237
13220
  const jsDeps = /* @__PURE__ */ new Set();
13238
13221
  const cssDeps = /* @__PURE__ */ new Set();
13239
13222
  const preloadDeps = this.rootConfig.modulePreload !== false ? /* @__PURE__ */ new Set() : null;
@@ -13286,19 +13269,69 @@ var Renderer = class {
13286
13269
  htmlAttrs: htmlContext.htmlAttrs,
13287
13270
  headAttrs: htmlContext.headAttrs,
13288
13271
  bodyAttrs: htmlContext.bodyAttrs,
13272
+ // Components passed as `<Head>` children are only rendered here, so they
13273
+ // need the same context providers and nonce hooks as the main render.
13289
13274
  headComponents: [
13290
- ...htmlContext.headComponents,
13275
+ withProviders(htmlContext.headComponents),
13291
13276
  ...styleTags,
13292
13277
  ...preloadTags,
13293
13278
  ...scriptTags
13294
13279
  ],
13295
- renderMode: options.renderMode
13280
+ renderMode: options2.renderMode,
13281
+ nonce
13296
13282
  });
13297
13283
  return { html };
13298
13284
  }
13285
+ /**
13286
+ * Runs a synchronous render function with a hook installed that auto-injects
13287
+ * the CSP nonce into `<script>`, `<style>` and `<link rel="stylesheet">`
13288
+ * vnodes.
13289
+ *
13290
+ * NOTE: `preactOptions.vnode` is a global, so the hook must be installed and
13291
+ * restored within a single synchronous block. Awaiting inside the swapped
13292
+ * window allows concurrent renders to interleave, leaking a stale hook (and
13293
+ * its nonce) into other requests.
13294
+ *
13295
+ * https://preactjs.com/guide/v10/options/
13296
+ */
13297
+ withNonceHooks(nonce, fn) {
13298
+ const injectNonce = (vnode) => {
13299
+ if (!vnode) {
13300
+ return;
13301
+ }
13302
+ if (vnode.type === "script" || vnode.type === "style") {
13303
+ vnode.props.nonce = nonce;
13304
+ }
13305
+ if (vnode.type === "link" && vnode.props.rel === "stylesheet") {
13306
+ vnode.props.nonce = nonce;
13307
+ }
13308
+ };
13309
+ const preactHook = preactOptions.vnode;
13310
+ const rootJsxHook = options.vnode;
13311
+ try {
13312
+ preactOptions.vnode = (vnode) => {
13313
+ injectNonce(vnode);
13314
+ if (preactHook) {
13315
+ preactHook(vnode);
13316
+ }
13317
+ };
13318
+ if (options !== preactOptions) {
13319
+ options.vnode = (vnode) => {
13320
+ injectNonce(vnode);
13321
+ if (rootJsxHook) {
13322
+ rootJsxHook(vnode);
13323
+ }
13324
+ };
13325
+ }
13326
+ return fn();
13327
+ } finally {
13328
+ preactOptions.vnode = preactHook;
13329
+ options.vnode = rootJsxHook;
13330
+ }
13331
+ }
13299
13332
  /** SSG renders a route. */
13300
- async renderRoute(route, options) {
13301
- const routeParams = options.routeParams;
13333
+ async renderRoute(route, options2) {
13334
+ const routeParams = options2.routeParams;
13302
13335
  if (route.locale) {
13303
13336
  routeParams.$locale = route.locale;
13304
13337
  }
@@ -13343,10 +13376,10 @@ var Renderer = class {
13343
13376
  });
13344
13377
  }
13345
13378
  /** Handles the SSR rendering of a route. */
13346
- async handleRoute(req, res, next, route, options) {
13347
- const defaultStatusCode = options?.defaultStatusCode || 200;
13348
- const routeParams = options?.routeParams || {};
13349
- const nextRouteHandler = options?.nextRouteHandler;
13379
+ async handleRoute(req, res, next, route, options2) {
13380
+ const defaultStatusCode = options2?.defaultStatusCode || 200;
13381
+ const routeParams = options2?.routeParams || {};
13382
+ const nextRouteHandler = options2?.nextRouteHandler;
13350
13383
  const fallbackLocales = route.isDefaultLocale ? getFallbackLocales(req) : [route.locale];
13351
13384
  const getPreferredLocale = (availableLocales) => {
13352
13385
  const lowerLocales = availableLocales.map((l) => l.toLowerCase());
@@ -13357,8 +13390,8 @@ var Renderer = class {
13357
13390
  }
13358
13391
  return req.rootConfig?.i18n?.defaultLocale || "en";
13359
13392
  };
13360
- const render404 = async (options2) => {
13361
- if (options2?.nextRoute && nextRouteHandler) {
13393
+ const render404 = async (options3) => {
13394
+ if (options3?.nextRoute && nextRouteHandler) {
13362
13395
  await nextRouteHandler();
13363
13396
  return;
13364
13397
  }
@@ -13367,7 +13400,7 @@ var Renderer = class {
13367
13400
  }
13368
13401
  next();
13369
13402
  };
13370
- const render = async (props2, options2) => {
13403
+ const render = async (props2, options3) => {
13371
13404
  if (!route.module.default) {
13372
13405
  console.error(`no default component exported in route: ${route.src}`);
13373
13406
  render404();
@@ -13376,8 +13409,8 @@ var Renderer = class {
13376
13409
  const securityConfig = getSecurityConfig(this.rootConfig);
13377
13410
  const cspEnabled = !!securityConfig.contentSecurityPolicy;
13378
13411
  const currentPath = req.path;
13379
- const locale = options2?.locale || route.locale;
13380
- const translations = options2?.translations;
13412
+ const locale = options3?.locale || route.locale;
13413
+ const translations = options3?.translations;
13381
13414
  const nonce = cspEnabled ? this.generateNonce() : void 0;
13382
13415
  const output = await this.renderComponent(route.module.default, props2, {
13383
13416
  currentPath,
@@ -13386,7 +13419,7 @@ var Renderer = class {
13386
13419
  locale,
13387
13420
  translations,
13388
13421
  nonce,
13389
- renderMode: options2?.renderMode
13422
+ renderMode: options3?.renderMode
13390
13423
  });
13391
13424
  let html = await transformHtml(output.html, this.rootConfig);
13392
13425
  if (req.viteServer) {
@@ -13398,7 +13431,7 @@ var Renderer = class {
13398
13431
  );
13399
13432
  }
13400
13433
  }
13401
- let statusCode = options2?.statusCode || defaultStatusCode;
13434
+ let statusCode = options3?.statusCode || defaultStatusCode;
13402
13435
  if (route.src === "routes/404.tsx") {
13403
13436
  statusCode = 404;
13404
13437
  } else if (route.src === "routes/401.tsx") {
@@ -13561,8 +13594,8 @@ var Renderer = class {
13561
13594
  async getJsxRenderFn(renderOptions) {
13562
13595
  const mode = renderOptions?.mode ?? this.rootConfig.jsxRenderer?.mode;
13563
13596
  if (mode) {
13564
- const options = { ...this.getJsxRenderOptions(), mode };
13565
- return (vnode) => renderJsxToString(vnode, options);
13597
+ const options2 = { ...this.getJsxRenderOptions(), mode };
13598
+ return (vnode) => renderJsxToString(vnode, options2);
13566
13599
  }
13567
13600
  const { renderToString } = await import("preact-render-to-string");
13568
13601
  if (!renderToString) {
@@ -13577,8 +13610,8 @@ var Renderer = class {
13577
13610
  * `preact-render-to-string` depending if the `jsxRenderer` config is set up
13578
13611
  * in `root.config.ts`.
13579
13612
  */
13580
- async renderJsx(vnode, options) {
13581
- const render = await this.getJsxRenderFn(options);
13613
+ async renderJsx(vnode, options2) {
13614
+ const render = await this.getJsxRenderFn(options2);
13582
13615
  return render(vnode);
13583
13616
  }
13584
13617
  getConfiguredStyleEntries() {
@@ -13586,14 +13619,14 @@ var Renderer = class {
13586
13619
  const basePath = this.rootConfig.base || "/";
13587
13620
  return styleEntries.map((entry) => entry.trim()).filter((entry) => entry).map((entry) => normalizeStyleEntry(entry, basePath));
13588
13621
  }
13589
- async renderHtml(html, options) {
13590
- const htmlAttrs = options?.htmlAttrs || {};
13591
- const headAttrs = options?.headAttrs || {};
13592
- const bodyAttrs = options?.bodyAttrs || {};
13622
+ async renderHtml(html, options2) {
13623
+ const htmlAttrs = options2?.htmlAttrs || {};
13624
+ const headAttrs = options2?.headAttrs || {};
13625
+ const bodyAttrs = options2?.bodyAttrs || {};
13593
13626
  const page = /* @__PURE__ */ jsxs4("html", { ...htmlAttrs, children: [
13594
13627
  /* @__PURE__ */ jsxs4("head", { ...headAttrs, children: [
13595
13628
  /* @__PURE__ */ jsx4("meta", { charSet: "utf-8" }),
13596
- options?.headComponents
13629
+ options2?.headComponents
13597
13630
  ] }),
13598
13631
  /* @__PURE__ */ jsx4(
13599
13632
  "body",
@@ -13603,7 +13636,9 @@ var Renderer = class {
13603
13636
  }
13604
13637
  )
13605
13638
  ] });
13606
- const content = await this.renderJsx(page, { mode: options?.renderMode });
13639
+ const renderJsxFn = await this.getJsxRenderFn({ mode: options2?.renderMode });
13640
+ const nonce = options2?.nonce;
13641
+ const content = nonce ? this.withNonceHooks(nonce, () => renderJsxFn(page)) : renderJsxFn(page);
13607
13642
  return `<!doctype html>
13608
13643
  ${content}`;
13609
13644
  }
@@ -13613,10 +13648,10 @@ ${content}`;
13613
13648
  }
13614
13649
  return str;
13615
13650
  }
13616
- async render404(options) {
13651
+ async render404(options2) {
13617
13652
  const basePath = this.rootConfig.base || "/";
13618
13653
  const notFoundPath = normalizeUrlPath(`${basePath}/404`);
13619
- const currentPath = options?.currentPath || notFoundPath;
13654
+ const currentPath = options2?.currentPath || notFoundPath;
13620
13655
  const [route, routeParams] = this.router.get(notFoundPath);
13621
13656
  if (route && route.src === "routes/404.tsx" && route.module.default) {
13622
13657
  const Component = route.module.default;
@@ -13651,8 +13686,8 @@ ${content}`;
13651
13686
  });
13652
13687
  return { html };
13653
13688
  }
13654
- async renderError(err, options) {
13655
- const currentPath = options?.currentPath || "/500";
13689
+ async renderError(err, options2) {
13690
+ const currentPath = options2?.currentPath || "/500";
13656
13691
  const [route, routeParams] = this.router.get("/500");
13657
13692
  if (route && route.src === "routes/500.tsx" && route.module.default) {
13658
13693
  const Component = route.module.default;