@antelopejs/dms-frontend 0.10.0 → 0.10.2

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 (75) hide show
  1. package/README.md +34 -11
  2. package/dist/commands/build-action.d.ts +3 -0
  3. package/dist/commands/build-action.js +74 -6
  4. package/dist/commands/build.js +7 -0
  5. package/dist/commands/clean-action.js +1 -1
  6. package/dist/commands/clean.js +1 -1
  7. package/dist/commands/dev-action.js +2 -2
  8. package/dist/commands/dev.js +1 -1
  9. package/dist/config.js +1 -0
  10. package/dist/derived-outputs.d.ts +1 -0
  11. package/dist/derived-outputs.js +3 -1
  12. package/dist/help.js +1 -1
  13. package/dist/internal/prepared-workspace.d.ts +12 -0
  14. package/dist/internal/prepared-workspace.js +74 -0
  15. package/dist/layer-watch.js +1 -1
  16. package/dist/locale-files.d.ts +4 -2
  17. package/dist/locale-files.js +11 -9
  18. package/dist/manifest.js +2 -1
  19. package/dist/ports.d.ts +3 -1
  20. package/dist/ports.js +1 -1
  21. package/dist/project-modules.d.ts +6 -1
  22. package/dist/project-modules.js +26 -16
  23. package/dist/verify-source-runner.js +2 -44
  24. package/dist/verify-source-workspace.d.ts +2 -0
  25. package/dist/verify-source-workspace.js +111 -0
  26. package/dist/workspace-setup.d.ts +1 -0
  27. package/dist/workspace-setup.js +3 -2
  28. package/dist/workspace.js +1 -1
  29. package/package.json +20 -7
  30. package/templates/vue/backend-boundary.mjs +132 -0
  31. package/templates/vue/client-chunking.mjs +2 -0
  32. package/templates/vue/entry-weight.mjs +9 -5
  33. package/templates/vue/frontend-build.ts +4 -3
  34. package/templates/vue/head-order.mjs +3 -2
  35. package/templates/vue/icon-api.mjs +2 -0
  36. package/templates/vue/server/auth/backend.mjs +4 -4
  37. package/templates/vue/server/auth/establish-endpoints.mjs +2 -2
  38. package/templates/vue/server/auth/session.mjs +5 -2
  39. package/templates/vue/server/auth-declarations.mjs +46 -0
  40. package/templates/vue/server/auth-hooks.mjs +41 -3
  41. package/templates/vue/server/backend-proxies.mjs +16 -12
  42. package/templates/vue/server/backend-response.mjs +3 -3
  43. package/templates/vue/server/cli-report.mjs +22 -1
  44. package/templates/vue/server/client-error.mjs +5 -0
  45. package/templates/vue/server/content.mjs +24 -11
  46. package/templates/vue/server/email-bundle-watch.mjs +9 -3
  47. package/templates/vue/server/email.mjs +5 -3
  48. package/templates/vue/server/generated-files.mjs +16 -6
  49. package/templates/vue/server/icons.mjs +15 -11
  50. package/templates/vue/server/in-place-render.mjs +3 -3
  51. package/templates/vue/server/inertia.mjs +17 -3
  52. package/templates/vue/server/module-event.mjs +28 -16
  53. package/templates/vue/server/module-route-types.mjs +2 -2
  54. package/templates/vue/server/module-routes.mjs +24 -76
  55. package/templates/vue/server/module-server.mjs +27 -17
  56. package/templates/vue/server/module-testing.mjs +23 -8
  57. package/templates/vue/server/page-props.mjs +6 -5
  58. package/templates/vue/server/page-redirects.mjs +1 -1
  59. package/templates/vue/server/path-patterns.mjs +22 -11
  60. package/templates/vue/server/proxy-headers.mjs +1 -1
  61. package/templates/vue/server/proxy-redirects.mjs +1 -0
  62. package/templates/vue/server/proxy-request.mjs +2 -1
  63. package/templates/vue/server/proxy-transfers.mjs +17 -11
  64. package/templates/vue/server/public-endpoints.mjs +12 -16
  65. package/templates/vue/server/request-failure.mjs +1 -1
  66. package/templates/vue/server/request-routing.mjs +6 -1
  67. package/templates/vue/server/request-target.mjs +22 -9
  68. package/templates/vue/server/route-rules.mjs +12 -6
  69. package/templates/vue/server/static-files.mjs +12 -7
  70. package/templates/vue/server/vite-errors.mjs +13 -2
  71. package/templates/vue/server/web-body.mjs +2 -1
  72. package/templates/vue/server-module.ts +2 -1
  73. package/templates/vue/server.mjs +41 -20
  74. package/templates/vue/ui-inertia-page.mjs +1 -1
  75. package/templates/vue/vite.config.ts +9 -0
