@antelopejs/dms-frontend 0.6.4 → 0.7.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/dist/commands/build-action.d.ts +2 -0
- package/dist/commands/build-action.js +37 -9
- package/dist/commands/build.js +9 -3
- package/dist/commands/snapshot-action.d.ts +6 -0
- package/dist/commands/snapshot-action.js +53 -0
- package/dist/commands/snapshot.d.ts +2 -0
- package/dist/commands/snapshot.js +55 -0
- package/dist/commands/start-action.d.ts +1 -0
- package/dist/commands/start-action.js +80 -5
- package/dist/commands/start.js +7 -2
- package/dist/config.d.ts +3 -1
- package/dist/config.js +14 -3
- package/dist/help.js +8 -3
- package/dist/index.js +2 -0
- package/dist/manifest.d.ts +7 -1
- package/dist/manifest.js +21 -17
- package/dist/materialize.js +11 -1
- package/dist/module-set.d.ts +11 -0
- package/dist/module-set.js +18 -0
- package/dist/private-values.d.ts +9 -0
- package/dist/private-values.js +77 -0
- package/dist/snapshot.d.ts +25 -0
- package/dist/snapshot.js +90 -0
- package/dist/workspace-setup.d.ts +7 -0
- package/dist/workspace-setup.js +55 -23
- package/dist/workspace.d.ts +1 -0
- package/dist/workspace.js +6 -0
- package/package.json +1 -1
- package/templates/vue/server/module-server.mjs +67 -5
|
@@ -7,9 +7,29 @@ const node_path_1 = require("node:path");
|
|
|
7
7
|
const cli_1 = require("@antelopejs/core/cli");
|
|
8
8
|
const child_output_1 = require("../child-output");
|
|
9
9
|
const common_1 = require("../common");
|
|
10
|
+
const snapshot_1 = require("../snapshot");
|
|
10
11
|
const output_1 = require("../output");
|
|
11
12
|
const vite_output_1 = require("../vite-output");
|
|
12
13
|
const workspace_task_1 = require("./workspace-task");
|
|
14
|
+
function buildSource(options) {
|
|
15
|
+
if (!options.from) {
|
|
16
|
+
return { backendUrl: (0, common_1.requireBackendUrl)(options.backendUrl) };
|
|
17
|
+
}
|
|
18
|
+
if (options.offline) {
|
|
19
|
+
throw new cli_1.CliError({
|
|
20
|
+
title: "--from and --offline cannot be combined",
|
|
21
|
+
reason: "A snapshot build never asks the backend already.",
|
|
22
|
+
fixes: ["Drop --offline"],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const snapshot = (0, snapshot_1.readSnapshot)(options.from);
|
|
26
|
+
return {
|
|
27
|
+
backendUrl: options.backendUrl
|
|
28
|
+
? (0, common_1.parseBackendUrl)(options.backendUrl)
|
|
29
|
+
: snapshot.backendUrl,
|
|
30
|
+
snapshot: { dir: options.from, snapshot },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
13
33
|
exports.BUILD_STEPS = [
|
|
14
34
|
{
|
|
15
35
|
script: "build:client",
|
|
@@ -102,19 +122,25 @@ async function runBuild(options) {
|
|
|
102
122
|
}
|
|
103
123
|
}
|
|
104
124
|
async function buildProduction(options, startedAt) {
|
|
105
|
-
const backendUrl = (
|
|
106
|
-
const
|
|
107
|
-
|
|
125
|
+
const { backendUrl, snapshot } = buildSource(options);
|
|
126
|
+
const bootstrapSecret = snapshot
|
|
127
|
+
? undefined
|
|
128
|
+
: (0, common_1.normalizeBootstrapSecret)(options.bootstrapSecret);
|
|
108
129
|
const ui = (0, cli_1.getProcessUi)();
|
|
109
130
|
(0, output_1.writeHeader)("build", [backendUrl, "production"]);
|
|
110
|
-
const { workspaceDir, layers, manifestFromCache, manifestFetchedAt } = await (0, workspace_task_1.setUpWorkspace)({
|
|
131
|
+
const { workspaceDir, layers, manifestFromCache, manifestFetchedAt, buildOrigin, } = await (0, workspace_task_1.setUpWorkspace)({
|
|
111
132
|
backendUrl,
|
|
112
133
|
force: !!options.force,
|
|
113
134
|
mode: "build",
|
|
114
|
-
offline: (0, common_1.flagOrEnv)(options.offline, "DMS_OFFLINE"),
|
|
135
|
+
offline: snapshot ? false : (0, common_1.flagOrEnv)(options.offline, "DMS_OFFLINE"),
|
|
115
136
|
bootstrapSecret,
|
|
137
|
+
workspaceDir: options.workspace,
|
|
138
|
+
snapshot,
|
|
116
139
|
});
|
|
117
|
-
if (
|
|
140
|
+
if (snapshot) {
|
|
141
|
+
ui.message("info", `Building from the snapshot of ${snapshot.snapshot.backendUrl}${(0, output_1.cachedAge)(snapshot.snapshot.takenAt)}`, { detail: `Modules digest ${snapshot.snapshot.digest}` });
|
|
142
|
+
}
|
|
143
|
+
else if (manifestFromCache) {
|
|
118
144
|
ui.message("warn", `Building from the manifest and layers archive${(0, output_1.cachedAge)(manifestFetchedAt)}`, { detail: "The output may not match the current backend." });
|
|
119
145
|
}
|
|
120
146
|
ui.message("info", "Building for production", {
|
|
@@ -129,7 +155,6 @@ async function buildProduction(options, startedAt) {
|
|
|
129
155
|
nameFile: (0, child_output_1.createFileNamer)(layers),
|
|
130
156
|
env: {
|
|
131
157
|
...process.env,
|
|
132
|
-
DMS_SESSION_SECRET: sessionSecret,
|
|
133
158
|
NODE_OPTIONS: "--max-old-space-size=4096",
|
|
134
159
|
NODE_PATH: (0, node_path_1.join)(workspaceDir, "node_modules"),
|
|
135
160
|
[vite_output_1.VITE_LOG_LEVEL_VARIABLE]: (0, vite_output_1.viteLogLevel)(isVerbose),
|
|
@@ -137,6 +162,7 @@ async function buildProduction(options, startedAt) {
|
|
|
137
162
|
};
|
|
138
163
|
for (const step of exports.BUILD_STEPS)
|
|
139
164
|
await runBuildStep(step, context);
|
|
165
|
+
(0, snapshot_1.writeBuildOrigin)(workspaceDir, buildOrigin);
|
|
140
166
|
(0, vite_output_1.reportViteWarnings)(context.warnings, isVerbose, ui);
|
|
141
167
|
(0, output_1.writeBlankLine)();
|
|
142
168
|
ui.summary({
|
|
@@ -145,8 +171,10 @@ async function buildProduction(options, startedAt) {
|
|
|
145
171
|
durationMs: Date.now() - startedAt,
|
|
146
172
|
nextSteps: [
|
|
147
173
|
{
|
|
148
|
-
command:
|
|
149
|
-
|
|
174
|
+
command: options.workspace
|
|
175
|
+
? `ajs dms start -b ${backendUrl} --workspace ${options.workspace}`
|
|
176
|
+
: `ajs dms start -b ${backendUrl}`,
|
|
177
|
+
description: "with DMS_SESSION_SECRET and the server's secrets in its environment",
|
|
150
178
|
},
|
|
151
179
|
],
|
|
152
180
|
});
|
package/dist/commands/build.js
CHANGED
|
@@ -39,9 +39,13 @@ const config_1 = require("../config");
|
|
|
39
39
|
const cli_1 = require("@antelopejs/core/cli");
|
|
40
40
|
const BUILD_EXAMPLES = [
|
|
41
41
|
{
|
|
42
|
-
description: "With
|
|
42
|
+
description: "With DMS_BOOTSTRAP_SECRET set in the environment or ./.env",
|
|
43
43
|
command: "ajs dms build -b https://dms.example.com",
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
description: "From a snapshot, without the backend or any secret, into the project (a container image)",
|
|
47
|
+
command: "ajs dms build --from dms-snapshot --workspace .dms-frontend",
|
|
48
|
+
},
|
|
45
49
|
{
|
|
46
50
|
description: "Rebuild from the cache while the backend is down",
|
|
47
51
|
command: "ajs dms build -b https://dms.example.com --offline",
|
|
@@ -50,8 +54,10 @@ const BUILD_EXAMPLES = [
|
|
|
50
54
|
function cmdBuild() {
|
|
51
55
|
const command = new commander_1.Command("build")
|
|
52
56
|
.summary("Build the production frontend")
|
|
53
|
-
.description("Build the production frontend from the manifest and layers archive the backend serves, into the workspace's dist directory.
|
|
54
|
-
.addOption(config_1.Options.backendUrl("Backend URL to build for; required, as -b or the variable"))
|
|
57
|
+
.description("Build the production frontend from the manifest and layers archive the backend serves, or from a snapshot of them, into the workspace's dist directory. The build holds no secret: the server reads its own from its environment when it starts.")
|
|
58
|
+
.addOption(config_1.Options.backendUrl("Backend URL to build for; required, as -b or the variable, unless --from names a snapshot"))
|
|
59
|
+
.addOption(new commander_1.Option("--from <dir>", "Build from the snapshot in this directory (ajs dms snapshot), without the backend"))
|
|
60
|
+
.addOption(config_1.Options.workspace("Build into this directory instead of the workspace under ~/.antelopejs, to ship it with the project"))
|
|
55
61
|
.addOption(config_1.Options.force("Extract the layers archive from scratch and reinstall the workspace dependencies"))
|
|
56
62
|
.addOption(config_1.Options.offline("Build from the last cached manifest and layers archive, without the backend"))
|
|
57
63
|
.addOption(config_1.Options.bootstrapSecret())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSnapshot = runSnapshot;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const cli_1 = require("@antelopejs/core/cli");
|
|
8
|
+
const common_1 = require("../common");
|
|
9
|
+
const output_1 = require("../output");
|
|
10
|
+
const snapshot_1 = require("../snapshot");
|
|
11
|
+
async function runSnapshot(options) {
|
|
12
|
+
const startedAt = Date.now();
|
|
13
|
+
const backendUrl = (0, common_1.requireBackendUrl)(options.backendUrl);
|
|
14
|
+
const bootstrapSecret = (0, common_1.normalizeBootstrapSecret)(options.bootstrapSecret);
|
|
15
|
+
if (!bootstrapSecret) {
|
|
16
|
+
throw new common_1.UsageError({
|
|
17
|
+
title: "A snapshot needs the bootstrap credential",
|
|
18
|
+
reason: "Without it the backend withholds the modules' backend proxies, and a build from the snapshot would not route them.",
|
|
19
|
+
fixes: [
|
|
20
|
+
"Set DMS_BOOTSTRAP_SECRET to the backend's frontend.bootstrapSecret",
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
(0, output_1.writeHeader)("snapshot", [backendUrl]);
|
|
25
|
+
const task = (0, cli_1.getProcessTasks)().start("Fetching the manifest and layers");
|
|
26
|
+
const scratch = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "dms-snapshot-"));
|
|
27
|
+
try {
|
|
28
|
+
const manifest = await (0, common_1.fetchManifest)(backendUrl, undefined, bootstrapSecret);
|
|
29
|
+
const layers = (0, node_path_1.join)(scratch, snapshot_1.SNAPSHOT_LAYERS_DIR);
|
|
30
|
+
await (0, common_1.downloadAndExtractLayers)(backendUrl, manifest.pack, layers, true, bootstrapSecret);
|
|
31
|
+
const snapshot = (0, snapshot_1.writeSnapshot)(options.output, backendUrl, manifest, layers);
|
|
32
|
+
task.succeed(`Fetched ${manifest.modules.length} frontend module${manifest.modules.length === 1 ? "" : "s"}`);
|
|
33
|
+
(0, output_1.writeBlankLine)();
|
|
34
|
+
(0, cli_1.getProcessUi)().summary({
|
|
35
|
+
headline: `Took a snapshot of modules ${snapshot.digest}, without private values`,
|
|
36
|
+
artifact: options.output,
|
|
37
|
+
durationMs: Date.now() - startedAt,
|
|
38
|
+
nextSteps: [
|
|
39
|
+
{
|
|
40
|
+
command: `ajs dms build --from ${options.output} --workspace .dms-frontend`,
|
|
41
|
+
description: "without the backend or any secret",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
(0, output_1.endInterruptedTask)(task, error, "Snapshot stopped");
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
(0, node_fs_1.rmSync)(scratch, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.cmdSnapshot = cmdSnapshot;
|
|
37
|
+
const commander_1 = require("commander");
|
|
38
|
+
const config_1 = require("../config");
|
|
39
|
+
const cli_1 = require("@antelopejs/core/cli");
|
|
40
|
+
const SNAPSHOT_EXAMPLES = [
|
|
41
|
+
{
|
|
42
|
+
description: "With DMS_BOOTSTRAP_SECRET set, after a backend release",
|
|
43
|
+
command: "ajs dms snapshot -b https://dms.example.com -o dms-snapshot",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
function cmdSnapshot() {
|
|
47
|
+
const command = new commander_1.Command("snapshot")
|
|
48
|
+
.summary("Keep what a build needs from the backend, without its secrets")
|
|
49
|
+
.description("Fetch the manifest and the layers archive the backend serves, and write them into a directory with every private value emptied, so that ajs dms build --from builds without the backend or any credential. Take it again when the backend's frontend modules change: start says when they no longer match.")
|
|
50
|
+
.addOption(config_1.Options.backendUrl("Backend URL to take the snapshot from; required, as -b or the variable"))
|
|
51
|
+
.addOption(new commander_1.Option("-o, --output <dir>", "Directory to write the snapshot to").default("dms-snapshot"))
|
|
52
|
+
.addOption(config_1.Options.bootstrapSecret())
|
|
53
|
+
.action(async (options) => (await Promise.resolve().then(() => __importStar(require("./snapshot-action")))).runSnapshot(options));
|
|
54
|
+
return (0, cli_1.withExamples)(command, SNAPSHOT_EXAMPLES);
|
|
55
|
+
}
|
|
@@ -10,7 +10,34 @@ const ports_1 = require("../ports");
|
|
|
10
10
|
const server_process_1 = require("../server-process");
|
|
11
11
|
const update_check_1 = require("../update-check");
|
|
12
12
|
const module_set_1 = require("../module-set");
|
|
13
|
+
const snapshot_1 = require("../snapshot");
|
|
14
|
+
const config_1 = require("../config");
|
|
15
|
+
const manifest_secrets_1 = require("../manifest-secrets");
|
|
16
|
+
const private_values_1 = require("../private-values");
|
|
13
17
|
const BUILD_TIME_MANIFEST = "the build-time manifest";
|
|
18
|
+
function builtPrivateOptions(workspaceDir) {
|
|
19
|
+
try {
|
|
20
|
+
const { modules } = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(workspaceDir, config_1.SERVER_OPTIONS_FILE), "utf8"));
|
|
21
|
+
return modules ?? {};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function reportUnfilledPrivateValues(workspaceDir) {
|
|
28
|
+
const unfilled = (0, private_values_1.unfilledPrivateValues)(builtPrivateOptions(workspaceDir), (0, private_values_1.parsePrivateOptions)(process.env[private_values_1.PRIVATE_OPTIONS_VARIABLE]), Object.values(manifest_secrets_1.MANIFEST_SECRET_ENV));
|
|
29
|
+
const ui = (0, cli_1.getProcessUi)();
|
|
30
|
+
unfilled.forEach(({ module, path }, index) => {
|
|
31
|
+
const isLast = index === unfilled.length - 1;
|
|
32
|
+
ui.message("warn", `${module}: private option ${path.join(".")} is not set`, {
|
|
33
|
+
details: isLast
|
|
34
|
+
? [
|
|
35
|
+
`${ui.symbols.levels.hint} Set ${unfilled.length === 1 ? "it" : "them"} in ${private_values_1.PRIVATE_OPTIONS_VARIABLE}, a JSON object of options by module id, with the values the backend uses`,
|
|
36
|
+
]
|
|
37
|
+
: [],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
14
41
|
const PRODUCTION = "production";
|
|
15
42
|
function serverNodeEnv() {
|
|
16
43
|
const nodeEnv = process.env.NODE_ENV || PRODUCTION;
|
|
@@ -24,22 +51,68 @@ function serverNodeEnv() {
|
|
|
24
51
|
}
|
|
25
52
|
return nodeEnv;
|
|
26
53
|
}
|
|
54
|
+
async function reportDrift(backendUrl, origin) {
|
|
55
|
+
const ui = (0, cli_1.getProcessUi)();
|
|
56
|
+
const bootstrapSecret = process.env.DMS_BOOTSTRAP_SECRET?.trim();
|
|
57
|
+
if (!origin)
|
|
58
|
+
return;
|
|
59
|
+
if (origin.authenticated === false) {
|
|
60
|
+
ui.message("info", `Built from modules ${origin.digest}; not compared with the backend`, {
|
|
61
|
+
detail: "The build read the manifest without DMS_BOOTSTRAP_SECRET, which leaves out the backend proxies.",
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!bootstrapSecret) {
|
|
66
|
+
ui.message("info", `Built from modules ${origin.digest}; not compared with the backend`, { detail: "Set DMS_BOOTSTRAP_SECRET to have start compare them." });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const drift = await (0, module_set_1.compareWithBackend)(backendUrl, origin.digest, snapshot_1.manifestDigest, bootstrapSecret);
|
|
70
|
+
if (drift.status === "matches") {
|
|
71
|
+
ui.message("info", `The backend serves the modules ${origin.digest} this build was made from`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (drift.status === "unknown") {
|
|
75
|
+
ui.message("info", `Built from modules ${origin.digest}; not compared with the backend: ${drift.reason}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const refresh = origin.source === "snapshot"
|
|
79
|
+
? `Refresh the snapshot and rebuild: ajs dms snapshot -b ${backendUrl} -o <dir>, then ajs dms build --from <dir>`
|
|
80
|
+
: `Rebuild: ajs dms build -b ${backendUrl}`;
|
|
81
|
+
ui.message("warn", `The backend serves modules ${drift.served}; this build was made from ${origin.digest} (${origin.source}${(0, output_1.cachedAge)(origin.takenAt)})`, {
|
|
82
|
+
details: [
|
|
83
|
+
"Pages of modules added or removed since, or whose routes changed, are missing or stale.",
|
|
84
|
+
`${ui.symbols.levels.hint} ${refresh}`,
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
27
88
|
async function runStart(options) {
|
|
28
89
|
const startedAt = Date.now();
|
|
29
|
-
const
|
|
90
|
+
const explicitWorkspace = options.workspace
|
|
91
|
+
? (0, node_path_1.resolve)(options.workspace)
|
|
92
|
+
: undefined;
|
|
93
|
+
const origin = explicitWorkspace
|
|
94
|
+
? (0, snapshot_1.readBuildOrigin)(explicitWorkspace)
|
|
95
|
+
: undefined;
|
|
96
|
+
const backendUrl = (0, common_1.requireBackendUrl)(options.backendUrl ?? origin?.backendUrl);
|
|
30
97
|
const requestedPort = (0, common_1.parsePort)(options.port);
|
|
31
98
|
const sessionSecret = (0, common_1.resolveSessionSecret)("start");
|
|
32
|
-
const workspaceDir = (0, common_1.getWorkspaceDir)(backendUrl);
|
|
99
|
+
const workspaceDir = explicitWorkspace ?? (0, common_1.getWorkspaceDir)(backendUrl);
|
|
33
100
|
const serverPath = (0, node_path_1.join)(workspaceDir, "server.mjs");
|
|
34
101
|
const clientPath = (0, node_path_1.join)(workspaceDir, "dist", "client", "index.html");
|
|
35
102
|
if (!(0, node_fs_1.existsSync)(serverPath) || !(0, node_fs_1.existsSync)(clientPath)) {
|
|
36
103
|
throw new cli_1.CliError({
|
|
37
104
|
title: `No production build for ${backendUrl}`,
|
|
38
105
|
reason: `${(0, output_1.showWorkspace)(workspaceDir)} has no built server or client.`,
|
|
39
|
-
fixes: [
|
|
106
|
+
fixes: [
|
|
107
|
+
explicitWorkspace
|
|
108
|
+
? `Build it first: ajs dms build --from <snapshot> --workspace ${options.workspace}`
|
|
109
|
+
: `Build it first: ajs dms build -b ${backendUrl}`,
|
|
110
|
+
],
|
|
40
111
|
});
|
|
41
112
|
}
|
|
42
|
-
(
|
|
113
|
+
if (!explicitWorkspace) {
|
|
114
|
+
(0, common_1.writeWorkspaceMeta)(workspaceDir, backendUrl, (0, common_1.canonicalizeBackendUrl)(backendUrl));
|
|
115
|
+
}
|
|
43
116
|
const builtAt = (0, node_fs_1.statSync)(clientPath).mtime.toISOString();
|
|
44
117
|
(0, output_1.writeHeader)("start", [
|
|
45
118
|
backendUrl,
|
|
@@ -57,7 +130,8 @@ async function runStart(options) {
|
|
|
57
130
|
}
|
|
58
131
|
const port = reserved.port;
|
|
59
132
|
const secrets = (0, common_1.resolveManifestSecrets)((0, common_1.collectManifestSecrets)((0, common_1.readCachedManifest)(workspaceDir)?.manifest.modules ?? []));
|
|
60
|
-
(0, common_1.reportManifestSecrets)(secrets, (count) => `Set ${count === 1 ? "it" : "them"}
|
|
133
|
+
(0, common_1.reportManifestSecrets)(secrets, (count) => `Set ${count === 1 ? "it" : "them"} in the server's environment, with the value${count === 1 ? "" : "s"} the backend uses`);
|
|
134
|
+
reportUnfilledPrivateValues(workspaceDir);
|
|
61
135
|
const secretSources = (0, common_1.describeSecretSources)(secrets.sources, BUILD_TIME_MANIFEST);
|
|
62
136
|
const nodeEnv = serverNodeEnv();
|
|
63
137
|
await reserved.release();
|
|
@@ -88,6 +162,7 @@ async function runStart(options) {
|
|
|
88
162
|
});
|
|
89
163
|
void (0, module_set_1.reportFrontendReady)(backendUrl, ((0, common_1.readCachedManifest)(workspaceDir)?.manifest.modules ?? []).map((module) => module.name), process.env.DMS_BOOTSTRAP_SECRET);
|
|
90
164
|
void (0, update_check_1.reportAvailableUpdate)({ isFollowed: true });
|
|
165
|
+
void reportDrift(backendUrl, origin ?? (0, snapshot_1.readBuildOrigin)(workspaceDir));
|
|
91
166
|
},
|
|
92
167
|
});
|
|
93
168
|
process.exitCode = code;
|
package/dist/commands/start.js
CHANGED
|
@@ -42,13 +42,18 @@ const START_EXAMPLES = [
|
|
|
42
42
|
description: "After ajs dms build -b https://dms.example.com",
|
|
43
43
|
command: "ajs dms start -b https://dms.example.com -p 3001",
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
description: "From a build shipped with the project, in a read-only container",
|
|
47
|
+
command: "ajs dms start --workspace .dms-frontend -p 3000",
|
|
48
|
+
},
|
|
45
49
|
];
|
|
46
50
|
function cmdStart() {
|
|
47
51
|
const command = new commander_1.Command("start")
|
|
48
52
|
.summary("Serve the production build")
|
|
49
|
-
.description("Serve the production frontend ajs dms build made for the same backend URL
|
|
50
|
-
.addOption(config_1.Options.backendUrl("Backend URL
|
|
53
|
+
.description("Serve the production frontend ajs dms build made for the same backend URL, or the one in --workspace. Reads DMS_SESSION_SECRET and the server's secrets (DMS_HTML_RENDER_SECRET, DMS_OAUTH_RELAY_SECRET) from its environment, and says once ready whether the backend still serves the modules the build was made from.")
|
|
54
|
+
.addOption(config_1.Options.backendUrl("Backend URL to serve; required, as -b or the variable, unless --workspace holds a build that records it"))
|
|
51
55
|
.addOption(config_1.Options.port)
|
|
56
|
+
.addOption(config_1.Options.workspace("Serve the build in this directory (ajs dms build --workspace); start writes nothing into it"))
|
|
52
57
|
.action(async (options) => (await Promise.resolve().then(() => __importStar(require("./start-action")))).runStart(options));
|
|
53
58
|
return (0, cli_1.withExamples)(command, START_EXAMPLES);
|
|
54
59
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare const DMS_FRONTEND_HOME: string;
|
|
|
6
6
|
export declare const DEPS_HASH_FILE = ".deps-hash";
|
|
7
7
|
export declare const WORKSPACE_DIR_MODE = 448;
|
|
8
8
|
export declare function writeSecretBearingFile(file: string, content: string): void;
|
|
9
|
+
export declare function writeShareableFile(file: string, content: string): void;
|
|
9
10
|
export declare const TEMPLATE_FILES: ReadonlyArray<readonly [string, string]>;
|
|
10
11
|
export declare const LAYERS_SUBDIR = "frontend-modules";
|
|
11
12
|
export declare const FRONTEND_MODULE_ENTRY = "dms.frontend.ts";
|
|
@@ -31,13 +32,14 @@ export declare const Options: {
|
|
|
31
32
|
force(description: string): Option;
|
|
32
33
|
offline(description: string): Option;
|
|
33
34
|
strict: Option;
|
|
35
|
+
workspace(description: string): Option;
|
|
34
36
|
bootstrapSecret(discovery?: string): Option;
|
|
35
37
|
};
|
|
36
38
|
export interface BootstrapDiscoveryOptions extends DiscoveryOptions {
|
|
37
39
|
cwd?: string;
|
|
38
40
|
}
|
|
39
41
|
export declare function normalizeBootstrapSecret(value: string | undefined, source?: string): string | undefined;
|
|
40
|
-
export type SessionSecretMode = "dev" | "
|
|
42
|
+
export type SessionSecretMode = "dev" | "start";
|
|
41
43
|
export declare function resolveSessionSecret(mode: SessionSecretMode, value?: string | undefined): string;
|
|
42
44
|
export declare function resolveBootstrapSecret(explicit: string | undefined, backendUrl: string, options?: BootstrapDiscoveryOptions): string | undefined;
|
|
43
45
|
export declare function sha256Hex(input: string): string;
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Options = exports.UsageError = exports.TAILWIND_SOURCE_GLOB = exports.PNPM_LIFECYCLE_SCRIPTS = exports.layerCopyIgnore = exports.LAYER_COPY_BLOCKLIST = exports.PUBLIC_ENDPOINTS_FILE = exports.AUTH_ESTABLISH_FILE = exports.SERVER_OPTIONS_FILE = exports.SERVER_MODULE_ENTRY = exports.FRONTEND_BUILD_ENTRY = exports.FRONTEND_MODULE_ENTRY = exports.LAYERS_SUBDIR = exports.TEMPLATE_FILES = exports.WORKSPACE_DIR_MODE = exports.DEPS_HASH_FILE = exports.DMS_FRONTEND_HOME = void 0;
|
|
7
7
|
exports.writeSecretBearingFile = writeSecretBearingFile;
|
|
8
|
+
exports.writeShareableFile = writeShareableFile;
|
|
8
9
|
exports.parseBackendUrl = parseBackendUrl;
|
|
9
10
|
exports.requireBackendUrl = requireBackendUrl;
|
|
10
11
|
exports.parsePort = parsePort;
|
|
@@ -30,9 +31,16 @@ const SECRET_BEARING_FILE_MODE = 0o600;
|
|
|
30
31
|
const TEMP_FILE_SUFFIX = ".tmp";
|
|
31
32
|
exports.WORKSPACE_DIR_MODE = 0o700;
|
|
32
33
|
function writeSecretBearingFile(file, content) {
|
|
34
|
+
writeFileAtomically(file, content, SECRET_BEARING_FILE_MODE);
|
|
35
|
+
}
|
|
36
|
+
const SHAREABLE_FILE_MODE = 0o644;
|
|
37
|
+
function writeShareableFile(file, content) {
|
|
38
|
+
writeFileAtomically(file, content, SHAREABLE_FILE_MODE);
|
|
39
|
+
}
|
|
40
|
+
function writeFileAtomically(file, content, mode) {
|
|
33
41
|
const temp = `${file}.${process.pid}${TEMP_FILE_SUFFIX}`;
|
|
34
|
-
(0, node_fs_1.writeFileSync)(temp, content, { mode
|
|
35
|
-
(0, node_fs_1.chmodSync)(temp,
|
|
42
|
+
(0, node_fs_1.writeFileSync)(temp, content, { mode });
|
|
43
|
+
(0, node_fs_1.chmodSync)(temp, mode);
|
|
36
44
|
(0, node_fs_1.renameSync)(temp, file);
|
|
37
45
|
}
|
|
38
46
|
exports.TEMPLATE_FILES = [
|
|
@@ -174,6 +182,9 @@ exports.Options = {
|
|
|
174
182
|
return new commander_1.Option("--offline", `${description} (env: DMS_OFFLINE)`);
|
|
175
183
|
},
|
|
176
184
|
strict: new commander_1.Option("--strict", "Exit 1 when the workspace cannot be prepared, and 2 when the backend URL is missing or invalid, instead of warning and exiting 0 (env: DMS_PREPARE_STRICT)"),
|
|
185
|
+
workspace(description) {
|
|
186
|
+
return new commander_1.Option("--workspace <dir>", description).env("DMS_WORKSPACE_DIR");
|
|
187
|
+
},
|
|
177
188
|
bootstrapSecret(discovery) {
|
|
178
189
|
return new commander_1.Option("--bootstrap-secret <secret>", discovery
|
|
179
190
|
? `${BOOTSTRAP_SECRET_DESCRIPTION}. ${discovery}`
|
|
@@ -226,7 +237,7 @@ function resolveSessionSecret(mode, value = process.env.DMS_SESSION_SECRET) {
|
|
|
226
237
|
title: `DMS_SESSION_SECRET ${describeSessionSecret(value)}`,
|
|
227
238
|
reason: mode === "dev"
|
|
228
239
|
? "dev generates an ephemeral secret only when the variable is not set at all."
|
|
229
|
-
: "
|
|
240
|
+
: "start signs sessions with it, so it must stay the same across restarts.",
|
|
230
241
|
fixes: [
|
|
231
242
|
"Create one: openssl rand -hex 32, then set it in the environment or ./.env",
|
|
232
243
|
],
|
package/dist/help.js
CHANGED
|
@@ -11,9 +11,10 @@ exports.ENVIRONMENT_VARIABLES = [
|
|
|
11
11
|
["DMS_BOOTSTRAP_SECRET", "Same as --bootstrap-secret"],
|
|
12
12
|
[
|
|
13
13
|
"DMS_SESSION_SECRET",
|
|
14
|
-
"Session key, 32 characters or more;
|
|
14
|
+
"Session key, 32 characters or more; start needs one, dev makes one when unset",
|
|
15
15
|
],
|
|
16
16
|
["DMS_OFFLINE", "Same as --offline, unless 0, false, no or off"],
|
|
17
|
+
["DMS_WORKSPACE_DIR", "Same as build and start --workspace"],
|
|
17
18
|
["DMS_PREPARE_STRICT", "Same as prepare --strict, read like DMS_OFFLINE"],
|
|
18
19
|
["PORT", "Same as -p"],
|
|
19
20
|
[
|
|
@@ -38,11 +39,15 @@ exports.ENVIRONMENT_VARIABLES = [
|
|
|
38
39
|
],
|
|
39
40
|
[
|
|
40
41
|
"DMS_HTML_RENDER_SECRET",
|
|
41
|
-
"E-mail render secret,
|
|
42
|
+
"E-mail render secret, the backend's htmlRender.serviceSecret; start needs it, a build keeps none",
|
|
42
43
|
],
|
|
43
44
|
[
|
|
44
45
|
"DMS_OAUTH_RELAY_SECRET",
|
|
45
|
-
"OAuth relay secret
|
|
46
|
+
"OAuth relay secret the backend expects; start needs it, a build keeps none",
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"DMS_PRIVATE_OPTIONS",
|
|
50
|
+
"Any module's private options for start, as JSON by module id; a build keeps none",
|
|
46
51
|
],
|
|
47
52
|
[
|
|
48
53
|
"DMS_AUTH_ESTABLISH_ENDPOINTS",
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const build_1 = require("./commands/build");
|
|
|
8
8
|
const clean_1 = require("./commands/clean");
|
|
9
9
|
const dev_1 = require("./commands/dev");
|
|
10
10
|
const prepare_1 = require("./commands/prepare");
|
|
11
|
+
const snapshot_1 = require("./commands/snapshot");
|
|
11
12
|
const start_1 = require("./commands/start");
|
|
12
13
|
const verify_source_1 = require("./commands/verify-source");
|
|
13
14
|
const workspaces_1 = require("./commands/workspaces");
|
|
@@ -116,6 +117,7 @@ const runCLI = async () => {
|
|
|
116
117
|
program.addCommand((0, dev_1.cmdDev)());
|
|
117
118
|
program.addCommand((0, build_1.cmdBuild)());
|
|
118
119
|
program.addCommand((0, start_1.cmdStart)());
|
|
120
|
+
program.addCommand((0, snapshot_1.cmdSnapshot)());
|
|
119
121
|
program.addCommand((0, prepare_1.cmdPrepare)());
|
|
120
122
|
program.addCommand((0, workspaces_1.cmdWorkspaces)());
|
|
121
123
|
program.addCommand((0, clean_1.cmdClean)());
|
package/dist/manifest.d.ts
CHANGED
|
@@ -29,13 +29,19 @@ export interface ManifestCacheEntry {
|
|
|
29
29
|
fetchedAt: string;
|
|
30
30
|
}
|
|
31
31
|
export declare function readCachedManifest(workspaceDir: string): ManifestCacheEntry | undefined;
|
|
32
|
+
export declare function writeCachedManifest(workspaceDir: string, manifest: Manifest, privateValues: PrivateValues, fetchedAt?: string): void;
|
|
33
|
+
export type PrivateValues = "keep" | "drop";
|
|
32
34
|
export type ManifestCacheUse = "fallback" | "offline" | "never";
|
|
33
35
|
export interface ResolvedManifest {
|
|
34
36
|
manifest: Manifest;
|
|
35
37
|
fromCache: boolean;
|
|
36
38
|
fetchedAt?: string;
|
|
37
39
|
}
|
|
38
|
-
export declare function resolveManifest(workspaceDir: string, backendUrl: string, cacheUse: ManifestCacheUse, clientUrl
|
|
40
|
+
export declare function resolveManifest(workspaceDir: string, backendUrl: string, cacheUse: ManifestCacheUse, { clientUrl, bootstrapSecret, privateValues, }?: {
|
|
41
|
+
clientUrl?: string;
|
|
42
|
+
bootstrapSecret?: string;
|
|
43
|
+
privateValues?: PrivateValues;
|
|
44
|
+
}): Promise<ResolvedManifest>;
|
|
39
45
|
export declare function assertCachedLayerPathsExist(modules: ServedWithPath[], fetchedAt?: string): void;
|
|
40
46
|
export type ServedWithPath = ManifestModule & {
|
|
41
47
|
path: string;
|
package/dist/manifest.js
CHANGED
|
@@ -7,12 +7,14 @@ exports.fetchFromBackend = fetchFromBackend;
|
|
|
7
7
|
exports.describeManifestVersion = describeManifestVersion;
|
|
8
8
|
exports.fetchManifest = fetchManifest;
|
|
9
9
|
exports.readCachedManifest = readCachedManifest;
|
|
10
|
+
exports.writeCachedManifest = writeCachedManifest;
|
|
10
11
|
exports.resolveManifest = resolveManifest;
|
|
11
12
|
exports.assertCachedLayerPathsExist = assertCachedLayerPathsExist;
|
|
12
13
|
const node_fs_1 = require("node:fs");
|
|
13
14
|
const node_path_1 = require("node:path");
|
|
14
15
|
const cli_1 = require("@antelopejs/core/cli");
|
|
15
16
|
const config_1 = require("./config");
|
|
17
|
+
const private_values_1 = require("./private-values");
|
|
16
18
|
const output_1 = require("./output");
|
|
17
19
|
const workspace_1 = require("./workspace");
|
|
18
20
|
class ManifestUnauthorizedError extends cli_1.CliError {
|
|
@@ -215,18 +217,19 @@ function readCachedManifest(workspaceDir) {
|
|
|
215
217
|
return undefined;
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
|
-
function writeCachedManifest(workspaceDir, manifest) {
|
|
219
|
-
const entry = {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
};
|
|
223
|
-
(0, config_1.writeSecretBearingFile)((0, node_path_1.join)(workspaceDir, MANIFEST_CACHE_FILE), `${JSON.stringify(entry, null, 2)}\n`);
|
|
220
|
+
function writeCachedManifest(workspaceDir, manifest, privateValues, fetchedAt = new Date().toISOString()) {
|
|
221
|
+
const entry = { manifest, fetchedAt };
|
|
222
|
+
const write = privateValues === "keep" ? config_1.writeSecretBearingFile : config_1.writeShareableFile;
|
|
223
|
+
write((0, node_path_1.join)(workspaceDir, MANIFEST_CACHE_FILE), `${JSON.stringify(entry, null, 2)}\n`);
|
|
224
224
|
}
|
|
225
|
-
async function resolveManifest(workspaceDir, backendUrl, cacheUse, clientUrl, bootstrapSecret) {
|
|
225
|
+
async function resolveManifest(workspaceDir, backendUrl, cacheUse, { clientUrl, bootstrapSecret, privateValues = "keep", } = {}) {
|
|
226
226
|
if (cacheUse !== "offline") {
|
|
227
227
|
try {
|
|
228
|
-
const
|
|
229
|
-
|
|
228
|
+
const served = await fetchManifest(backendUrl, clientUrl, bootstrapSecret);
|
|
229
|
+
const manifest = privateValues === "keep"
|
|
230
|
+
? served
|
|
231
|
+
: { ...served, modules: (0, private_values_1.withoutPrivateValues)(served.modules) };
|
|
232
|
+
writeCachedManifest(workspaceDir, manifest, privateValues);
|
|
230
233
|
return { manifest, fromCache: false };
|
|
231
234
|
}
|
|
232
235
|
catch (err) {
|
|
@@ -239,13 +242,13 @@ async function resolveManifest(workspaceDir, backendUrl, cacheUse, clientUrl, bo
|
|
|
239
242
|
throw err;
|
|
240
243
|
if (cacheUse === "never")
|
|
241
244
|
throw cacheNotUsed(err, backendUrl, cached);
|
|
242
|
-
return fromCachedEntry(cached);
|
|
245
|
+
return fromCachedEntry(workspaceDir, cached, privateValues);
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
const cached = readCachedManifest(workspaceDir);
|
|
246
249
|
if (!cached)
|
|
247
250
|
throw new NoCachedManifestError();
|
|
248
|
-
return fromCachedEntry(cached);
|
|
251
|
+
return fromCachedEntry(workspaceDir, cached, privateValues);
|
|
249
252
|
}
|
|
250
253
|
function cacheNotUsed(failure, backendUrl, cached) {
|
|
251
254
|
const problem = failure instanceof cli_1.CliError
|
|
@@ -261,12 +264,13 @@ function cacheNotUsed(failure, backendUrl, cached) {
|
|
|
261
264
|
],
|
|
262
265
|
}, { cause: failure });
|
|
263
266
|
}
|
|
264
|
-
function fromCachedEntry(cached) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
267
|
+
function fromCachedEntry(workspaceDir, cached, privateValues) {
|
|
268
|
+
let { manifest } = cached;
|
|
269
|
+
if (privateValues === "drop") {
|
|
270
|
+
manifest = { ...manifest, modules: (0, private_values_1.withoutPrivateValues)(manifest.modules) };
|
|
271
|
+
writeCachedManifest(workspaceDir, manifest, "drop", cached.fetchedAt);
|
|
272
|
+
}
|
|
273
|
+
return { manifest, fromCache: true, fetchedAt: cached.fetchedAt };
|
|
270
274
|
}
|
|
271
275
|
function assertCachedLayerPathsExist(modules, fetchedAt) {
|
|
272
276
|
const missing = modules.filter((mod) => !(0, node_fs_1.existsSync)(mod.path));
|
package/dist/materialize.js
CHANGED
|
@@ -114,11 +114,21 @@ function writePublicEndpoints(workspaceDir, layers) {
|
|
|
114
114
|
};
|
|
115
115
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspaceDir, config_1.PUBLIC_ENDPOINTS_FILE), `${JSON.stringify(declared, null, 2)}\n`);
|
|
116
116
|
}
|
|
117
|
+
function holdsValue(value) {
|
|
118
|
+
if (typeof value === "string")
|
|
119
|
+
return value !== "";
|
|
120
|
+
if (value !== null && typeof value === "object")
|
|
121
|
+
return Object.values(value).some(holdsValue);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
117
124
|
function writeServerOptions(workspaceDir, layers) {
|
|
118
125
|
const modules = Object.fromEntries(layers.flatMap((layer) => layer.privateOptions
|
|
119
126
|
? [[(0, layers_1.getLayerSafeName)(layer), layer.privateOptions]]
|
|
120
127
|
: []));
|
|
121
|
-
|
|
128
|
+
const write = Object.values(modules).some(holdsValue)
|
|
129
|
+
? config_1.writeSecretBearingFile
|
|
130
|
+
: config_1.writeShareableFile;
|
|
131
|
+
write((0, node_path_1.join)(workspaceDir, config_1.SERVER_OPTIONS_FILE), `${JSON.stringify({ modules, environment: manifest_secrets_1.MANIFEST_SECRET_ENV }, null, 2)}\n`);
|
|
122
132
|
}
|
|
123
133
|
function writeFrontendModuleRegistry(workspaceDir, layers) {
|
|
124
134
|
const registry = (0, derived_outputs_1.createFrontendModuleRegistry)(workspaceDir, layers);
|
package/dist/module-set.d.ts
CHANGED
|
@@ -14,3 +14,14 @@ export interface ModuleSetFollowOptions {
|
|
|
14
14
|
export declare function moduleSetFingerprint(modules: readonly ModuleSetMember[]): string;
|
|
15
15
|
export declare function followModuleSet(options: ModuleSetFollowOptions): () => void;
|
|
16
16
|
export declare function reportFrontendReady(backendUrl: string, moduleNames: readonly string[], bootstrapSecret?: string): Promise<boolean>;
|
|
17
|
+
export type BuildDrift = {
|
|
18
|
+
status: "matches";
|
|
19
|
+
served: string;
|
|
20
|
+
} | {
|
|
21
|
+
status: "drifted";
|
|
22
|
+
served: string;
|
|
23
|
+
} | {
|
|
24
|
+
status: "unknown";
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function compareWithBackend(backendUrl: string, builtDigest: string, digestOf: (modules: readonly ManifestModule[]) => string, bootstrapSecret?: string): Promise<BuildDrift>;
|
package/dist/module-set.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.MODULE_SET_POLL_INTERVAL_MS = void 0;
|
|
|
4
4
|
exports.moduleSetFingerprint = moduleSetFingerprint;
|
|
5
5
|
exports.followModuleSet = followModuleSet;
|
|
6
6
|
exports.reportFrontendReady = reportFrontendReady;
|
|
7
|
+
exports.compareWithBackend = compareWithBackend;
|
|
7
8
|
const manifest_1 = require("./manifest");
|
|
8
9
|
const workspace_1 = require("./workspace");
|
|
9
10
|
exports.MODULE_SET_POLL_INTERVAL_MS = 3000;
|
|
@@ -82,3 +83,20 @@ async function reportFrontendReady(backendUrl, moduleNames, bootstrapSecret) {
|
|
|
82
83
|
return false;
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
const DRIFT_CHECK_TIMEOUT_MS = 10_000;
|
|
87
|
+
async function compareWithBackend(backendUrl, builtDigest, digestOf, bootstrapSecret) {
|
|
88
|
+
try {
|
|
89
|
+
const manifest = await Promise.race([
|
|
90
|
+
(0, manifest_1.fetchManifest)(backendUrl, undefined, bootstrapSecret),
|
|
91
|
+
new Promise((_resolve, reject) => setTimeout(() => reject(new Error("the backend did not answer in time")), DRIFT_CHECK_TIMEOUT_MS).unref()),
|
|
92
|
+
]);
|
|
93
|
+
const served = digestOf(manifest.modules);
|
|
94
|
+
return { status: served === builtDigest ? "matches" : "drifted", served };
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
status: "unknown",
|
|
99
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FrontendModuleOptions, ManifestModule } from "./workspace";
|
|
2
|
+
export declare function withoutPrivateValues(modules: readonly ManifestModule[]): ManifestModule[];
|
|
3
|
+
export declare const PRIVATE_OPTIONS_VARIABLE = "DMS_PRIVATE_OPTIONS";
|
|
4
|
+
export interface UnfilledPrivateValue {
|
|
5
|
+
module: string;
|
|
6
|
+
path: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function parsePrivateOptions(raw: string | undefined): Record<string, FrontendModuleOptions>;
|
|
9
|
+
export declare function unfilledPrivateValues(modules: Readonly<Record<string, FrontendModuleOptions>>, given: Readonly<Record<string, FrontendModuleOptions>>, covered: readonly (readonly string[])[]): UnfilledPrivateValue[];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PRIVATE_OPTIONS_VARIABLE = void 0;
|
|
4
|
+
exports.withoutPrivateValues = withoutPrivateValues;
|
|
5
|
+
exports.parsePrivateOptions = parsePrivateOptions;
|
|
6
|
+
exports.unfilledPrivateValues = unfilledPrivateValues;
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
function emptied(value) {
|
|
9
|
+
if (typeof value === "string")
|
|
10
|
+
return "";
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.map(emptied);
|
|
13
|
+
if (value !== null && typeof value === "object")
|
|
14
|
+
return emptiedOptions(value);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function emptiedOptions(options) {
|
|
18
|
+
return Object.fromEntries(Object.entries(options).map(([key, value]) => [key, emptied(value)]));
|
|
19
|
+
}
|
|
20
|
+
function withoutPrivateValues(modules) {
|
|
21
|
+
return modules.map((module) => module.privateOptions
|
|
22
|
+
? { ...module, privateOptions: emptiedOptions(module.privateOptions) }
|
|
23
|
+
: { ...module });
|
|
24
|
+
}
|
|
25
|
+
exports.PRIVATE_OPTIONS_VARIABLE = "DMS_PRIVATE_OPTIONS";
|
|
26
|
+
function isOptions(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function parsePrivateOptions(raw) {
|
|
30
|
+
if (!raw?.trim())
|
|
31
|
+
return {};
|
|
32
|
+
let given;
|
|
33
|
+
try {
|
|
34
|
+
given = JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
given = undefined;
|
|
38
|
+
}
|
|
39
|
+
if (!isOptions(given) || !Object.values(given).every(isOptions)) {
|
|
40
|
+
throw new config_1.UsageError({
|
|
41
|
+
title: `${exports.PRIVATE_OPTIONS_VARIABLE} is not a JSON object of options by module id`,
|
|
42
|
+
fixes: [
|
|
43
|
+
`Set it like ${exports.PRIVATE_OPTIONS_VARIABLE}='{"<module id>":{"<option>":"<value>"}}', with the ids of generated-server-options.json`,
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return given;
|
|
48
|
+
}
|
|
49
|
+
function readPath(options, path) {
|
|
50
|
+
let value = options;
|
|
51
|
+
for (const key of path) {
|
|
52
|
+
if (typeof value !== "object" || value === null)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (!Object.prototype.hasOwnProperty.call(value, key))
|
|
55
|
+
return undefined;
|
|
56
|
+
value = value[key];
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function* emptyStrings(value, path = []) {
|
|
61
|
+
if (value === "")
|
|
62
|
+
yield path;
|
|
63
|
+
else if (typeof value === "object" && value !== null)
|
|
64
|
+
for (const [key, child] of Object.entries(value))
|
|
65
|
+
yield* emptyStrings(child, [...path, key]);
|
|
66
|
+
}
|
|
67
|
+
function unfilledPrivateValues(modules, given, covered) {
|
|
68
|
+
const isCovered = (path) => covered.some((other) => other.length === path.length &&
|
|
69
|
+
other.every((key, index) => key === path[index]));
|
|
70
|
+
return Object.entries(modules).flatMap(([module, options]) => [...emptyStrings(options)]
|
|
71
|
+
.filter((path) => path.length > 0 && !isCovered(path))
|
|
72
|
+
.filter((path) => {
|
|
73
|
+
const value = readPath(given[module], path);
|
|
74
|
+
return typeof value !== "string" || value === "";
|
|
75
|
+
})
|
|
76
|
+
.map((path) => ({ module, path })));
|
|
77
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Manifest, ManifestModule } from "./workspace";
|
|
2
|
+
export declare const SNAPSHOT_FILE = "snapshot.json";
|
|
3
|
+
export declare const SNAPSHOT_LAYERS_DIR = "layers";
|
|
4
|
+
export declare const BUILD_ORIGIN_FILE = ".build-origin.json";
|
|
5
|
+
declare const SNAPSHOT_FORMAT = 1;
|
|
6
|
+
export interface FrontendSnapshot {
|
|
7
|
+
format: typeof SNAPSHOT_FORMAT;
|
|
8
|
+
backendUrl: string;
|
|
9
|
+
takenAt: string;
|
|
10
|
+
digest: string;
|
|
11
|
+
manifest: Manifest;
|
|
12
|
+
}
|
|
13
|
+
export interface BuildOrigin {
|
|
14
|
+
source: "snapshot" | "backend";
|
|
15
|
+
backendUrl: string;
|
|
16
|
+
takenAt: string;
|
|
17
|
+
digest: string;
|
|
18
|
+
authenticated: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function manifestDigest(modules: readonly ManifestModule[]): string;
|
|
21
|
+
export declare function writeSnapshot(dir: string, backendUrl: string, manifest: Manifest, layersSource: string): FrontendSnapshot;
|
|
22
|
+
export declare function readSnapshot(dir: string): FrontendSnapshot;
|
|
23
|
+
export declare function writeBuildOrigin(workspaceDir: string, origin: BuildOrigin): void;
|
|
24
|
+
export declare function readBuildOrigin(workspaceDir: string): BuildOrigin | undefined;
|
|
25
|
+
export {};
|
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BUILD_ORIGIN_FILE = exports.SNAPSHOT_LAYERS_DIR = exports.SNAPSHOT_FILE = void 0;
|
|
4
|
+
exports.manifestDigest = manifestDigest;
|
|
5
|
+
exports.writeSnapshot = writeSnapshot;
|
|
6
|
+
exports.readSnapshot = readSnapshot;
|
|
7
|
+
exports.writeBuildOrigin = writeBuildOrigin;
|
|
8
|
+
exports.readBuildOrigin = readBuildOrigin;
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
|
+
const cli_1 = require("@antelopejs/core/cli");
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const module_set_1 = require("./module-set");
|
|
15
|
+
const private_values_1 = require("./private-values");
|
|
16
|
+
exports.SNAPSHOT_FILE = "snapshot.json";
|
|
17
|
+
exports.SNAPSHOT_LAYERS_DIR = "layers";
|
|
18
|
+
exports.BUILD_ORIGIN_FILE = ".build-origin.json";
|
|
19
|
+
const SNAPSHOT_FORMAT = 1;
|
|
20
|
+
const DIGEST_LENGTH = 16;
|
|
21
|
+
function manifestDigest(modules) {
|
|
22
|
+
return (0, node_crypto_1.createHash)("sha256")
|
|
23
|
+
.update((0, module_set_1.moduleSetFingerprint)(modules.map(({ path: _path, ...module }) => module)))
|
|
24
|
+
.digest("hex")
|
|
25
|
+
.slice(0, DIGEST_LENGTH);
|
|
26
|
+
}
|
|
27
|
+
function writeSnapshot(dir, backendUrl, manifest, layersSource) {
|
|
28
|
+
const modules = (0, private_values_1.withoutPrivateValues)(manifest.modules).map(({ path: _path, ...module }) => module);
|
|
29
|
+
const snapshot = {
|
|
30
|
+
format: SNAPSHOT_FORMAT,
|
|
31
|
+
backendUrl: (0, config_1.canonicalizeBackendUrl)(backendUrl),
|
|
32
|
+
takenAt: new Date().toISOString(),
|
|
33
|
+
digest: manifestDigest(modules),
|
|
34
|
+
manifest: { pack: manifest.pack, modules },
|
|
35
|
+
};
|
|
36
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
37
|
+
const layers = (0, node_path_1.join)(dir, exports.SNAPSHOT_LAYERS_DIR);
|
|
38
|
+
(0, node_fs_1.rmSync)(layers, { recursive: true, force: true });
|
|
39
|
+
(0, node_fs_1.cpSync)(layersSource, layers, { recursive: true });
|
|
40
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, exports.SNAPSHOT_FILE), `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
41
|
+
return snapshot;
|
|
42
|
+
}
|
|
43
|
+
function readSnapshot(dir) {
|
|
44
|
+
const file = (0, node_path_1.join)(dir, exports.SNAPSHOT_FILE);
|
|
45
|
+
const refresh = "Take one: ajs dms snapshot -b <backend url> -o <dir>";
|
|
46
|
+
if (!(0, node_fs_1.existsSync)(file)) {
|
|
47
|
+
throw new cli_1.CliError({
|
|
48
|
+
title: `No snapshot in ${dir}`,
|
|
49
|
+
reason: `${exports.SNAPSHOT_FILE} is missing.`,
|
|
50
|
+
fixes: [refresh],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
let snapshot;
|
|
54
|
+
try {
|
|
55
|
+
snapshot = JSON.parse((0, node_fs_1.readFileSync)(file, "utf8"));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new cli_1.CliError({ title: `${file} is not valid JSON`, fixes: [refresh] }, { cause: error });
|
|
59
|
+
}
|
|
60
|
+
if (snapshot.format !== SNAPSHOT_FORMAT ||
|
|
61
|
+
!Array.isArray(snapshot.manifest?.modules) ||
|
|
62
|
+
typeof snapshot.backendUrl !== "string") {
|
|
63
|
+
throw new cli_1.CliError({
|
|
64
|
+
title: `${file} is not a snapshot this loader can build from`,
|
|
65
|
+
reason: `It needs format ${SNAPSHOT_FORMAT}, a backend URL and a manifest.`,
|
|
66
|
+
fixes: [refresh],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, exports.SNAPSHOT_LAYERS_DIR))) {
|
|
70
|
+
throw new cli_1.CliError({
|
|
71
|
+
title: `The snapshot in ${dir} has no ${exports.SNAPSHOT_LAYERS_DIR} directory`,
|
|
72
|
+
fixes: [refresh],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return snapshot;
|
|
76
|
+
}
|
|
77
|
+
function writeBuildOrigin(workspaceDir, origin) {
|
|
78
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspaceDir, exports.BUILD_ORIGIN_FILE), `${JSON.stringify(origin, null, 2)}\n`);
|
|
79
|
+
}
|
|
80
|
+
function readBuildOrigin(workspaceDir) {
|
|
81
|
+
try {
|
|
82
|
+
const origin = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(workspaceDir, exports.BUILD_ORIGIN_FILE), "utf8"));
|
|
83
|
+
return typeof origin.digest === "string"
|
|
84
|
+
? origin
|
|
85
|
+
: undefined;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -2,6 +2,7 @@ import { type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
|
2
2
|
import { type PathMapper } from "./child-output";
|
|
3
3
|
import { type ManifestSecrets } from "./manifest-secrets";
|
|
4
4
|
import { ResolvedLayer } from "./workspace";
|
|
5
|
+
import { type BuildOrigin, type FrontendSnapshot } from "./snapshot";
|
|
5
6
|
export declare function computeDepsHash(layerPaths: string[], workspacePackage?: string, installInputs?: readonly string[]): string;
|
|
6
7
|
export declare function needsInstall(workspaceDir: string, layerPaths: string[], force: boolean): boolean;
|
|
7
8
|
export declare function saveDepsHash(workspaceDir: string, layerPaths: string[]): void;
|
|
@@ -27,6 +28,11 @@ export interface SetupWorkspaceOptions {
|
|
|
27
28
|
clientUrl?: string;
|
|
28
29
|
workspaceKey?: string;
|
|
29
30
|
bootstrapSecret?: string;
|
|
31
|
+
workspaceDir?: string;
|
|
32
|
+
snapshot?: {
|
|
33
|
+
dir: string;
|
|
34
|
+
snapshot: FrontendSnapshot;
|
|
35
|
+
};
|
|
30
36
|
beforeInstall?: () => Promise<void>;
|
|
31
37
|
}
|
|
32
38
|
export interface SetupWorkspaceResult {
|
|
@@ -35,5 +41,6 @@ export interface SetupWorkspaceResult {
|
|
|
35
41
|
manifestFromCache: boolean;
|
|
36
42
|
manifestFetchedAt?: string;
|
|
37
43
|
manifestSecrets: ManifestSecrets;
|
|
44
|
+
buildOrigin: BuildOrigin;
|
|
38
45
|
}
|
|
39
46
|
export declare function setupWorkspace(opts: SetupWorkspaceOptions): Promise<SetupWorkspaceResult>;
|
package/dist/workspace-setup.js
CHANGED
|
@@ -19,6 +19,7 @@ const manifest_secrets_1 = require("./manifest-secrets");
|
|
|
19
19
|
const renderer_range_1 = require("./renderer-range");
|
|
20
20
|
const materialize_1 = require("./materialize");
|
|
21
21
|
const workspace_1 = require("./workspace");
|
|
22
|
+
const snapshot_1 = require("./snapshot");
|
|
22
23
|
const cancellation_1 = require("./cancellation");
|
|
23
24
|
const output_1 = require("./output");
|
|
24
25
|
const config_1 = require("./config");
|
|
@@ -219,14 +220,25 @@ function manifestCacheUse(mode, offline) {
|
|
|
219
220
|
return "offline";
|
|
220
221
|
return mode === "build" ? "never" : "fallback";
|
|
221
222
|
}
|
|
222
|
-
async function
|
|
223
|
+
async function snapshotLayers({ dir, snapshot }, workspaceDir) {
|
|
224
|
+
const { manifest, takenAt } = snapshot;
|
|
225
|
+
const cacheDir = (0, node_path_1.join)(dir, snapshot_1.SNAPSHOT_LAYERS_DIR);
|
|
226
|
+
(0, layers_1.assertCachedArchivesExist)(manifest.modules, cacheDir, takenAt);
|
|
227
|
+
(0, manifest_1.writeCachedManifest)(workspaceDir, manifest, "drop", takenAt);
|
|
228
|
+
return {
|
|
229
|
+
manifest,
|
|
230
|
+
fromCache: false,
|
|
231
|
+
layers: await (0, layers_1.buildLayersFromCache)(manifest.modules, cacheDir),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function servedLayers(opts, workspaceDir) {
|
|
223
235
|
const { backendUrl, force, mode, offline, clientUrl, bootstrapSecret } = opts;
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
236
|
+
const resolved = await (0, manifest_1.resolveManifest)(workspaceDir, backendUrl, manifestCacheUse(mode, offline), {
|
|
237
|
+
clientUrl,
|
|
238
|
+
bootstrapSecret,
|
|
239
|
+
privateValues: mode === "build" ? "drop" : "keep",
|
|
240
|
+
});
|
|
228
241
|
const { manifest, fromCache, fetchedAt } = resolved;
|
|
229
|
-
let layers;
|
|
230
242
|
if (mode === "dev") {
|
|
231
243
|
(0, layers_1.assertLayerPathsServed)(manifest.modules, backendUrl, {
|
|
232
244
|
bootstrapSecret,
|
|
@@ -235,26 +247,45 @@ async function setupWorkspace(opts) {
|
|
|
235
247
|
if (fromCache) {
|
|
236
248
|
(0, manifest_1.assertCachedLayerPathsExist)(manifest.modules, fetchedAt);
|
|
237
249
|
}
|
|
238
|
-
layers
|
|
250
|
+
return { ...resolved, layers: (0, layers_1.buildLayersFromPaths)(manifest.modules) };
|
|
239
251
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
(0, layers_1.assertCachedArchivesExist)(manifest.modules, cacheDir, fetchedAt);
|
|
252
|
+
const cacheDir = (0, node_path_1.join)(workspaceDir, ".layers-cache");
|
|
253
|
+
if (fromCache) {
|
|
254
|
+
if (!(0, node_fs_1.existsSync)(cacheDir)) {
|
|
255
|
+
throw new cli_1.CliError({
|
|
256
|
+
title: "No layers archive was downloaded for this workspace",
|
|
257
|
+
fixes: [
|
|
258
|
+
"Run once without --offline, with the backend reachable, first",
|
|
259
|
+
],
|
|
260
|
+
});
|
|
252
261
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
262
|
+
(0, layers_1.assertCachedArchivesExist)(manifest.modules, cacheDir, fetchedAt);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
await (0, layers_1.downloadAndExtractLayers)(backendUrl, manifest.pack, cacheDir, force, bootstrapSecret);
|
|
257
266
|
}
|
|
267
|
+
return {
|
|
268
|
+
...resolved,
|
|
269
|
+
layers: await (0, layers_1.buildLayersFromCache)(manifest.modules, cacheDir),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async function setupWorkspace(opts) {
|
|
273
|
+
const { backendUrl, force } = opts;
|
|
274
|
+
const workspaceKey = opts.workspaceKey ?? (0, config_1.canonicalizeBackendUrl)(backendUrl);
|
|
275
|
+
const workspaceDir = opts.workspaceDir
|
|
276
|
+
? (0, workspace_1.ensureExplicitWorkspace)(opts.workspaceDir)
|
|
277
|
+
: (0, workspace_1.ensureWorkspace)(workspaceKey);
|
|
278
|
+
(0, workspace_1.writeWorkspaceMeta)(workspaceDir, backendUrl, workspaceKey);
|
|
279
|
+
const { manifest, fromCache, fetchedAt, layers } = opts.snapshot
|
|
280
|
+
? await snapshotLayers(opts.snapshot, workspaceDir)
|
|
281
|
+
: await servedLayers(opts, workspaceDir);
|
|
282
|
+
const buildOrigin = {
|
|
283
|
+
source: opts.snapshot ? "snapshot" : "backend",
|
|
284
|
+
backendUrl: (0, config_1.canonicalizeBackendUrl)(backendUrl),
|
|
285
|
+
takenAt: opts.snapshot?.snapshot.takenAt ?? fetchedAt ?? new Date().toISOString(),
|
|
286
|
+
digest: (0, snapshot_1.manifestDigest)(manifest.modules),
|
|
287
|
+
authenticated: !!opts.snapshot || (!!opts.bootstrapSecret && !fromCache),
|
|
288
|
+
};
|
|
258
289
|
(0, renderer_range_1.assertLayersSupportRenderer)(layers, { workspaceDir });
|
|
259
290
|
(0, materialize_1.copyStaticTemplates)(workspaceDir);
|
|
260
291
|
(0, materialize_1.writeWorkspacePackageJson)(workspaceDir, layers);
|
|
@@ -274,5 +305,6 @@ async function setupWorkspace(opts) {
|
|
|
274
305
|
manifestFromCache: fromCache,
|
|
275
306
|
manifestFetchedAt: fetchedAt,
|
|
276
307
|
manifestSecrets: (0, manifest_secrets_1.collectManifestSecrets)(manifest.modules),
|
|
308
|
+
buildOrigin,
|
|
277
309
|
};
|
|
278
310
|
}
|
package/dist/workspace.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ export declare function projectWorkspaceKey(projectDir: string): string;
|
|
|
56
56
|
export declare function projectDirFromWorkspaceKey(workspaceKey: string | undefined): string | undefined;
|
|
57
57
|
export declare function getWorkspaceDir(backendUrl: string): string;
|
|
58
58
|
export declare function ensureWorkspace(workspaceKey: string): string;
|
|
59
|
+
export declare function ensureExplicitWorkspace(dir: string): string;
|
|
59
60
|
export interface WorkspaceEntry {
|
|
60
61
|
dir: string;
|
|
61
62
|
backendUrl: string;
|
package/dist/workspace.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.projectWorkspaceKey = projectWorkspaceKey;
|
|
|
6
6
|
exports.projectDirFromWorkspaceKey = projectDirFromWorkspaceKey;
|
|
7
7
|
exports.getWorkspaceDir = getWorkspaceDir;
|
|
8
8
|
exports.ensureWorkspace = ensureWorkspace;
|
|
9
|
+
exports.ensureExplicitWorkspace = ensureExplicitWorkspace;
|
|
9
10
|
exports.listWorkspaces = listWorkspaces;
|
|
10
11
|
exports.directorySize = directorySize;
|
|
11
12
|
exports.writeWorkspaceMeta = writeWorkspaceMeta;
|
|
@@ -38,6 +39,11 @@ function ensureWorkspace(workspaceKey) {
|
|
|
38
39
|
(0, node_fs_1.chmodSync)(dir, config_1.WORKSPACE_DIR_MODE);
|
|
39
40
|
return dir;
|
|
40
41
|
}
|
|
42
|
+
function ensureExplicitWorkspace(dir) {
|
|
43
|
+
const absolute = (0, node_path_1.resolve)(dir);
|
|
44
|
+
(0, node_fs_1.mkdirSync)(absolute, { recursive: true });
|
|
45
|
+
return absolute;
|
|
46
|
+
}
|
|
41
47
|
function listWorkspaces() {
|
|
42
48
|
const listing = { workspaces: [], skipped: [] };
|
|
43
49
|
if (!(0, node_fs_1.existsSync)(config_1.DMS_FRONTEND_HOME))
|
package/package.json
CHANGED
|
@@ -49,15 +49,33 @@ function readJsonFile(url, fallback) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
53
|
+
|
|
54
|
+
/** The object holding `path`'s last key, when every key up to it is own. */
|
|
55
|
+
function holderOf(target, path) {
|
|
54
56
|
let node = target;
|
|
55
57
|
for (const key of path.slice(0, -1)) {
|
|
56
|
-
if (
|
|
58
|
+
if (UNSAFE_KEYS.has(key) || !Object.hasOwn(node, key)) return undefined;
|
|
57
59
|
node = node[key];
|
|
60
|
+
if (typeof node !== "object" || node === null) return undefined;
|
|
58
61
|
}
|
|
62
|
+
return UNSAFE_KEYS.has(path.at(-1)) ? undefined : node;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Sets `value` at `path` when the object holding it exists but lacks it. */
|
|
66
|
+
function fillPath(target, path, value) {
|
|
67
|
+
const holder = holderOf(target, path);
|
|
68
|
+
const key = path.at(-1);
|
|
69
|
+
if (holder && (holder[key] === undefined || holder[key] === ""))
|
|
70
|
+
holder[key] = value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Sets `value` at `path` only where the build left an empty string. */
|
|
74
|
+
function fillEmptied(target, path, value) {
|
|
75
|
+
const holder = holderOf(target, path);
|
|
59
76
|
const key = path.at(-1);
|
|
60
|
-
if (
|
|
77
|
+
if (holder && Object.hasOwn(holder, key) && holder[key] === "")
|
|
78
|
+
holder[key] = value;
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
/**
|
|
@@ -77,12 +95,56 @@ export function privateOptionsWithFallbacks(modules, environment, env) {
|
|
|
77
95
|
return modules;
|
|
78
96
|
}
|
|
79
97
|
|
|
98
|
+
/** Each string leaf of `value`, array items included, with its path. */
|
|
99
|
+
function* stringLeaves(value, path = []) {
|
|
100
|
+
if (typeof value === "string") yield [path, value];
|
|
101
|
+
else if (typeof value === "object" && value !== null)
|
|
102
|
+
for (const [key, child] of Object.entries(value))
|
|
103
|
+
yield* stringLeaves(child, [...path, key]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fills the private values a build left empty from `DMS_PRIVATE_OPTIONS`, a
|
|
108
|
+
* JSON object of private options by module id
|
|
109
|
+
* (`{"acme__shop-frontend-vue":{"apiKey":"…"}}`): any module's secrets, where
|
|
110
|
+
* `DMS_HTML_RENDER_SECRET` and `DMS_OAUTH_RELAY_SECRET` name two of them. Only
|
|
111
|
+
* a value the build emptied is filled, array items included: a value the
|
|
112
|
+
* manifest carries wins, and no option or section is added or replaced.
|
|
113
|
+
*
|
|
114
|
+
* @param modules The private options by module id
|
|
115
|
+
* @param json The variable's value; nothing is filled when unset
|
|
116
|
+
* @param warn Reports a value that is not a JSON object, never its content
|
|
117
|
+
*/
|
|
118
|
+
export function privateOptionsFromJson(modules, json, warn) {
|
|
119
|
+
if (!json) return modules;
|
|
120
|
+
let given;
|
|
121
|
+
try {
|
|
122
|
+
given = JSON.parse(json);
|
|
123
|
+
} catch {
|
|
124
|
+
given = undefined;
|
|
125
|
+
}
|
|
126
|
+
if (typeof given !== "object" || given === null || Array.isArray(given)) {
|
|
127
|
+
warn("DMS_PRIVATE_OPTIONS is not a JSON object of options by module id");
|
|
128
|
+
return modules;
|
|
129
|
+
}
|
|
130
|
+
for (const [id, options] of Object.entries(given)) {
|
|
131
|
+
if (UNSAFE_KEYS.has(id) || !Object.hasOwn(modules, id)) continue;
|
|
132
|
+
for (const [path, value] of stringLeaves(options))
|
|
133
|
+
if (path.length > 0) fillEmptied(modules[id], path, value);
|
|
134
|
+
}
|
|
135
|
+
return modules;
|
|
136
|
+
}
|
|
137
|
+
|
|
80
138
|
function readPrivateOptions(env = process.env) {
|
|
81
139
|
const { modules = {}, environment = {} } = readJsonFile(
|
|
82
140
|
SERVER_OPTIONS_FILE,
|
|
83
141
|
{},
|
|
84
142
|
);
|
|
85
|
-
return
|
|
143
|
+
return privateOptionsFromJson(
|
|
144
|
+
privateOptionsWithFallbacks(modules, environment, env),
|
|
145
|
+
env.DMS_PRIVATE_OPTIONS,
|
|
146
|
+
(message) => console.warn(message),
|
|
147
|
+
);
|
|
86
148
|
}
|
|
87
149
|
|
|
88
150
|
function readPublicOptions() {
|