@blinkk/root 3.0.6 → 3.1.1

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.
Files changed (39) hide show
  1. package/dist/build-worker.js +108 -0
  2. package/dist/build-worker.js.map +7 -0
  3. package/dist/chunk-3PWR4F2R.js +77 -0
  4. package/dist/chunk-3PWR4F2R.js.map +7 -0
  5. package/dist/{chunk-3EFECH2D.js → chunk-E4F6CMO3.js} +439 -553
  6. package/dist/chunk-E4F6CMO3.js.map +7 -0
  7. package/dist/{chunk-7HK6F5RQ.js → chunk-I3RCAIW7.js} +1 -75
  8. package/dist/chunk-I3RCAIW7.js.map +7 -0
  9. package/dist/chunk-KHO3HMX6.js +291 -0
  10. package/dist/chunk-KHO3HMX6.js.map +7 -0
  11. package/dist/chunk-M3E4RKS7.js +374 -0
  12. package/dist/chunk-M3E4RKS7.js.map +7 -0
  13. package/dist/{chunk-6NBKAR5Y.js → chunk-TM6QRBGS.js} +31 -1
  14. package/dist/chunk-TM6QRBGS.js.map +7 -0
  15. package/dist/chunk-WX62JK5E.js +81 -0
  16. package/dist/chunk-WX62JK5E.js.map +7 -0
  17. package/dist/{chunk-7PSEEE6C.js → chunk-YU22SAIG.js} +31 -317
  18. package/dist/chunk-YU22SAIG.js.map +7 -0
  19. package/dist/cli/build-page.d.ts +57 -0
  20. package/dist/cli/build-worker-pool.d.ts +46 -0
  21. package/dist/cli/build-worker.d.ts +4 -0
  22. package/dist/cli/build.d.ts +6 -0
  23. package/dist/cli.js +8 -4
  24. package/dist/core/security.d.ts +14 -0
  25. package/dist/functions.js +8 -4
  26. package/dist/functions.js.map +1 -1
  27. package/dist/middleware/common.d.ts +18 -0
  28. package/dist/middleware/compression.d.ts +12 -0
  29. package/dist/middleware/middleware.d.ts +1 -0
  30. package/dist/middleware.js +6 -1
  31. package/dist/node.js +7 -5
  32. package/dist/render/render.d.ts +15 -9
  33. package/dist/render.js +42 -95
  34. package/dist/render.js.map +2 -2
  35. package/package.json +2 -2
  36. package/dist/chunk-3EFECH2D.js.map +0 -7
  37. package/dist/chunk-6NBKAR5Y.js.map +0 -7
  38. package/dist/chunk-7HK6F5RQ.js.map +0 -7
  39. package/dist/chunk-7PSEEE6C.js.map +0 -7
package/dist/cli.js CHANGED
@@ -8,11 +8,15 @@ import {
8
8
  dev,
9
9
  preview,
10
10
  start
11
- } from "./chunk-3EFECH2D.js";
12
- import "./chunk-7HK6F5RQ.js";
13
- import "./chunk-6NBKAR5Y.js";
14
- import "./chunk-7PSEEE6C.js";
11
+ } from "./chunk-E4F6CMO3.js";
12
+ import "./chunk-YU22SAIG.js";
13
+ import "./chunk-I3RCAIW7.js";
14
+ import "./chunk-M3E4RKS7.js";
15
+ import "./chunk-KHO3HMX6.js";
16
+ import "./chunk-3PWR4F2R.js";
15
17
  import "./chunk-XSNCF7WU.js";