@@ -39,11 +39,11 @@ const cli_1 = require("@antelopejs/core/cli");
39
39
  const child_output_1 = require("./child-output");
40
40
  const ensure_1 = require("./internal/ensure");
41
41
  const guards_1 = require("./internal/guards");
42
- const plain_object_1 = require("./internal/plain-object");
43
42
  const vite_manifest_1 = require("./internal/vite-manifest");
44
43
  const common_1 = require("./common");
45
44
  const output_1 = require("./output");
46
45
  const temporary_workspace_1 = require("./temporary-workspace");
46
+ const verify_source_workspace_1 = require("./verify-source-workspace");
47
47
  const verify_source_client_bundle_1 = require("./verify-source-client-bundle");
48
48
  const verify_source_build_1 = require("./verify-source-build");
49
49
  const verify_source_result_1 = require("./verify-source-result");
@@ -51,48 +51,6 @@ const verify_source_sources_1 = require("./verify-source-sources");
51
51
  const vite_output_1 = require("./vite-output");
52
52
  const EMAIL_BUILD_SIZE_CEILING_BYTES = 256 * 1024;
53
53
  const NON_EMAIL_RUNTIME_PATTERN = /DisplayRichText|ApexCharts|Tiptap|FlowCanvas|app\/.*table|components\/table/i;
