@antelopejs/dms-frontend 0.9.3 → 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 +46 -29
- package/dist/commands/build-action.d.ts +3 -0
- package/dist/commands/build-action.js +77 -8
- 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 +4 -3
- package/dist/commands/dev.js +1 -1
- package/dist/commands/prepare-action.js +1 -1
- package/dist/commands/start-action.js +3 -24
- package/dist/commands/workspace-task.js +2 -1
- package/dist/config.js +1 -0
- package/dist/derived-outputs.d.ts +1 -0
- package/dist/derived-outputs.js +6 -4
- package/dist/discovery.js +17 -6
- package/dist/entry-weight.js +15 -0
- package/dist/fs-sync.js +7 -4
- package/dist/help.js +1 -1
- package/dist/index.js +2 -1
- package/dist/internal/built-workspace.d.ts +4 -0
- package/dist/internal/built-workspace.js +43 -0
- package/dist/internal/ensure.d.ts +1 -0
- package/dist/internal/ensure.js +8 -0
- package/dist/internal/locale-catalogs.d.ts +8 -3
- package/dist/internal/locale-catalogs.js +16 -6
- package/dist/internal/manifest-decoder.d.ts +7 -0
- package/dist/internal/manifest-decoder.js +81 -0
- package/dist/internal/own-release.d.ts +5 -0
- package/dist/internal/own-release.js +20 -0
- package/dist/internal/plain-object.d.ts +1 -0
- package/dist/internal/plain-object.js +6 -0
- package/dist/internal/prepared-workspace.d.ts +12 -0
- package/dist/internal/prepared-workspace.js +74 -0
- package/dist/internal/text-list.d.ts +1 -0
- package/dist/internal/text-list.js +10 -0
- package/dist/internal/vite-manifest.d.ts +6 -0
- package/dist/internal/vite-manifest.js +24 -0
- package/dist/layer-watch.js +25 -24
- package/dist/layers.d.ts +1 -1
- package/dist/layers.js +3 -1
- package/dist/locale-files.d.ts +6 -3
- package/dist/locale-files.js +15 -9
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +46 -32
- package/dist/materialize.js +6 -2
- package/dist/module-set.js +2 -1
- package/dist/node-options.d.ts +1 -0
- package/dist/node-options.js +8 -0
- package/dist/output.d.ts +15 -9
- package/dist/output.js +30 -18
- package/dist/ports.d.ts +3 -1
- package/dist/ports.js +1 -1
- package/dist/private-values.d.ts +1 -1
- package/dist/private-values.js +5 -3
- package/dist/project-modules.d.ts +5 -1
- package/dist/project-modules.js +23 -15
- package/dist/renderer-range.js +5 -3
- package/dist/server-process.js +1 -1
- package/dist/verify-source-build.js +10 -2
- package/dist/verify-source-client-bundle.d.ts +1 -0
- package/dist/verify-source-client-bundle.js +30 -0
- package/dist/verify-source-runner.js +36 -94
- package/dist/verify-source-sources.js +49 -11
- 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 +4 -3
- package/package.json +11 -7
- package/skills/dms-frontend/SKILL.md +1 -1
- package/templates/vue/app-runtime.ts +2 -9
- package/templates/vue/backend-boundary.mjs +132 -0
- package/templates/vue/client-chunking.mjs +2 -0
- package/templates/vue/client-server-guard.mjs +16 -0
- package/templates/vue/compress-assets.mjs +9 -1
- package/templates/vue/entry-weight.mjs +29 -3
- package/templates/vue/frontend-build.ts +4 -3
- package/templates/vue/frontend-module.ts +1 -0
- package/templates/vue/frontend-runtime/module-registration.ts +2 -2
- package/templates/vue/frontend-runtime/state.ts +2 -2
- package/templates/vue/frontend-runtime/types.ts +8 -2
- package/templates/vue/head-order.mjs +10 -2
- package/templates/vue/icon-api.mjs +2 -0
- package/templates/vue/icon-scan.mjs +4 -0
- package/templates/vue/server/auth/backend.mjs +42 -5
- package/templates/vue/server/auth/client-ip.mjs +52 -4
- package/templates/vue/server/auth/establish-endpoints.mjs +11 -7
- package/templates/vue/server/auth/session.mjs +86 -4
- package/templates/vue/server/auth-declarations.mjs +46 -0
- package/templates/vue/server/auth-hooks.mjs +55 -7
- package/templates/vue/server/backend-proxies.mjs +78 -18
- package/templates/vue/server/backend-request.mjs +61 -0
- package/templates/vue/server/backend-response.mjs +23 -6
- package/templates/vue/server/built-files.mjs +7 -0
- package/templates/vue/server/cli-report.mjs +75 -12
- package/templates/vue/server/client-error.mjs +5 -0
- package/templates/vue/server/client-manifest.mjs +23 -1
- package/templates/vue/server/content.mjs +40 -10
- package/templates/vue/server/dev-private-files.mjs +17 -1
- package/templates/vue/server/dev-styles.mjs +38 -0
- package/templates/vue/server/email-bundle-watch.mjs +9 -3
- package/templates/vue/server/email.mjs +14 -6
- package/templates/vue/server/favicon.mjs +3 -0
- package/templates/vue/server/generated-files.mjs +221 -0
- package/templates/vue/server/homepage.mjs +21 -9
- package/templates/vue/server/html-tag.mjs +4 -0
- package/templates/vue/server/icons.mjs +93 -16
- package/templates/vue/server/in-place-render.mjs +35 -0
- package/templates/vue/server/inertia.mjs +76 -3
- package/templates/vue/server/module-event.mjs +118 -25
- package/templates/vue/server/module-route-types.mjs +78 -0
- package/templates/vue/server/module-routes.mjs +151 -96
- package/templates/vue/server/module-server.mjs +121 -40
- package/templates/vue/server/module-testing.mjs +36 -12
- package/templates/vue/server/page-props.mjs +94 -3
- package/templates/vue/server/page-redirects.mjs +37 -0
- package/templates/vue/server/path-patterns.mjs +35 -12
- package/templates/vue/server/proxy-compression.mjs +22 -0
- package/templates/vue/server/proxy-headers.mjs +12 -2
- package/templates/vue/server/proxy-redirects.mjs +21 -5
- package/templates/vue/server/proxy-request.mjs +58 -0
- package/templates/vue/server/proxy-transfers.mjs +44 -10
- package/templates/vue/server/public-endpoints.mjs +139 -28
- package/templates/vue/server/public-options.mjs +20 -6
- package/templates/vue/server/request-failure.mjs +24 -1
- package/templates/vue/server/request-routing.mjs +32 -10
- package/templates/vue/server/request-target.mjs +30 -11
- package/templates/vue/server/route-rules.mjs +48 -18
- package/templates/vue/server/ssr-module.mjs +32 -0
- package/templates/vue/server/static-files.mjs +35 -7
- package/templates/vue/server/type-guards.mjs +71 -0
- package/templates/vue/server/vite-errors.mjs +45 -7
- package/templates/vue/server/web-body.mjs +5 -1
- package/templates/vue/server-module.ts +12 -2
- package/templates/vue/server.mjs +162 -157
- package/templates/vue/sfc-global-types.mjs +112 -13
- package/templates/vue/ssr-renderer.ts +2 -2
- package/templates/vue/ui-inertia-page.mjs +16 -1
- package/templates/vue/ui-kbd.mjs +2 -1
- package/templates/vue/ui-request-state.mjs +2 -1
- package/templates/vue/vite.config.ts +9 -0
|
@@ -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"] }],
|
|
@@ -2,11 +2,20 @@ import { relative, sep } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
const SERVER_ENTRY = /(?:^|[\\/])dms\.server\.[cm]?[jt]sx?$/;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} id
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
5
9
|
function stripQuery(id) {
|
|
6
10
|
const query = id.indexOf("?");
|
|
7
11
|
return query === -1 ? id : id.slice(0, query);
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} file
|
|
16
|
+
* @param {readonly string[]} moduleRoots
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
10
19
|
function insideModuleServerDirectory(file, moduleRoots) {
|
|
11
20
|
return moduleRoots.some((root) => {
|
|
12
21
|
const path = relative(root, file);
|
|
@@ -20,6 +29,10 @@ function insideModuleServerDirectory(file, moduleRoots) {
|
|
|
20
29
|
* Whether `id`, a resolved module id, is server code: a module's server entry
|
|
21
30
|
* (`dms.server.*`) or a file under a `server/` directory of a module. Installed
|
|
22
31
|
* packages never are: `node_modules` has its own `server` directories.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} id
|
|
34
|
+
* @param {readonly string[]} moduleRoots
|
|
35
|
+
* @returns {boolean}
|
|
23
36
|
*/
|
|
24
37
|
export function isServerSource(id, moduleRoots) {
|
|
25
38
|
if (id.startsWith("\0")) return false;
|
|
@@ -44,6 +57,9 @@ export function isServerSource(id, moduleRoots) {
|
|
|
44
57
|
* import; the guard sits in the module graph instead, where every resolved file
|
|
45
58
|
* passes. Kept beside vite.config.ts rather than inside it so the plugin can be
|
|
46
59
|
* exercised on its own, the way head-order.mjs is.
|
|
60
|
+
*
|
|
61
|
+
* @param {readonly string[]} moduleRoots
|
|
62
|
+
* @returns {import("vite").Plugin}
|
|
47
63
|
*/
|
|
48
64
|
export function rejectServerSourcesInClient(moduleRoots) {
|
|
49
65
|
let ssrBuild = false;
|
|
@@ -16,10 +16,14 @@ const MINIMUM_SIZE_BYTES = 1_024;
|
|
|
16
16
|
const compressBrotli = promisify(brotliCompress);
|
|
17
17
|
const compressGzip = promisify(gzip);
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} directory
|
|
21
|
+
* @returns {Promise<string[]>}
|
|
22
|
+
*/
|
|
19
23
|
async function assetFiles(directory) {
|
|
20
24
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
21
25
|
const files = await Promise.all(
|
|
22
|
-
entries.map((entry) => {
|
|
26
|
+
entries.map(async (entry) => {
|
|
23
27
|
const path = join(directory, entry.name);
|
|
24
28
|
return entry.isDirectory() ? assetFiles(path) : [path];
|
|
25
29
|
}),
|
|
@@ -27,6 +31,10 @@ async function assetFiles(directory) {
|
|
|
27
31
|
return files.flat();
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} path
|
|
36
|
+
* @returns {Promise<void>}
|
|
37
|
+
*/
|
|
30
38
|
async function compressAsset(path) {
|
|
31
39
|
if (!COMPRESSIBLE_EXTENSIONS.has(extname(path))) return;
|
|
32
40
|
const content = await readFile(path);
|
|
@@ -10,11 +10,20 @@ export const TRANSLATIONS_OWNER = "translations";
|
|
|
10
10
|
|
|
11
11
|
const LOCALE_CATALOG = /[\\/]locales\.generated[\\/]/;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} id
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
13
17
|
function stripQuery(id) {
|
|
14
18
|
const query = id.indexOf("?");
|
|
15
19
|
return query === -1 ? id : id.slice(0, query);
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} root
|
|
24
|
+
* @param {string} file
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
18
27
|
function isInside(root, file) {
|
|
19
28
|
const path = relative(root, file);
|
|
20
29
|
return Boolean(path) && !path.startsWith("..") && !path.startsWith(sep);
|
|
@@ -26,7 +35,9 @@ function isInside(root, file) {
|
|
|
26
35
|
* what every page shares (the renderer's runtime, the UI and icon libraries,
|
|
27
36
|
* an installed package).
|
|
28
37
|
*
|
|
38
|
+
* @param {string} id
|
|
29
39
|
* @param {{ root: string, name?: string, packageName?: string, id: string }[]} modules
|
|
40
|
+
* @returns {string}
|
|
30
41
|
*/
|
|
31
42
|
export function entryOwner(id, modules) {
|
|
32
43
|
if (id.startsWith("\0")) return SHARED_OWNER;
|
|
@@ -42,20 +53,29 @@ export function entryOwner(id, modules) {
|
|
|
42
53
|
/**
|
|
43
54
|
* The files a page loads before its first line runs: the entry chunks and
|
|
44
55
|
* their static imports, transitively, by file name.
|
|
56
|
+
*
|
|
57
|
+
* @param {import("vite").Rollup.OutputBundle} bundle
|
|
58
|
+
* @returns {import("vite").Rollup.OutputChunk[]}
|
|
45
59
|
*/
|
|
46
60
|
function entryClosure(bundle) {
|
|
47
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
|
+
*/
|
|
48
66
|
(output) => output.type === "chunk",
|
|
49
67
|
);
|
|
50
68
|
const byName = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
|
|
51
|
-
|
|
69
|
+
/** @type {Map<string, import("vite").Rollup.OutputChunk>} */
|
|
70
|
+
const seen = new Map();
|
|
71
|
+
/** @param {import("vite").Rollup.OutputChunk | undefined} chunk */
|
|
52
72
|
const visit = (chunk) => {
|
|
53
73
|
if (!chunk || seen.has(chunk.fileName)) return;
|
|
54
|
-
seen.
|
|
74
|
+
seen.set(chunk.fileName, chunk);
|
|
55
75
|
for (const file of chunk.imports ?? []) visit(byName.get(file));
|
|
56
76
|
};
|
|
57
77
|
for (const chunk of chunks) if (chunk.isEntry) visit(chunk);
|
|
58
|
-
return [...seen
|
|
78
|
+
return [...seen.values()];
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
/**
|
|
@@ -64,10 +84,13 @@ function entryClosure(bundle) {
|
|
|
64
84
|
* rendered for each (`renderedLength`, before minification): an estimate that
|
|
65
85
|
* needs nothing the bundle does not already hold.
|
|
66
86
|
*
|
|
87
|
+
* @param {import("vite").Rollup.OutputBundle} bundle
|
|
88
|
+
* @param {{ root: string, name?: string, packageName?: string, id: string }[]} modules
|
|
67
89
|
* @returns {{ files: string[], bytes: number, owners: Record<string, number> }}
|
|
68
90
|
*/
|
|
69
91
|
export function attributeEntry(bundle, modules) {
|
|
70
92
|
const files = entryClosure(bundle);
|
|
93
|
+
/** @type {Record<string, number>} */
|
|
71
94
|
const owners = {};
|
|
72
95
|
let bytes = 0;
|
|
73
96
|
for (const chunk of files) {
|
|
@@ -109,6 +132,9 @@ export function attributeEntry(bundle, modules) {
|
|
|
109
132
|
*
|
|
110
133
|
* The weight is recorded once the bundle is written: plugins that generate the
|
|
111
134
|
* bundle after this one still rewrite the chunks' code.
|
|
135
|
+
*
|
|
136
|
+
* @param {{ root: string, name?: string, packageName?: string, id: string }[]} modules
|
|
137
|
+
* @returns {import("vite").Plugin}
|
|
112
138
|
*/
|
|
113
139
|
export function recordEntryWeight(modules) {
|
|
114
140
|
let ssrBuild = false;
|
|
@@ -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
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { usePage } from "@inertiajs/vue3";
|
|
2
|
-
import { ofetch } from "ofetch";
|
|
3
2
|
import {
|
|
4
3
|
type Component,
|
|
5
4
|
type EffectScope,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
type DmsPluginRegistrationOptions,
|
|
20
19
|
type DmsPluginSetup,
|
|
21
20
|
type DmsRoute,
|
|
21
|
+
type DmsServerFetch,
|
|
22
22
|
type DmsSharedPagePayload,
|
|
23
23
|
type RouteLocationRaw,
|
|
24
24
|
} from "./types";
|
|
@@ -212,7 +212,7 @@ export interface DmsFrontendRuntime {
|
|
|
212
212
|
/** Whether to warn about what a development render does wrong. */
|
|
213
213
|
devWarnings?: boolean;
|
|
214
214
|
runtimeHooks: Map<string, DmsRuntimeHook[]>;
|
|
215
|
-
serverFetch?:
|
|
215
|
+
serverFetch?: DmsServerFetch;
|
|
216
216
|
currentError: Ref<Error | null>;
|
|
217
217
|
route: DmsRoute;
|
|
218
218
|
currentRoutePattern?: string;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ofetch } from "ofetch";
|
|
2
1
|
import {
|
|
3
2
|
type App,
|
|
4
3
|
effectScope,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
import {
|
|
20
19
|
type DmsAppContext,
|
|
21
20
|
type DmsRoute,
|
|
21
|
+
type DmsServerFetch,
|
|
22
22
|
type DmsRuntimeConfig,
|
|
23
23
|
} from "./types";
|
|
24
24
|
|
|
@@ -65,7 +65,7 @@ export function forgetBrowserRuntime(runtime: DmsFrontendRuntime): void {
|
|
|
65
65
|
|
|
66
66
|
/** Creates mutable runtime state owned by one Vue application. */
|
|
67
67
|
export function createDmsFrontendRuntime(
|
|
68
|
-
serverFetch?:
|
|
68
|
+
serverFetch?: DmsServerFetch,
|
|
69
69
|
hydratedAsyncData?: Record<string, unknown>,
|
|
70
70
|
isServer = false,
|
|
71
71
|
requestCookies?: string,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type InertiaLinkProps } from "@inertiajs/vue3";
|
|
2
|
-
import { type FetchOptions
|
|
2
|
+
import { type FetchOptions } from "ofetch";
|
|
3
3
|
import {
|
|
4
4
|
type App,
|
|
5
5
|
type ComputedRef,
|
|
@@ -246,6 +246,12 @@ export interface UseAsyncDataOptions<T> {
|
|
|
246
246
|
*/
|
|
247
247
|
refreshInterval?: MaybeRefOrGetter<number | null | undefined>;
|
|
248
248
|
}
|
|
249
|
+
/** What the runtime calls to read the backend: `ofetch`, or a stand-in for it. */
|
|
250
|
+
export type DmsServerFetch = (
|
|
251
|
+
request: string,
|
|
252
|
+
options?: FetchOptions,
|
|
253
|
+
) => Promise<unknown>;
|
|
254
|
+
|
|
249
255
|
/**
|
|
250
256
|
* `useDmsFetch`'s options: ofetch's request options, of which the method, the
|
|
251
257
|
* query, the params and the body may be refs or getters, and the async data
|
|
@@ -271,7 +277,7 @@ export interface UseFetchOptions<T>
|
|
|
271
277
|
context: DmsFetchTransformContext,
|
|
272
278
|
) => T | Promise<T>;
|
|
273
279
|
/** Replaces the fetch function; it receives the run's signal. */
|
|
274
|
-
$fetch?:
|
|
280
|
+
$fetch?: DmsServerFetch;
|
|
275
281
|
}
|
|
276
282
|
export interface DmsErrorData {
|
|
277
283
|
statusCode?: number;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const SCRIPT = /<script[^>]*\stype="module"[^>]*><\/script>/;
|
|
2
2
|
const STYLESHEET = /[ \t]*<link rel="stylesheet"[^>]*>\n?/;
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} html
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
4
8
|
function deprioritize(html) {
|
|
5
9
|
return html.replace(SCRIPT, (tag) =>
|
|
6
10
|
tag.includes("fetchpriority")
|
|
@@ -24,12 +28,16 @@ function deprioritize(html) {
|
|
|
24
28
|
* HTTP/1.1 the generated server speaks. Kept beside vite.config.ts rather than
|
|
25
29
|
* inside it so the transform can be exercised on its own, the way
|
|
26
30
|
* client-manifest.mjs is.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} html
|
|
33
|
+
* @returns {string}
|
|
27
34
|
*/
|
|
28
35
|
export function orderHeadForFirstPaint(html) {
|
|
29
36
|
const prioritized = deprioritize(html);
|
|
30
37
|
const stylesheet = prioritized.match(STYLESHEET);
|
|
31
|
-
const
|
|
32
|
-
|
|
38
|
+
const stylesheetAt = prioritized.search(STYLESHEET);
|
|
39
|
+
const scriptAt = prioritized.search(SCRIPT);
|
|
40
|
+
if (!stylesheet || scriptAt < 0 || stylesheetAt < scriptAt)
|
|
33
41
|
return prioritized;
|
|
34
42
|
return prioritized
|
|
35
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 };
|
|
@@ -19,6 +19,10 @@ const SOURCE_SCRIPT_GLOB = "app/**/*.ts";
|
|
|
19
19
|
* fetched from the Iconify API at run time. Every frontend source root lives
|
|
20
20
|
* under the workspace (`frontend-modules/`, enforced when the registry is
|
|
21
21
|
* written), so a relative glob reaches it. Globs are POSIX on every platform.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} viteRoot
|
|
24
|
+
* @param {readonly string[]} sourceRoots
|
|
25
|
+
* @returns {string[]}
|
|
22
26
|
*/
|
|
23
27
|
export function iconScanGlobs(viteRoot, sourceRoots) {
|
|
24
28
|
const sourceGlobs = sourceRoots.map((root) =>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { REDIRECT_STATUSES } from "../proxy-redirects.mjs";
|
|
2
|
+
import { isObjectLike, isString } from "../type-guards.mjs";
|
|
2
3
|
import { clientCountryHeader, clientIp, clientIpHeader } from "./client-ip.mjs";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -14,6 +15,10 @@ export const CLIENT_HEADERS = [
|
|
|
14
15
|
];
|
|
15
16
|
|
|
16
17
|
export class UpstreamError extends Error {
|
|
18
|
+
/**
|
|
19
|
+
* @param {number} status
|
|
20
|
+
* @param {string} message
|
|
21
|
+
*/
|
|
17
22
|
constructor(status, message) {
|
|
18
23
|
super(message);
|
|
19
24
|
this.status = status;
|
|
@@ -27,9 +32,15 @@ const PUBLIC_AUTH_MESSAGE = /^error\.[a-z0-9_.-]{1,100}$/;
|
|
|
27
32
|
*/
|
|
28
33
|
export const GENERIC_ERROR_MESSAGE = "error.500.description";
|
|
29
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @param {unknown} data A backend answer: any JSON value, or the text of one
|
|
37
|
+
* that is not JSON
|
|
38
|
+
* @returns {string | undefined}
|
|
39
|
+
*/
|
|
30
40
|
export function publicBackendMessage(data) {
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
/** @type {unknown} */
|
|
42
|
+
const message = isObjectLike(data) ? data.message : data;
|
|
43
|
+
return isString(message) && PUBLIC_AUTH_MESSAGE.test(message)
|
|
33
44
|
? message
|
|
34
45
|
: undefined;
|
|
35
46
|
}
|
|
@@ -37,8 +48,10 @@ export function publicBackendMessage(data) {
|
|
|
37
48
|
/**
|
|
38
49
|
* The headers of a client request the server relays to the backend.
|
|
39
50
|
*
|
|
40
|
-
* @param request The client
|
|
41
|
-
*
|
|
51
|
+
* @param {import("./client-ip.mjs").ForwardedRequest} request The client
|
|
52
|
+
* request
|
|
53
|
+
* @param {readonly string[]} names Lowercase names of the request headers to
|
|
54
|
+
* copy when present
|
|
42
55
|
* @returns The client country header, then the copied headers
|
|
43
56
|
*/
|
|
44
57
|
export function relayedHeaders(request, names) {
|
|
@@ -46,7 +59,7 @@ export function relayedHeaders(request, names) {
|
|
|
46
59
|
...clientCountryHeader(request),
|
|
47
60
|
...Object.fromEntries(
|
|
48
61
|
names.flatMap((name) =>
|
|
49
|
-
request.headers[name] ? [[name, request.headers[name]]] : [],
|
|
62
|
+
request.headers[name] ? [[name, String(request.headers[name])]] : [],
|
|
50
63
|
),
|
|
51
64
|
),
|
|
52
65
|
};
|
|
@@ -58,8 +71,22 @@ export function relayedHeaders(request, names) {
|
|
|
58
71
|
* `options.headers` adds request headers; the client's address and the
|
|
59
72
|
* relayed client headers are set after them, so no caller replaces those.
|
|
60
73
|
* A redirect is not followed: it is no JSON answer, so it fails with a 502.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} path
|
|
76
|
+
* @param {import("./client-ip.mjs").ForwardedRequest} request The client
|
|
77
|
+
* request
|
|
78
|
+
* @param {{
|
|
79
|
+
* method?: string,
|
|
80
|
+
* body?: unknown,
|
|
81
|
+
* token?: string,
|
|
82
|
+
* relay?: boolean,
|
|
83
|
+
* headers?: Record<string, string>
|
|
84
|
+
* }} [options]
|
|
85
|
+
* @returns {Promise<unknown>} The answer's JSON, `{}` for a successful one that
|
|
86
|
+
* is not JSON
|
|
61
87
|
*/
|
|
62
88
|
export async function backend(path, request, options = {}) {
|
|
89
|
+
/** @type {Record<string, string>} */
|
|
63
90
|
const headers = Object.fromEntries(
|
|
64
91
|
Object.entries(options.headers ?? {}).map(([name, value]) => [
|
|
65
92
|
name.toLowerCase(),
|
|
@@ -87,6 +114,7 @@ export async function backend(path, request, options = {}) {
|
|
|
87
114
|
throw new UpstreamError(502, GENERIC_ERROR_MESSAGE);
|
|
88
115
|
}
|
|
89
116
|
const text = await response.text();
|
|
117
|
+
/** @type {unknown} */
|
|
90
118
|
let data;
|
|
91
119
|
try {
|
|
92
120
|
data = JSON.parse(text);
|
|
@@ -105,12 +133,21 @@ export async function backend(path, request, options = {}) {
|
|
|
105
133
|
}
|
|
106
134
|
|
|
107
135
|
export class RequestBodyError extends Error {
|
|
136
|
+
/**
|
|
137
|
+
* @param {number} status
|
|
138
|
+
* @param {string} message
|
|
139
|
+
*/
|
|
108
140
|
constructor(status, message) {
|
|
109
141
|
super(message);
|
|
110
142
|
this.status = status;
|
|
111
143
|
}
|
|
112
144
|
}
|
|
113
145
|
|
|
146
|
+
/**
|
|
147
|
+
* @param {import("node:http").ServerResponse} response
|
|
148
|
+
* @param {number} status
|
|
149
|
+
* @param {unknown} value Anything `JSON.stringify` writes
|
|
150
|
+
*/
|
|
114
151
|
export function json(response, status, value) {
|
|
115
152
|
response.writeHead(status, { "content-type": "application/json" });
|
|
116
153
|
response.end(JSON.stringify(value));
|