@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.
- package/README.md +13 -2
- package/dist/assets.js +25 -0
- package/dist/audit/catalog/categories/best-practices/CATEGORY.md +21 -0
- package/dist/audit/catalog/categories/best-practices/rules/commerce-api-client.md +20 -0
- package/dist/audit/catalog/categories/best-practices/rules/error-boundaries.md +19 -0
- package/dist/audit/catalog/categories/best-practices/rules/production-integrations.md +20 -0
- package/dist/audit/catalog/categories/best-practices/rules/route-revalidation.md +19 -0
- package/dist/audit/catalog/categories/best-practices/rules/server-data-loading.md +19 -0
- package/dist/audit/catalog/categories/performance/CATEGORY.md +18 -0
- package/dist/audit/catalog/categories/performance/rules/app-config-size.js +15 -0
- package/dist/audit/catalog/categories/performance/rules/app-config-size.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/cache-control.js +59 -0
- package/dist/audit/catalog/categories/performance/rules/cache-control.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/cls-budget.js +13 -0
- package/dist/audit/catalog/categories/performance/rules/cls-budget.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/content-encoding.js +35 -0
- package/dist/audit/catalog/categories/performance/rules/content-encoding.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/dom-size.js +18 -0
- package/dist/audit/catalog/categories/performance/rules/dom-size.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/html-document-size.js +15 -0
- package/dist/audit/catalog/categories/performance/rules/html-document-size.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/image-dimensions.js +141 -0
- package/dist/audit/catalog/categories/performance/rules/image-dimensions.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/image-transformation.js +117 -0
- package/dist/audit/catalog/categories/performance/rules/image-transformation.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/javascript-size.js +126 -0
- package/dist/audit/catalog/categories/performance/rules/javascript-size.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/large-images.js +187 -0
- package/dist/audit/catalog/categories/performance/rules/large-images.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-budget.js +13 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-budget.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.js +227 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.md +27 -0
- package/dist/audit/catalog/categories/performance/rules/page-data-size.js +15 -0
- package/dist/audit/catalog/categories/performance/rules/page-data-size.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.js +97 -0
- package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.md +73 -0
- package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.js +201 -0
- package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/server-request-waterfall.md +85 -0
- package/dist/audit/catalog/categories/performance/rules/tbt-budget.js +13 -0
- package/dist/audit/catalog/categories/performance/rules/tbt-budget.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/third-party-fonts.js +265 -0
- package/dist/audit/catalog/categories/performance/rules/third-party-fonts.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/ttfb-budget.js +17 -0
- package/dist/audit/catalog/categories/performance/rules/ttfb-budget.md +20 -0
- package/dist/audit/catalog/categories/security/CATEGORY.md +17 -0
- package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.js +37 -0
- package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.md +20 -0
- package/dist/audit/catalog/categories/security/rules/content-security-policy.js +40 -0
- package/dist/audit/catalog/categories/security/rules/content-security-policy.md +20 -0
- package/dist/audit/catalog/categories/security/rules/csp-runtime-compatibility.md +24 -0
- package/dist/audit/catalog/categories/security/rules/dependency-vulnerabilities.md +19 -0
- package/dist/audit/catalog/categories/security/rules/dynamic-code-execution.md +19 -0
- package/dist/audit/catalog/categories/security/rules/hardcoded-credentials.md +19 -0
- package/dist/audit/catalog/categories/security/rules/html-config-secrets.js +46 -0
- package/dist/audit/catalog/categories/security/rules/html-config-secrets.md +22 -0
- package/dist/audit/catalog/categories/security/rules/protected-route-authorization.md +19 -0
- package/dist/audit/catalog/categories/security/rules/public-client-secrets.md +20 -0
- package/dist/audit/catalog/categories/security/rules/raw-html-sinks.md +20 -0
- package/dist/audit/catalog/categories/security/rules/strict-transport-security.js +183 -0
- package/dist/audit/catalog/categories/security/rules/strict-transport-security.md +24 -0
- package/dist/audit/catalog/categories/seo/CATEGORY.md +17 -0
- package/dist/audit/catalog/categories/seo/rules/canonical-url.md +20 -0
- package/dist/audit/catalog/categories/seo/rules/locale-alternates.md +19 -0
- package/dist/audit/catalog/categories/seo/rules/meta-description.md +19 -0
- package/dist/audit/catalog/categories/seo/rules/page-title.md +19 -0
- package/dist/audit/catalog/categories/seo/rules/robots-indexing.md +20 -0
- package/dist/audit/catalog/categories/seo/rules/structured-data.md +19 -0
- package/dist/audit/instructions/evaluate-rule.md +42 -0
- package/dist/audit/schemas/audit-result.schema.json +1237 -0
- package/dist/audit/ui/assets/banner-brand-default-CDRB3_S0.png +0 -0
- package/dist/audit/ui/assets/banner-group-public-default-BGRJzVY8.png +0 -0
- package/dist/audit/ui/assets/banner-user-default-BW81Ba3z.png +0 -0
- package/dist/audit/ui/assets/bg-info@2x-CzpP0vaE.png +0 -0
- package/dist/audit/ui/assets/einstein-figure-B1xFOo8O.svg +1 -0
- package/dist/audit/ui/assets/group_avatar_160-CrxVHlYA.png +0 -0
- package/dist/audit/ui/assets/group_avatar_200-CSLS8I0r.png +0 -0
- package/dist/audit/ui/assets/index-BKCWlO9b.css +1 -0
- package/dist/audit/ui/assets/index-CUPt8e4A.js +44 -0
- package/dist/audit/ui/assets/profile_avatar_160-CtQTUBo0.png +0 -0
- package/dist/audit/ui/assets/profile_avatar_200-DAztxshF.png +0 -0
- package/dist/audit/ui/assets/symbols-BRmpfDbO.svg +1 -0
- package/dist/audit/ui/assets/symbols-D0Ne6690.svg +1 -0
- package/dist/audit/ui/einstein.png +0 -0
- package/dist/audit/ui/index.html +13 -0
- package/dist/audit.js +1896 -0
- package/dist/cartridge-services/index.d.ts +5 -0
- package/dist/cartridge-services/index.d.ts.map +1 -1
- package/dist/cartridge-services/index.js +94 -7
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/catalog.js +89 -0
- package/dist/commands/audit/catalog.js +27 -0
- package/dist/commands/audit/finalize.js +42 -0
- package/dist/commands/audit/prepare.js +84 -0
- package/dist/commands/audit/progress.js +51 -0
- package/dist/commands/audit/serve.js +63 -0
- package/dist/commands/config/aggregate-extensions.js +1 -0
- package/dist/commands/create-bundle.js +2 -1
- package/dist/commands/create-instructions.js +1 -0
- package/dist/commands/create-storefront.js +1 -0
- package/dist/commands/deploy-cartridge.js +11 -3
- package/dist/commands/dev.js +2 -0
- package/dist/commands/extensions/create.js +1 -0
- package/dist/commands/extensions/install.js +1 -0
- package/dist/commands/extensions/list.js +1 -0
- package/dist/commands/extensions/remove.js +1 -0
- package/dist/commands/generate-cartridge.js +49 -5
- package/dist/commands/locales/aggregate-extensions.js +1 -0
- package/dist/commands/prepare-local.js +1 -0
- package/dist/commands/preview.js +2 -0
- package/dist/commands/push.js +3 -1
- package/dist/commands/validate-cartridge.js +13 -4
- package/dist/config.js +13 -2
- package/dist/document-size.js +231 -0
- package/dist/events.js +688 -0
- package/dist/flags2.js +34 -0
- package/dist/generate-cartridge.js +41 -6
- package/dist/har.js +205 -0
- package/dist/images.js +146 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/live.js +277 -0
- package/dist/logger.js +1 -4
- package/dist/measured-run.js +91 -0
- package/dist/metric-budget.js +125 -0
- package/dist/mrt/ssr.mjs +19 -19
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +26 -26
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/package.js +5 -0
- package/dist/sanitize.js +185 -0
- package/dist/schema.js +121 -0
- package/dist/server.js +8 -60
- package/dist/server2.js +185 -0
- package/dist/ts-import.js +57 -0
- package/package.json +22 -5
package/dist/package.js
ADDED
package/dist/sanitize.js
ADDED
|
@@ -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 {
|
|
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$
|
|
604
|
-
if (!existsSync(path$
|
|
551
|
+
function readPackageMetadata(path$2) {
|
|
552
|
+
if (!existsSync(path$2)) return null;
|
|
605
553
|
try {
|
|
606
|
-
return JSON.parse(readFileSync(path$
|
|
554
|
+
return JSON.parse(readFileSync(path$2, "utf8"));
|
|
607
555
|
} catch (error) {
|
|
608
|
-
logger.debug(`Health check: failed to parse package.json at ${path$
|
|
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$
|
|
705
|
-
builtRegistryPath = path$
|
|
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);
|
package/dist/server2.js
ADDED
|
@@ -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 };
|