54
- function removeDevelopmentDependencies(root) {
55
- const packagePath = (0, node_path_1.join)(root, "package.json");
56
- if (!(0, node_fs_1.existsSync)(packagePath))
57
- return;
58
- const packageData = JSON.parse((0, node_fs_1.readFileSync)(packagePath, "utf8"));
59
- if (!(0, plain_object_1.isPlainObject)(packageData))
60
- return;
61
- delete packageData.devDependencies;
62
- (0, node_fs_1.writeFileSync)(packagePath, `${JSON.stringify(packageData, null, 2)}\n`);
63
- }
64
- function materializeWorkspace(workspace, layers, localPackages) {
65
- const templateRoot = (0, node_path_1.join)((0, common_1.getPackageRoot)(), "templates", "vue");
66
- const templateFiles = (0, node_fs_1.readdirSync)(templateRoot).filter((file) => !file.startsWith("npmrc"));
67
- for (const file of templateFiles)
68
- (0, node_fs_1.cpSync)((0, node_path_1.join)(templateRoot, file), (0, node_path_1.join)(workspace, file), {
69
- recursive: true,
70
- });
71
- (0, common_1.materializeLayers)(workspace, layers);
72
- const registry = (0, common_1.createFrontendModuleRegistry)(workspace, layers);
73
- registry.modules.forEach((module) => {
74
- removeDevelopmentDependencies(module.root);
75
- });
76
- (0, common_1.writeFrontendModuleRegistry)(workspace, layers);
77
- (0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspace, "dms-main.css"), '@import "tailwindcss";\n@import "@nuxt/ui";\n');
78
- (0, common_1.writeWorkspacePackageJson)(workspace, layers);
79
- const packagePath = (0, node_path_1.join)(workspace, "package.json");
80
- const workspacePackage = JSON.parse((0, node_fs_1.readFileSync)(packagePath, "utf8"));
81
- (0, ensure_1.ensure)((0, plain_object_1.isPlainObject)(workspacePackage), "The workspace package.json is not a JSON object");
82
- workspacePackage.pnpm = {
83
- overrides: {
84
- ...Object.fromEntries(layers.map((layer) => [
85
- String(layer.packageName),
86
- "workspace:*",
87
- ])),
88
- ...Object.fromEntries(Object.entries(localPackages).map(([name, path]) => [
89
- name,
90
- `link:${(0, node_path_1.resolve)(path)}`,
91
- ])),
92
- },
93
- };
94
- (0, node_fs_1.writeFileSync)(packagePath, `${JSON.stringify(workspacePackage, null, 2)}\n`);
95
- }
96
54
  async function checkServerModulesBundle(workspace) {
97
55
  const root = (0, node_path_1.join)(workspace, "dist", "server-modules");
98
56
  const javascript = (0, node_fs_1.readdirSync)(root, { recursive: true, encoding: "utf8" })
@@ -216,7 +174,7 @@ async function verifySources() {
216
174
  env: { ...process.env, [vite_output_1.VITE_LOG_LEVEL_VARIABLE]: (0, vite_output_1.viteLogLevel)(isVerbose) },
217
175
  };
218
176
  const tasks = (0, cli_1.getProcessTasks)();
219
- await tasks.run(`Materializing ${(0, cli_1.pluralize)(layers.length, "module")}`, async () => materializeWorkspace(workspace, layers, options.localPackages), { done: `Materialized ${(0, cli_1.pluralize)(layers.length, "module")}` });
177
+ await tasks.run(`Materializing ${(0, cli_1.pluralize)(layers.length, "module")}`, async () => (0, verify_source_workspace_1.materializeWorkspace)(workspace, layers, options.localPackages), { done: `Materialized ${(0, cli_1.pluralize)(layers.length, "module")}` });
220
178
  await (0, common_1.installDeps)(workspace, context.mapLine, ["--ignore-scripts"]);
221
179
  await (0, verify_source_build_1.prepareTypes)(context);
222
180
  await (0, verify_source_build_1.buildAndTypeCheck)(context);
@@ -0,0 +1,2 @@
1
+ import { type ResolvedLayer } from "./common";
2
+ export declare function materializeWorkspace(workspace: string, layers: ResolvedLayer[], localPackages: Record<string, string>): void;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.materializeWorkspace = materializeWorkspace;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const yaml_1 = require("yaml");
7
+ const common_1 = require("./common");
8
+ const ensure_1 = require("./internal/ensure");
9
+ const guards_1 = require("./internal/guards");
10
+ const plain_object_1 = require("./internal/plain-object");
11
+ const output_1 = require("./output");
12
+ function removeDevelopmentDependencies(root) {
13
+ const packagePath = (0, node_path_1.join)(root, "package.json");
14
+ if (!(0, node_fs_1.existsSync)(packagePath))
15
+ return;
16
+ const packageData = JSON.parse((0, node_fs_1.readFileSync)(packagePath, "utf8"));
17
+ if (!(0, plain_object_1.isPlainObject)(packageData))
18
+ return;
19
+ delete packageData.devDependencies;
20
+ (0, node_fs_1.writeFileSync)(packagePath, `${JSON.stringify(packageData, null, 2)}\n`);
21
+ }
22
+ const PATH_PROTOCOLS = ["file:", "link:"];
23
+ const WORKSPACE_PROTOCOL = "workspace:";
24
+ const LOCKFILE = "pnpm-lock.yaml";
25
+ const WORKSPACE_FILE = "pnpm-workspace.yaml";
26
+ const OWN_LOCKFILES = /^\s*shared-workspace-lockfile\s*=\s*false\s*$/m;
27
+ function findUp(directory, file) {
28
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(directory, file)))
29
+ return directory;
30
+ const parent = (0, node_path_1.dirname)(directory);
31
+ return parent === directory ? undefined : findUp(parent, file);
32
+ }
33
+ function sharesLockfile(root) {
34
+ const npmrc = (0, node_path_1.join)(root, ".npmrc");
35
+ if ((0, node_fs_1.existsSync)(npmrc) && OWN_LOCKFILES.test((0, node_fs_1.readFileSync)(npmrc, "utf8")))
36
+ return false;
37
+ const settings = (0, yaml_1.parse)((0, node_fs_1.readFileSync)((0, node_path_1.join)(root, WORKSPACE_FILE), "utf8"));
38
+ return !((0, plain_object_1.isPlainObject)(settings) && settings.sharedWorkspaceLockfile === false);
39
+ }
40
+ function findLockfile(directory) {
41
+ const workspaceRoot = findUp(directory, WORKSPACE_FILE);
42
+ const lockfileRoot = workspaceRoot && sharesLockfile(workspaceRoot)
43
+ ? workspaceRoot
44
+ : findUp(directory, LOCKFILE);
45
+ if (!lockfileRoot)
46
+ return undefined;
47
+ const lockfile = (0, node_path_1.join)(lockfileRoot, LOCKFILE);
48
+ const outsideWorkspace = workspaceRoot !== undefined &&
49
+ (0, node_path_1.relative)(workspaceRoot, lockfileRoot).startsWith("..");
50
+ return (0, node_fs_1.existsSync)(lockfile) && !outsideWorkspace ? lockfile : undefined;
51
+ }
52
+ function readLayerOverrides(layer) {
53
+ const lockfile = findLockfile(layer.path);
54
+ if (!lockfile)
55
+ return [];
56
+ const root = (0, node_path_1.dirname)(lockfile);
57
+ const overrides = [];
58
+ for (const document of (0, yaml_1.parseAllDocuments)((0, node_fs_1.readFileSync)(lockfile, "utf8"))) {
59
+ const [error] = document.errors;
60
+ (0, ensure_1.ensure)(error === undefined, `${(0, output_1.showPath)(lockfile)} is not a pnpm lockfile`, error?.message);
61
+ const lock = document.toJS();
62
+ if (!(0, plain_object_1.isPlainObject)(lock) || !(0, plain_object_1.isPlainObject)(lock.overrides))
63
+ continue;
64
+ for (const [name, spec] of Object.entries(lock.overrides)) {
65
+ if (!(0, guards_1.isText)(spec) || spec.startsWith(WORKSPACE_PROTOCOL))
66
+ continue;
67
+ overrides.push([name, resolveSpec(root, spec)]);
68
+ }
69
+ }
70
+ return overrides;
71
+ }
72
+ function resolveSpec(root, spec) {
73
+ const protocol = PATH_PROTOCOLS.find((prefix) => spec.startsWith(prefix));
74
+ if (!protocol)
75
+ return spec;
76
+ const path = spec.slice(protocol.length);
77
+ return (0, node_path_1.isAbsolute)(path) ? spec : `${protocol}${(0, node_path_1.resolve)(root, path)}`;
78
+ }
79
+ function materializeWorkspace(workspace, layers, localPackages) {
80
+ const templateRoot = (0, node_path_1.join)((0, common_1.getPackageRoot)(), "templates", "vue");
81
+ const templateFiles = (0, node_fs_1.readdirSync)(templateRoot).filter((file) => !file.startsWith("npmrc"));
82
+ for (const file of templateFiles)
83
+ (0, node_fs_1.cpSync)((0, node_path_1.join)(templateRoot, file), (0, node_path_1.join)(workspace, file), {
84
+ recursive: true,
85
+ });
86
+ (0, common_1.materializeLayers)(workspace, layers);
87
+ const registry = (0, common_1.createFrontendModuleRegistry)(workspace, layers);
88
+ registry.modules.forEach((module) => {
89
+ removeDevelopmentDependencies(module.root);
90
+ });
91
+ (0, common_1.writeFrontendModuleRegistry)(workspace, layers);
92
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspace, "dms-main.css"), '@import "tailwindcss";\n@import "@nuxt/ui";\n');
93
+ (0, common_1.writeWorkspacePackageJson)(workspace, layers);
94
+ const packagePath = (0, node_path_1.join)(workspace, "package.json");
95
+ const workspacePackage = JSON.parse((0, node_fs_1.readFileSync)(packagePath, "utf8"));
96
+ (0, ensure_1.ensure)((0, plain_object_1.isPlainObject)(workspacePackage), "The workspace package.json is not a JSON object");
97
+ workspacePackage.pnpm = {
98
+ overrides: {
99
+ ...Object.fromEntries(layers.flatMap(readLayerOverrides)),
100
+ ...Object.fromEntries(layers.map((layer) => [
101
+ String(layer.packageName),
102
+ "workspace:*",
103
+ ])),
104
+ ...Object.fromEntries(Object.entries(localPackages).map(([name, path]) => [
105
+ name,
106
+ `link:${(0, node_path_1.resolve)(path)}`,
107
+ ])),
108
+ },
109
+ };
110
+ (0, node_fs_1.writeFileSync)(packagePath, `${JSON.stringify(workspacePackage, null, 2)}\n`);
111
+ }
@@ -31,6 +31,7 @@ export interface SetupWorkspaceOptions {
31
31
  workspaceDir?: string;
32
32
  project?: ProjectModulesOptions;
33
33
  beforeInstall?: () => Promise<void>;
34
+ install?: boolean;
34
35
  }
