@antelopejs/dms-frontend 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -11
- package/dist/commands/build-action.d.ts +3 -0
- package/dist/commands/build-action.js +74 -6
- package/dist/commands/build.js +7 -0
- package/dist/commands/clean-action.js +1 -1
- package/dist/commands/clean.js +1 -1
- package/dist/commands/dev-action.js +2 -2
- package/dist/commands/dev.js +1 -1
- package/dist/config.js +1 -0
- package/dist/derived-outputs.d.ts +1 -0
- package/dist/derived-outputs.js +3 -1
- package/dist/help.js +1 -1
- package/dist/internal/prepared-workspace.d.ts +12 -0
- package/dist/internal/prepared-workspace.js +74 -0
- package/dist/layer-watch.js +1 -1
- package/dist/locale-files.d.ts +4 -2
- package/dist/locale-files.js +11 -9
- package/dist/manifest.js +2 -1
- package/dist/ports.d.ts +3 -1
- package/dist/ports.js +1 -1
- package/dist/project-modules.d.ts +5 -1
- package/dist/project-modules.js +10 -7
- package/dist/verify-source-runner.js +2 -44
- package/dist/verify-source-workspace.d.ts +2 -0
- package/dist/verify-source-workspace.js +111 -0
- package/dist/workspace-setup.d.ts +1 -0
- package/dist/workspace-setup.js +3 -2
- package/package.json +7 -5
- package/templates/vue/backend-boundary.mjs +132 -0
- package/templates/vue/client-chunking.mjs +2 -0
- package/templates/vue/entry-weight.mjs +9 -5
- package/templates/vue/frontend-build.ts +4 -3
- package/templates/vue/head-order.mjs +3 -2
- package/templates/vue/icon-api.mjs +2 -0
- package/templates/vue/server/auth/backend.mjs +4 -4
- package/templates/vue/server/auth/establish-endpoints.mjs +2 -2
- package/templates/vue/server/auth/session.mjs +5 -2
- package/templates/vue/server/auth-declarations.mjs +46 -0
- package/templates/vue/server/auth-hooks.mjs +41 -3
- package/templates/vue/server/backend-proxies.mjs +16 -12
- package/templates/vue/server/backend-response.mjs +3 -3
- package/templates/vue/server/cli-report.mjs +22 -1
- package/templates/vue/server/client-error.mjs +5 -0
- package/templates/vue/server/content.mjs +24 -11
- package/templates/vue/server/email-bundle-watch.mjs +9 -3
- package/templates/vue/server/email.mjs +5 -3
- package/templates/vue/server/generated-files.mjs +16 -6
- package/templates/vue/server/icons.mjs +15 -11
- package/templates/vue/server/in-place-render.mjs +3 -3
- package/templates/vue/server/inertia.mjs +17 -3
- package/templates/vue/server/module-event.mjs +28 -16
- package/templates/vue/server/module-route-types.mjs +2 -2
- package/templates/vue/server/module-routes.mjs +24 -76
- package/templates/vue/server/module-server.mjs +27 -17
- package/templates/vue/server/module-testing.mjs +23 -8
- package/templates/vue/server/page-props.mjs +6 -5
- package/templates/vue/server/page-redirects.mjs +1 -1
- package/templates/vue/server/path-patterns.mjs +22 -11
- package/templates/vue/server/proxy-headers.mjs +1 -1
- package/templates/vue/server/proxy-redirects.mjs +1 -0
- package/templates/vue/server/proxy-request.mjs +2 -1
- package/templates/vue/server/proxy-transfers.mjs +17 -11
- package/templates/vue/server/public-endpoints.mjs +12 -16
- package/templates/vue/server/request-failure.mjs +1 -1
- package/templates/vue/server/request-routing.mjs +6 -1
- package/templates/vue/server/request-target.mjs +22 -9
- package/templates/vue/server/route-rules.mjs +12 -6
- package/templates/vue/server/static-files.mjs +12 -7
- package/templates/vue/server/vite-errors.mjs +13 -2
- package/templates/vue/server/web-body.mjs +2 -1
- package/templates/vue/server-module.ts +2 -1
- package/templates/vue/server.mjs +41 -20
- package/templates/vue/ui-inertia-page.mjs +1 -1
- package/templates/vue/vite.config.ts +9 -0
|
@@ -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
|
+
}
|
package/dist/workspace-setup.js
CHANGED
|
@@ -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 =
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms-frontend",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelope",
|
|
@@ -66,7 +66,7 @@
|
|
|
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",
|
|
@@ -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.
|
|
94
|
+
"@antelopejs/core": "^1.21.0",
|
|
94
95
|
"@antelopejs/interface-dms": "^0.10.0",
|
|
95
|
-
"@antelopejs/tooling-configs": "^0.
|
|
96
|
+
"@antelopejs/tooling-configs": "^0.3.3",
|
|
96
97
|
"@iconify/utils": "^3.1.0",
|
|
97
98
|
"@iconify/vue": "^5.0.1",
|
|
98
99
|
"@inertiajs/vue3": "^3.7.1",
|
|
@@ -104,6 +105,7 @@
|
|
|
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",
|
|
109
111
|
"ofetch": "^1.5.1",
|
|
@@ -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 {
|
|
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 {
|
|
66
|
-
const seen = new
|
|
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.
|
|
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
|
|
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
|
|
39
|
-
|
|
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 =
|
|
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
|
|
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].
|
|
75
|
-
|
|
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([
|
|
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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readSession, setRequestSession } from "./auth/session.mjs";
|
|
2
2
|
import { createServerEvent } from "./module-event.mjs";
|
|
3
|
-
import { compilePathPattern } from "./path-patterns.mjs";
|
|
3
|
+
import { compilePathPattern, PATH_PATTERN_FORMS } from "./path-patterns.mjs";
|
|
4
|
+
import { isFunction, isRecord, isString } from "./type-guards.mjs";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_SIGN_IN_PATH = "/auth";
|
|
6
7
|
const DEFAULT_ONBOARDING_PATH = "/onboarding";
|
|
@@ -17,7 +18,44 @@ const HOOK_ROUTE = Object.freeze({ bodyLimit: 0 });
|
|
|
17
18
|
*/
|
|
18
19
|
function defaultExempt(signInPath) {
|
|
19
20
|
const signIn = signInPath === "/" ? "/" : `${signInPath}/**`;
|
|
20
|
-
return [signIn, DEFAULT_ONBOARDING_PATH].
|
|
21
|
+
return [signIn, DEFAULT_ONBOARDING_PATH].flatMap(
|
|
22
|
+
(pattern) => compilePathPattern(pattern) ?? [],
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const AUTH_HOOKS = ["refreshSession", "publicSession"];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Why an auth hooks registration is ignored, or `undefined`.
|
|
30
|
+
*
|
|
31
|
+
* @param {unknown} auth
|
|
32
|
+
*/
|
|
33
|
+
export function authRejection(auth) {
|
|
34
|
+
if (!isRecord(auth)) return "it is not an object";
|
|
35
|
+
for (const hook of AUTH_HOOKS)
|
|
36
|
+
if (auth[hook] !== undefined && !isFunction(auth[hook]))
|
|
37
|
+
return `${hook} is not a function`;
|
|
38
|
+
if (
|
|
39
|
+
auth.signInPath !== undefined &&
|
|
40
|
+
(!isString(auth.signInPath) ||
|
|
41
|
+
!compilePathPattern(auth.signInPath) ||
|
|
42
|
+
auth.signInPath.includes("*"))
|
|
43
|
+
)
|
|
44
|
+
return "signInPath is not a page path";
|
|
45
|
+
return exemptionRejection(auth.signInExempt);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {unknown} signInExempt */
|
|
49
|
+
function exemptionRejection(signInExempt) {
|
|
50
|
+
if (signInExempt === undefined) return undefined;
|
|
51
|
+
if (!Array.isArray(signInExempt))
|
|
52
|
+
return `signInExempt is not a list of ${PATH_PATTERN_FORMS} patterns`;
|
|
53
|
+
for (const pattern of signInExempt)
|
|
54
|
+
if (!compilePathPattern(pattern))
|
|
55
|
+
return isString(pattern)
|
|
56
|
+
? `signInExempt holds "${pattern}", which is not ${PATH_PATTERN_FORMS}`
|
|
57
|
+
: "signInExempt holds an entry that is not a string";
|
|
58
|
+
return undefined;
|
|
21
59
|
}
|
|
22
60
|
|
|
23
61
|
/**
|
|
@@ -72,7 +110,7 @@ export function serverAuth(provider, context) {
|
|
|
72
110
|
* Renews the request's session after the backend refused it. Returns the
|
|
73
111
|
* new session, which the rest of the request runs with, or `undefined`.
|
|
74
112
|
*
|
|
75
|
-
* @param {import("
|
|
113
|
+
* @param {import("./request-target.mjs").ServerRequest} request
|
|
76
114
|
* @param {import("node:http").ServerResponse} response
|
|
77
115
|
*/
|
|
78
116
|
async refresh(request, response) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { compilePathPattern } from "./path-patterns.mjs";
|
|
2
|
-
import { isNonNullObject } from "./type-guards.mjs";
|
|
1
|
+
import { compilePathPattern, PATH_PATTERN_FORMS } from "./path-patterns.mjs";
|
|
2
|
+
import { isNonNullObject, isString } from "./type-guards.mjs";
|
|
3
3
|
|
|
4
4
|
/** @typedef {import("../server-module.ts").DmsBackendProxyOptions} ProxyOptions */
|
|
5
5
|
/**
|
|
@@ -53,12 +53,13 @@ function literalPrefix(pattern) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Why a proxy declaration is ignored, or `undefined`.
|
|
55
55
|
*
|
|
56
|
-
* @param {string} pattern
|
|
56
|
+
* @param {string | undefined} pattern
|
|
57
57
|
* @param {ProxyOptions | undefined} options
|
|
58
58
|
*/
|
|
59
59
|
export function proxyRejection(pattern, options) {
|
|
60
60
|
const compiled = compilePathPattern(pattern);
|
|
61
|
-
if (!compiled
|
|
61
|
+
if (!compiled || !isString(pattern))
|
|
62
|
+
return `the pattern is not ${PATH_PATTERN_FORMS}`;
|
|
62
63
|
const prefix = literalPrefix(pattern);
|
|
63
64
|
if (prefix === "/")
|
|
64
65
|
return "its first segment is not literal: it would take every page";
|
|
@@ -81,15 +82,15 @@ export function proxyRejection(pattern, options) {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/**
|
|
84
|
-
* @param {
|
|
85
|
+
* @param {import("./path-patterns.mjs").CompiledPathPattern} compiled A pattern `proxyRejection` accepted
|
|
85
86
|
* @param {ProxyOptions | undefined} options
|
|
86
87
|
* @param {string} owner Who declared it
|
|
87
88
|
* @returns {BackendProxy}
|
|
88
89
|
*/
|
|
89
|
-
function compileProxy(
|
|
90
|
+
function compileProxy(compiled, options, owner) {
|
|
90
91
|
return {
|
|
91
|
-
pattern:
|
|
92
|
-
path: pattern,
|
|
92
|
+
pattern: compiled,
|
|
93
|
+
path: compiled.pattern,
|
|
93
94
|
owner,
|
|
94
95
|
auth: options?.auth ?? DEFAULT_AUTH,
|
|
95
96
|
maxBodyBytes: options?.maxBodyBytes,
|
|
@@ -122,15 +123,18 @@ export function resolveBackendProxies(declared, manifest, warn) {
|
|
|
122
123
|
* @param {string} owner
|
|
123
124
|
*/
|
|
124
125
|
const add = (pattern, options, owner) => {
|
|
125
|
-
const
|
|
126
|
+
const compiled = compilePathPattern(pattern);
|
|
126
127
|
const current = kept.get(pattern);
|
|
127
|
-
|
|
128
|
+
const reason =
|
|
129
|
+
proxyRejection(pattern, options) ??
|
|
130
|
+
(current && `${current.owner} declares it`);
|
|
131
|
+
if (!compiled || reason) {
|
|
128
132
|
warn(
|
|
129
|
-
`${owner} declares the backend proxy "${pattern}", which is ignored: ${reason
|
|
133
|
+
`${owner} declares the backend proxy "${pattern}", which is ignored: ${reason}.`,
|
|
130
134
|
);
|
|
131
135
|
return;
|
|
132
136
|
}
|
|
133
|
-
kept.set(pattern, compileProxy(
|
|
137
|
+
kept.set(pattern, compileProxy(compiled, options, owner));
|
|
134
138
|
};
|
|
135
139
|
for (const { id, pattern, options } of declared)
|
|
136
140
|
add(pattern, options, `Frontend module ${id}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GENERIC_ERROR_MESSAGE } from "./auth/backend.mjs";
|
|
2
2
|
import { isFrontendVisit, redirectFrontendVisit } from "./inertia.mjs";
|
|
3
|
-
import { isString } from "./type-guards.mjs";
|
|
3
|
+
import { isObjectLike, isString } from "./type-guards.mjs";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* What a JSON caller gets when the backend fails: the generic i18n key of
|
|
@@ -45,7 +45,7 @@ async function refusalCode(response) {
|
|
|
45
45
|
try {
|
|
46
46
|
/** @type {unknown} */
|
|
47
47
|
const data = JSON.parse(text);
|
|
48
|
-
value =
|
|
48
|
+
value = isObjectLike(data) ? data.message : data;
|
|
49
49
|
} catch {}
|
|
50
50
|
return isString(value) && REFUSAL_CODE.test(value) ? value : undefined;
|
|
51
51
|
}
|
|
@@ -67,7 +67,7 @@ export async function backendResponseError(response) {
|
|
|
67
67
|
* qualifies: a refused destination sent to itself again would loop.
|
|
68
68
|
*
|
|
69
69
|
* @param {unknown} error What the request failed with
|
|
70
|
-
* @param {import("
|
|
70
|
+
* @param {import("./request-target.mjs").ServerRequest} request
|
|
71
71
|
* @param {import("node:http").ServerResponse} response
|
|
72
72
|
* @param {() => Promise<typeof import("../ssr-renderer.ts")>} loadRenderer
|
|
73
73
|
* @returns {Promise<boolean>} Whether the visit was redirected
|