@antelopejs/dms-frontend 0.6.4 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,8 @@ export interface BuildOptions {
5
5
  force?: boolean;
6
6
  offline?: boolean;
7
7
  bootstrapSecret?: string;
8
+ from?: string;
9
+ workspace?: string;
8
10
  }
9
11
  interface BuildStep {
10
12
  script: string;
@@ -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 = (0, common_1.requireBackendUrl)(options.backendUrl);
106
- const sessionSecret = (0, common_1.resolveSessionSecret)("build");
107
- const bootstrapSecret = (0, common_1.normalizeBootstrapSecret)(options.bootstrapSecret);
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 (manifestFromCache) {
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: `ajs dms start -b ${backendUrl}`,
149
- description: "with the DMS_SESSION_SECRET this build used",
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
  });
@@ -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 DMS_SESSION_SECRET set in the environment or ./.env",
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. Needs a DMS_SESSION_SECRET of 32 characters or more.")
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,6 @@
1
+ export interface SnapshotOptions {
2
+ backendUrl?: string;
3
+ output: string;
4
+ bootstrapSecret?: string;
5
+ }
6
+ export declare function runSnapshot(options: SnapshotOptions): Promise<void>;
@@ -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,2 @@
1
+ import { Command } from "commander";
2
+ export declare function cmdSnapshot(): Command;
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  export interface StartOptions {
2
2
  backendUrl?: string;
3
3
  port: string;
4
+ workspace?: string;
4
5
  }
5
6
  export declare function runStart(options: StartOptions): Promise<void>;
@@ -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 backendUrl = (0, common_1.requireBackendUrl)(options.backendUrl);
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: [`Build it first: ajs dms build -b ${backendUrl}`],
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
- (0, common_1.writeWorkspaceMeta)(workspaceDir, backendUrl, (0, common_1.canonicalizeBackendUrl)(backendUrl));
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"}, or rebuild with DMS_BOOTSTRAP_SECRET so the manifest carries ${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;
@@ -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. Needs the DMS_SESSION_SECRET the build used.")
50
- .addOption(config_1.Options.backendUrl("Backend URL the build was made for; required, as -b or the variable"))
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" | "build" | "start";
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: SECRET_BEARING_FILE_MODE });
35
- (0, node_fs_1.chmodSync)(temp, SECRET_BEARING_FILE_MODE);
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
- : "build and start sign sessions with it, so it must stay the same across restarts.",
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; build and start need one, dev makes one when unset",
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, used when the backend publishes none in the manifest",
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, used when the backend publishes none in the manifest",
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",
@@ -58,7 +63,7 @@ exports.ENVIRONMENT_VARIABLES = [
58
63
  ],
59
64
  [
60
65
  "DMS_BACKEND_WAKE_BUDGET_MS",
61
- "How long a GET or HEAD waits out a backend's cold-start 503, in ms (default: 30000; 0: never)",
66
+ "How long a GET or HEAD waits out a waking or starting backend's 503, in ms (default: 30000; 0: never)",
62
67
  ],
63
68
  ["DMS_CLIENT_BASE_URL", "Public frontend URL used in links and e-mails"],
64
69
  ["NO_COLOR", "Same as --no-color"],
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)());
@@ -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?: string, bootstrapSecret?: string): Promise<ResolvedManifest>;
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
- manifest,
221
- fetchedAt: new Date().toISOString(),
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 manifest = await fetchManifest(backendUrl, clientUrl, bootstrapSecret);
229
- writeCachedManifest(workspaceDir, manifest);
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
- return {
266
- manifest: cached.manifest,
267
- fromCache: true,
268
- fetchedAt: cached.fetchedAt,
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));