@antelopejs/dms-frontend 0.7.1 → 0.9.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.
Files changed (98) hide show
  1. package/README.md +4 -4
  2. package/dist/commands/build-action.d.ts +8 -5
  3. package/dist/commands/build-action.js +16 -41
  4. package/dist/commands/build.js +9 -15
  5. package/dist/commands/dev-action.d.ts +2 -0
  6. package/dist/commands/dev-action.js +14 -3
  7. package/dist/commands/dev.js +3 -1
  8. package/dist/commands/start-action.d.ts +2 -0
  9. package/dist/commands/start-action.js +65 -47
  10. package/dist/commands/start.js +8 -6
  11. package/dist/common.d.ts +1 -0
  12. package/dist/common.js +1 -0
  13. package/dist/config.d.ts +2 -0
  14. package/dist/config.js +9 -0
  15. package/dist/derived-outputs.d.ts +1 -5
  16. package/dist/derived-outputs.js +48 -35
  17. package/dist/dev-modules.d.ts +5 -0
  18. package/dist/dev-modules.js +88 -0
  19. package/dist/entry-weight.d.ts +24 -0
  20. package/dist/entry-weight.js +86 -0
  21. package/dist/fs-sync.d.ts +1 -0
  22. package/dist/fs-sync.js +6 -0
  23. package/dist/help.js +12 -4
  24. package/dist/index.js +4 -6
  25. package/dist/layer-watch.js +7 -2
  26. package/dist/layers.d.ts +2 -3
  27. package/dist/layers.js +1 -95
  28. package/dist/locale-files.d.ts +6 -0
  29. package/dist/locale-files.js +75 -0
  30. package/dist/manifest.d.ts +8 -8
  31. package/dist/manifest.js +17 -39
  32. package/dist/materialize.js +6 -0
  33. package/dist/module-set.d.ts +14 -12
  34. package/dist/module-set.js +39 -31
  35. package/dist/private-values.d.ts +3 -2
  36. package/dist/private-values.js +16 -5
  37. package/dist/project-modules-child.d.ts +13 -0
  38. package/dist/project-modules-child.js +26 -0
  39. package/dist/project-modules.d.ts +47 -0
  40. package/dist/project-modules.js +268 -0
  41. package/dist/workspace-setup.d.ts +3 -7
  42. package/dist/workspace-setup.js +79 -62
  43. package/dist/workspace.d.ts +8 -4
  44. package/dist/workspace.js +1 -1
  45. package/package.json +13 -8
  46. package/skills/dms-frontend/SKILL.md +2 -2
  47. package/templates/vue/app-runtime.ts +8 -1
  48. package/templates/vue/client-chunking.mjs +61 -0
  49. package/templates/vue/email-renderer.ts +6 -1
  50. package/templates/vue/entry-weight.mjs +132 -0
  51. package/templates/vue/frontend-build-loader.ts +1 -1
  52. package/templates/vue/frontend-module.ts +168 -1
  53. package/templates/vue/gitignore +1 -0
  54. package/templates/vue/main.ts +32 -2
  55. package/templates/vue/network-error.ts +3 -1
  56. package/templates/vue/package.json +3 -3
  57. package/templates/vue/patches/@vueuse__core@14.4.0.patch +24 -0
  58. package/templates/vue/pnpm-workspace.yaml +5 -0
  59. package/templates/vue/prepare-types.mjs +2 -7
  60. package/templates/vue/public-options.ts +19 -0
  61. package/templates/vue/server/auth/backend.mjs +20 -3
  62. package/templates/vue/server/auth/client-ip.mjs +64 -22
  63. package/templates/vue/server/auth/session.mjs +4 -11
  64. package/templates/vue/server/backend-proxies.mjs +13 -2
  65. package/templates/vue/server/backend-response.mjs +2 -13
  66. package/templates/vue/server/cli-report.mjs +42 -18
  67. package/templates/vue/server/client-manifest.mjs +34 -58
  68. package/templates/vue/server/dev-private-files.mjs +1 -1
  69. package/templates/vue/server/dev-styles.mjs +16 -5
  70. package/templates/vue/server/homepage.mjs +4 -1
  71. package/templates/vue/server/in-place-render.mjs +110 -0
  72. package/templates/vue/server/module-event.mjs +23 -8
  73. package/templates/vue/server/module-server.mjs +4 -4
  74. package/templates/vue/server/module-testing.mjs +5 -3
  75. package/templates/vue/server/page-props.mjs +10 -0
  76. package/templates/vue/server/proxy-compression.mjs +15 -8
  77. package/templates/vue/server/proxy-headers.mjs +12 -1
  78. package/templates/vue/server/proxy-redirects.mjs +66 -0
  79. package/templates/vue/server/proxy-transfers.mjs +110 -0
  80. package/templates/vue/server/public-endpoints.mjs +2 -2
  81. package/templates/vue/server/public-options.mjs +49 -0
  82. package/templates/vue/server/request-routing.mjs +20 -0
  83. package/templates/vue/server/web-body.mjs +24 -0
  84. package/templates/vue/server-module.ts +19 -7
  85. package/templates/vue/server.mjs +98 -78
  86. package/templates/vue/sfc-global-types.mjs +119 -0
  87. package/templates/vue/ssr-renderer.ts +27 -1
  88. package/templates/vue/ui-kbd.mjs +45 -0
  89. package/templates/vue/vite.config.ts +104 -47
  90. package/templates/vue/vite.email.config.ts +6 -3
  91. package/templates/vue/vite.server.config.ts +5 -2
  92. package/dist/commands/snapshot-action.d.ts +0 -6
  93. package/dist/commands/snapshot-action.js +0 -53
  94. package/dist/commands/snapshot.d.ts +0 -2
  95. package/dist/commands/snapshot.js +0 -55
  96. package/dist/snapshot.d.ts +0 -25
  97. package/dist/snapshot.js +0 -90
  98. package/templates/vue/server/backend-wake.mjs +0 -129