18
+ import "./chunk-TM6QRBGS.js";
19
+ import "./chunk-WX62JK5E.js";
16
20
  export {
17
21
  CliRunner,
18
22
  build,
@@ -0,0 +1,14 @@
1
+ import { RootConfig, RootSecurityConfig } from './config.js';
2
+ import { Response } from './types.js';
3
+ /**
4
+ * Returns the `security` config value with default values inserted wherever
5
+ * a user config value is blank or set to `true`.
6
+ */
7
+ export declare function getSecurityConfig(rootConfig: RootConfig): Required<RootSecurityConfig>;
8
+ /**
9
+ * Sets security-related HTTP headers.
10
+ */
11
+ export declare function setSecurityHeaders(res: Response, options: {
12
+ securityConfig: Required<RootSecurityConfig>;
13
+ nonce?: string;
14
+ }): void;
package/dist/functions.js CHANGED
@@ -1,11 +1,15 @@
1
1
  import {
2
2
  createPreviewServer,
3
3
  createProdServer
4
- } from "./chunk-3EFECH2D.js";
5
- import "./chunk-7HK6F5RQ.js";
6
- import "./chunk-6NBKAR5Y.js";
7
- import "./chunk-7PSEEE6C.js";
4
+ } from "./chunk-E4F6CMO3.js";
5
+ import "./chunk-YU22SAIG.js";
6
+ import "./chunk-I3RCAIW7.js";
7
+ import "./chunk-M3E4RKS7.js";
8
+ import "./chunk-KHO3HMX6.js";
9
+ import "./chunk-3PWR4F2R.js";
8
10
  import "./chunk-XSNCF7WU.js";
11
+ import "./chunk-TM6QRBGS.js";
12
+ import "./chunk-WX62JK5E.js";
9
13
 
10
14
  // src/functions/server.ts
11
15
  import path from "node:path";
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/functions/server.ts"],
4
4
  "sourcesContent": ["import path from 'node:path';\nimport {HttpsOptions, onRequest} from 'firebase-functions/v2/https';\nimport {createPreviewServer, createProdServer} from '../cli/cli.js';\nimport {Server} from '../core/types.js';\n\nexport interface ProdServerOptions {\n rootDir?: string;\n mode?: 'preview' | 'production';\n httpsOptions?: HttpsOptions;\n}\n\n/**\n * Firebase Function that runs a Root.js server running in SSR mode.\n */\nexport function server(options?: ProdServerOptions) {\n let rootServer: Server;\n const rootDir = path.resolve(options?.rootDir || process.cwd());\n return onRequest(options?.httpsOptions || {}, async (req, res) => {\n if (!rootServer) {\n if (options?.mode === 'preview') {\n rootServer = await createPreviewServer({rootDir});\n } else {\n rootServer = await createProdServer({rootDir});\n }\n }\n await rootServer(req, res);\n });\n}\n"],
5
- "mappings": ";;;;;;;;;;AAAA,OAAO,UAAU;AACjB,SAAsB,iBAAgB;AAa/B,SAAS,OAAO,SAA6B;AAClD,MAAI;AACJ,QAAM,UAAU,KAAK,QAAQ,SAAS,WAAW,QAAQ,IAAI,CAAC;AAC9D,SAAO,UAAU,SAAS,gBAAgB,CAAC,GAAG,OAAO,KAAK,QAAQ;AAChE,QAAI,CAAC,YAAY;AACf,UAAI,SAAS,SAAS,WAAW;AAC/B,qBAAa,MAAM,oBAAoB,EAAC,QAAO,CAAC;AAAA,MAClD,OAAO;AACL,qBAAa,MAAM,iBAAiB,EAAC,QAAO,CAAC;AAAA,MAC/C;AAAA,IACF;AACA,UAAM,WAAW,KAAK,GAAG;AAAA,EAC3B,CAAC;AACH;",
5
+ "mappings": ";;;;;;;;;;;;;;AAAA,OAAO,UAAU;AACjB,SAAsB,iBAAgB;AAa/B,SAAS,OAAO,SAA6B;AAClD,MAAI;AACJ,QAAM,UAAU,KAAK,QAAQ,SAAS,WAAW,QAAQ,IAAI,CAAC;AAC9D,SAAO,UAAU,SAAS,gBAAgB,CAAC,GAAG,OAAO,KAAK,QAAQ;AAChE,QAAI,CAAC,YAAY;AACf,UAAI,SAAS,SAAS,WAAW;AAC/B,qBAAa,MAAM,oBAAoB,EAAC,QAAO,CAAC;AAAA,MAClD,OAAO;AACL,qBAAa,MAAM,iBAAiB,EAAC,QAAO,CAAC;AAAA,MAC/C;AAAA,IACF;AACA,UAAM,WAAW,KAAK,GAAG;AAAA,EAC3B,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -6,6 +6,24 @@ import { Request, Response, NextFunction } from '../core/types.js';
6
6
  export declare function rootProjectMiddleware(options: {
7
7
  rootConfig: RootConfig;
8
8
  }): (req: Request, _: Response, next: NextFunction) => void;