35
36
  export interface SetupWorkspaceResult {
36
37
  workspaceDir: string;
@@ -299,7 +299,8 @@ async function setupWorkspace(opts) {
299
299
  writeDerivedOutputs(workspaceDir, layers);
300
300
  (0, materialize_1.writeDmsMainCss)(workspaceDir, layers);
301
301
  const layerPaths = layers.map((layer) => layer.path);
302
- const shouldInstall = needsInstall(workspaceDir, layerPaths, opts.force);
302
+ const shouldInstall = opts.install !== false &&
303
+ needsInstall(workspaceDir, layerPaths, opts.force);
303
304
  if (shouldInstall) {
304
305
  await opts.beforeInstall?.();
305
306
  await installDeps(workspaceDir, (0, child_output_1.createPathMapper)(workspaceDir, layers));
@@ -321,6 +322,6 @@ function writeDerivedOutputs(workspaceDir, layers) {
321
322
  if (!(error instanceof locale_files_1.InvalidLocaleFileError))
322
323
  throw error;
323
324
  const showSource = (0, child_output_1.createPathMapper)(workspaceDir, layers);
324
- throw new locale_files_1.InvalidLocaleFileError(showSource(error.path), error.reason);
325
+ throw new locale_files_1.InvalidLocaleFileError(showSource(error.path), error.reason, error.details);
325
326
  }
326
327
  }
package/dist/workspace.js CHANGED
@@ -105,7 +105,7 @@ function directorySize(dir) {
105
105
  continue;
106
106
  }
107
107
  for (const entry of entries) {
108
- const path = (0, node_path_1.join)(current, entry.name);
108
+ const path = `${current}${node_path_1.sep}${entry.name}`;
109
109
  if (entry.isDirectory()) {
110
110
  pending.push(path);
111
111
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -60,20 +60,20 @@
60
60
  "scripts": {
61
61
  "build": "rimraf dist && tsc",
62
62
  "prepack": "pnpm run build",
63
- "release": "pnpm run lint && pnpm run format:check && pnpm run test && pnpm run test:cli && release-it",
63
+ "release": "pnpm run typecheck && pnpm run lint && pnpm run format:check && pnpm run test && pnpm run test:cli && release-it",
64
64
  "test": "node --import tsx --test 'tests/!(cli|cli-*).test.{ts,mts}'",
65
65
  "test:cli": "pnpm run build && DMS_PREBUILT=1 node --import tsx --test tests/cli.test.ts 'tests/cli-*.test.{ts,mts}'",
66
66
  "test:real-source": "pnpm run build && node dist/verify-source-runner.js",
67
67
  "format": "oxfmt .",
68
68
  "lint": "oxlint && antelopejs-check-interface-ranges && pnpm run docs:reference:check && pnpm run source-checks",
69
- "typecheck": "tsc --noEmit",
69
+ "typecheck": "tsc --noEmit && tsx scripts/typecheck-projects.ts tsconfig.templates.json tsconfig.tests.json",
70
70
  "lint:fix": "oxlint --fix --max-warnings=1000000 && oxfmt .",
71
71
  "knip": "knip --dependencies",
72
72
  "knip:all": "knip",
73
73
  "docs:reference": "tsx scripts/docs-reference.ts",
74
74
  "docs:reference:check": "tsx scripts/docs-reference.ts --check",
75
75
  "source-checks": "tsx scripts/source-checks.ts",
76
- "prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
76
+ "prepare": "if [ -e .git ] && command -v git >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1 && GIT_DIR=.git GIT_WORK_TREE=. git rev-parse --git-dir >/dev/null 2>&1; then GIT_DIR=.git GIT_WORK_TREE=. lefthook install; fi",
77
77
  "checks": "antelopejs-checks",
78
78
  "size": "antelopejs-check-size",
79
79
  "sync": "antelopejs-sync",
@@ -87,12 +87,13 @@
87
87
  "chokidar": "^4.0.3",
88
88
  "commander": "^13.1.0",
89
89
  "ignore": "^7.0.5",
90
- "semver": "^7.7.4"
90
+ "semver": "^7.7.4",
91
+ "yaml": "^2.9.1"
91
92
  },
92
93
  "devDependencies": {
93
- "@antelopejs/core": "^1.20.0",
94
+ "@antelopejs/core": "^1.21.2",
94
95
  "@antelopejs/interface-dms": "^0.10.0",
95
- "@antelopejs/tooling-configs": "^0.2.17",
96
+ "@antelopejs/tooling-configs": "^0.5.1",
96
97
  "@iconify/utils": "^3.1.0",
97
98
  "@iconify/vue": "^5.0.1",
98
99
  "@inertiajs/vue3": "^3.7.1",
@@ -104,15 +105,20 @@
104
105
  "@vueuse/core": "14.4.0",
105
106
  "defu": "^6.1.4",
106
107
  "devalue": "^5.9.4",
108
+ "fast-check": "^4.10.2",
107
109
  "knip": "^6.34.0",
108
110
  "lefthook": "^2.2.2",
111
+ "mongodb-memory-server": "^11.3.0",
109
112
  "ofetch": "^1.5.1",
110
113
  "oxfmt": "^0.66.0",
111
114
  "oxlint": "1.81.0",
112
115
  "oxlint-tsgolint": "^7.0.2001",
116
+ "pixelmatch": "^8.0.0",
117
+ "playwright-core": "^1.56.1",
113
118
  "release-it": "^19.2.4",
114
119
  "release-it-changelogen": "^0.1.0",
115
120
  "rimraf": "^6.1.2",
121
+ "sharp": "^0.35.5",
116
122
  "tsx": "^4.23.15",
117
123
  "typescript": "^6.0.3",
118
124
  "unplugin-auto-import": "^20.2.0",
@@ -137,6 +143,13 @@
137
143
  "pnpm": {
138
144
  "patchedDependencies": {
139
145
  "@vueuse/core@14.4.0": "templates/vue/patches/@vueuse__core@14.4.0.patch"
146
+ },
147
+ "packageExtensions": {
148
+ "@antelopejs/core@1.21": {
149
+ "dependencies": {
150
+ "@types/mocha": "^10.0.10"
151
+ }
152
+ }
140
153
  }
141
154
  },
142
155
  "antelopeJs": {
@@ -0,0 +1,132 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
3
+ import { isRecord, isString } from "./server/type-guards.mjs";
4
+
5
+ const BACKEND_SOURCE = "src";
6
+ const QUERY_OR_HASH = /[?#].*$/;
7
+
8
+ /**
9
+ * @param {string} manifest
10
+ * @returns {string | undefined}
11
+ */
12
+ function manifestName(manifest) {
13
+ try {
14
+ /** @type {unknown} */
15
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
16
+ return isRecord(parsed) && isString(parsed.name) ? parsed.name : undefined;
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * @typedef {{ root: string, name: string }} BackendPackage
24
+ */
25
+
26
+ /**
27
+ * The backend package that holds `directory`: the nearest `package.json` above
28
+ * it, when it names one of `backendPackages`.
29
+ *
30
+ * @param {string} directory
31
+ * @param {ReadonlySet<string>} backendPackages
32
+ * @param {Map<string, BackendPackage | null>} known
33
+ * @returns {BackendPackage | null}
34
+ */
35
+ function owningBackend(directory, backendPackages, known) {
36
+ const cached = known.get(directory);
37
+ if (cached !== undefined) return cached;
38
+ const manifest = join(directory, "package.json");
39
+ const parent = dirname(directory);
40
+ /** @type {BackendPackage | null} */
41
+ let owner = null;
42
+ if (existsSync(manifest)) {
43
+ const name = manifestName(manifest);
44
+ owner =
45
+ name !== undefined && backendPackages.has(name)
46
+ ? { root: directory, name }
47
+ : null;
48
+ } else if (parent !== directory) {
49
+ owner = owningBackend(parent, backendPackages, known);
50
+ }
51
+ known.set(directory, owner);
52
+ return owner;
53
+ }
54
+
55
+ /**
56
+ * The real path of a resolved module id under the `src` of a backend package,
57
+ * and that package's name, or `undefined`. The package that owns the file
58
+ * decides, not where it lies, so a backend linked or installed into the
59
+ * workspace is caught as well as its source checkout. Virtual ids and ids that
60
+ * are not files are ignored.
61
+ *
62
+ * @param {string} id
63
+ * @param {ReadonlySet<string>} backendPackages
64
+ * @param {Map<string, BackendPackage | null>} known
65
+ * @returns {{ file: string, name: string } | undefined}
66
+ */
67
+ function backendSourceFile(id, backendPackages, known) {
68
+ const path = id.replace(QUERY_OR_HASH, "");
69
+ if (!isAbsolute(path)) return undefined;
70
+ let file;
71
+ try {
72
+ file = realpathSync(path);
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ const owner = owningBackend(dirname(file), backendPackages, known);
77
+ if (owner === null) return undefined;
78
+ return relative(owner.root, file).split(sep)[0] === BACKEND_SOURCE
79
+ ? { file, name: owner.name }
80
+ : undefined;
81
+ }
82
+
83
+ /**
84
+ * @param {ReadonlySet<string>} backendPackages
85
+ * @returns {import("vite").Plugin}
86
+ */
87
+ function boundaryCheck(backendPackages) {
88
+ /** @type {Map<string, BackendPackage | null>} */
89
+ const known = new Map();
90
+ return {
91
+ name: "dms-backend-boundary",
92
+ enforce: "pre",
93
+ async resolveId(source, importer, options) {
94
+ const resolved = await this.resolve(source, importer, {
95
+ ...options,
96
+ skipSelf: true,
97
+ });
98
+ if (resolved === null) return null;
99
+ const backend = backendSourceFile(resolved.id, backendPackages, known);
100
+ if (backend === undefined) return resolved;
101
+ const from =
102
+ importer === undefined
103
+ ? "an entry point"
104
+ : importer.replace(QUERY_OR_HASH, "");
105
+ return this.error(
106
+ `Backend boundary: ${from} imports ${backend.file}, which is under the src of ${backend.name}. ` +
107
+ "The frontend reaches the backend only through its HTTP interface.",
108
+ );
109
+ },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Fail the build, a worker build and the dev server when a module of the
115
+ * frontend resolves to a file under the `src` of a backend package: the package
116
+ * that holds a module's layer, as the registry names it. The browser and the
117
+ * server renderer reach the backend only through its HTTP interface.
118
+ *
119
+ * The check reads what the whole resolution chain returns, so an alias, a
120
+ * symlink and an id marked external are all seen through. Vite builds workers
121
+ * with their own plugin list, so the plugin adds the check there too.
122
+ *
123
+ * @param {readonly string[]} backendPackages
124
+ * @returns {import("vite").Plugin}
125
+ */
126
+ export function rejectBackendSources(backendPackages) {
127
+ const names = new Set(backendPackages);
128
+ return {
129
+ ...boundaryCheck(names),
130
+ config: () => ({ worker: { plugins: () => [boundaryCheck(names)] } }),
131
+ };
132
+ }
@@ -18,6 +18,8 @@
18
18
  * quarter to four fifths, but every group is loaded whole by each page that
19
19
  * uses any of it, and the anonymous pages pay 70 to 115 KB of Brotli for code
20
20
  * only the signed-in shell uses.
21
+ *
22
+ * @type {import("vite").Rolldown.CodeSplittingOptions}
21
23
  */
22
24
  export const clientCodeSplitting = {
23
25
  groups: [{ name: "entry", tags: ["$initial"] }],
@@ -55,23 +55,27 @@ export function entryOwner(id, modules) {
55
55
  * their static imports, transitively, by file name.
56
56
  *
57
57
  * @param {import("vite").Rollup.OutputBundle} bundle
58
- * @returns {(import("vite").Rollup.OutputChunk | undefined)[]}
58
+ * @returns {import("vite").Rollup.OutputChunk[]}
59
59
  */
60
60
  function entryClosure(bundle) {
61
61
  const chunks = Object.values(bundle).filter(
62
+ /**
63
+ * @param {import("vite").Rollup.OutputChunk | import("vite").Rollup.OutputAsset} output
64
+ * @returns {output is import("vite").Rollup.OutputChunk}
65
+ */
62
66
  (output) => output.type === "chunk",
63
67
  );
64
68
  const byName = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
65
- /** @type {Set<string>} */
66
- const seen = new Set();
69
+ /** @type {Map<string, import("vite").Rollup.OutputChunk>} */
70
+ const seen = new Map();
67
71
  /** @param {import("vite").Rollup.OutputChunk | undefined} chunk */
68
72
  const visit = (chunk) => {
69
73
  if (!chunk || seen.has(chunk.fileName)) return;
70
- seen.add(chunk.fileName);
74
+ seen.set(chunk.fileName, chunk);
71
75
  for (const file of chunk.imports ?? []) visit(byName.get(file));
72
76
  };
73
77
  for (const chunk of chunks) if (chunk.isEntry) visit(chunk);
74
- return [...seen].map((file) => byName.get(file));
78
+ return [...seen.values()];
75
79
  }
76
80
 
77
81
  /**
@@ -31,10 +31,11 @@ export interface DmsFrontendBuild {
31
31
 
32
32
  /**
33
33
  * Adds Vite plugins to the application's build, for `ajs dms dev`,
34
- * `ajs dms build` and the preview alike.
34
+ * `ajs dms build` and the preview alike, and to the bundle of each web worker
35
+ * it starts with `new Worker(new URL(...))`.
35
36
  *
36
- * `create` runs in the build process each time Vite loads its configuration,
37
- * and returns real Vite plugin objects (an array, each with a `name`; false,
37
+ * `create` runs in the build process each time Vite loads its configuration
38
+ * and once more for each worker bundle, and returns real Vite plugin objects (an array, each with a `name`; false,
38
39
  * null and undefined entries are skipped and nested arrays flattened, as in a
39
40
  * Vite configuration) or nothing. It is synchronous: a Promise is refused,
40
41
  * because the configuration is built synchronously. The plugins come after
@@ -35,8 +35,9 @@ function deprioritize(html) {
35
35
  export function orderHeadForFirstPaint(html) {
36
36
  const prioritized = deprioritize(html);
37
37
  const stylesheet = prioritized.match(STYLESHEET);
38
- const script = prioritized.match(SCRIPT);
39
- if (!stylesheet || !script || stylesheet.index < script.index)
38
+ const stylesheetAt = prioritized.search(STYLESHEET);
39
+ const scriptAt = prioritized.search(SCRIPT);
40
+ if (!stylesheet || scriptAt < 0 || stylesheetAt < scriptAt)
40
41
  return prioritized;
41
42
  return prioritized
42
43
  .replace(STYLESHEET, "")
@@ -9,6 +9,8 @@ export const ICON_API_PATH = "/api/_dms/icons/";
9
9
  /**
10
10
  * The `@iconify/vue` API provider serving every icon the client bundle lacks
11
11
  * from `origin`, instead of `api.iconify.design` and its fallbacks.
12
+ *
13
+ * @param {string} origin
12
14
  */
13
15
  export function sameOriginIconProvider(origin) {
14
16
  return { resources: [origin], path: ICON_API_PATH };
@@ -1,5 +1,5 @@
1
1
  import { REDIRECT_STATUSES } from "../proxy-redirects.mjs";
2
- import { isString } from "../type-guards.mjs";
2
+ import { isObjectLike, isString } from "../type-guards.mjs";
3
3
  import { clientCountryHeader, clientIp, clientIpHeader } from "./client-ip.mjs";
4
4
 
5
5
  /**
@@ -39,7 +39,7 @@ export const GENERIC_ERROR_MESSAGE = "error.500.description";
39
39
  */
40
40
  export function publicBackendMessage(data) {
41
41
  /** @type {unknown} */
42
- const message = isString(data) ? data : data?.message;
42
+ const message = isObjectLike(data) ? data.message : data;
43
43
  return isString(message) && PUBLIC_AUTH_MESSAGE.test(message)
44
44
  ? message
45
45
  : undefined;
@@ -59,7 +59,7 @@ export function relayedHeaders(request, names) {
59
59
  ...clientCountryHeader(request),
60
60
  ...Object.fromEntries(
61
61
  names.flatMap((name) =>
62
- request.headers[name] ? [[name, request.headers[name]]] : [],
62
+ request.headers[name] ? [[name, String(request.headers[name])]] : [],
63
63
  ),
64
64
  ),
65
65
  };
@@ -86,7 +86,7 @@ export function relayedHeaders(request, names) {
86
86
  * is not JSON
87
87
  */
88
88
  export async function backend(path, request, options = {}) {
89
- /** @type {Record<string, string | string[]>} */
89
+ /** @type {Record<string, string>} */
90
90
  const headers = Object.fromEntries(
91
91
  Object.entries(options.headers ?? {}).map(([name, value]) => [
92
92
  name.toLowerCase(),
@@ -71,8 +71,8 @@ export function allowedEstablishEndpoints(
71
71
  .map((entry) => entry.trim());
72
72
  return [
73
73
  ...new Set(
74
- [...declared, ...fromEnvironment].filter(
75
- (entry) => isString(entry) && BACKEND_API_PATH.test(entry),
74
+ [...declared, ...fromEnvironment].flatMap((entry) =>
75
+ isString(entry) && BACKEND_API_PATH.test(entry) ? [entry] : [],
76
76
  ),
77
77
  ),
78
78
  ];
@@ -96,7 +96,10 @@ function decode(value) {
96
96
  );
97
97
  decipher.setAuthTag(payload.subarray(12, 28));
98
98
  return JSON.parse(
99
- Buffer.concat([decipher.update(payload.subarray(28)), decipher.final()]),
99
+ Buffer.concat([
100
+ decipher.update(payload.subarray(28)),
101
+ decipher.final(),
102
+ ]).toString(),
100
103
  );
101
104
  } catch {
102
105
  return undefined;
@@ -150,7 +153,7 @@ function appendCookie(response, name, value, cookieAttributes) {
150
153
  response.setHeader(
151
154
  "set-cookie",
152
155
  current
153
- ? [cookie, ...(Array.isArray(current) ? current : [current])]
156
+ ? [cookie, ...(Array.isArray(current) ? current : [String(current)])]
154
157
  : cookie,
155
158
  );
156
159
  }
@@ -0,0 +1,46 @@
1
+ import { authRejection } from "./auth-hooks.mjs";
2
+ import { compilePathPattern } from "./path-patterns.mjs";
3
+
4
+ /**
5
+ * @import { AuthProvider, DeclaredModule } from "./module-route-types.mjs"
6
+ */
7
+
8
+ /**
9
+ * The auth hooks: those of the highest-priority module that registers them.
10
+ * A module registering them twice, or after another one, is reported.
11
+ *
12
+ * @param {DeclaredModule[]} modules
13
+ * @param {(message: string) => void} warn
14
+ */
15
+ export function resolveAuth(modules, warn) {
16
+ /** @type {AuthProvider | undefined} */
17
+ let provider;
18
+ for (const module of modules) {
19
+ for (const auth of module.declared.auth) {
20
+ const reason = authRejection(auth);
21
+ if (reason) {
22
+ warn(
23
+ `Frontend module ${module.id} registers auth hooks, which are ignored: ${reason}.`,
24
+ );
25
+ continue;
26
+ }
27
+ if (provider) {
28
+ warn(
29
+ `Frontend module ${module.id} registers auth hooks, which are ignored: ${provider.id} (priority ${provider.priority ?? 0}) provides them.`,
30
+ );
31
+ continue;
32
+ }
33
+ provider = {
34
+ id: module.id,
35
+ priority: module.priority ?? 0,
36
+ signInPath: auth.signInPath,
37
+ signInExempt: auth.signInExempt?.flatMap(
38
+ (pattern) => compilePathPattern(pattern) ?? [],
39
+ ),
40
+ refreshSession: auth.refreshSession?.bind(auth),
41
+ publicSession: auth.publicSession?.bind(auth),
42
+ };
43
+ }
44
+ }
45
+ return provider;
46
+ }