@@ -9,16 +9,25 @@ import {
9
9
  normalizePath,
10
10
  type Plugin,
11
11
  } from "vite";
12
+ import { clientCodeSplitting, onBuildLog } from "./client-chunking.mjs";
13
+ import { recordEntryWeight } from "./entry-weight.mjs";
12
14
  import { rejectServerSourcesInClient } from "./client-server-guard.mjs";
13
15
  import { uiInertiaPage } from "./ui-inertia-page.mjs";
16
+ import { uiKbd } from "./ui-kbd.mjs";
14
17
  import { uiRequestState } from "./ui-request-state.mjs";
15
- import { autoImportDirectories } from "./frontend-build-loader";
16
- import { frontendBuilds } from "./frontend-builds.generated";
18
+ import { autoImportDirectories } from "./frontend-build-loader.ts";
19
+ import { frontendBuilds } from "./frontend-builds.generated.ts";
17
20
  import { orderHeadForFirstPaint } from "./head-order.mjs";
18
21
  import { iconScanGlobs } from "./icon-scan.mjs";
22
+ import {
23
+ sfcGlobalTypesWatcher,
24
+ writeSfcGlobalTypes,
25
+ } from "./sfc-global-types.mjs";
19
26
 
20
27
  interface FrontendModuleRegistryEntry {
21
28
  id: string;
29
+ name?: string;
30
+ packageName?: string;
22
31
  root: string;
23
32
  priority: number;
24
33
  }
@@ -27,7 +36,10 @@ interface FrontendModuleRegistry {
27
36
  modules: FrontendModuleRegistryEntry[];
28
37
  }
29
38
 
30
- const registryPath = resolve(__dirname, "generated-frontend-modules.json");
39
+ const registryPath = resolve(
40
+ import.meta.dirname,
41
+ "generated-frontend-modules.json",
42
+ );
31
43
  const registry = JSON.parse(
32
44
  readFileSync(registryPath, "utf8"),
33
45
  ) as FrontendModuleRegistry;
@@ -65,6 +77,19 @@ const importDirectories = autoImportDirectories(
65
77
  frontendBuilds,
66
78
  (message) => logger.warn(message),
67
79
  );
