@antelopejs/dms-frontend 0.6.3 → 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 +95 -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 +16 -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/frontend-module.ts +9 -6
- package/templates/vue/server/backend-response.mjs +13 -2
- package/templates/vue/server/backend-wake.mjs +129 -0
- package/templates/vue/server/module-server.mjs +67 -5
- package/templates/vue/server.mjs +30 -7
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
|
@@ -1070,9 +1070,11 @@ const warnedServerReads = new Set<string>();
|
|
|
1070
1070
|
|
|
1071
1071
|
/**
|
|
1072
1072
|
* Warns, in a development server render, about a backend read no async data
|
|
1073
|
-
* handler makes: an immediate watcher's, a setup's own `await $fetch
|
|
1074
|
-
* server waits for it, but the page carries none of it, so
|
|
1075
|
-
* it again and may hydrate with other data.
|
|
1073
|
+
* handler makes: an immediate watcher's, a setup's own `await $fetch`, a
|
|
1074
|
+
* middleware's. The server waits for it, but the page carries none of it, so
|
|
1075
|
+
* the browser reads it again and may hydrate with other data. Code that hands
|
|
1076
|
+
* the answer over itself (`useDmsState` with `transfer: true`) looks the same
|
|
1077
|
+
* from here, so the message names that exception rather than guess.
|
|
1076
1078
|
*/
|
|
1077
1079
|
function warnUntransferredRead(
|
|
1078
1080
|
runtime: DmsFrontendRuntime,
|
|
@@ -1089,9 +1091,10 @@ function warnUntransferredRead(
|
|
|
1089
1091
|
warnedServerReads.add(request);
|
|
1090
1092
|
console.warn(
|
|
1091
1093
|
`DMS: ${request} was read during a server render outside useDmsFetch and useDmsAsyncData ` +
|
|
1092
|
-
"(an immediate watcher, a setup's own await): the
|
|
1093
|
-
"
|
|
1094
|
-
"
|
|
1094
|
+
"(an immediate watcher, a setup's own await, a middleware): unless the code hands the " +
|
|
1095
|
+
"answer over (useDmsState with transfer: true), the page does not carry it and the " +
|
|
1096
|
+
"browser reads it again. Load it with useDmsFetch or useDmsAsyncData, in a middleware " +
|
|
1097
|
+
"or a plugin too, or leave it to the browser (server: false, onMounted).",
|
|
1095
1098
|
);
|
|
1096
1099
|
}
|
|
1097
1100
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// allows.
|
|
5
5
|
|
|
6
6
|
import { GENERIC_ERROR_MESSAGE } from "./auth/backend.mjs";
|
|
7
|
+
import { isColdStart } from "./backend-wake.mjs";
|
|
7
8
|
import { isFrontendVisit, redirectFrontendVisit } from "./inertia.mjs";
|
|
8
9
|
|
|
9
10
|
// What a JSON caller gets when the backend fails: the generic i18n key of the
|
|
@@ -19,10 +20,13 @@ const FORBIDDEN = 403;
|
|
|
19
20
|
const FRONTEND_ORIGIN = "http://frontend.local";
|
|
20
21
|
|
|
21
22
|
export class BackendResponseError extends Error {
|
|
22
|
-
constructor(status, code) {
|
|
23
|
+
constructor(status, code, retryAfter) {
|
|
23
24
|
super(`DMS backend returned ${status}`);
|
|
24
25
|
this.status = status;
|
|
25
26
|
this.code = code;
|
|
27
|
+
// Set only for a cold-start answer, so the visitor is told when to try
|
|
28
|
+
// again instead of getting an opaque failure.
|
|
29
|
+
this.retryAfter = retryAfter;
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -43,7 +47,14 @@ async function refusalCode(response) {
|
|
|
43
47
|
|
|
44
48
|
/** The error for a failed backend response, with its typed code if any. */
|
|
45
49
|
export async function backendResponseError(response) {
|
|
46
|
-
|
|
50
|
+
const retryAfter = isColdStart(response)
|
|
51
|
+
? (response.headers.get("retry-after") ?? undefined)
|
|
52
|
+
: undefined;
|
|
53
|
+
return new BackendResponseError(
|
|
54
|
+
response.status,
|
|
55
|
+
await refusalCode(response),
|
|
56
|
+
retryAfter,
|
|
57
|
+
);
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/**
|