9
+ /**
10
+ * Middleware that sets security-related HTTP headers (e.g.
11
+ * `Strict-Transport-Security`) on all responses using the `server.security`
12
+ * config in root.config.ts.
13
+ *
14
+ * The renderer sets these headers on rendered HTML responses, but responses
15
+ * that bypass the renderer (e.g. redirects, static files, plugin-served
16
+ * responses, 404s) would otherwise be served without them. Per
17
+ * https://hstspreload.org, HSTS preload eligibility requires the HSTS header
18
+ * on redirects as well.
19
+ *
20
+ * NOTE: `Content-Security-Policy` is excluded here since the CSP is
21
+ * document-specific and depends on a per-request nonce, which is generated by
22
+ * the renderer at render time.
23
+ */
24
+ export declare function securityHeadersMiddleware(options: {
25
+ rootConfig: RootConfig;
26
+ }): (req: Request, res: Response, next: NextFunction) => void;
9
27
  /**
10
28
  * Middleware that injects HTTP headers from the `server.headers` config in
11
29
  * root.config.ts.
@@ -0,0 +1,12 @@
1
+ import { Request, Response, NextFunction } from '../core/types.js';
2
+ /**
3
+ * Response compression middleware shared by the preview and prod servers.
4
+ *
5
+ * Uses brotli when the client accepts it (`Accept-Encoding: br`), falling
6
+ * back to gzip. Brotli quality 4 is used as a balance between compression
7
+ * ratio and CPU cost for dynamically-rendered responses — it generally
8
+ * compresses better than gzip's default (level 6) at comparable speed.
9
+ * Pre-compressed static assets should still be served with higher quality
10
+ * settings by a CDN or static file server where possible.
11
+ */
12
+ export declare function compressionMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
@@ -1,3 +1,4 @@
1
1
  export * from './common.js';
2
+ export * from './compression.js';
2
3
  export * from './multipart.js';
3
4
  export * from './session.js';
@@ -1,18 +1,23 @@
1
1
  import {
2
2
  SESSION_COOKIE,
3
3
  Session,
4
+ compressionMiddleware,
4
5
  headersMiddleware,
5
6
  multipartMiddleware,
6
7
  rootProjectMiddleware,
8
+ securityHeadersMiddleware,
7
9
  sessionMiddleware,
8
10
  trailingSlashMiddleware
9
- } from "./chunk-6NBKAR5Y.js";
11
+ } from "./chunk-TM6QRBGS.js";
12
+ import "./chunk-WX62JK5E.js";
10
13
  export {
11
14
  SESSION_COOKIE,
12
15
  Session,
16
+ compressionMiddleware,
13
17
  headersMiddleware,
14
18
  multipartMiddleware,
15
19
  rootProjectMiddleware,
20
+ securityHeadersMiddleware,
16
21
  sessionMiddleware,
17
22
  trailingSlashMiddleware
18
23
  };
package/dist/node.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import {
2
- bundleRootConfig,
3
2
  collectPods,
4
3
  createViteServer,
4
+ invalidatePodCache,
5
+ viteSsrLoadModule
6
+ } from "./chunk-YU22SAIG.js";
7
+ import {
8
+ bundleRootConfig,
5
9
  flattenPackageDepsFromMonorepo,
6
10
  getMonorepoPackageDeps,
7
- invalidatePodCache,
8
11
  loadBundledConfig,
9
12
  loadPackageJson,
10
13
  loadRootConfig,
11
- loadRootConfigWithDeps,
12
- viteSsrLoadModule
13
- } from "./chunk-7PSEEE6C.js";
14
+ loadRootConfigWithDeps
15
+ } from "./chunk-KHO3HMX6.js";
14
16
  import "./chunk-XSNCF7WU.js";
