@lark-apaas/fullstack-cli 1.1.59-alpha.20260718151739 → 1.1.59-alpha.20260719084243
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/dist/index.js +466 -114
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs37 from "fs";
|
|
3
|
+
import path33 from "path";
|
|
4
4
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5
5
|
import { config as dotenvConfig } from "dotenv";
|
|
6
6
|
|
|
@@ -4921,7 +4921,7 @@ var PROMPT_PATTERNS = [
|
|
|
4921
4921
|
{ pattern: /proceed\?/i, answer: "y\n" }
|
|
4922
4922
|
];
|
|
4923
4923
|
async function executeShadcnAdd(registryItemPath) {
|
|
4924
|
-
return new Promise((
|
|
4924
|
+
return new Promise((resolve9) => {
|
|
4925
4925
|
let output = "";
|
|
4926
4926
|
const args = ["--yes", "shadcn@3.8.2", "add", registryItemPath];
|
|
4927
4927
|
const ptyProcess = pty.spawn("npx", args, {
|
|
@@ -4947,7 +4947,7 @@ async function executeShadcnAdd(registryItemPath) {
|
|
|
4947
4947
|
});
|
|
4948
4948
|
const timeoutId = setTimeout(() => {
|
|
4949
4949
|
ptyProcess.kill();
|
|
4950
|
-
|
|
4950
|
+
resolve9({
|
|
4951
4951
|
success: false,
|
|
4952
4952
|
files: [],
|
|
4953
4953
|
error: "\u6267\u884C\u8D85\u65F6"
|
|
@@ -4958,7 +4958,7 @@ async function executeShadcnAdd(registryItemPath) {
|
|
|
4958
4958
|
const success = exitCode === 0;
|
|
4959
4959
|
const filePaths = parseOutput(output);
|
|
4960
4960
|
const files = filePaths.map(toFileInfo);
|
|
4961
|
-
|
|
4961
|
+
resolve9({
|
|
4962
4962
|
success,
|
|
4963
4963
|
files,
|
|
4964
4964
|
error: success ? void 0 : output || `Process exited with code ${exitCode}`
|
|
@@ -4969,12 +4969,12 @@ async function executeShadcnAdd(registryItemPath) {
|
|
|
4969
4969
|
|
|
4970
4970
|
// src/commands/component/add.handler.ts
|
|
4971
4971
|
function runActionPluginInit() {
|
|
4972
|
-
return new Promise((
|
|
4972
|
+
return new Promise((resolve9) => {
|
|
4973
4973
|
execFile("fullstack-cli", ["action-plugin", "init"], { cwd: process.cwd(), stdio: "ignore" }, (error) => {
|
|
4974
4974
|
if (error) {
|
|
4975
4975
|
debug("action-plugin init \u5931\u8D25: %s", error.message);
|
|
4976
4976
|
}
|
|
4977
|
-
|
|
4977
|
+
resolve9();
|
|
4978
4978
|
});
|
|
4979
4979
|
});
|
|
4980
4980
|
}
|
|
@@ -8624,19 +8624,58 @@ async function buildPreviewServerArtifact(options) {
|
|
|
8624
8624
|
|
|
8625
8625
|
// src/commands/preview-artifact/producer.ts
|
|
8626
8626
|
import * as crypto2 from "crypto";
|
|
8627
|
-
import * as
|
|
8628
|
-
import * as
|
|
8627
|
+
import * as fs34 from "fs";
|
|
8628
|
+
import * as path30 from "path";
|
|
8629
8629
|
import { spawnSync as spawnSync8 } from "child_process";
|
|
8630
8630
|
|
|
8631
8631
|
// src/commands/preview-artifact/generation.ts
|
|
8632
8632
|
import * as crypto from "crypto";
|
|
8633
8633
|
import * as fs30 from "fs";
|
|
8634
8634
|
import * as path26 from "path";
|
|
8635
|
-
var PREVIEW_ARTIFACT_SCHEMA_VERSION =
|
|
8635
|
+
var PREVIEW_ARTIFACT_SCHEMA_VERSION = 2;
|
|
8636
|
+
var PREVIEW_ARTIFACT_PROTOCOL_VERSION = 2;
|
|
8636
8637
|
var MANIFEST_FILE = "manifest.json";
|
|
8637
8638
|
var PROBE_FILE = "probe.json";
|
|
8638
8639
|
var SERVER_FILE = "server.bundle.cjs";
|
|
8639
8640
|
var CLIENT_FILE = "client.zip";
|
|
8641
|
+
var RUNTIME_ONLY_ENVIRONMENT_KEYS = /* @__PURE__ */ new Set([
|
|
8642
|
+
"CLIENT_DEV_HOST",
|
|
8643
|
+
"CLIENT_DEV_PORT",
|
|
8644
|
+
"FORCE_AUTHN_ACCESS_KEY",
|
|
8645
|
+
"FORCE_AUTHN_ACCESS_SECRET",
|
|
8646
|
+
"FORCE_AUTHN_PREVIEW_SESSION_ID",
|
|
8647
|
+
"MIAODA_HMR_WS_TOKEN",
|
|
8648
|
+
"MIAODA_PREVIEW_RUN_ID",
|
|
8649
|
+
"PREVIEW_RUN_ID",
|
|
8650
|
+
"SANDBOX_COOKIE",
|
|
8651
|
+
"SANDBOX_ID",
|
|
8652
|
+
"SERVER_PORT",
|
|
8653
|
+
"preview_run_id"
|
|
8654
|
+
]);
|
|
8655
|
+
var RUNTIME_ONLY_ENVIRONMENT_PREFIXES = ["PREVIEW_ARTIFACT_"];
|
|
8656
|
+
function previewArtifactBuildEnvironment(environment, appBasePath) {
|
|
8657
|
+
const buildEnvironment = {};
|
|
8658
|
+
for (const [key, value] of Object.entries(environment ?? {})) {
|
|
8659
|
+
if (typeof value === "string" && !RUNTIME_ONLY_ENVIRONMENT_KEYS.has(key) && !RUNTIME_ONLY_ENVIRONMENT_PREFIXES.some((prefix) => key.startsWith(prefix))) {
|
|
8660
|
+
buildEnvironment[key] = value;
|
|
8661
|
+
}
|
|
8662
|
+
}
|
|
8663
|
+
buildEnvironment.CLIENT_BASE_PATH = appBasePath;
|
|
8664
|
+
buildEnvironment.NODE_ENV = environment?.NODE_ENV ?? "production";
|
|
8665
|
+
return buildEnvironment;
|
|
8666
|
+
}
|
|
8667
|
+
function previewArtifactBuildEnvironmentHash(environment, appBasePath) {
|
|
8668
|
+
const buildEnvironment = previewArtifactBuildEnvironment(
|
|
8669
|
+
environment,
|
|
8670
|
+
appBasePath
|
|
8671
|
+
);
|
|
8672
|
+
const canonical = Object.fromEntries(
|
|
8673
|
+
Object.entries(buildEnvironment).sort(
|
|
8674
|
+
([left], [right]) => left.localeCompare(right)
|
|
8675
|
+
)
|
|
8676
|
+
);
|
|
8677
|
+
return crypto.createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
8678
|
+
}
|
|
8640
8679
|
function parseNodeMajor(nodeVersion) {
|
|
8641
8680
|
const match = /^v?(\d+)(?:\.|$)/.exec(nodeVersion.trim());
|
|
8642
8681
|
if (!match) {
|
|
@@ -8683,8 +8722,10 @@ function createPreviewArtifactManifest(options) {
|
|
|
8683
8722
|
const generationDir = path26.resolve(options.generationDir);
|
|
8684
8723
|
const manifest = {
|
|
8685
8724
|
schemaVersion: PREVIEW_ARTIFACT_SCHEMA_VERSION,
|
|
8725
|
+
protocolVersion: PREVIEW_ARTIFACT_PROTOCOL_VERSION,
|
|
8686
8726
|
generationId: options.generationId,
|
|
8687
8727
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8728
|
+
buildEnvironmentSha256: options.buildEnvironmentSha256,
|
|
8688
8729
|
runtime: runtimeIdentity(
|
|
8689
8730
|
options.nodeVersion,
|
|
8690
8731
|
options.platform,
|
|
@@ -8723,6 +8764,7 @@ function recordPreviewArtifactProbe(options) {
|
|
|
8723
8764
|
coverage: {
|
|
8724
8765
|
html: options.coverage.html,
|
|
8725
8766
|
businessApi: options.coverage.businessApi,
|
|
8767
|
+
clientAssets: Array.from(new Set(options.coverage.clientAssets)).sort(),
|
|
8726
8768
|
actionPlugins: Array.from(new Set(options.coverage.actionPlugins)).sort()
|
|
8727
8769
|
}
|
|
8728
8770
|
};
|
|
@@ -8777,6 +8819,9 @@ function promotePreviewArtifactGeneration(generationDirInput) {
|
|
|
8777
8819
|
if (!probe.coverage.businessApi) {
|
|
8778
8820
|
reasons.push("probe did not verify business API");
|
|
8779
8821
|
}
|
|
8822
|
+
if (!Array.isArray(probe.coverage.clientAssets) || probe.coverage.clientAssets.length === 0) {
|
|
8823
|
+
reasons.push("probe did not verify client entry assets");
|
|
8824
|
+
}
|
|
8780
8825
|
const verifiedPlugins = new Set(probe.coverage.actionPlugins);
|
|
8781
8826
|
const missingPlugins = manifest.actionPlugins.filter(
|
|
8782
8827
|
(plugin) => !verifiedPlugins.has(plugin)
|
|
@@ -8804,11 +8849,18 @@ import * as net from "net";
|
|
|
8804
8849
|
import * as os4 from "os";
|
|
8805
8850
|
import * as path27 from "path";
|
|
8806
8851
|
import { spawn as spawn2, spawnSync as spawnSync7 } from "child_process";
|
|
8852
|
+
|
|
8853
|
+
// src/commands/preview-artifact/readiness.ts
|
|
8854
|
+
function isPreviewBusinessResponseReady(response) {
|
|
8855
|
+
return response.status !== 404 && response.status < 500 && !response.headers.get("content-type")?.includes("text/html");
|
|
8856
|
+
}
|
|
8857
|
+
|
|
8858
|
+
// src/commands/preview-artifact/probe.ts
|
|
8807
8859
|
function sleep(ms) {
|
|
8808
|
-
return new Promise((
|
|
8860
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
8809
8861
|
}
|
|
8810
8862
|
async function reservePort() {
|
|
8811
|
-
return new Promise((
|
|
8863
|
+
return new Promise((resolve9, reject) => {
|
|
8812
8864
|
const server = net.createServer();
|
|
8813
8865
|
server.unref();
|
|
8814
8866
|
server.once("error", reject);
|
|
@@ -8820,7 +8872,7 @@ async function reservePort() {
|
|
|
8820
8872
|
}
|
|
8821
8873
|
server.close((error) => {
|
|
8822
8874
|
if (error) reject(error);
|
|
8823
|
-
else
|
|
8875
|
+
else resolve9(address.port);
|
|
8824
8876
|
});
|
|
8825
8877
|
});
|
|
8826
8878
|
});
|
|
@@ -8871,6 +8923,77 @@ function rejectSymlinks(root) {
|
|
|
8871
8923
|
}
|
|
8872
8924
|
}
|
|
8873
8925
|
}
|
|
8926
|
+
function isPathInside(parent, candidate) {
|
|
8927
|
+
const relative5 = path27.relative(parent, candidate);
|
|
8928
|
+
return relative5 !== "" && relative5 !== ".." && !relative5.startsWith(`..${path27.sep}`) && !path27.isAbsolute(relative5);
|
|
8929
|
+
}
|
|
8930
|
+
function clientEntryAssetReferences(html, appBasePath) {
|
|
8931
|
+
const local = /* @__PURE__ */ new Set();
|
|
8932
|
+
const localExecutable = /* @__PURE__ */ new Set();
|
|
8933
|
+
const unsupportedExternal = /* @__PURE__ */ new Set();
|
|
8934
|
+
const tags = /<(script|link)\b[^>]*>/gi;
|
|
8935
|
+
const readAttribute = (tag, name) => new RegExp(`\\b${name}=["']([^"']+)["']`, "i").exec(tag)?.[1];
|
|
8936
|
+
const base = new URL(appBasePath, "http://preview.local");
|
|
8937
|
+
for (const match of html.matchAll(tags)) {
|
|
8938
|
+
const tagName = match[1]?.toLowerCase();
|
|
8939
|
+
const tag = match[0];
|
|
8940
|
+
let linkRel = [];
|
|
8941
|
+
if (tagName === "link") {
|
|
8942
|
+
linkRel = readAttribute(tag, "rel")?.toLowerCase().split(/\s+/) ?? [];
|
|
8943
|
+
if (!linkRel.some(
|
|
8944
|
+
(value2) => [
|
|
8945
|
+
"stylesheet",
|
|
8946
|
+
"modulepreload",
|
|
8947
|
+
"preload",
|
|
8948
|
+
"icon",
|
|
8949
|
+
"manifest"
|
|
8950
|
+
].includes(value2)
|
|
8951
|
+
)) {
|
|
8952
|
+
continue;
|
|
8953
|
+
}
|
|
8954
|
+
}
|
|
8955
|
+
const value = readAttribute(
|
|
8956
|
+
tag,
|
|
8957
|
+
tagName === "script" ? "src" : "href"
|
|
8958
|
+
)?.trim();
|
|
8959
|
+
if (!value || value.startsWith("data:") || value.startsWith("#")) continue;
|
|
8960
|
+
const resolved = new URL(value, base);
|
|
8961
|
+
if (resolved.origin !== base.origin) {
|
|
8962
|
+
if (tagName === "script" || linkRel.some(
|
|
8963
|
+
(value2) => ["stylesheet", "modulepreload", "preload"].includes(value2)
|
|
8964
|
+
)) {
|
|
8965
|
+
unsupportedExternal.add(resolved.toString());
|
|
8966
|
+
}
|
|
8967
|
+
continue;
|
|
8968
|
+
}
|
|
8969
|
+
local.add(resolved.pathname);
|
|
8970
|
+
if (tagName === "script") localExecutable.add(resolved.pathname);
|
|
8971
|
+
}
|
|
8972
|
+
return {
|
|
8973
|
+
local: Array.from(local).sort(),
|
|
8974
|
+
localExecutable: Array.from(localExecutable).sort(),
|
|
8975
|
+
unsupportedExternal: Array.from(unsupportedExternal).sort()
|
|
8976
|
+
};
|
|
8977
|
+
}
|
|
8978
|
+
function resolveClientEntryAsset(clientRoot, appBasePath, requestPath) {
|
|
8979
|
+
let decoded;
|
|
8980
|
+
try {
|
|
8981
|
+
decoded = decodeURIComponent(requestPath);
|
|
8982
|
+
} catch {
|
|
8983
|
+
return void 0;
|
|
8984
|
+
}
|
|
8985
|
+
const normalizedBase = appBasePath.endsWith("/") ? appBasePath : `${appBasePath}/`;
|
|
8986
|
+
const relative5 = decoded.startsWith(normalizedBase) ? decoded.slice(normalizedBase.length) : decoded.replace(/^\/+/, "");
|
|
8987
|
+
if (!relative5 || relative5 === "index.html") return void 0;
|
|
8988
|
+
const candidate = path27.resolve(clientRoot, relative5);
|
|
8989
|
+
if (!isPathInside(clientRoot, candidate)) return void 0;
|
|
8990
|
+
try {
|
|
8991
|
+
const stat = fs31.lstatSync(candidate);
|
|
8992
|
+
return stat.isFile() && !stat.isSymbolicLink() ? candidate : void 0;
|
|
8993
|
+
} catch {
|
|
8994
|
+
return void 0;
|
|
8995
|
+
}
|
|
8996
|
+
}
|
|
8874
8997
|
function extractClientArchive(archivePath, destination) {
|
|
8875
8998
|
if (fs31.existsSync(destination)) {
|
|
8876
8999
|
throw new Error("client archive destination must not already exist");
|
|
@@ -8928,6 +9051,7 @@ async function runPreviewArtifactProbe(options) {
|
|
|
8928
9051
|
const coverage = {
|
|
8929
9052
|
html: false,
|
|
8930
9053
|
businessApi: false,
|
|
9054
|
+
clientAssets: [],
|
|
8931
9055
|
actionPlugins: []
|
|
8932
9056
|
};
|
|
8933
9057
|
const reasons = [];
|
|
@@ -8940,10 +9064,8 @@ async function runPreviewArtifactProbe(options) {
|
|
|
8940
9064
|
path27.join(generationDir, "server.bundle.cjs"),
|
|
8941
9065
|
runtimeBundle
|
|
8942
9066
|
);
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
path27.join(runtimeRoot, "dist", "client")
|
|
8946
|
-
);
|
|
9067
|
+
const clientRoot = path27.join(runtimeRoot, "dist", "client");
|
|
9068
|
+
extractClientArchive(path27.join(generationDir, "client.zip"), clientRoot);
|
|
8947
9069
|
isolatedNodeModulesPresent = fs31.existsSync(
|
|
8948
9070
|
path27.join(runtimeRoot, "node_modules")
|
|
8949
9071
|
);
|
|
@@ -8974,12 +9096,34 @@ async function runPreviewArtifactProbe(options) {
|
|
|
8974
9096
|
`HTML probe failed with status ${htmlResponse?.status ?? "unreachable"}`
|
|
8975
9097
|
);
|
|
8976
9098
|
}
|
|
9099
|
+
if (coverage.html && htmlResponse) {
|
|
9100
|
+
const html = await htmlResponse.text();
|
|
9101
|
+
const entryReferences = clientEntryAssetReferences(
|
|
9102
|
+
html,
|
|
9103
|
+
options.appBasePath
|
|
9104
|
+
);
|
|
9105
|
+
const entryAssets = entryReferences.local;
|
|
9106
|
+
coverage.clientAssets = entryReferences.localExecutable;
|
|
9107
|
+
if (entryReferences.localExecutable.length === 0) {
|
|
9108
|
+
reasons.push("client HTML has no local executable entry asset");
|
|
9109
|
+
}
|
|
9110
|
+
for (const externalAsset of entryReferences.unsupportedExternal) {
|
|
9111
|
+
reasons.push(
|
|
9112
|
+
`external client entry asset is unsupported: ${externalAsset}`
|
|
9113
|
+
);
|
|
9114
|
+
}
|
|
9115
|
+
for (const entryAsset of entryAssets) {
|
|
9116
|
+
if (!resolveClientEntryAsset(clientRoot, options.appBasePath, entryAsset)) {
|
|
9117
|
+
reasons.push(`client entry asset is unavailable: ${entryAsset}`);
|
|
9118
|
+
}
|
|
9119
|
+
}
|
|
9120
|
+
}
|
|
8977
9121
|
const apiResponse = await fetchUntil(
|
|
8978
9122
|
new URL(options.businessApiPath, origin).toString(),
|
|
8979
9123
|
timeoutAt
|
|
8980
9124
|
);
|
|
8981
9125
|
coverage.businessApi = Boolean(
|
|
8982
|
-
apiResponse && apiResponse
|
|
9126
|
+
apiResponse && isPreviewBusinessResponseReady(apiResponse)
|
|
8983
9127
|
);
|
|
8984
9128
|
if (!coverage.businessApi) {
|
|
8985
9129
|
reasons.push(
|
|
@@ -8990,10 +9134,16 @@ async function runPreviewArtifactProbe(options) {
|
|
|
8990
9134
|
await sleep(25);
|
|
8991
9135
|
}
|
|
8992
9136
|
if (fs31.existsSync(registryFile)) {
|
|
8993
|
-
const parsed = JSON.parse(
|
|
8994
|
-
|
|
9137
|
+
const parsed = JSON.parse(
|
|
9138
|
+
fs31.readFileSync(registryFile, "utf8")
|
|
9139
|
+
);
|
|
9140
|
+
if (Array.isArray(parsed) && parsed.every((value) => typeof value === "string")) {
|
|
8995
9141
|
coverage.actionPlugins = parsed.filter((value) => typeof value === "string").sort();
|
|
9142
|
+
} else {
|
|
9143
|
+
reasons.push("Action Plugin registry evidence is invalid");
|
|
8996
9144
|
}
|
|
9145
|
+
} else {
|
|
9146
|
+
reasons.push("Action Plugin registry evidence is unavailable");
|
|
8997
9147
|
}
|
|
8998
9148
|
} catch (error) {
|
|
8999
9149
|
reasons.push(error instanceof Error ? error.message : String(error));
|
|
@@ -9013,14 +9163,44 @@ async function runPreviewArtifactProbe(options) {
|
|
|
9013
9163
|
return { ...probe, isolatedNodeModulesPresent };
|
|
9014
9164
|
}
|
|
9015
9165
|
|
|
9016
|
-
// src/commands/preview-artifact/
|
|
9166
|
+
// src/commands/preview-artifact/environment.ts
|
|
9017
9167
|
import * as fs32 from "fs";
|
|
9018
9168
|
import * as path28 from "path";
|
|
9169
|
+
var ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9170
|
+
function readPreviewArtifactEnvironmentFile(filePath) {
|
|
9171
|
+
const resolved = path28.resolve(filePath);
|
|
9172
|
+
const parsed = JSON.parse(fs32.readFileSync(resolved, "utf8"));
|
|
9173
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9174
|
+
throw new Error("preview artifact environment file must contain an object");
|
|
9175
|
+
}
|
|
9176
|
+
const environment = {};
|
|
9177
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
9178
|
+
if (!ENVIRONMENT_NAME.test(key) || typeof value !== "string") {
|
|
9179
|
+
throw new Error(`invalid environment variable in ${resolved}: ${key}`);
|
|
9180
|
+
}
|
|
9181
|
+
environment[key] = value;
|
|
9182
|
+
}
|
|
9183
|
+
return environment;
|
|
9184
|
+
}
|
|
9185
|
+
function previewArtifactRuntimeEnvironment(environment) {
|
|
9186
|
+
return {
|
|
9187
|
+
...environment,
|
|
9188
|
+
NODE_ENV: environment?.NODE_ENV ?? "production",
|
|
9189
|
+
// Probe and switcher connect through 127.0.0.1. Linux may resolve
|
|
9190
|
+
// `localhost` to IPv6-only ::1, so the isolated Nest process must bind to
|
|
9191
|
+
// the same explicit private endpoint instead of inheriting app defaults.
|
|
9192
|
+
SERVER_HOST: "127.0.0.1"
|
|
9193
|
+
};
|
|
9194
|
+
}
|
|
9195
|
+
|
|
9196
|
+
// src/commands/preview-artifact/store.ts
|
|
9197
|
+
import * as fs33 from "fs";
|
|
9198
|
+
import * as path29 from "path";
|
|
9019
9199
|
var GENERATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
9020
9200
|
function readCurrentPointer(storeRoot) {
|
|
9021
9201
|
try {
|
|
9022
9202
|
const pointer = readJson3(
|
|
9023
|
-
|
|
9203
|
+
path29.join(storeRoot, "current.json")
|
|
9024
9204
|
);
|
|
9025
9205
|
return pointer.schemaVersion === PREVIEW_ARTIFACT_SCHEMA_VERSION && GENERATION_ID_PATTERN.test(pointer.generationId) ? pointer : void 0;
|
|
9026
9206
|
} catch {
|
|
@@ -9028,25 +9208,25 @@ function readCurrentPointer(storeRoot) {
|
|
|
9028
9208
|
}
|
|
9029
9209
|
}
|
|
9030
9210
|
function readJson3(filePath) {
|
|
9031
|
-
return JSON.parse(
|
|
9211
|
+
return JSON.parse(fs33.readFileSync(filePath, "utf8"));
|
|
9032
9212
|
}
|
|
9033
9213
|
function writeJsonAtomic2(filePath, value) {
|
|
9034
|
-
|
|
9214
|
+
fs33.mkdirSync(path29.dirname(filePath), { recursive: true });
|
|
9035
9215
|
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
9036
|
-
|
|
9216
|
+
fs33.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}
|
|
9037
9217
|
`, {
|
|
9038
9218
|
mode: 384
|
|
9039
9219
|
});
|
|
9040
|
-
|
|
9220
|
+
fs33.renameSync(temporaryPath, filePath);
|
|
9041
9221
|
}
|
|
9042
9222
|
function pruneSupersededGenerations(generationsRoot, retainedGenerationIds) {
|
|
9043
9223
|
const retained = new Set(retainedGenerationIds);
|
|
9044
|
-
for (const entry of
|
|
9224
|
+
for (const entry of fs33.readdirSync(generationsRoot, {
|
|
9045
9225
|
withFileTypes: true
|
|
9046
9226
|
})) {
|
|
9047
9227
|
if (retained.has(entry.name)) continue;
|
|
9048
9228
|
try {
|
|
9049
|
-
|
|
9229
|
+
fs33.rmSync(path29.join(generationsRoot, entry.name), {
|
|
9050
9230
|
recursive: true,
|
|
9051
9231
|
force: true
|
|
9052
9232
|
});
|
|
@@ -9058,13 +9238,13 @@ function parseNodeMajor2(nodeVersion) {
|
|
|
9058
9238
|
const match = /^v?(\d+)(?:\.|$)/.exec(nodeVersion.trim());
|
|
9059
9239
|
return match ? Number(match[1]) : null;
|
|
9060
9240
|
}
|
|
9061
|
-
function
|
|
9062
|
-
const
|
|
9063
|
-
return
|
|
9241
|
+
function isPathInside2(parent, candidate) {
|
|
9242
|
+
const relative5 = path29.relative(parent, candidate);
|
|
9243
|
+
return relative5 !== "" && !relative5.startsWith(`..${path29.sep}`) && relative5 !== ".." && !path29.isAbsolute(relative5);
|
|
9064
9244
|
}
|
|
9065
9245
|
function validateRegularDirectory(directoryPath, label) {
|
|
9066
9246
|
try {
|
|
9067
|
-
const stat =
|
|
9247
|
+
const stat = fs33.lstatSync(directoryPath);
|
|
9068
9248
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
9069
9249
|
return [`${label} must be a regular directory`];
|
|
9070
9250
|
}
|
|
@@ -9085,10 +9265,10 @@ function validateGeneration(generationDir, runtime) {
|
|
|
9085
9265
|
let probe;
|
|
9086
9266
|
try {
|
|
9087
9267
|
manifest = readJson3(
|
|
9088
|
-
|
|
9268
|
+
path29.join(generationDir, "manifest.json")
|
|
9089
9269
|
);
|
|
9090
9270
|
probe = readJson3(
|
|
9091
|
-
|
|
9271
|
+
path29.join(generationDir, "probe.json")
|
|
9092
9272
|
);
|
|
9093
9273
|
} catch (error) {
|
|
9094
9274
|
return {
|
|
@@ -9097,7 +9277,7 @@ function validateGeneration(generationDir, runtime) {
|
|
|
9097
9277
|
]
|
|
9098
9278
|
};
|
|
9099
9279
|
}
|
|
9100
|
-
if (manifest.schemaVersion !== PREVIEW_ARTIFACT_SCHEMA_VERSION || probe.schemaVersion !== PREVIEW_ARTIFACT_SCHEMA_VERSION) {
|
|
9280
|
+
if (manifest.schemaVersion !== PREVIEW_ARTIFACT_SCHEMA_VERSION || manifest.protocolVersion !== PREVIEW_ARTIFACT_PROTOCOL_VERSION || probe.schemaVersion !== PREVIEW_ARTIFACT_SCHEMA_VERSION) {
|
|
9101
9281
|
reasons.push("generation schema version is incompatible");
|
|
9102
9282
|
}
|
|
9103
9283
|
if (!GENERATION_ID_PATTERN.test(manifest.generationId) || probe.generationId !== manifest.generationId) {
|
|
@@ -9109,8 +9289,14 @@ function validateGeneration(generationDir, runtime) {
|
|
|
9109
9289
|
if (!probe.success) {
|
|
9110
9290
|
reasons.push("generation runtime probe did not pass");
|
|
9111
9291
|
}
|
|
9112
|
-
|
|
9113
|
-
|
|
9292
|
+
if (!Array.isArray(probe.coverage?.clientAssets) || probe.coverage.clientAssets.length === 0) {
|
|
9293
|
+
reasons.push("generation client asset evidence is invalid");
|
|
9294
|
+
}
|
|
9295
|
+
if (!/^[a-f0-9]{64}$/.test(manifest.buildEnvironmentSha256)) {
|
|
9296
|
+
reasons.push("build environment identity is invalid");
|
|
9297
|
+
}
|
|
9298
|
+
const serverPath = path29.join(generationDir, "server.bundle.cjs");
|
|
9299
|
+
const clientPath = path29.join(generationDir, "client.zip");
|
|
9114
9300
|
try {
|
|
9115
9301
|
if (sha256File(serverPath) !== manifest.files.server.sha256) {
|
|
9116
9302
|
reasons.push("server.bundle.cjs hash does not match manifest");
|
|
@@ -9132,11 +9318,11 @@ function validateGeneration(generationDir, runtime) {
|
|
|
9132
9318
|
return { manifest, reasons };
|
|
9133
9319
|
}
|
|
9134
9320
|
function publishPreviewArtifactGeneration(options) {
|
|
9135
|
-
const storeRoot =
|
|
9321
|
+
const storeRoot = path29.resolve(options.storeRoot);
|
|
9136
9322
|
const previousPointer = readCurrentPointer(storeRoot);
|
|
9137
|
-
const stagingRoot =
|
|
9138
|
-
const stagingDir =
|
|
9139
|
-
if (!
|
|
9323
|
+
const stagingRoot = path29.join(storeRoot, ".staging");
|
|
9324
|
+
const stagingDir = path29.resolve(options.stagingDir);
|
|
9325
|
+
if (!isPathInside2(stagingRoot, stagingDir)) {
|
|
9140
9326
|
return {
|
|
9141
9327
|
published: false,
|
|
9142
9328
|
reasons: [
|
|
@@ -9149,23 +9335,23 @@ function publishPreviewArtifactGeneration(options) {
|
|
|
9149
9335
|
return { published: false, reasons: validation.reasons };
|
|
9150
9336
|
}
|
|
9151
9337
|
const generationId = validation.manifest.generationId;
|
|
9152
|
-
const generationsRoot =
|
|
9153
|
-
const generationDir =
|
|
9154
|
-
|
|
9155
|
-
if (
|
|
9338
|
+
const generationsRoot = path29.join(storeRoot, "generations");
|
|
9339
|
+
const generationDir = path29.join(generationsRoot, generationId);
|
|
9340
|
+
fs33.mkdirSync(generationsRoot, { recursive: true });
|
|
9341
|
+
if (fs33.existsSync(generationDir)) {
|
|
9156
9342
|
return {
|
|
9157
9343
|
published: false,
|
|
9158
9344
|
reasons: [`generation already exists: ${generationId}`]
|
|
9159
9345
|
};
|
|
9160
9346
|
}
|
|
9161
9347
|
try {
|
|
9162
|
-
|
|
9348
|
+
fs33.renameSync(stagingDir, generationDir);
|
|
9163
9349
|
const pointer = {
|
|
9164
9350
|
schemaVersion: PREVIEW_ARTIFACT_SCHEMA_VERSION,
|
|
9165
9351
|
generationId,
|
|
9166
9352
|
publishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9167
9353
|
};
|
|
9168
|
-
writeJsonAtomic2(
|
|
9354
|
+
writeJsonAtomic2(path29.join(storeRoot, "current.json"), pointer);
|
|
9169
9355
|
pruneSupersededGenerations(
|
|
9170
9356
|
generationsRoot,
|
|
9171
9357
|
[generationId, previousPointer?.generationId].filter(
|
|
@@ -9183,10 +9369,10 @@ function publishPreviewArtifactGeneration(options) {
|
|
|
9183
9369
|
}
|
|
9184
9370
|
}
|
|
9185
9371
|
function resolveCurrentPreviewArtifact(options) {
|
|
9186
|
-
const storeRoot =
|
|
9372
|
+
const storeRoot = path29.resolve(options.storeRoot);
|
|
9187
9373
|
let pointer;
|
|
9188
9374
|
try {
|
|
9189
|
-
pointer = readJson3(
|
|
9375
|
+
pointer = readJson3(path29.join(storeRoot, "current.json"));
|
|
9190
9376
|
} catch (error) {
|
|
9191
9377
|
return {
|
|
9192
9378
|
resolved: false,
|
|
@@ -9205,7 +9391,7 @@ function resolveCurrentPreviewArtifact(options) {
|
|
|
9205
9391
|
if (nodeMajor == null) {
|
|
9206
9392
|
return { resolved: false, reasons: ["runtime Node.js version is invalid"] };
|
|
9207
9393
|
}
|
|
9208
|
-
const generationDir =
|
|
9394
|
+
const generationDir = path29.join(
|
|
9209
9395
|
storeRoot,
|
|
9210
9396
|
"generations",
|
|
9211
9397
|
pointer.generationId
|
|
@@ -9224,6 +9410,12 @@ function resolveCurrentPreviewArtifact(options) {
|
|
|
9224
9410
|
reasons: ["current pointer does not match generation manifest"]
|
|
9225
9411
|
};
|
|
9226
9412
|
}
|
|
9413
|
+
if (validation.manifest.buildEnvironmentSha256 !== options.buildEnvironmentSha256) {
|
|
9414
|
+
return {
|
|
9415
|
+
resolved: false,
|
|
9416
|
+
reasons: ["build environment is incompatible with generation"]
|
|
9417
|
+
};
|
|
9418
|
+
}
|
|
9227
9419
|
return {
|
|
9228
9420
|
resolved: true,
|
|
9229
9421
|
generationId: pointer.generationId,
|
|
@@ -9248,27 +9440,27 @@ function runBuildCommand(projectRoot, buildCommand, environment) {
|
|
|
9248
9440
|
return [`production build failed with exit ${result.status ?? "unknown"}`];
|
|
9249
9441
|
}
|
|
9250
9442
|
function createClientArchive(clientDir, archivePath) {
|
|
9251
|
-
if (!
|
|
9443
|
+
if (!fs34.existsSync(path30.join(clientDir, "index.html"))) {
|
|
9252
9444
|
return [`client build output is missing: ${clientDir}`];
|
|
9253
9445
|
}
|
|
9254
9446
|
const result = spawnSync8("zip", ["-qry", archivePath, "."], {
|
|
9255
9447
|
cwd: clientDir,
|
|
9256
9448
|
encoding: "utf8"
|
|
9257
9449
|
});
|
|
9258
|
-
if (result.status === 0 &&
|
|
9450
|
+
if (result.status === 0 && fs34.existsSync(archivePath)) return [];
|
|
9259
9451
|
return [`client archive failed with exit ${result.status ?? "unknown"}`];
|
|
9260
9452
|
}
|
|
9261
9453
|
function readActionPlugins2(metadataFile) {
|
|
9262
|
-
const metadata = JSON.parse(
|
|
9454
|
+
const metadata = JSON.parse(fs34.readFileSync(metadataFile, "utf8"));
|
|
9263
9455
|
if (!Array.isArray(metadata.actionPlugins)) return [];
|
|
9264
9456
|
return metadata.actionPlugins.filter((value) => typeof value === "string").sort();
|
|
9265
9457
|
}
|
|
9266
9458
|
async function producePreviewArtifactGeneration(options) {
|
|
9267
9459
|
const startedAt = Date.now();
|
|
9268
|
-
const projectRoot =
|
|
9269
|
-
const storeRoot =
|
|
9460
|
+
const projectRoot = path30.resolve(options.projectRoot);
|
|
9461
|
+
const storeRoot = path30.resolve(options.storeRoot);
|
|
9270
9462
|
const generationId = options.generationId ?? defaultGenerationId();
|
|
9271
|
-
const stagingDir =
|
|
9463
|
+
const stagingDir = path30.join(
|
|
9272
9464
|
storeRoot,
|
|
9273
9465
|
".staging",
|
|
9274
9466
|
`${generationId}.${process.pid}.${Date.now()}`
|
|
@@ -9280,16 +9472,20 @@ async function producePreviewArtifactGeneration(options) {
|
|
|
9280
9472
|
reasons,
|
|
9281
9473
|
...probe ? { probe } : {}
|
|
9282
9474
|
});
|
|
9283
|
-
|
|
9475
|
+
fs34.mkdirSync(stagingDir, { recursive: true });
|
|
9284
9476
|
try {
|
|
9477
|
+
const buildEnvironment = previewArtifactBuildEnvironment(
|
|
9478
|
+
options.environment,
|
|
9479
|
+
options.appBasePath
|
|
9480
|
+
);
|
|
9285
9481
|
const buildReasons = runBuildCommand(
|
|
9286
9482
|
projectRoot,
|
|
9287
9483
|
options.buildCommand ?? { command: "npm", args: ["run", "build:prod"] },
|
|
9288
|
-
|
|
9484
|
+
buildEnvironment
|
|
9289
9485
|
);
|
|
9290
9486
|
if (buildReasons.length > 0) return failed(buildReasons);
|
|
9291
|
-
const serverOutfile =
|
|
9292
|
-
const serverMetadataFile =
|
|
9487
|
+
const serverOutfile = path30.join(stagingDir, "server.bundle.cjs");
|
|
9488
|
+
const serverMetadataFile = path30.join(stagingDir, "server-build.json");
|
|
9293
9489
|
const serverResult = await buildPreviewServerArtifact({
|
|
9294
9490
|
projectRoot,
|
|
9295
9491
|
entry: "dist/main.js",
|
|
@@ -9298,25 +9494,29 @@ async function producePreviewArtifactGeneration(options) {
|
|
|
9298
9494
|
});
|
|
9299
9495
|
if (!serverResult.built) return failed(serverResult.reasons);
|
|
9300
9496
|
const archiveReasons = createClientArchive(
|
|
9301
|
-
|
|
9302
|
-
|
|
9497
|
+
path30.join(projectRoot, "dist", "client"),
|
|
9498
|
+
path30.join(stagingDir, "client.zip")
|
|
9303
9499
|
);
|
|
9304
9500
|
if (archiveReasons.length > 0) return failed(archiveReasons);
|
|
9305
9501
|
const actionPlugins = readActionPlugins2(serverMetadataFile);
|
|
9306
|
-
|
|
9502
|
+
fs34.rmSync(serverMetadataFile, { force: true });
|
|
9307
9503
|
createPreviewArtifactManifest({
|
|
9308
9504
|
generationDir: stagingDir,
|
|
9309
9505
|
generationId,
|
|
9310
9506
|
nodeVersion: process.version,
|
|
9311
9507
|
platform: process.platform,
|
|
9312
9508
|
arch: process.arch,
|
|
9509
|
+
buildEnvironmentSha256: previewArtifactBuildEnvironmentHash(
|
|
9510
|
+
options.environment,
|
|
9511
|
+
options.appBasePath
|
|
9512
|
+
),
|
|
9313
9513
|
actionPlugins
|
|
9314
9514
|
});
|
|
9315
9515
|
const probe = await runPreviewArtifactProbe({
|
|
9316
9516
|
generationDir: stagingDir,
|
|
9317
9517
|
appBasePath: options.appBasePath,
|
|
9318
9518
|
businessApiPath: options.businessApiPath,
|
|
9319
|
-
environment: options.environment
|
|
9519
|
+
environment: previewArtifactRuntimeEnvironment(options.environment)
|
|
9320
9520
|
});
|
|
9321
9521
|
if (!probe.success) return failed(probe.reasons, probe);
|
|
9322
9522
|
const promotion = promotePreviewArtifactGeneration(stagingDir);
|
|
@@ -9336,23 +9536,25 @@ async function producePreviewArtifactGeneration(options) {
|
|
|
9336
9536
|
} catch (error) {
|
|
9337
9537
|
return failed([error instanceof Error ? error.message : String(error)]);
|
|
9338
9538
|
} finally {
|
|
9339
|
-
|
|
9539
|
+
fs34.rmSync(stagingDir, { recursive: true, force: true });
|
|
9340
9540
|
}
|
|
9341
9541
|
}
|
|
9342
9542
|
|
|
9343
9543
|
// src/commands/preview-artifact/runtime.ts
|
|
9344
|
-
import * as
|
|
9544
|
+
import * as fs36 from "fs";
|
|
9345
9545
|
import * as net3 from "net";
|
|
9346
|
-
import * as
|
|
9546
|
+
import * as path32 from "path";
|
|
9347
9547
|
import { spawn as spawn3 } from "child_process";
|
|
9348
9548
|
|
|
9349
9549
|
// src/commands/preview-artifact/switcher.ts
|
|
9350
9550
|
import * as http from "http";
|
|
9351
9551
|
import * as net2 from "net";
|
|
9552
|
+
import * as fs35 from "fs";
|
|
9553
|
+
import * as path31 from "path";
|
|
9352
9554
|
function targetOrigin(target) {
|
|
9353
9555
|
return `http://${target.host}:${target.port}`;
|
|
9354
9556
|
}
|
|
9355
|
-
function proxyHttpRequest(target, request2, response) {
|
|
9557
|
+
function proxyHttpRequest(target, request2, response, fallback) {
|
|
9356
9558
|
const upstream = http.request(
|
|
9357
9559
|
{
|
|
9358
9560
|
host: target.host,
|
|
@@ -9362,6 +9564,13 @@ function proxyHttpRequest(target, request2, response) {
|
|
|
9362
9564
|
headers: request2.headers
|
|
9363
9565
|
},
|
|
9364
9566
|
(upstreamResponse) => {
|
|
9567
|
+
const upstreamContentType = upstreamResponse.headers["content-type"];
|
|
9568
|
+
const isHtmlFallback = typeof upstreamContentType === "string" && upstreamContentType.toLowerCase().includes("text/html");
|
|
9569
|
+
if (fallback && (upstreamResponse.statusCode === 404 || isHtmlFallback)) {
|
|
9570
|
+
upstreamResponse.resume();
|
|
9571
|
+
fallback();
|
|
9572
|
+
return;
|
|
9573
|
+
}
|
|
9365
9574
|
response.writeHead(
|
|
9366
9575
|
upstreamResponse.statusCode ?? 502,
|
|
9367
9576
|
upstreamResponse.headers
|
|
@@ -9382,6 +9591,72 @@ function proxyHttpRequest(target, request2, response) {
|
|
|
9382
9591
|
});
|
|
9383
9592
|
request2.pipe(upstream);
|
|
9384
9593
|
}
|
|
9594
|
+
function cachedAssetFile(request2, cachedAssetFiles) {
|
|
9595
|
+
if (request2.method !== "GET" && request2.method !== "HEAD") return void 0;
|
|
9596
|
+
if (!cachedAssetFiles) return void 0;
|
|
9597
|
+
const pathname = new URL(request2.url ?? "/", "http://preview.local").pathname;
|
|
9598
|
+
return cachedAssetFiles.get(pathname);
|
|
9599
|
+
}
|
|
9600
|
+
function contentType(filePath) {
|
|
9601
|
+
switch (path31.extname(filePath).toLowerCase()) {
|
|
9602
|
+
case ".js":
|
|
9603
|
+
case ".mjs":
|
|
9604
|
+
return "text/javascript; charset=utf-8";
|
|
9605
|
+
case ".css":
|
|
9606
|
+
return "text/css; charset=utf-8";
|
|
9607
|
+
case ".json":
|
|
9608
|
+
case ".map":
|
|
9609
|
+
return "application/json; charset=utf-8";
|
|
9610
|
+
case ".svg":
|
|
9611
|
+
return "image/svg+xml";
|
|
9612
|
+
case ".png":
|
|
9613
|
+
return "image/png";
|
|
9614
|
+
case ".jpg":
|
|
9615
|
+
case ".jpeg":
|
|
9616
|
+
return "image/jpeg";
|
|
9617
|
+
case ".gif":
|
|
9618
|
+
return "image/gif";
|
|
9619
|
+
case ".webp":
|
|
9620
|
+
return "image/webp";
|
|
9621
|
+
case ".ico":
|
|
9622
|
+
return "image/x-icon";
|
|
9623
|
+
case ".woff":
|
|
9624
|
+
return "font/woff";
|
|
9625
|
+
case ".woff2":
|
|
9626
|
+
return "font/woff2";
|
|
9627
|
+
case ".ttf":
|
|
9628
|
+
return "font/ttf";
|
|
9629
|
+
case ".wasm":
|
|
9630
|
+
return "application/wasm";
|
|
9631
|
+
default:
|
|
9632
|
+
return "application/octet-stream";
|
|
9633
|
+
}
|
|
9634
|
+
}
|
|
9635
|
+
function serveCachedAsset(request2, response, filePath) {
|
|
9636
|
+
let stat;
|
|
9637
|
+
try {
|
|
9638
|
+
stat = fs35.lstatSync(filePath);
|
|
9639
|
+
} catch {
|
|
9640
|
+
response.writeHead(404).end();
|
|
9641
|
+
return;
|
|
9642
|
+
}
|
|
9643
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
9644
|
+
response.writeHead(404).end();
|
|
9645
|
+
return;
|
|
9646
|
+
}
|
|
9647
|
+
response.writeHead(200, {
|
|
9648
|
+
"content-type": contentType(filePath),
|
|
9649
|
+
"content-length": stat.size,
|
|
9650
|
+
"cache-control": "no-store"
|
|
9651
|
+
});
|
|
9652
|
+
if (request2.method === "HEAD") {
|
|
9653
|
+
response.end();
|
|
9654
|
+
return;
|
|
9655
|
+
}
|
|
9656
|
+
const stream = fs35.createReadStream(filePath);
|
|
9657
|
+
stream.on("error", () => response.destroy());
|
|
9658
|
+
stream.pipe(response);
|
|
9659
|
+
}
|
|
9385
9660
|
function proxyUpgrade(target, request2, socket, head) {
|
|
9386
9661
|
const upstream = net2.connect(target.port, target.host, () => {
|
|
9387
9662
|
const headers = Object.entries(request2.headers).flatMap(([name, value]) => {
|
|
@@ -9400,14 +9675,19 @@ ${headers}\r
|
|
|
9400
9675
|
upstream.on("error", () => socket.destroy());
|
|
9401
9676
|
socket.on("error", () => upstream.destroy());
|
|
9402
9677
|
}
|
|
9403
|
-
async function freshTargetReady(target, healthPath) {
|
|
9678
|
+
async function freshTargetReady(target, healthPath, businessPath) {
|
|
9404
9679
|
try {
|
|
9405
9680
|
const response = await fetch(new URL(healthPath, targetOrigin(target)), {
|
|
9406
9681
|
signal: AbortSignal.timeout(1e3)
|
|
9407
9682
|
});
|
|
9408
9683
|
if (response.status !== 200) return false;
|
|
9409
9684
|
const body = await response.json();
|
|
9410
|
-
|
|
9685
|
+
if (body.ready !== true) return false;
|
|
9686
|
+
const businessResponse = await fetch(
|
|
9687
|
+
new URL(businessPath, targetOrigin(target)),
|
|
9688
|
+
{ signal: AbortSignal.timeout(1e3), redirect: "manual" }
|
|
9689
|
+
);
|
|
9690
|
+
return isPreviewBusinessResponseReady(businessResponse);
|
|
9411
9691
|
} catch {
|
|
9412
9692
|
return false;
|
|
9413
9693
|
}
|
|
@@ -9420,8 +9700,8 @@ function createPreviewArtifactSwitcher(options) {
|
|
|
9420
9700
|
let closed = false;
|
|
9421
9701
|
let pollTimer;
|
|
9422
9702
|
let settleCutover;
|
|
9423
|
-
const cutover = new Promise((
|
|
9424
|
-
settleCutover =
|
|
9703
|
+
const cutover = new Promise((resolve9) => {
|
|
9704
|
+
settleCutover = resolve9;
|
|
9425
9705
|
});
|
|
9426
9706
|
const currentTarget = () => active === "cached" ? options.cachedTarget : options.freshTarget;
|
|
9427
9707
|
const server = http.createServer((request2, response) => {
|
|
@@ -9436,7 +9716,14 @@ function createPreviewArtifactSwitcher(options) {
|
|
|
9436
9716
|
);
|
|
9437
9717
|
return;
|
|
9438
9718
|
}
|
|
9439
|
-
|
|
9719
|
+
const assetFile = cachedAssetFile(request2, options.cachedAssetFiles);
|
|
9720
|
+
if (active === "cached" && assetFile) {
|
|
9721
|
+
serveCachedAsset(request2, response, assetFile);
|
|
9722
|
+
return;
|
|
9723
|
+
}
|
|
9724
|
+
const target = currentTarget();
|
|
9725
|
+
const fallback = active === "fresh" && assetFile ? () => serveCachedAsset(request2, response, assetFile) : void 0;
|
|
9726
|
+
proxyHttpRequest(target, request2, response, fallback);
|
|
9440
9727
|
});
|
|
9441
9728
|
server.on("upgrade", (request2, socket, head) => {
|
|
9442
9729
|
proxyUpgrade(currentTarget(), request2, socket, head);
|
|
@@ -9444,7 +9731,11 @@ function createPreviewArtifactSwitcher(options) {
|
|
|
9444
9731
|
const schedulePoll = () => {
|
|
9445
9732
|
if (closed || active === "fresh") return;
|
|
9446
9733
|
pollTimer = setTimeout(async () => {
|
|
9447
|
-
if (await freshTargetReady(
|
|
9734
|
+
if (await freshTargetReady(
|
|
9735
|
+
options.freshTarget,
|
|
9736
|
+
healthPath,
|
|
9737
|
+
options.freshBusinessPath
|
|
9738
|
+
)) {
|
|
9448
9739
|
active = "fresh";
|
|
9449
9740
|
settleCutover?.({
|
|
9450
9741
|
switched: true,
|
|
@@ -9464,7 +9755,7 @@ function createPreviewArtifactSwitcher(options) {
|
|
|
9464
9755
|
},
|
|
9465
9756
|
cutover,
|
|
9466
9757
|
start() {
|
|
9467
|
-
return new Promise((
|
|
9758
|
+
return new Promise((resolve9, reject) => {
|
|
9468
9759
|
server.once("error", reject);
|
|
9469
9760
|
server.listen(options.listenPort, listenHost, () => {
|
|
9470
9761
|
const address = server.address();
|
|
@@ -9473,7 +9764,7 @@ function createPreviewArtifactSwitcher(options) {
|
|
|
9473
9764
|
return;
|
|
9474
9765
|
}
|
|
9475
9766
|
schedulePoll();
|
|
9476
|
-
|
|
9767
|
+
resolve9({
|
|
9477
9768
|
host: listenHost,
|
|
9478
9769
|
port: address.port,
|
|
9479
9770
|
origin: `http://${listenHost}:${address.port}`
|
|
@@ -9492,8 +9783,8 @@ function createPreviewArtifactSwitcher(options) {
|
|
|
9492
9783
|
});
|
|
9493
9784
|
settleCutover = void 0;
|
|
9494
9785
|
}
|
|
9495
|
-
return new Promise((
|
|
9496
|
-
server.close(() =>
|
|
9786
|
+
return new Promise((resolve9) => {
|
|
9787
|
+
server.close(() => resolve9());
|
|
9497
9788
|
server.closeAllConnections?.();
|
|
9498
9789
|
});
|
|
9499
9790
|
}
|
|
@@ -9502,10 +9793,10 @@ function createPreviewArtifactSwitcher(options) {
|
|
|
9502
9793
|
|
|
9503
9794
|
// src/commands/preview-artifact/runtime.ts
|
|
9504
9795
|
function sleep2(ms) {
|
|
9505
|
-
return new Promise((
|
|
9796
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
9506
9797
|
}
|
|
9507
9798
|
async function reservePort2() {
|
|
9508
|
-
return new Promise((
|
|
9799
|
+
return new Promise((resolve9, reject) => {
|
|
9509
9800
|
const server = net3.createServer();
|
|
9510
9801
|
server.unref();
|
|
9511
9802
|
server.once("error", reject);
|
|
@@ -9517,7 +9808,7 @@ async function reservePort2() {
|
|
|
9517
9808
|
}
|
|
9518
9809
|
server.close((error) => {
|
|
9519
9810
|
if (error) reject(error);
|
|
9520
|
-
else
|
|
9811
|
+
else resolve9(address.port);
|
|
9521
9812
|
});
|
|
9522
9813
|
});
|
|
9523
9814
|
});
|
|
@@ -9534,27 +9825,53 @@ function stopProcessGroup2(child) {
|
|
|
9534
9825
|
}
|
|
9535
9826
|
}
|
|
9536
9827
|
}
|
|
9537
|
-
function
|
|
9538
|
-
const
|
|
9539
|
-
return
|
|
9828
|
+
function isPathInside3(parent, candidate) {
|
|
9829
|
+
const relative5 = path32.relative(parent, candidate);
|
|
9830
|
+
return relative5 !== "" && relative5 !== ".." && !relative5.startsWith(`..${path32.sep}`) && !path32.isAbsolute(relative5);
|
|
9540
9831
|
}
|
|
9541
9832
|
function ensureRegularRuntimeRoot(runtimeRoot) {
|
|
9542
|
-
|
|
9543
|
-
const stat =
|
|
9833
|
+
fs36.mkdirSync(runtimeRoot, { recursive: true });
|
|
9834
|
+
const stat = fs36.lstatSync(runtimeRoot);
|
|
9544
9835
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
9545
9836
|
throw new Error("runtime root must be a regular directory");
|
|
9546
9837
|
}
|
|
9547
9838
|
}
|
|
9839
|
+
function collectCachedAssetFiles(clientRoot, appBasePath) {
|
|
9840
|
+
const files = /* @__PURE__ */ new Map();
|
|
9841
|
+
const basePath = appBasePath.endsWith("/") ? appBasePath : `${appBasePath}/`;
|
|
9842
|
+
const visit = (directory) => {
|
|
9843
|
+
for (const entry of fs36.readdirSync(directory, { withFileTypes: true })) {
|
|
9844
|
+
const absolute = path32.join(directory, entry.name);
|
|
9845
|
+
if (entry.isDirectory()) {
|
|
9846
|
+
visit(absolute);
|
|
9847
|
+
continue;
|
|
9848
|
+
}
|
|
9849
|
+
if (!entry.isFile() || entry.isSymbolicLink()) continue;
|
|
9850
|
+
const relative5 = path32.relative(clientRoot, absolute).split(path32.sep).join("/");
|
|
9851
|
+
if (relative5 === "index.html") continue;
|
|
9852
|
+
files.set(
|
|
9853
|
+
new URL(`/${relative5}`, "http://preview.local").pathname,
|
|
9854
|
+
absolute
|
|
9855
|
+
);
|
|
9856
|
+
files.set(
|
|
9857
|
+
new URL(relative5, `http://preview.local${basePath}`).pathname,
|
|
9858
|
+
absolute
|
|
9859
|
+
);
|
|
9860
|
+
}
|
|
9861
|
+
};
|
|
9862
|
+
visit(clientRoot);
|
|
9863
|
+
return files;
|
|
9864
|
+
}
|
|
9548
9865
|
function writeMarkerAtomic(markerFile, marker) {
|
|
9549
|
-
|
|
9866
|
+
fs36.mkdirSync(path32.dirname(markerFile), { recursive: true });
|
|
9550
9867
|
const temporaryFile = `${markerFile}.${process.pid}.${Date.now()}.tmp`;
|
|
9551
|
-
|
|
9868
|
+
fs36.writeFileSync(temporaryFile, `${JSON.stringify(marker, null, 2)}
|
|
9552
9869
|
`, {
|
|
9553
9870
|
mode: 384
|
|
9554
9871
|
});
|
|
9555
|
-
|
|
9872
|
+
fs36.renameSync(temporaryFile, markerFile);
|
|
9556
9873
|
}
|
|
9557
|
-
async function waitForCachedRuntime(origin, appBasePath, timeoutMs, child) {
|
|
9874
|
+
async function waitForCachedRuntime(origin, appBasePath, businessApiPath, timeoutMs, child) {
|
|
9558
9875
|
const timeoutAt = Date.now() + timeoutMs;
|
|
9559
9876
|
while (Date.now() < timeoutAt) {
|
|
9560
9877
|
if (child.exitCode != null || child.signalCode != null) {
|
|
@@ -9567,7 +9884,15 @@ async function waitForCachedRuntime(origin, appBasePath, timeoutMs, child) {
|
|
|
9567
9884
|
redirect: "manual",
|
|
9568
9885
|
signal: AbortSignal.timeout(1e3)
|
|
9569
9886
|
});
|
|
9570
|
-
if (response.status === 200)
|
|
9887
|
+
if (response.status === 200) {
|
|
9888
|
+
const businessResponse = await fetch(new URL(businessApiPath, origin), {
|
|
9889
|
+
redirect: "manual",
|
|
9890
|
+
signal: AbortSignal.timeout(1e3)
|
|
9891
|
+
});
|
|
9892
|
+
if (isPreviewBusinessResponseReady(businessResponse)) {
|
|
9893
|
+
return;
|
|
9894
|
+
}
|
|
9895
|
+
}
|
|
9571
9896
|
} catch {
|
|
9572
9897
|
}
|
|
9573
9898
|
await sleep2(50);
|
|
@@ -9579,11 +9904,15 @@ async function startPreviewArtifactRuntime(options) {
|
|
|
9579
9904
|
storeRoot: options.storeRoot,
|
|
9580
9905
|
nodeVersion: process.version,
|
|
9581
9906
|
platform: process.platform,
|
|
9582
|
-
arch: process.arch
|
|
9907
|
+
arch: process.arch,
|
|
9908
|
+
buildEnvironmentSha256: previewArtifactBuildEnvironmentHash(
|
|
9909
|
+
options.environment,
|
|
9910
|
+
options.appBasePath
|
|
9911
|
+
)
|
|
9583
9912
|
});
|
|
9584
9913
|
if (!resolved.resolved) return { started: false, reasons: resolved.reasons };
|
|
9585
|
-
const runtimeRoot =
|
|
9586
|
-
const markerFile =
|
|
9914
|
+
const runtimeRoot = path32.resolve(options.runtimeRoot);
|
|
9915
|
+
const markerFile = path32.resolve(options.markerFile);
|
|
9587
9916
|
let runtimeDir;
|
|
9588
9917
|
let cachedRuntime;
|
|
9589
9918
|
let switcher;
|
|
@@ -9593,25 +9922,26 @@ async function startPreviewArtifactRuntime(options) {
|
|
|
9593
9922
|
closed = true;
|
|
9594
9923
|
stopProcessGroup2(cachedRuntime);
|
|
9595
9924
|
await switcher?.close();
|
|
9596
|
-
|
|
9597
|
-
if (runtimeDir)
|
|
9925
|
+
fs36.rmSync(markerFile, { force: true });
|
|
9926
|
+
if (runtimeDir) fs36.rmSync(runtimeDir, { recursive: true, force: true });
|
|
9598
9927
|
};
|
|
9599
9928
|
try {
|
|
9600
9929
|
ensureRegularRuntimeRoot(runtimeRoot);
|
|
9601
|
-
if (!
|
|
9930
|
+
if (!isPathInside3(runtimeRoot, markerFile)) {
|
|
9602
9931
|
throw new Error("runtime marker must be inside the runtime root");
|
|
9603
9932
|
}
|
|
9604
|
-
runtimeDir =
|
|
9605
|
-
|
|
9933
|
+
runtimeDir = fs36.mkdtempSync(
|
|
9934
|
+
path32.join(runtimeRoot, `${resolved.generationId}-`)
|
|
9606
9935
|
);
|
|
9607
|
-
const runtimeBundle =
|
|
9608
|
-
|
|
9609
|
-
|
|
9936
|
+
const runtimeBundle = path32.join(runtimeDir, "server.bundle.cjs");
|
|
9937
|
+
fs36.copyFileSync(
|
|
9938
|
+
path32.join(resolved.generationDir, "server.bundle.cjs"),
|
|
9610
9939
|
runtimeBundle
|
|
9611
9940
|
);
|
|
9941
|
+
const clientRoot = path32.join(runtimeDir, "dist", "client");
|
|
9612
9942
|
extractClientArchive(
|
|
9613
|
-
|
|
9614
|
-
|
|
9943
|
+
path32.join(resolved.generationDir, "client.zip"),
|
|
9944
|
+
clientRoot
|
|
9615
9945
|
);
|
|
9616
9946
|
const cachedServerPort = options.cachedServerPort === 0 ? await reservePort2() : options.cachedServerPort;
|
|
9617
9947
|
cachedRuntime = spawn3(process.execPath, [runtimeBundle], {
|
|
@@ -9620,7 +9950,7 @@ async function startPreviewArtifactRuntime(options) {
|
|
|
9620
9950
|
stdio: "ignore",
|
|
9621
9951
|
env: {
|
|
9622
9952
|
...process.env,
|
|
9623
|
-
...options.environment,
|
|
9953
|
+
...previewArtifactRuntimeEnvironment(options.environment),
|
|
9624
9954
|
SERVER_PORT: String(cachedServerPort)
|
|
9625
9955
|
}
|
|
9626
9956
|
});
|
|
@@ -9628,6 +9958,7 @@ async function startPreviewArtifactRuntime(options) {
|
|
|
9628
9958
|
await waitForCachedRuntime(
|
|
9629
9959
|
cachedOrigin,
|
|
9630
9960
|
options.appBasePath,
|
|
9961
|
+
options.businessApiPath,
|
|
9631
9962
|
options.startupTimeoutMs ?? 3e4,
|
|
9632
9963
|
cachedRuntime
|
|
9633
9964
|
);
|
|
@@ -9637,6 +9968,11 @@ async function startPreviewArtifactRuntime(options) {
|
|
|
9637
9968
|
generationId: resolved.generationId,
|
|
9638
9969
|
cachedTarget: { host: "127.0.0.1", port: cachedServerPort },
|
|
9639
9970
|
freshTarget: { host: "127.0.0.1", port: options.freshClientPort },
|
|
9971
|
+
freshBusinessPath: options.businessApiPath,
|
|
9972
|
+
cachedAssetFiles: collectCachedAssetFiles(
|
|
9973
|
+
clientRoot,
|
|
9974
|
+
options.appBasePath
|
|
9975
|
+
),
|
|
9640
9976
|
pollIntervalMs: options.pollIntervalMs
|
|
9641
9977
|
});
|
|
9642
9978
|
const address = await switcher.start();
|
|
@@ -9656,9 +9992,6 @@ async function startPreviewArtifactRuntime(options) {
|
|
|
9656
9992
|
runtimeMarker.status = "fresh-serving";
|
|
9657
9993
|
runtimeMarker.cutoverAt = result.at;
|
|
9658
9994
|
writeMarkerAtomic(markerFile, runtimeMarker);
|
|
9659
|
-
await sleep2(options.cachedDrainMs ?? 5e3);
|
|
9660
|
-
stopProcessGroup2(cachedRuntime);
|
|
9661
|
-
cachedRuntime = void 0;
|
|
9662
9995
|
}
|
|
9663
9996
|
return result;
|
|
9664
9997
|
});
|
|
@@ -9748,9 +10081,17 @@ var previewArtifactGenerationCommand = {
|
|
|
9748
10081
|
program.command(this.name).description(this.description).option("--project-root <dir>", "Application project root", process.cwd()).requiredOption("--store-root <dir>", "Immutable generation store root").option("--generation-id <id>", "Explicit immutable generation ID").requiredOption("--app-base-path <path>", "Application HTML probe path").requiredOption(
|
|
9749
10082
|
"--business-api-path <path>",
|
|
9750
10083
|
"Business API probe path that must not return 404/5xx"
|
|
10084
|
+
).requiredOption(
|
|
10085
|
+
"--environment-file <file>",
|
|
10086
|
+
"JSON application environment passed only to build/probe children"
|
|
9751
10087
|
).action(
|
|
9752
10088
|
async (options) => {
|
|
9753
|
-
const result = await producePreviewArtifactGeneration(
|
|
10089
|
+
const result = await producePreviewArtifactGeneration({
|
|
10090
|
+
...options,
|
|
10091
|
+
environment: readPreviewArtifactEnvironmentFile(
|
|
10092
|
+
options.environmentFile
|
|
10093
|
+
)
|
|
10094
|
+
});
|
|
9754
10095
|
console.log(JSON.stringify(result));
|
|
9755
10096
|
if (!result.published) process.exitCode = 2;
|
|
9756
10097
|
}
|
|
@@ -9799,9 +10140,20 @@ var previewArtifactRuntimeCommand = {
|
|
|
9799
10140
|
).requiredOption(
|
|
9800
10141
|
"--app-base-path <path>",
|
|
9801
10142
|
"Application HTML readiness path"
|
|
10143
|
+
).requiredOption(
|
|
10144
|
+
"--business-api-path <path>",
|
|
10145
|
+
"Business API path revalidated with the current runtime environment"
|
|
10146
|
+
).requiredOption(
|
|
10147
|
+
"--environment-file <file>",
|
|
10148
|
+
"JSON application environment passed only to cached application child"
|
|
9802
10149
|
).action(
|
|
9803
10150
|
async (options) => {
|
|
9804
|
-
const runtime = await startPreviewArtifactRuntime(
|
|
10151
|
+
const runtime = await startPreviewArtifactRuntime({
|
|
10152
|
+
...options,
|
|
10153
|
+
environment: readPreviewArtifactEnvironmentFile(
|
|
10154
|
+
options.environmentFile
|
|
10155
|
+
)
|
|
10156
|
+
});
|
|
9805
10157
|
if (!runtime.started) {
|
|
9806
10158
|
console.log(JSON.stringify(runtime));
|
|
9807
10159
|
process.exitCode = 2;
|
|
@@ -9855,12 +10207,12 @@ var commands = [
|
|
|
9855
10207
|
];
|
|
9856
10208
|
|
|
9857
10209
|
// src/index.ts
|
|
9858
|
-
var envPath =
|
|
9859
|
-
if (
|
|
10210
|
+
var envPath = path33.join(process.cwd(), ".env");
|
|
10211
|
+
if (fs37.existsSync(envPath)) {
|
|
9860
10212
|
dotenvConfig({ path: envPath });
|
|
9861
10213
|
}
|
|
9862
|
-
var __dirname =
|
|
9863
|
-
var pkg = JSON.parse(
|
|
10214
|
+
var __dirname = path33.dirname(fileURLToPath5(import.meta.url));
|
|
10215
|
+
var pkg = JSON.parse(fs37.readFileSync(path33.join(__dirname, "../package.json"), "utf-8"));
|
|
9864
10216
|
var cli = new FullstackCLI(pkg.version);
|
|
9865
10217
|
cli.useAll(commands);
|
|
9866
10218
|
cli.run();
|