@salesforce/storefront-next-dev 1.4.0 → 1.4.1-alpha.0

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 (137) hide show
  1. package/README.md +13 -2
  2. package/dist/assets.js +25 -0
  3. package/dist/audit/catalog/categories/best-practices/CATEGORY.md +21 -0
  4. package/dist/audit/catalog/categories/best-practices/rules/commerce-api-client.md +20 -0
  5. package/dist/audit/catalog/categories/best-practices/rules/error-boundaries.md +19 -0
  6. package/dist/audit/catalog/categories/best-practices/rules/production-integrations.md +20 -0
  7. package/dist/audit/catalog/categories/best-practices/rules/route-revalidation.md +19 -0
  8. package/dist/audit/catalog/categories/best-practices/rules/server-data-loading.md +19 -0
  9. package/dist/audit/catalog/categories/performance/CATEGORY.md +18 -0
  10. package/dist/audit/catalog/categories/performance/rules/app-config-size.js +15 -0
  11. package/dist/audit/catalog/categories/performance/rules/app-config-size.md +22 -0
  12. package/dist/audit/catalog/categories/performance/rules/cache-control.js +59 -0
  13. package/dist/audit/catalog/categories/performance/rules/cache-control.md +20 -0
  14. package/dist/audit/catalog/categories/performance/rules/cls-budget.js +13 -0
  15. package/dist/audit/catalog/categories/performance/rules/cls-budget.md +20 -0
  16. package/dist/audit/catalog/categories/performance/rules/content-encoding.js +35 -0
  17. package/dist/audit/catalog/categories/performance/rules/content-encoding.md +20 -0
  18. package/dist/audit/catalog/categories/performance/rules/dom-size.js +18 -0
  19. package/dist/audit/catalog/categories/performance/rules/dom-size.md +22 -0
  20. package/dist/audit/catalog/categories/performance/rules/html-document-size.js +15 -0
  21. package/dist/audit/catalog/categories/performance/rules/html-document-size.md +20 -0
  22. package/dist/audit/catalog/categories/performance/rules/image-dimensions.js +141 -0
  23. package/dist/audit/catalog/categories/performance/rules/image-dimensions.md +22 -0
  24. package/dist/audit/catalog/categories/performance/rules/image-transformation.js +117 -0
  25. package/dist/audit/catalog/categories/performance/rules/image-transformation.md +22 -0
  26. package/dist/audit/catalog/categories/performance/rules/javascript-size.js +126 -0
  27. package/dist/audit/catalog/categories/performance/rules/javascript-size.md +20 -0
  28. package/dist/audit/catalog/categories/performance/rules/large-images.js +187 -0
  29. package/dist/audit/catalog/categories/performance/rules/large-images.md +22 -0
  30. package/dist/audit/catalog/categories/performance/rules/lcp-budget.js +13 -0
  31. package/dist/audit/catalog/categories/performance/rules/lcp-budget.md +20 -0
  32. package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.js +227 -0
  33. package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.md +27 -0
  34. package/dist/audit/catalog/categories/performance/rules/page-data-size.js +15 -0
  35. package/dist/audit/catalog/categories/performance/rules/page-data-size.md +22 -0
  36. package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.js +97 -0
  37. package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.md +73 -0
  38. package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.js +201 -0
  39. package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.md +20 -0
  40. package/dist/audit/catalog/categories/performance/rules/server-request-waterfall.md +85 -0
  41. package/dist/audit/catalog/categories/performance/rules/tbt-budget.js +13 -0
  42. package/dist/audit/catalog/categories/performance/rules/tbt-budget.md +20 -0
  43. package/dist/audit/catalog/categories/performance/rules/third-party-fonts.js +265 -0
  44. package/dist/audit/catalog/categories/performance/rules/third-party-fonts.md +20 -0
  45. package/dist/audit/catalog/categories/performance/rules/ttfb-budget.js +17 -0
  46. package/dist/audit/catalog/categories/performance/rules/ttfb-budget.md +20 -0
  47. package/dist/audit/catalog/categories/security/CATEGORY.md +17 -0
  48. package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.js +37 -0
  49. package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.md +20 -0
  50. package/dist/audit/catalog/categories/security/rules/content-security-policy.js +40 -0
  51. package/dist/audit/catalog/categories/security/rules/content-security-policy.md +20 -0
  52. package/dist/audit/catalog/categories/security/rules/csp-runtime-compatibility.md +24 -0
  53. package/dist/audit/catalog/categories/security/rules/dependency-vulnerabilities.md +19 -0
  54. package/dist/audit/catalog/categories/security/rules/dynamic-code-execution.md +19 -0
  55. package/dist/audit/catalog/categories/security/rules/hardcoded-credentials.md +19 -0
  56. package/dist/audit/catalog/categories/security/rules/html-config-secrets.js +46 -0
  57. package/dist/audit/catalog/categories/security/rules/html-config-secrets.md +22 -0
  58. package/dist/audit/catalog/categories/security/rules/protected-route-authorization.md +19 -0
  59. package/dist/audit/catalog/categories/security/rules/public-client-secrets.md +20 -0
  60. package/dist/audit/catalog/categories/security/rules/raw-html-sinks.md +20 -0
  61. package/dist/audit/catalog/categories/security/rules/strict-transport-security.js +183 -0
  62. package/dist/audit/catalog/categories/security/rules/strict-transport-security.md +24 -0
  63. package/dist/audit/catalog/categories/seo/CATEGORY.md +17 -0
  64. package/dist/audit/catalog/categories/seo/rules/canonical-url.md +20 -0
  65. package/dist/audit/catalog/categories/seo/rules/locale-alternates.md +19 -0
  66. package/dist/audit/catalog/categories/seo/rules/meta-description.md +19 -0
  67. package/dist/audit/catalog/categories/seo/rules/page-title.md +19 -0
  68. package/dist/audit/catalog/categories/seo/rules/robots-indexing.md +20 -0
  69. package/dist/audit/catalog/categories/seo/rules/structured-data.md +19 -0
  70. package/dist/audit/instructions/evaluate-rule.md +42 -0
  71. package/dist/audit/schemas/audit-result.schema.json +1237 -0
  72. package/dist/audit/ui/assets/banner-brand-default-CDRB3_S0.png +0 -0
  73. package/dist/audit/ui/assets/banner-group-public-default-BGRJzVY8.png +0 -0
  74. package/dist/audit/ui/assets/banner-user-default-BW81Ba3z.png +0 -0
  75. package/dist/audit/ui/assets/bg-info@2x-CzpP0vaE.png +0 -0
  76. package/dist/audit/ui/assets/einstein-figure-B1xFOo8O.svg +1 -0
  77. package/dist/audit/ui/assets/group_avatar_160-CrxVHlYA.png +0 -0
  78. package/dist/audit/ui/assets/group_avatar_200-CSLS8I0r.png +0 -0
  79. package/dist/audit/ui/assets/index-BKCWlO9b.css +1 -0
  80. package/dist/audit/ui/assets/index-CUPt8e4A.js +44 -0
  81. package/dist/audit/ui/assets/profile_avatar_160-CtQTUBo0.png +0 -0
  82. package/dist/audit/ui/assets/profile_avatar_200-DAztxshF.png +0 -0
  83. package/dist/audit/ui/assets/symbols-BRmpfDbO.svg +1 -0
  84. package/dist/audit/ui/assets/symbols-D0Ne6690.svg +1 -0
  85. package/dist/audit/ui/einstein.png +0 -0
  86. package/dist/audit/ui/index.html +13 -0
  87. package/dist/audit.js +1896 -0
  88. package/dist/cartridge-services/index.d.ts +5 -0
  89. package/dist/cartridge-services/index.d.ts.map +1 -1
  90. package/dist/cartridge-services/index.js +94 -7
  91. package/dist/cartridge-services/index.js.map +1 -1
  92. package/dist/catalog.js +89 -0
  93. package/dist/commands/audit/catalog.js +27 -0
  94. package/dist/commands/audit/finalize.js +42 -0
  95. package/dist/commands/audit/prepare.js +84 -0
  96. package/dist/commands/audit/progress.js +51 -0
  97. package/dist/commands/audit/serve.js +63 -0
  98. package/dist/commands/config/aggregate-extensions.js +1 -0
  99. package/dist/commands/create-bundle.js +2 -1
  100. package/dist/commands/create-instructions.js +1 -0
  101. package/dist/commands/create-storefront.js +1 -0
  102. package/dist/commands/deploy-cartridge.js +11 -3
  103. package/dist/commands/dev.js +2 -0
  104. package/dist/commands/extensions/create.js +1 -0
  105. package/dist/commands/extensions/install.js +1 -0
  106. package/dist/commands/extensions/list.js +1 -0
  107. package/dist/commands/extensions/remove.js +1 -0
  108. package/dist/commands/generate-cartridge.js +49 -5
  109. package/dist/commands/locales/aggregate-extensions.js +1 -0
  110. package/dist/commands/prepare-local.js +1 -0
  111. package/dist/commands/preview.js +2 -0
  112. package/dist/commands/push.js +3 -1
  113. package/dist/commands/validate-cartridge.js +13 -4
  114. package/dist/config.js +13 -2
  115. package/dist/document-size.js +231 -0
  116. package/dist/events.js +688 -0
  117. package/dist/flags2.js +34 -0
  118. package/dist/generate-cartridge.js +41 -6
  119. package/dist/har.js +205 -0
  120. package/dist/images.js +146 -0
  121. package/dist/index.js +3 -3
  122. package/dist/index.js.map +1 -1
  123. package/dist/live.js +277 -0
  124. package/dist/logger.js +1 -4
  125. package/dist/measured-run.js +91 -0
  126. package/dist/metric-budget.js +125 -0
  127. package/dist/mrt/ssr.mjs +19 -19
  128. package/dist/mrt/ssr.mjs.map +1 -1
  129. package/dist/mrt/streamingHandler.mjs +26 -26
  130. package/dist/mrt/streamingHandler.mjs.map +1 -1
  131. package/dist/package.js +5 -0
  132. package/dist/sanitize.js +185 -0
  133. package/dist/schema.js +121 -0
  134. package/dist/server.js +8 -60
  135. package/dist/server2.js +185 -0
  136. package/dist/ts-import.js +57 -0
  137. package/package.json +22 -5