15
17
  export {
16
18
  bundleRootConfig,
@@ -13,6 +13,10 @@ export declare class Renderer {
13
13
  });
14
14
  /** Returns a route from the router. */
15
15
  getRoute(url: string): [Route | undefined, Record<string, string>];
16
+ /** Returns all routes that match a given url path. */
17
+ getRouteMatches(url: string): Array<[Route, Record<string, string>]>;
18
+ /** Walks all routes registered with the router. */
19
+ walkRoutes(cb: (urlPathFormat: string, route: Route) => void | Promise<void>): Promise<void>;
16
20
  handle(req: Request, res: Response, next: NextFunction): Promise<void>;
17
21
  private renderComponent;
18
22
  /** SSG renders a route. */
@@ -45,6 +49,17 @@ export declare class Renderer {
45
49
  */
46
50
  getSitemap(): Promise<Sitemap>;
47
51
  private getJsxRenderOptions;
52
+ /**
53
+ * Resolves a synchronous JSX render function, using either the
54
+ * `@blinkk/root/jsx` package or `preact-render-to-string` depending if the
55
+ * `jsxRenderer` config is set up in `root.config.ts`.
56
+ *
57
+ * Resolving the renderer up front (rather than awaiting inside the render
58
+ * call) allows callers that temporarily swap global state (e.g. the
59
+ * `preact.options.vnode` nonce hook) to render synchronously without
60
+ * yielding to the event loop.
61
+ */
62
+ private getJsxRenderFn;
48
63
  /**
49
64
  * Renders JSX via either the `@blinkk/root/jsx` package or
50
65
  * `preact-render-to-string` depending if the `jsxRenderer` config is set up
@@ -75,17 +90,8 @@ export declare class Renderer {
75
90
  * automatically adds the JS/CSS deps to the page.
76
91
  */
77
92
  private collectElementDeps;
78
- /**
79
- * Returns the `security` config value with default values inserted wherever
80
- * a user config value is blank or set to `true`.
81
- */
82
- private getSecurityConfig;
83
93
  /**
84
94
  * Generates a random string that can be used as the "nonce" value for CSP.
85
95
  */
86
96
  private generateNonce;
87
- /**
88
- * Sets security-related HTTP headers.
89
- */
90
- private setSecurityHeaders;
91
97
  }
package/dist/render.js CHANGED
@@ -7,14 +7,20 @@ import {
7
7
  testPathHasParams
8
8
  } from "./chunk-KW7KTYGC.js";
9
9
  import {
10
- RouteTrie,
10
+ RouteTrie
11
+ } from "./chunk-I3RCAIW7.js";
12
+ import {
11
13
  parseTagNames,
12
14
  transformHtml
13
- } from "./chunk-7HK6F5RQ.js";
15
+ } from "./chunk-3PWR4F2R.js";
14
16
  import {
15
17
  renderJsxToString
16
18
  } from "./chunk-X2C2MEJ2.js";
17
19
  import "./chunk-3Z5TNZEP.js";
20
+ import {
21
+ getSecurityConfig,
22
+ setSecurityHeaders
23
+ } from "./chunk-WX62JK5E.js";
18
24
 
19
25
  // src/render/render.tsx
20
26
  import crypto from "node:crypto";