80
+ /**
81
+ * The auto-imported types, as declarations Vue's SFC compiler reads: without
82
+ * them a prop typed with one compiles to the wrong runtime type.
83
+ */
84
+ const sfcGlobalTypesFile = resolve(
85
+ import.meta.dirname,
86
+ "sfc-global-types.generated.d.ts",
87
+ );
88
+ await writeSfcGlobalTypes(
89
+ importDirectories,
90
+ import.meta.dirname,
91
+ sfcGlobalTypesFile,
92
+ );
68
93
  /**
69
94
  * Dependencies pre-bundled by name. A module's own dependencies need no entry:
70
95
  * the startup crawl below (`optimizerEntries`) finds them in its sources. A
@@ -109,7 +134,7 @@ const optimizedDependencies = [
109
134
  * `normalizePath` is Vite's own answer to this, the identity on POSIX.
110
135
  */
111
136
  const optimizerEntries = [
112
- resolve(__dirname, "main.ts"),
137
+ resolve(import.meta.dirname, "main.ts"),
113
138
  ...moduleRoots.map((root) => resolve(root, "dms.frontend.ts")),
114
139
  ...frontendSourceRoots.flatMap((root) => [
115
140
  resolve(root, "app/**/*.vue"),
@@ -128,7 +153,7 @@ function paintBeforeHydrate(): Plugin {
128
153
 
129
154
  const uiLinkImport = "@nuxt/ui/components/Link.vue";
130
155
  const uiInertiaLinkImport = resolve(
131
- __dirname,
156
+ import.meta.dirname,
132
157
  "node_modules/@nuxt/ui/dist/runtime/vue/overrides/inertia/Link.vue",
133
158
  );
134
159
  /**
@@ -149,6 +174,8 @@ export default defineConfig({
149
174
  // Fails a client build that imports a dms.server.ts or a file under a
150
175
  // module's server/ directory, however it is imported.
151
176
  rejectServerSourcesInClient(moduleRoots),
177
+ // What each module adds to the client entry, which `ajs dms build` reports.
178
+ recordEntryWeight(registry.modules),
152
179
  {
153
180
  name: "dms-ui-inertia-link",
154
181
  enforce: "pre",
@@ -156,14 +183,25 @@ export default defineConfig({
156
183
  return id === uiLinkImport ? uiInertiaLinkImport : null;
157
184
  },
158
185
  },
186
+ uiKbd(isNuxtUiFile),
159
187
  uiInertiaPage(isNuxtUiFile),
160
188
  uiRequestState(isNuxtUiFile),
161
- vue(),
189
+ vue({ script: { globalTypeFiles: [sfcGlobalTypesFile] } }),
190
+ sfcGlobalTypesWatcher(
191
+ importDirectories,
192
+ import.meta.dirname,
193
+ sfcGlobalTypesFile,
194
+ ),
162
195
  ui({
163
196
  router: "inertia",
164
197
  icon: {
165
198
  clientBundle: {
166
- scan: { globInclude: iconScanGlobs(__dirname, frontendSourceRoots) },
199
+ scan: {
200
+ globInclude: iconScanGlobs(
201
+ import.meta.dirname,
202
+ frontendSourceRoots,
203
+ ),
204
+ },
167
205
  },
168
206
  },
169
207
  components: { dirs: [] },
@@ -179,42 +217,45 @@ export default defineConfig({
179
217
  // backslashes intact, where `\U`, `\M` and `\f` are read as escape
180
218
  // sequences: the specifier the bundler then resolves is a mangled
181
219
  // path that cannot exist. Keep it POSIX.
182
- [normalizePath(resolve(__dirname, "frontend-module.ts"))]: [
183
- "$fetch",
184
- "abortDmsNavigation",
185
- "clearDmsError",
186
- "createDmsError",
187
- "dmsRequestKey",
188
- "defineDmsAppConfig",
189
- "defineDmsPlugin",
190
- "defineDmsMiddleware",
191
- "defineDmsPageMeta",
192
- "navigateDms",
193
- "preloadDmsComponents",
194
- "refreshDmsData",
195
- "refreshDmsPageData",
196
- "setDmsAsyncDataScope",
197
- "showDmsError",
198
- "useDmsAsyncData",
199
- "useDmsData",
200
- "useDmsCookie",
201
- "useDmsFetch",
202
- "useHead",
203
- "useI18n",
204
- "useDmsError",
205
- "useDmsPagedData",
206
- "useDmsPageLoading",
207
- "useDmsApp",
208
- "useDmsRoute",
209
- "useDmsRouter",
210
- "useDmsRuntimeConfig",
211
- "useSeoMeta",
212
- "useDmsState",
213
- "useDmsAppConfig",
214
- "DmsClientOnly",
215
- "DmsLink",
216
- "useDmsUserSession",
217
- ],
220
+ [normalizePath(resolve(import.meta.dirname, "frontend-module.ts"))]:
221
+ [
222
+ "$fetch",
223
+ "abortDmsNavigation",
224
+ "clearDmsError",
225
+ "createDmsError",
226
+ "dmsRequestKey",
227
+ "defineDmsAppConfig",
228
+ "defineDmsPlugin",
229
+ "defineDmsMiddleware",
230
+ "defineDmsPageMeta",
231
+ "navigateDms",
232
+ "preloadDmsComponents",
233
+ "refreshDmsData",
234
+ "refreshDmsPageData",
235
+ "setDmsAsyncDataScope",
236
+ "showDmsError",
237
+ "useDmsAsyncData",
238
+ "useDmsData",
239
+ "useDmsCookie",
240
+ "useDmsFetch",
241
+ "useHead",
242
+ "useI18n",
243
+ "useDmsError",
244
+ "useDmsPagedData",
245
+ "useDmsPageLoading",
246
+ "isDmsHydrating",
247
+ "useDmsApp",
248
+ "useDmsRoute",
249
+ "useDmsRouter",
250
+ "useDmsRuntimeConfig",
251
+ "useSeoMeta",
252
+ "useDmsState",
253
+ "useDmsAppConfig",
254
+ "DmsClientOnly",
255
+ "DmsLink",
256
+ "useDmsUserSession",
257
+ "provideDmsKbdPlatform",
258
+ ],
218
259
  },
219
260
  ],
220
261
  dts: "auto-imports.d.ts",
@@ -228,13 +269,16 @@ export default defineConfig({
228
269
  // Nuxt UI's `Icon` must share one copy for the same-origin provider to apply.
229
270
  dedupe: ["vue", "reka-ui", "@nuxt/ui", "@vueuse/core", "@iconify/vue"],
230
271
  alias: {
231
- "#dms/frontend-module": resolve(__dirname, "frontend-module.ts"),
272
+ "#dms/frontend-module": resolve(
273
+ import.meta.dirname,
274
+ "frontend-module.ts",
275
+ ),
232
276
  // For the server's development loader only: no browser module imports
233
277
  // a `dms.server.ts`.
234
- "#dms/server": resolve(__dirname, "server-module.ts"),
278
+ "#dms/server": resolve(import.meta.dirname, "server-module.ts"),
235
279
  "#build/nuxt-icon-client-bundle": "virtual:nuxt-ui-icons",
236
280
  "#shortcuts-aggregated": resolve(
237
- __dirname,
281
+ import.meta.dirname,
238
282
  "shortcuts-aggregated.generated.ts",
239
283
  ),
240
284
  ...stableModuleAliases,
@@ -252,5 +296,18 @@ export default defineConfig({
252
296
  },
253
297
  ssr: { noExternal: ["@nuxt/icon", "@nuxt/ui"] },
254
298
  server: { strictPort: true, allowedHosts: [".onamp.dev"] },
255
- build: { manifest: true, outDir: "dist/client" },
299
+ build: {
300
+ manifest: true,
301
+ outDir: "dist/client",
302
+ rolldownOptions: { onLog: onBuildLog },
303
+ },
304
+ // The browser bundle only: the server bundles are single Node programs, for
305
+ // which a file count means nothing.
306
+ environments: {
307
+ client: {
308
+ build: {
309
+ rolldownOptions: { output: { codeSplitting: clientCodeSplitting } },
310
+ },
311
+ },
312
+ },
256
313
  });
@@ -14,7 +14,10 @@ interface FrontendModuleRegistry {
14
14
  }
15
15
 
16
16
  const registry = JSON.parse(
17
- readFileSync(resolve(__dirname, "generated-frontend-modules.json"), "utf8"),
17
+ readFileSync(
18
+ resolve(import.meta.dirname, "generated-frontend-modules.json"),
19
+ "utf8",
20
+ ),
18
21
  ) as FrontendModuleRegistry;
19
22
  const moduleAliases = Object.fromEntries(
20
23
  registry.modules.map((module) => [`@frontend/${module.id}`, module.root]),
@@ -42,7 +45,7 @@ export default defineConfig({
42
45
  {
43
46
  name: "dms-email-locale-assets",
44
47
  buildStart() {
45
- const directory = resolve(__dirname, "locales.generated");
48
+ const directory = resolve(import.meta.dirname, "locales.generated");
46
49
  for (const file of readdirSync(directory).filter((name) =>
47
50
  name.endsWith(".json"),
48
51
  )) {
@@ -66,7 +69,7 @@ export default defineConfig({
66
69
  // Inlined verbatim as a module specifier in the generated import, so
67
70
  // it has to be POSIX: a native Windows path would reach the parser
68
71
  // with its backslashes read as string escapes. See vite.config.ts.
69
- [normalizePath(resolve(__dirname, "email-runtime.ts"))]: [
72
+ [normalizePath(resolve(import.meta.dirname, "email-runtime.ts"))]: [
70
73
  "useDmsAppConfig",
71
74
  "useDmsRuntimeConfig",
72
75
  "defineDmsAppConfig",
@@ -12,7 +12,10 @@ interface FrontendModuleRegistry {
12
12
  }
13
13
 
14
14
  const registry = JSON.parse(
15
- readFileSync(resolve(__dirname, "generated-frontend-modules.json"), "utf8"),
15
+ readFileSync(
16
+ resolve(import.meta.dirname, "generated-frontend-modules.json"),
17
+ "utf8",
18
+ ),
16
19
  ) as FrontendModuleRegistry;
17
20
  const moduleAliases = Object.fromEntries(
18
21
  registry.modules.map((module) => [`@frontend/${module.id}`, module.root]),
@@ -44,7 +47,7 @@ export default defineConfig({
44
47
  logLevel,
45
48
  resolve: {
46
49
  alias: {
47
- "#dms/server": resolve(__dirname, "server-module.ts"),
50
+ "#dms/server": resolve(import.meta.dirname, "server-module.ts"),
48
51
  ...stableModuleAliases,
49
52
  ...moduleAliases,
50
53
  },
@@ -1,6 +0,0 @@
1
- export interface SnapshotOptions {
2
- backendUrl?: string;
3
- output: string;
4
- bootstrapSecret?: string;
5
- }
6
- export declare function runSnapshot(options: SnapshotOptions): Promise<void>;
@@ -1,53 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function cmdSnapshot(): Command;
@@ -1,55 +0,0 @@
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,25 +0,0 @@
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 DELETED
@@ -1,90 +0,0 @@
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
- }
@@ -1,129 +0,0 @@
1
- // Waiting out a backend that is waking from sleep or still starting.
2
- //
3
- // A platform that scales the backend to zero answers the first requests with
4
- // a 503 while it starts a replica, marked with the cold-start header and a
5
- // Retry-After in whole seconds; a DMS backend that listens before it is ready
6
- // answers 503 with a Retry-After too, without the header. A 503 with a
7
- // Retry-After in whole seconds is the signal, with or without the header; a
8
- // 503 without one goes out as it came. Only a GET or a HEAD is repeated; a
9
- // POST, PATCH or DELETE is sent once.
10
- //
11
- // The wait starts with the first waitable answer and lasts at most the
12
- // budget, the repeated requests included: one still waiting for its answer
13
- // when the budget runs out is aborted, and no request starts after it. It
14
- // ends at once when the client leaves.
15
- //
16
- // Lives apart from server.mjs so the file stays under the size the linter
17
- // allows.
18
-
19
- import { setTimeout as sleep } from "node:timers/promises";
20
-
21
- /** Header a platform marks the 503 of a backend it is waking with. */
22
- export const COLD_START_HEADER = "x-antelope-cold-start";
23
- const COLD_START_VALUE = "1";
24
- const UNAVAILABLE = 503;
25
- const RETRYABLE_METHODS = new Set(["GET", "HEAD"]);
26
- const DEFAULT_BUDGET_MS = 30_000;
27
- const MIN_DELAY_MS = 1_000;
28
- // Repeats after the first request, so a GET is sent 16 times at most. They
29
- // are capped as well as the time, so a marked answer with `Retry-After: 0`
30
- // cannot multiply a GET.
31
- const MAX_REPEATS = 15;
32
- const MAX_DELAY_MS = 5_000;
33
- const MS_PER_SECOND = 1_000;
34
- const WHOLE_SECONDS = /^\d+$/;
35
- const WHOLE_MILLISECONDS = /^\d+$/;
36
-
37
- /**
38
- * Whether `response` is a 503 asking to come back after a Retry-After in
39
- * whole seconds: a platform waking the backend (with the cold-start header)
40
- * or a backend still starting.
41
- */
42
- export function isWaitable(response) {
43
- return (
44
- response.status === UNAVAILABLE && WHOLE_SECONDS.test(retryAfter(response))
45
- );
46
- }
47
-
48
- function retryAfter(response) {
49
- return (response.headers.get("retry-after") ?? "").trim();
50
- }
51
-
52
- /**
53
- * How long a request may wait for a waking backend, from
54
- * DMS_BACKEND_WAKE_BUDGET_MS. 0 turns the wait off; anything but a whole
55
- * number of milliseconds keeps the default.
56
- */
57
- export function wakeBudgetMs(env = process.env) {
58
- const raw = (env.DMS_BACKEND_WAKE_BUDGET_MS ?? "").trim();
59
- return WHOLE_MILLISECONDS.test(raw) ? Number(raw) : DEFAULT_BUDGET_MS;
60
- }
61
-
62
- /** The pause a waitable answer's Retry-After asks for, kept between bounds. */
63
- function retryDelayMs(response) {
64
- return Math.min(
65
- MAX_DELAY_MS,
66
- Math.max(MIN_DELAY_MS, Number(retryAfter(response)) * MS_PER_SECOND),
67
- );
68
- }
69
-
70
- /**
71
- * What the client gets once the budget is spent: the last waitable answer's
72
- * status, Retry-After and cold-start header if it had one, without the body
73
- * its pause already discarded.
74
- */
75
- function expired(cold) {
76
- const headers = { "retry-after": retryAfter(cold) };
77
- if (cold.headers.get(COLD_START_HEADER) === COLD_START_VALUE)
78
- headers[COLD_START_HEADER] = COLD_START_VALUE;
79
- return new Response(null, { status: UNAVAILABLE, headers });
80
- }
81
-
82
- /**
83
- * Sends one repeat, aborted if no answer has come by `deadline`. Once the
84
- * answer has come the deadline no longer applies, so a ready backend's body
85
- * streams for as long as it needs; only the client leaving ends it.
86
- * Resolves to undefined when the deadline aborted the request.
87
- */
88
- async function repeat(url, init, deadline, now) {
89
- const late = new AbortController();
90
- const timer = setTimeout(() => late.abort(), Math.max(0, deadline - now()));
91
- const signal = init.signal
92
- ? AbortSignal.any([init.signal, late.signal])
93
- : late.signal;
94
- try {
95
- return await fetch(url, { ...init, signal });
96
- } catch (error) {
97
- if (late.signal.aborted && !init.signal?.aborted) return undefined;
98
- throw error;
99
- } finally {
100
- clearTimeout(timer);
101
- }
102
- }
103
-
104
- /**
105
- * `fetch`, waiting out waitable 503s to a GET or a HEAD until `budgetMs`
106
- * from the first of them is spent or MAX_REPEATS repeats are sent. Then the
107
- * caller gets the 503 with its `Retry-After` to relay. The client
108
- * leaving, through `init.signal`, ends the wait with its AbortError.
109
- */
110
- export async function fetchWaking(
111
- url,
112
- init = {},
113
- { budgetMs = wakeBudgetMs(), now = Date.now } = {},
114
- ) {
115
- const method = (init.method ?? "GET").toUpperCase();
116
- let response = await fetch(url, init);
117
- if (!RETRYABLE_METHODS.has(method)) return response;
118
- const deadline = now() + budgetMs;
119
- for (let repeats = 0; isWaitable(response); repeats += 1) {
120
- const cold = response;
121
- const delay = retryDelayMs(cold);
122
- if (repeats >= MAX_REPEATS || now() + delay > deadline) return cold;
123
- cold.body?.cancel().catch(() => {});
124
- await sleep(delay, undefined, { signal: init.signal });
125
- if (now() >= deadline) return expired(cold);
126
- response = (await repeat(url, init, deadline, now)) ?? expired(cold);
127
- }
128
- return response;
129
- }