@@ -0,0 +1,5 @@
1
+ //#region package.json
2
+ var version = "1.4.1-alpha.0";
3
+
4
+ //#endregion
5
+ export { version as t };
@@ -0,0 +1,185 @@
1
+ //#region src/audit/sanitize.ts
2
+ /**
3
+ * Copyright 2026 Salesforce, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ const bearer = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
18
+ const secretKey = new RegExp(`\\b([\\w-]{0,64}?)(?:api[_-]?key|api[_-]?signature|access[_-]?token|token|secret|password|passwd|authorization|set-cookie|cookie|session[_-]?id|sessionid|credentials?|csrf[_-]?token|private[_-]?key)(?:[_-]?(?:access[_-]?)?(?:key|value|hash|id|token))?\\\\?["']?\\s*[:=]\\s*`, "gi");
19
+ const knownToken = /(?<![\w-])eyJ[\w-]{1,1024}\.eyJ[\w-]{1,16384}\.[\w-]*|\b[rs]k_(?:live|test)_[A-Za-z0-9]{10,}|\bwhsec_[A-Za-z0-9]{20,}|\bgh[opsru]_[A-Za-z0-9]{20,}|\bgithub_pat_\w{20,}|\bnpm_[A-Za-z0-9]{36}\b|\bx(?:ox[abeprs]|app)-(?![\w-]{0,253}\.[A-Za-z])[A-Za-z0-9-]{10,}|\bsk-(?:ant|proj)-(?![\w-]{0,253}\.[A-Za-z])[\w-]{20,}|\b(?:AKIA|ASIA)[0-9A-Z]{16}\b|(?<=hooks\.slack\.com\/services\/)[A-Za-z0-9/]+/g;
20
+ const pemBegin = /-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----/;
21
+ const pemEnd = /-----END [A-Z ]{0,40}PRIVATE KEY-----/;
22
+ const pemLine = /(?:[ \t]*(?:\r?\n|(?:\\r)?\\n))*[ \t]*(?:(?:Proc-Type|DEK-Info):[^\r\n\\]*|[A-Za-z0-9+/=]{16,})/y;
23
+ const maxKeyLength = 16384;
24
+ /** Not after `@` or `~`: `@/components/header` and `~/lib` are import aliases, not files on disk. */
25
+ const absolutePath = /(?<![\w.@~-])(?:[A-Za-z]:[\\/]|\/)(?:[^\s<>'"`]+[\\/])+[^\s<>'"`,.;:]*/g;
26
+ const webUrl = /https?:\/\/[^\s<>'"`]+/gi;
27
+ /** Returns the first match at or after an index. Indexes must not decrease; reusing the last match keeps repeated searches linear. */
28
+ function finder(pattern, text) {
29
+ const global = new RegExp(pattern.source, "g");
30
+ let found;
31
+ return (from) => {
32
+ if (found === void 0 || found && found.index < from) {
33
+ global.lastIndex = from;
34
+ found = global.exec(text);
35
+ }
36
+ return found;
37
+ };
38
+ }
39
+ /** Redacts each private key from its BEGIN line to its END line. A key without an END line before the next key
40
+ * (or within maxKeyLength) is redacted up to its last key-like line, so prose after a lone header stays. */
41
+ function redactPrivateKeys(text) {
42
+ const nextBegin = finder(pemBegin, text);
43
+ const nextEnd = finder(pemEnd, text);
44
+ let out = "";
45
+ let last = 0;
46
+ for (let begin = nextBegin(0); begin; begin = nextBegin(last)) {
47
+ const start = begin.index + begin[0].length;
48
+ const end = nextEnd(start);
49
+ const following = nextBegin(start);
50
+ let stop = start;
51
+ if (end && end.index - start <= maxKeyLength && (!following || following.index > end.index)) stop = end.index + end[0].length;
52
+ else for (pemLine.lastIndex = stop; pemLine.exec(text); pemLine.lastIndex = stop) stop = pemLine.lastIndex;
53
+ out += `${text.slice(last, begin.index)}[REDACTED]`;
54
+ last = stop;
55
+ }
56
+ return out + text.slice(last);
57
+ }
58
+ const quoteAt = (text, index) => {
59
+ if (text[index] === "\"" || text[index] === "'") return text[index];
60
+ return text[index] === "\\" && (text[index + 1] === "\"" || text[index + 1] === "'") ? text.slice(index, index + 2) : "";
61
+ };
62
+ const lineEnd = (text, index) => {
63
+ const end = text.indexOf("\n", index);
64
+ return end === -1 ? text.length : end;
65
+ };
66
+ const skip = (text, index, pattern) => {
67
+ while (index < text.length && pattern.test(text.charAt(index))) index++;
68
+ return index;
69
+ };
70
+ const wordChar = /[^\s"',;}&\\]/;
71
+ const tokenChar = /[\w.~+/=-]/;
72
+ const schemeCredential = /[ \t]+[^\s"',;}&\\]+/y;
73
+ /**
74
+ * Reads the value that starts at `start`: a quoted string up to its unescaped closing quote, a list or object up
75
+ * to its matching bracket (plus any token glued to it), an auth scheme with its credential, or one word. An
76
+ * unclosed quote or bracket takes the rest of the line. `core` is the value without its quotes.
77
+ */
78
+ function scanValue(text, start) {
79
+ const quote = quoteAt(text, start);
80
+ if (quote) {
81
+ for (let index = start + quote.length; index < text.length && text[index] !== "\n"; index += text[index] === "\\" && text[index + 1] !== "\n" ? 2 : 1) if (text.startsWith(quote, index)) return {
82
+ end: index + quote.length,
83
+ quote,
84
+ core: text.slice(start + quote.length, index)
85
+ };
86
+ } else if (text[start] === "[" || text[start] === "{") {
87
+ let depth = 0;
88
+ let inQuote = "";
89
+ for (let index = start; index < text.length && text[index] !== "\n"; index++) {
90
+ if (inQuote) {
91
+ if (text.startsWith(inQuote, index)) {
92
+ index += inQuote.length - 1;
93
+ inQuote = "";
94
+ } else if (text[index] === "\\") index++;
95
+ continue;
96
+ }
97
+ inQuote = quoteAt(text, index);
98
+ if (inQuote) index += inQuote.length - 1;
99
+ else if (text[index] === "[" || text[index] === "{") depth++;
100
+ else if ((text[index] === "]" || text[index] === "}") && --depth === 0) {
101
+ const end$1 = skip(text, index + 1, tokenChar);
102
+ return {
103
+ end: end$1,
104
+ quote: "",
105
+ core: text.slice(start, end$1)
106
+ };
107
+ }
108
+ }
109
+ } else {
110
+ let end$1 = skip(text, start, wordChar);
111
+ if (/^(?:Basic|Bearer|Digest|Token)$/i.test(text.slice(start, end$1))) {
112
+ schemeCredential.lastIndex = end$1;
113
+ if (schemeCredential.exec(text)) end$1 = schemeCredential.lastIndex;
114
+ }
115
+ return {
116
+ end: end$1,
117
+ quote: "",
118
+ core: text.slice(start, end$1)
119
+ };
120
+ }
121
+ const end = lineEnd(text, start);
122
+ return {
123
+ end,
124
+ quote: "",
125
+ core: text.slice(start, end)
126
+ };
127
+ }
128
+ /** Values that can't be secrets: an exact placeholder, booleans and null under any name (`hasPassword: true`),
129
+ * and numbers under a page, count or limit name (`pageToken=2`). */
130
+ function keepValue(prefix, value) {
131
+ return /^(?:\[REDACTED\]|(?:Basic|Bearer|Digest|Token) \[REDACTED\]|true|false|null|undefined)$/i.test(value) || /^-?\d+(?:\.\d+)?$/.test(value) && /page|max|min|count|limit|total|num/i.test(prefix);
132
+ }
133
+ /** Redacts the value of every secret-named assignment. Each value is read and decided on once, and the search
134
+ * resumes after it, so a value is never rewritten twice and the work stays linear. */
135
+ function redactAssignments(text) {
136
+ let out = "";
137
+ let last = 0;
138
+ secretKey.lastIndex = 0;
139
+ for (let key = secretKey.exec(text); key; key = secretKey.exec(text)) {
140
+ const start = key.index + key[0].length;
141
+ const { end, quote, core } = scanValue(text, start);
142
+ if (end === start) continue;
143
+ if (!keepValue(key[1] ?? "", core)) {
144
+ const scheme = /^(Basic|Bearer|Digest|Token)\s+\S/i.exec(core)?.[1];
145
+ out += `${text.slice(last, start)}${quote}${scheme ? `${scheme} ` : ""}[REDACTED]${quote}`;
146
+ last = end;
147
+ }
148
+ secretKey.lastIndex = end;
149
+ }
150
+ return out + text.slice(last);
151
+ }
152
+ function sanitizeUrl(value) {
153
+ const parsed = new URL(value);
154
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new Error("URL must use https (or http with --allow-http)");
155
+ parsed.username = "";
156
+ parsed.password = "";
157
+ for (const key of [...parsed.searchParams.keys()]) parsed.searchParams.set(key, "[REDACTED]");
158
+ parsed.hash = "";
159
+ return parsed.toString();
160
+ }
161
+ function sanitizeText(value) {
162
+ const urls = [];
163
+ let sanitized = redactPrivateKeys(value.replaceAll("\0", "")).replace(knownToken, "[REDACTED]").replace(webUrl, (url) => {
164
+ try {
165
+ urls.push(sanitizeUrl(url));
166
+ return `\u0000URL${urls.length - 1}\u0000`;
167
+ } catch {
168
+ return "[REDACTED_URL]";
169
+ }
170
+ });
171
+ sanitized = redactAssignments(sanitized.replace(bearer, "Bearer [REDACTED]")).replace(absolutePath, "[REDACTED_PATH]");
172
+ return sanitized.replace(/\u0000URL(\d+)\u0000/g, (match, index) => urls[Number(index)] ?? match);
173
+ }
174
+ function sanitizeJsonValue(value, key) {
175
+ if (typeof value === "string") {
176
+ if (key === "pointer" && value.startsWith("/")) return value.split("/").map((token, index) => index === 0 ? "" : sanitizeText(token)).join("/");
177
+ return sanitizeText(value);
178
+ }
179
+ if (Array.isArray(value)) return value.map((item) => sanitizeJsonValue(item));
180
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [childKey, sanitizeJsonValue(child, childKey)]));
181
+ return value;
182
+ }
183
+
184
+ //#endregion
185
+ export { sanitizeText as n, sanitizeUrl as r, sanitizeJsonValue as t };
package/dist/schema.js ADDED
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/audit/bundle/schema.ts
4
+ /** Where `prepare` writes the bundle analysis, relative to the run folder. */
5
+ const BUNDLE_DIR = "bundle";
6
+ const BUNDLE_ANALYSIS = `${BUNDLE_DIR}/analysis.json`;
7
+ /** The build's visualizer report, copied into the run for the user to open. */
8
+ const BUNDLE_VISUALIZER = `${BUNDLE_DIR}/client-bundle-size.html`;
9
+ const BUNDLE_SCHEMA_VERSION = 2;
10
+ /**
11
+ * The smallest saving worth a recommendation: 30 KiB of minified JavaScript, about 10 KiB over the network. Below it a
12
+ * change costs more review than it saves; the big wins (a 100–200 KiB library loaded for a closed overlay) are far above.
13
+ */
14
+ const MIN_RECOMMENDED_BYTES = 30 * 1024;
15
+ const bytes = z.number().int().nonnegative();
16
+ /** A source file: project-relative (`src/components/header.tsx`) or within a package (`zod/v4/core/schemas.js`). */
17
+ const sourceRef = z.object({
18
+ file: z.string(),
19
+ line: z.number().int().positive().optional(),
20
+ excerpt: z.string().optional()
21
+ }).strict();
22
+ /** Who owns a module: an npm package, a project folder, or bytes the bundler adds. */
23
+ const ownerKindSchema = z.enum([
24
+ "npm",
25
+ "project",
26
+ "build"
27
+ ]);
28
+ /** What the browser downloaded on the deployed site: sizes only, since its files can't be tied to the local build. */
29
+ const liveTotalsSchema = z.object({
30
+ scripts: z.number().int().nonnegative(),
31
+ decodedBytes: bytes,
32
+ transferBytes: bytes,
33
+ beforeLoad: z.number().int().nonnegative(),
34
+ beforeLoadDecodedBytes: bytes,
35
+ beforeLoadTransferBytes: bytes
36
+ }).strict();
37
+ const bundlePackageSchema = z.object({
38
+ name: z.string(),
39
+ kind: ownerKindSchema,
40
+ bytes,
41
+ modules: z.number().int().nonnegative(),
42
+ largestModules: z.array(z.object({
43
+ file: z.string(),
44
+ bytes
45
+ }).strict()),
46
+ versions: z.array(z.string()).optional(),
47
+ importers: z.array(sourceRef),
48
+ importerCount: z.number().int().nonnegative(),
49
+ deferrableBytes: bytes.optional()
50
+ }).strict();
51
+ const bundleCandidateSchema = z.object({
52
+ importer: sourceRef,
53
+ target: z.string(),
54
+ package: z.string(),
55
+ savedBytes: bytes,
56
+ savedModules: z.number().int().positive(),
57
+ lazyElsewhere: z.array(z.string()).optional()
58
+ }).strict();
59
+ const bundleChunkSchema = z.object({
60
+ file: z.string(),
61
+ bytes,
62
+ owners: z.array(z.object({
63
+ name: z.string(),
64
+ bytes
65
+ }).strict())
66
+ }).strict();
67
+ /** The page's routes in the local build: everything here comes from building the project folder, not from the live site. */
68
+ const localPageSchema = z.object({
69
+ chunks: z.number().int().nonnegative(),
70
+ bytes,
71
+ largest: z.array(bundleChunkSchema),
72
+ packages: z.array(bundlePackageSchema),
73
+ candidates: z.array(bundleCandidateSchema)
74
+ }).strict();
75
+ const bundlePageSchema = z.object({
76
+ page: z.string(),
77
+ runId: z.string(),
78
+ lighthouseArtifact: z.string(),
79
+ observedLoadMs: z.number().nullable(),
80
+ live: liveTotalsSchema,
81
+ route: z.object({
82
+ ids: z.array(z.string()).min(1),
83
+ source: z.enum(["page", "url"])
84
+ }).strict().nullable(),
85
+ local: localPageSchema.nullable()
86
+ }).strict();
87
+ const bundleAnalysisSchema = z.object({
88
+ schemaVersion: z.literal(BUNDLE_SCHEMA_VERSION),
89
+ status: z.enum([
90
+ "complete",
91
+ "partial",
92
+ "unavailable"
93
+ ]),
94
+ notes: z.array(z.string()),
95
+ build: z.object({
96
+ packageManager: z.string(),
97
+ durationMs: z.object({
98
+ analyze: z.number().nullable(),
99
+ plain: z.number().nullable()
100
+ }).strict(),
101
+ visualizer: z.string().nullable(),
102
+ chunks: z.number().int().nonnegative(),
103
+ withMaps: z.number().int().nonnegative(),
104
+ bytes,
105
+ attribution: z.object({
106
+ modules: bytes,
107
+ unmappedModules: bytes,
108
+ preloadLists: bytes,
109
+ routerManifest: bytes,
110
+ glue: bytes
111
+ }).strict()
112
+ }).strict().nullable(),
113
+ pages: z.array(bundlePageSchema),
114
+ skipped: z.array(z.object({
115
+ page: z.string(),
116
+ reason: z.string()
117
+ }).strict())
118
+ }).strict();
119
+
120
+ //#endregion
121
+ export { MIN_RECOMMENDED_BYTES as a, bundlePackageSchema as c, BUNDLE_VISUALIZER as i, bundlePageSchema as l, BUNDLE_DIR as n, bundleAnalysisSchema as o, BUNDLE_SCHEMA_VERSION as r, bundleCandidateSchema as s, BUNDLE_ANALYSIS as t };
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t as logger } from "./logger.js";
2
- import { o as loadRuntimeConfig } from "./config.js";
2
+ import { t as importTypescript } from "./ts-import.js";
3
+ import { c as loadRuntimeConfig } from "./config.js";
3
4
  import path from "path";
4
5
  import chalk from "chalk";
5
6
  import express from "express";
@@ -21,59 +22,6 @@ import { Resource } from "@opentelemetry/resources";
21
22
  import { registerInstrumentations } from "@opentelemetry/instrumentation";
22
23
  import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
23
24
 
24
- //#region src/server/ts-import.ts
25
- /**
26
- * Parse TypeScript paths from tsconfig.json and convert to jiti alias format.
27
- *
28
- * @param tsconfigPath - Path to tsconfig.json
29
- * @param projectDirectory - Project root directory for resolving relative paths
30
- * @returns Record of alias mappings for jiti
31
- *
32
- * @example
33
- * // tsconfig.json: { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
34
- * // Returns: { "@/": "/absolute/path/to/src/" }
35
- */
36
- function parseTsconfigPaths(tsconfigPath, projectDirectory) {
37
- const alias = {};
38
- if (!existsSync(tsconfigPath)) return alias;
39
- try {
40
- const tsconfigContent = readFileSync(tsconfigPath, "utf-8");
41
- const tsconfig = JSON.parse(tsconfigContent);
42
- const paths = tsconfig.compilerOptions?.paths;
43
- const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
44
- if (paths) {
45
- for (const [key, values] of Object.entries(paths)) if (values && values.length > 0) {
46
- const aliasKey = key.replace(/\/\*$/, "/");
47
- alias[aliasKey] = resolve(projectDirectory, baseUrl, values[0].replace(/\/\*$/, "/").replace(/^\.\//, ""));
48
- }
49
- }
50
- } catch {}
51
- const sortedAlias = {};
52
- Object.keys(alias).sort((a, b) => b.length - a.length).forEach((key) => {
53
- sortedAlias[key] = alias[key];
54
- });
55
- return sortedAlias;
56
- }
57
- /**
58
- * Import a TypeScript file using jiti with proper path alias resolution.
59
- * This is a cross-platform alternative to tsx that works on Windows.
60
- *
61
- * @param filePath - Absolute path to the TypeScript file to import
62
- * @param options - Import options including project directory
63
- * @returns The imported module
64
- */
65
- async function importTypescript(filePath, options) {
66
- const { projectDirectory, tsconfigPath = resolve(projectDirectory, "tsconfig.json") } = options;
67
- const { createJiti } = await import("jiti");
68
- const alias = parseTsconfigPaths(tsconfigPath, projectDirectory);
69
- return createJiti(import.meta.url, {
70
- fsCache: false,
71
- interopDefault: true,
72
- alias
73
- }).import(filePath);
74
- }
75
-
76
- //#endregion
77
25
  //#region src/server/config.ts
78
26
  /**
79
27
  * This is a temporary function before we move the config implementation from
@@ -600,12 +548,12 @@ const HEALTH_ENDPOINT_PATH = "/sfdc-health";
600
548
  * console.log(metadata?.version);
601
549
  * ```
602
550
  */
603
- function readPackageMetadata(path$1) {
604
- if (!existsSync(path$1)) return null;
551
+ function readPackageMetadata(path$2) {
552
+ if (!existsSync(path$2)) return null;
605
553
  try {
606
- return JSON.parse(readFileSync(path$1, "utf8"));
554
+ return JSON.parse(readFileSync(path$2, "utf8"));
607
555
  } catch (error) {
608
- logger.debug(`Health check: failed to parse package.json at ${path$1}`, error);
556
+ logger.debug(`Health check: failed to parse package.json at ${path$2}`, error);
609
557
  return null;
610
558
  }
611
559
  }
@@ -701,8 +649,8 @@ async function createServer(options) {
701
649
  } else {
702
650
  const possiblePaths = RELATIVE_MIDDLEWARE_REGISTRY_BUILT_PATHS.map((p) => resolve(projectDirectory, p));
703
651
  let builtRegistryPath = null;
704
- for (const path$1 of possiblePaths) if (existsSync(path$1)) {
705
- builtRegistryPath = path$1;
652
+ for (const path$2 of possiblePaths) if (existsSync(path$2)) {
653
+ builtRegistryPath = path$2;
706
654
  break;
707
655
  }
708
656
  if (builtRegistryPath) registry = await import(pathToFileURL(builtRegistryPath).href);
@@ -0,0 +1,185 @@
1
+ import "./package.js";
2
+ import { t as AUDIT_ROOT } from "./assets.js";
3
+ import "./sanitize.js";
4
+ import { i as BUNDLE_VISUALIZER } from "./schema.js";
5
+ import { b as listRuns, n as deriveStatus, o as readEvents, p as preparedSchema, r as endsRun, s as LIGHTHOUSE_REPORT, v as RUN_ID, x as runDir } from "./events.js";
6
+ import { constants } from "node:fs";
7
+ import path from "node:path";
8
+ import { access, readFile, rm } from "node:fs/promises";
9
+ import { z } from "zod";
10
+ import { Hono } from "hono";
11
+ import { serve } from "@hono/node-server";
12
+ import { serveStatic } from "@hono/node-server/serve-static";
13
+ import { streamSSE } from "hono/streaming";
14
+
15
+ //#region src/audit/server.ts
16
+ const defaultUiDir = path.join(AUDIT_ROOT, "ui");
17
+ const POLL_MS = 500;
18
+ async function fileExists(file) {
19
+ try {
20
+ await access(file, constants.R_OK);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ const preparedPagesSchema = z.object({ pages: preparedSchema.shape.pages });
27
+ /** Absolute archive directory for `label`, from the run's prepared.json
28
+ * (undefined when it is missing, unreadable, or has no such page). */
29
+ async function preparedArchiveDir(run, label) {
30
+ const preparedPath = path.join(run, "prepared.json");
31
+ if (!await fileExists(preparedPath)) return void 0;
32
+ let raw;
33
+ try {
34
+ raw = JSON.parse(await readFile(preparedPath, "utf8"));
35
+ } catch {
36
+ return;
37
+ }
38
+ const prepared = preparedPagesSchema.safeParse(raw);
39
+ const page = prepared.success ? prepared.data.pages.find((item) => item.label === label) : void 0;
40
+ return page && path.resolve(run, page.archiveDir);
41
+ }
42
+ /** A run reports `complete` only once run.complete has landed and the result file is actually readable. */
43
+ async function statusOf(run) {
44
+ if (!run.eventsFile || !await fileExists(run.eventsFile)) return await fileExists(run.resultPath) ? "complete" : "preparing";
45
+ const derived = deriveStatus(await readEvents(run.eventsFile));
46
+ if (derived === "complete" && !await fileExists(run.resultPath)) return "running";
47
+ return derived;
48
+ }
49
+ function createApp(options) {
50
+ const uiDir = options.uiDir ?? defaultUiDir;
51
+ const app = new Hono();
52
+ /** The run `id` names (the latest when absent); undefined when there is no such run. */
53
+ const findRun = async (id) => {
54
+ if (!options.workspace) {
55
+ if (id || !options.input) return void 0;
56
+ return {
57
+ dir: path.dirname(options.input),
58
+ resultPath: options.input
59
+ };
60
+ }
61
+ const runs = await listRuns(options.workspace);
62
+ const runId = id ?? runs[0];
63
+ if (!runId || !runs.includes(runId)) return void 0;
64
+ const dir = runDir(options.workspace, runId);
65
+ return {
66
+ id: runId,
67
+ dir,
68
+ resultPath: path.join(dir, "audit-result.json"),
69
+ eventsFile: path.join(dir, "events.ndjson")
70
+ };
71
+ };
72
+ const requestedRun = (context) => findRun(context.req.query("run"));
73
+ app.get("/api/status", async (context) => {
74
+ const run = await requestedRun(context);
75
+ const status = run ? await statusOf(run) : "preparing";
76
+ return context.json({
77
+ status,
78
+ phase: status,
79
+ runId: run?.id
80
+ });
81
+ });
82
+ app.get("/api/runs", async (context) => {
83
+ const { workspace } = options;
84
+ if (!workspace) return context.json([]);
85
+ const runs = await Promise.all((await listRuns(workspace)).map(async (id) => ({
86
+ id,
87
+ complete: await fileExists(path.join(runDir(workspace, id), "audit-result.json"))
88
+ })));
89
+ return context.json(runs);
90
+ });
91
+ app.delete("/api/runs/:id", async (context) => {
92
+ const id = context.req.param("id");
93
+ if (!options.workspace || !RUN_ID.test(id)) return context.json({ error: "No such run." }, 404);
94
+ const runs = await listRuns(options.workspace);
95
+ if (!runs.includes(id)) return context.json({ error: "No such run." }, 404);
96
+ if (id === runs[0]) return context.json({ error: "The latest run cannot be deleted." }, 409);
97
+ await rm(runDir(options.workspace, id), {
98
+ recursive: true,
99
+ force: true
100
+ });
101
+ return context.body(null, 204);
102
+ });
103
+ app.get("/api/events", (context) => {
104
+ if (!options.workspace) return context.json({ error: "This server is not watching a run." }, 409);
105
+ return streamSSE(context, async (stream) => {
106
+ let closed = false;
107
+ stream.onAbort(() => {
108
+ closed = true;
109
+ });
110
+ let eventsFile;
111
+ let sent = 0;
112
+ while (!closed) {
113
+ eventsFile ??= (await findRun())?.eventsFile;
114
+ const events = eventsFile ? await readEvents(eventsFile) : [];
115
+ for (; sent < events.length; sent++) {
116
+ await stream.writeSSE({ data: JSON.stringify(events[sent]) });
117
+ if (endsRun(events[sent])) {
118
+ await stream.writeSSE({
119
+ event: "end",
120
+ data: ""
121
+ });
122
+ closed = true;
123
+ break;
124
+ }
125
+ }
126
+ if (closed) break;
127
+ await stream.sleep(POLL_MS);
128
+ }
129
+ });
130
+ });
131
+ app.get("/api/audit", async (context) => {
132
+ const run = await requestedRun(context);
133
+ if (!run) return context.json({ error: "No such run." }, 404);
134
+ if (await statusOf(run) !== "complete") return context.json({ error: "Audit is not complete." }, 409);
135
+ return context.json(JSON.parse(await readFile(run.resultPath, "utf8")));
136
+ });
137
+ app.get("/api/pages/:label/report", async (context) => {
138
+ const run = await requestedRun(context);
139
+ if (!run) return context.json({ error: "No such run." }, 404);
140
+ if (await statusOf(run) !== "complete") return context.json({ error: "Audit is not complete." }, 409);
141
+ const archiveDir = await preparedArchiveDir(run.dir, context.req.param("label"));
142
+ const report = archiveDir && path.join(archiveDir, LIGHTHOUSE_REPORT);
143
+ if (!report || !await fileExists(report)) return context.json({ error: "No Lighthouse report for this page." }, 404);
144
+ context.header("Content-Security-Policy", "sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox");
145
+ return context.html(await readFile(report, "utf8"));
146
+ });
147
+ app.get("/api/bundle/visualizer", async (context) => {
148
+ const run = await requestedRun(context);
149
+ if (!run) return context.json({ error: "No such run." }, 404);
150
+ if (await statusOf(run) !== "complete") return context.json({ error: "Audit is not complete." }, 409);
151
+ const report = path.join(run.dir, BUNDLE_VISUALIZER);
152
+ if (!await fileExists(report)) return context.json({ error: "This run has no bundle visualizer report." }, 404);
153
+ context.header("Content-Security-Policy", "sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox");
154
+ return context.html(await readFile(report, "utf8"));
155
+ });
156
+ app.use("/*", serveStatic({ root: path.relative(process.cwd(), uiDir) || "." }));
157
+ app.get("/*", serveStatic({ path: path.relative(process.cwd(), path.join(uiDir, "index.html")) }));
158
+ return app;
159
+ }
160
+ async function startServer(options) {
161
+ const app = createApp(options);
162
+ const port = options.port ?? 8787;
163
+ const host = options.host ?? "localhost";
164
+ const server = await new Promise((resolve$1, reject) => {
165
+ const listening = serve({
166
+ fetch: app.fetch,
167
+ port,
168
+ hostname: host
169
+ }, () => {
170
+ listening.off("error", fail);
171
+ resolve$1(listening);
172
+ });
173
+ function fail(error) {
174
+ reject(error.code === "EADDRINUSE" ? /* @__PURE__ */ new Error(`Port ${port} is in use; pass --port <n> to use another one`) : error);
175
+ }
176
+ listening.once("error", fail);
177
+ });
178
+ return {
179
+ url: `http://${host}:${port}`,
180
+ close: () => server.close()
181
+ };
182
+ }
183
+
184
+ //#endregion
185
+ export { createApp, startServer };
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ //#region src/server/ts-import.ts
5
+ /**
6
+ * Parse TypeScript paths from tsconfig.json and convert to jiti alias format.
7
+ *
8
+ * @param tsconfigPath - Path to tsconfig.json
9
+ * @param projectDirectory - Project root directory for resolving relative paths
10
+ * @returns Record of alias mappings for jiti
11
+ *
12
+ * @example
13
+ * // tsconfig.json: { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
14
+ * // Returns: { "@/": "/absolute/path/to/src/" }
15
+ */
16
+ function parseTsconfigPaths(tsconfigPath, projectDirectory) {
17
+ const alias = {};
18
+ if (!existsSync(tsconfigPath)) return alias;
19
+ try {
20
+ const tsconfigContent = readFileSync(tsconfigPath, "utf-8");
21
+ const tsconfig = JSON.parse(tsconfigContent);
22
+ const paths = tsconfig.compilerOptions?.paths;
23
+ const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
24
+ if (paths) {
25
+ for (const [key, values] of Object.entries(paths)) if (values && values.length > 0) {
26
+ const aliasKey = key.replace(/\/\*$/, "/");
27
+ alias[aliasKey] = resolve(projectDirectory, baseUrl, values[0].replace(/\/\*$/, "/").replace(/^\.\//, ""));
28
+ }
29
+ }
30
+ } catch {}
31
+ const sortedAlias = {};
32
+ Object.keys(alias).sort((a, b) => b.length - a.length).forEach((key) => {
33
+ sortedAlias[key] = alias[key];
34
+ });
35
+ return sortedAlias;
36
+ }
37
+ /**
38
+ * Import a TypeScript file using jiti with proper path alias resolution.
39
+ * This is a cross-platform alternative to tsx that works on Windows.
40
+ *
41
+ * @param filePath - Absolute path to the TypeScript file to import
42
+ * @param options - Import options including project directory
43
+ * @returns The imported module
44
+ */
45
+ async function importTypescript(filePath, options) {
46
+ const { projectDirectory, tsconfigPath = resolve(projectDirectory, "tsconfig.json") } = options;
47
+ const { createJiti } = await import("jiti");
48
+ const alias = parseTsconfigPaths(tsconfigPath, projectDirectory);
49
+ return createJiti(import.meta.url, {
50
+ fsCache: false,
51
+ interopDefault: true,
52
+ alias
53
+ }).import(filePath);
54
+ }
55
+
56
+ //#endregion
57
+ export { importTypescript as t };