@@ -13119,6 +13125,14 @@ var Renderer = class {
13119
13125
  getRoute(url) {
13120
13126
  return this.router.get(url);
13121
13127
  }
13128
+ /** Returns all routes that match a given url path. */
13129
+ getRouteMatches(url) {
13130
+ return this.router.matchAll(url);
13131
+ }
13132
+ /** Walks all routes registered with the router. */
13133
+ async walkRoutes(cb) {
13134
+ await this.router.walk(cb);
13135
+ }
13122
13136
  async handle(req, res, next) {
13123
13137
  let url = req.path;
13124
13138
  if (url.includes("%")) {
@@ -13170,6 +13184,7 @@ var Renderer = class {
13170
13184
  scriptDeps: []
13171
13185
  };
13172
13186
  const vdom = /* @__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 }) }) }) });
13187
+ const renderJsxFn = await this.getJsxRenderFn();
13173
13188
  const preactHook = preactOptions.vnode;
13174
13189
  let mainHtml;
13175
13190
  try {
@@ -13187,11 +13202,9 @@ var Renderer = class {
13187
13202
  preactHook(vnode);
13188
13203
  }
13189
13204
  };
13190
- mainHtml = await this.renderJsx(vdom);
13191
- preactOptions.vnode = preactHook;
13192
- } catch (err) {
13205
+ mainHtml = renderJsxFn(vdom);
13206
+ } finally {
13193
13207
  preactOptions.vnode = preactHook;
13194
- throw err;
13195
13208
  }
13196
13209
  const jsDeps = /* @__PURE__ */ new Set();
13197
13210
  const cssDeps = /* @__PURE__ */ new Set();
@@ -13321,7 +13334,7 @@ var Renderer = class {
13321
13334
  render404();
13322
13335
  return;
13323
13336
  }
13324
- const securityConfig = this.getSecurityConfig();
13337
+ const securityConfig = getSecurityConfig(this.rootConfig);
13325
13338
  const cspEnabled = !!securityConfig.contentSecurityPolicy;
13326
13339
  const currentPath = req.path;
13327
13340
  const locale = options2?.locale || route.locale;
@@ -13365,7 +13378,7 @@ var Renderer = class {
13365
13378
  }
13366
13379
  res.status(statusCode);
13367
13380
  res.set({ "Content-Type": "text/html" });
13368
- this.setSecurityHeaders(res, {
13381
+ setSecurityHeaders(res, {
13369
13382
  securityConfig,
13370
13383
  nonce
13371
13384
  });
@@ -13493,13 +13506,19 @@ var Renderer = class {
13493
13506
  return this.rootConfig.jsxRenderer || {};
13494
13507
  }
13495
13508
  /**
13496
- * Renders JSX via either the `@blinkk/root/jsx` package or
13497
- * `preact-render-to-string` depending if the `jsxRenderer` config is set up
13498
- * in `root.config.ts`.
13509
+ * Resolves a synchronous JSX render function, using either the
13510
+ * `@blinkk/root/jsx` package or `preact-render-to-string` depending if the
13511
+ * `jsxRenderer` config is set up in `root.config.ts`.
13512
+ *
13513
+ * Resolving the renderer up front (rather than awaiting inside the render
13514
+ * call) allows callers that temporarily swap global state (e.g. the
13515
+ * `preact.options.vnode` nonce hook) to render synchronously without
13516
+ * yielding to the event loop.
13499
13517
  */
13500
- async renderJsx(vnode) {
13518
+ async getJsxRenderFn() {
13501
13519
  if (this.rootConfig.jsxRenderer?.mode) {
13502
- return renderJsxToString(vnode, this.getJsxRenderOptions());
13520
+ const options = this.getJsxRenderOptions();
13521
+ return (vnode) => renderJsxToString(vnode, options);
13503
13522
  }
13504
13523
  const { renderToString } = await import("preact-render-to-string");
13505
13524
  if (!renderToString) {
@@ -13507,7 +13526,16 @@ var Renderer = class {
13507
13526
  'failed to render jsx. either install preact-render-to-string or add the "jsxRenderer" config to root.config.ts'
13508
13527
  );
13509
13528
  }
13510
- return renderToString(vnode);
13529
+ return (vnode) => renderToString(vnode);
13530
+ }
13531
+ /**
13532
+ * Renders JSX via either the `@blinkk/root/jsx` package or
13533
+ * `preact-render-to-string` depending if the `jsxRenderer` config is set up
13534
+ * in `root.config.ts`.
13535
+ */
13536
+ async renderJsx(vnode) {
13537
+ const render = await this.getJsxRenderFn();
13538
+ return render(vnode);
13511
13539
  }
13512
13540
  getConfiguredStyleEntries() {
13513
13541
  const styleEntries = this.rootConfig.styles?.entries || [];
@@ -13679,94 +13707,13 @@ ${content}`;
13679
13707
  );
13680
13708
  return { jsDeps, cssDeps };
13681
13709
  }
13682
- /**
13683
- * Returns the `security` config value with default values inserted wherever
13684
- * a user config value is blank or set to `true`.
13685
- */
13686
- getSecurityConfig() {
13687
- const userConfig = this.rootConfig.server?.security || {};
13688
- const securityConfig = {};
13689
- if (isTrueOrUndefined(userConfig.contentSecurityPolicy)) {
13690
- securityConfig.contentSecurityPolicy = {
13691
- directives: {
13692
- "base-uri": ["'none'"],
13693
- "object-src": ["'none'"],
13694
- // NOTE: nonce is automatically added to this list.
13695
- "script-src": [
13696
- "'unsafe-inline'",
13697
- "'unsafe-eval'",
13698
- "'strict-dynamic' https: http:"
13699
- ]
13700
- },
13701
- reportOnly: true
13702
- };
13703
- } else {
13704
- securityConfig.contentSecurityPolicy = userConfig.contentSecurityPolicy;
13705
- }
13706
- if (isTrueOrUndefined(userConfig.xFrameOptions)) {
13707
- securityConfig.xFrameOptions = "SAMEORIGIN";
13708
- } else {
13709
- securityConfig.xFrameOptions = userConfig.xFrameOptions;
13710
- }
13711
- securityConfig.strictTransportSecurity = userConfig.strictTransportSecurity ?? true;
13712
- securityConfig.xContentTypeOptions = userConfig.xContentTypeOptions ?? true;
13713
- securityConfig.xXssProtection = userConfig.xXssProtection ?? true;
13714
- return securityConfig;
13715
- }
13716
13710
  /**
13717
13711
  * Generates a random string that can be used as the "nonce" value for CSP.
13718
13712
  */
13719
13713
  generateNonce() {
13720
13714
  return crypto.randomBytes(16).toString("base64");
13721
13715
  }
13722
- /**
13723
- * Sets security-related HTTP headers.
13724
- */
13725
- setSecurityHeaders(res, options) {
13726
- const securityConfig = options.securityConfig;
13727
- const contentSecurityPolicy = securityConfig.contentSecurityPolicy;
13728
- if (typeof contentSecurityPolicy === "object") {
13729
- const directives = { ...contentSecurityPolicy.directives };
13730
- if (options.nonce) {
13731
- const scriptSrc = directives["script-src"] || [
13732
- "'unsafe-inline'",
13733
- "'unsafe-eval'",
13734
- "'strict-dynamic' https: http:"
13735
- ];
13736
- const scriptSrcWithNonce = [...scriptSrc, `'nonce-${options.nonce}'`];
13737
- directives["script-src"] = scriptSrcWithNonce;
13738
- }
13739
- const headerSegments = [];
13740
- Object.entries(directives).forEach(([key, values]) => {
13741
- headerSegments.push([key, ...values].join(" "));
13742
- });
13743
- const csp = headerSegments.join("; ");
13744
- if (contentSecurityPolicy.reportOnly === false) {
13745
- res.setHeader("content-security-policy", csp);
13746
- } else {
13747
- res.setHeader("content-security-policy-report-only", csp);
13748
- }
13749
- }
13750
- if (typeof securityConfig.xFrameOptions === "string") {
13751
- res.setHeader("x-frame-options", securityConfig.xFrameOptions);
13752
- }
13753
- if (securityConfig.strictTransportSecurity) {
13754
- res.setHeader(
13755
- "strict-transport-security",
13756
- "max-age=63072000; includeSubdomains; preload"
13757
- );
13758
- }
13759
- if (securityConfig.xContentTypeOptions) {
13760
- res.setHeader("x-content-type-options", "nosniff");
13761
- }
13762
- if (securityConfig.xXssProtection) {
13763
- res.setHeader("x-xss-protection", "1; mode=block");
13764
- }
13765
- }
13766
13716
  };
13767
- function isTrueOrUndefined(value) {
13768
- return value === true || value === void 0;
13769
- }
13770
13717
  function sortLocales(a, b) {
13771
13718
  if (a === "x-default") {
13772
13719
  return -1;