@antelopejs/dms-frontend 0.9.1 → 0.9.3
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 +9 -1
- package/dist/cancellation.js +2 -1
- package/dist/config.js +11 -7
- package/dist/derived-outputs.js +6 -113
- package/dist/dev-modules.js +3 -2
- package/dist/discovery.js +19 -12
- package/dist/fs-sync.js +1 -2
- package/dist/internal/guards.d.ts +5 -0
- package/dist/internal/guards.js +22 -0
- package/dist/internal/locale-catalogs.d.ts +6 -0
- package/dist/internal/locale-catalogs.js +115 -0
- package/dist/layer-aliases.js +2 -1
- package/dist/layers.js +2 -1
- package/dist/locale-files.js +9 -4
- package/dist/manifest-secrets.js +11 -4
- package/dist/manifest.js +11 -5
- package/dist/materialize.js +6 -5
- package/dist/module-set.js +2 -4
- package/dist/ports.js +8 -1
- package/dist/private-values.js +26 -17
- package/dist/project-modules.js +17 -11
- package/dist/renderer-range.js +2 -1
- package/dist/server-process.js +17 -16
- package/dist/update-check.js +23 -16
- package/dist/verify-source-runner.js +2 -2
- package/dist/workspace-setup.js +1 -2
- package/dist/workspace.js +11 -10
- package/package.json +7 -4
- package/skills/dms-frontend/SKILL.md +1 -1
- package/skills/dms-frontend/cases.json +77 -0
- package/templates/vue/app-runtime.ts +88 -7
- package/templates/vue/client-chunking.mjs +0 -5
- package/templates/vue/client-server-guard.mjs +10 -13
- package/templates/vue/entry-weight.mjs +12 -14
- package/templates/vue/frontend-module.ts +159 -3563
- package/templates/vue/frontend-runtime/async-data-entry.ts +320 -0
- package/templates/vue/frontend-runtime/async-data.ts +404 -0
- package/templates/vue/frontend-runtime/components.ts +386 -0
- package/templates/vue/frontend-runtime/cookies.ts +125 -0
- package/templates/vue/frontend-runtime/data-access.ts +132 -0
- package/templates/vue/frontend-runtime/dms-error.ts +11 -0
- package/templates/vue/frontend-runtime/errors.ts +145 -0
- package/templates/vue/frontend-runtime/fetch.ts +306 -0
- package/templates/vue/frontend-runtime/module-registration.ts +247 -0
- package/templates/vue/frontend-runtime/paged-data.ts +191 -0
- package/templates/vue/frontend-runtime/payload.ts +172 -0
- package/templates/vue/frontend-runtime/route.ts +328 -0
- package/templates/vue/frontend-runtime/sdk.ts +300 -0
- package/templates/vue/frontend-runtime/state.ts +167 -0
- package/templates/vue/frontend-runtime/types.ts +361 -0
- package/templates/vue/frontend-runtime/ui.ts +247 -0
- package/templates/vue/globals.d.ts +5 -0
- package/templates/vue/head-order.mjs +11 -13
- package/templates/vue/icon-bundle.ts +31 -0
- package/templates/vue/message-subset.ts +65 -0
- package/templates/vue/network-error.ts +4 -0
- package/templates/vue/prepare-types.mjs +0 -8
- package/templates/vue/server/auth/backend.mjs +4 -2
- package/templates/vue/server/auth/establish-endpoints.mjs +14 -9
- package/templates/vue/server/auth/session.mjs +4 -3
- package/templates/vue/server/auth-hooks.mjs +6 -6
- package/templates/vue/server/backend-proxies.mjs +6 -6
- package/templates/vue/server/backend-response.mjs +15 -14
- package/templates/vue/server/built-files.mjs +10 -8
- package/templates/vue/server/cli-report.mjs +7 -5
- package/templates/vue/server/client-error.mjs +11 -8
- package/templates/vue/server/client-manifest.mjs +9 -8
- package/templates/vue/server/content.mjs +2 -1
- package/templates/vue/server/dev-private-files.mjs +20 -15
- package/templates/vue/server/dev-styles.mjs +22 -25
- package/templates/vue/server/email-bundle-watch.mjs +0 -12
- package/templates/vue/server/email.mjs +20 -15
- package/templates/vue/server/favicon.mjs +12 -11
- package/templates/vue/server/homepage.mjs +8 -10
- package/templates/vue/server/icons.mjs +18 -9
- package/templates/vue/server/in-place-render.mjs +6 -8
- package/templates/vue/server/listen-host.mjs +2 -2
- package/templates/vue/server/module-event.mjs +5 -5
- package/templates/vue/server/module-routes.mjs +11 -7
- package/templates/vue/server/module-server.mjs +8 -7
- package/templates/vue/server/module-testing.mjs +7 -7
- package/templates/vue/server/page-props.mjs +14 -14
- package/templates/vue/server/path-patterns.mjs +4 -4
- package/templates/vue/server/proxy-compression.mjs +21 -13
- package/templates/vue/server/proxy-headers.mjs +5 -3
- package/templates/vue/server/proxy-redirects.mjs +7 -6
- package/templates/vue/server/proxy-transfers.mjs +15 -11
- package/templates/vue/server/public-endpoints.mjs +27 -19
- package/templates/vue/server/public-options.mjs +9 -7
- package/templates/vue/server/request-failure.mjs +4 -6
- package/templates/vue/server/request-routing.mjs +8 -11
- package/templates/vue/server/request-target.mjs +6 -6
- package/templates/vue/server/route-rules.mjs +7 -5
- package/templates/vue/server/static-files.mjs +19 -10
- package/templates/vue/server/web-body.mjs +3 -4
- package/templates/vue/server.mjs +32 -19
- package/templates/vue/sfc-global-types.mjs +27 -24
- package/templates/vue/ssr-renderer.ts +10 -1
- package/templates/vue/ui-inertia-page.mjs +7 -7
- package/templates/vue/ui-kbd.mjs +10 -10
- package/templates/vue/ui-lazy-icons.mjs +44 -0
- package/templates/vue/ui-request-state.mjs +9 -9
- package/templates/vue/vite.config.ts +2 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** The `<script>` the server render leaves in the document for {@link DmsMessageSubset}. */
|
|
2
|
+
export const SSR_MESSAGES_ID = "dms-ssr-messages";
|
|
3
|
+
|
|
4
|
+
type Messages = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The messages a server render read from a locale's catalog, as a catalog of
|
|
8
|
+
* their own, with the locale they belong to.
|
|
9
|
+
*/
|
|
10
|
+
export interface DmsMessageSubset {
|
|
11
|
+
locale: string;
|
|
12
|
+
messages: Messages;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MessageReadRecorder {
|
|
16
|
+
/** The catalog to give the render: every read through it is recorded. */
|
|
17
|
+
messages: Messages;
|
|
18
|
+
/** What the render read so far. */
|
|
19
|
+
subset: () => Messages;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isTraversable(value: unknown): value is object {
|
|
23
|
+
return typeof value === "object" && value !== null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isMessages(value: unknown): value is Messages {
|
|
27
|
+
return isTraversable(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Slot = () => Messages;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wraps a catalog so that the messages read through it are collected, with
|
|
34
|
+
* the path they sit at, into a catalog of their own.
|
|
35
|
+
*
|
|
36
|
+
* A message is recorded when a string is read from the catalog, whether by a
|
|
37
|
+
* lookup (`t`), an existence check (`te`) or a walk over a subtree (`tm`).
|
|
38
|
+
*/
|
|
39
|
+
export function recordMessageReads(messages: Messages): MessageReadRecorder {
|
|
40
|
+
const used: Messages = {};
|
|
41
|
+
const place = (source: object, parent: Slot, key: string): Slot => {
|
|
42
|
+
return () => {
|
|
43
|
+
const holder = parent();
|
|
44
|
+
const existing = holder[key];
|
|
45
|
+
if (isMessages(existing)) return existing;
|
|
46
|
+
const container: Messages = Array.isArray(source) ? [] : {};
|
|
47
|
+
holder[key] = container;
|
|
48
|
+
return container;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const wrap = <T extends object>(source: T, slot: Slot): T =>
|
|
52
|
+
new Proxy(source, {
|
|
53
|
+
get(target, key, receiver) {
|
|
54
|
+
const value: unknown = Reflect.get(target, key, receiver);
|
|
55
|
+
if (typeof key !== "string" || key === "__proto__") return value;
|
|
56
|
+
if (typeof value === "string") {
|
|
57
|
+
slot()[key] = value;
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
if (isTraversable(value)) return wrap(value, place(value, slot, key));
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return { messages: wrap(messages, () => used), subset: () => used };
|
|
65
|
+
}
|
|
@@ -15,6 +15,10 @@ export interface DmsNetworkErrorTranslator {
|
|
|
15
15
|
|
|
16
16
|
const NETWORK_ERROR_TOAST_ID = "dms-network-error";
|
|
17
17
|
const NETWORK_ERROR_KEY = "error.network";
|
|
18
|
+
/** The messages a module overrides the built-in texts with. */
|
|
19
|
+
export const NETWORK_ERROR_KEYS = ["title", "description"].map(
|
|
20
|
+
(name) => `${NETWORK_ERROR_KEY}.${name}`,
|
|
21
|
+
);
|
|
18
22
|
const DEFAULT_LOCALE = "en";
|
|
19
23
|
// Shipped with the renderer, since no module is guaranteed to translate the
|
|
20
24
|
// case; a module overrides them with `error.network.title` and
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// Writes the files Vite generates and the type check reads (auto-imports.d.ts
|
|
2
|
-
// and the Nuxt UI templates) without building anything: it resolves
|
|
3
|
-
// vite.config.ts and runs its build hooks over an empty entry, in seconds
|
|
4
|
-
// where a client build takes a minute. `ajs dms verify-source` runs it first
|
|
5
|
-
// (`pnpm run build:types`), so the type check no longer waits for the client
|
|
6
|
-
// bundle.
|
|
7
|
-
|
|
8
1
|
import { build } from "vite";
|
|
9
2
|
|
|
10
3
|
const ENTRY = "dms-prepare-types-entry";
|
|
@@ -23,7 +16,6 @@ await build({
|
|
|
23
16
|
build: {
|
|
24
17
|
write: false,
|
|
25
18
|
emptyOutDir: false,
|
|
26
|
-
// The entry is empty on purpose; Rolldown does not warn about it.
|
|
27
19
|
rolldownOptions: { input: ENTRY },
|
|
28
20
|
},
|
|
29
21
|
});
|
|
@@ -21,8 +21,10 @@ export class UpstreamError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const PUBLIC_AUTH_MESSAGE = /^error\.[a-z0-9_.-]{1,100}$/;
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The API error contract's generic key: the technical cause of a failure the
|
|
26
|
+
* backend did not describe stays in the server log.
|
|
27
|
+
*/
|
|
26
28
|
export const GENERIC_ERROR_MESSAGE = "error.500.description";
|
|
27
29
|
|
|
28
30
|
export function publicBackendMessage(data) {
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// The backend endpoints a module may open a session from: what the backend's
|
|
2
|
-
// modules declared in the frontend manifest, and what the environment adds
|
|
3
|
-
// (`DMS_AUTH_ESTABLISH_ENDPOINTS`). The module that owns sign-in reads them as
|
|
4
|
-
// `event.app.authEstablishEndpoints`.
|
|
5
|
-
|
|
6
1
|
import { readFileSync } from "node:fs";
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Absolute backend API paths only: no scheme, no authority, no query string,
|
|
5
|
+
* and no segment that could climb out of `/api/`.
|
|
6
|
+
*/
|
|
10
7
|
const BACKEND_API_PATH =
|
|
11
8
|
/^\/api\/[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9][A-Za-z0-9._~-]*)*$/;
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Written at the workspace root by the loader, from the endpoints each
|
|
12
|
+
* backend module declared in the frontend manifest. This file sits two
|
|
13
|
+
* directories up.
|
|
14
|
+
*/
|
|
15
15
|
const DECLARED_ENDPOINTS_FILE = new URL(
|
|
16
16
|
"../../generated-auth-establish.json",
|
|
17
17
|
import.meta.url,
|
|
@@ -49,6 +49,11 @@ export function readDeclaredEstablishEndpoints(file = DECLARED_ENDPOINTS_FILE) {
|
|
|
49
49
|
* stays refused, so no backend endpoint that happens to mint a token pair can
|
|
50
50
|
* be turned into a login by a request from the browser.
|
|
51
51
|
*
|
|
52
|
+
* The backend endpoints a module may open a session from: what the backend's
|
|
53
|
+
* modules declared in the frontend manifest, and what the environment adds
|
|
54
|
+
* (`DMS_AUTH_ESTABLISH_ENDPOINTS`). The module that owns sign-in reads them as
|
|
55
|
+
* `event.app.authEstablishEndpoints`.
|
|
56
|
+
*
|
|
52
57
|
* @param declaration Comma-separated absolute backend paths from the environment
|
|
53
58
|
* @param declared Paths declared by the backend's frontend modules
|
|
54
59
|
* @returns The well-formed backend API paths, without duplicates
|
|
@@ -15,6 +15,9 @@ const MAX_AGE = 60 * 60 * 24 * 30;
|
|
|
15
15
|
* frontends on localhost:3000 and localhost:3001 would overwrite each other's
|
|
16
16
|
* session, each failing to decrypt the other's and signing the user out. The
|
|
17
17
|
* suffix follows the backend, so a restart keeps the cookies it wrote.
|
|
18
|
+
*
|
|
19
|
+
* A `DMS_API_BASE_URL` that is not a URL is hashed as written: the raw string
|
|
20
|
+
* still tells two backends apart.
|
|
18
21
|
*/
|
|
19
22
|
function cookieScope() {
|
|
20
23
|
const backendUrl = (process.env.DMS_API_BASE_URL ?? "").trim();
|
|
@@ -24,9 +27,7 @@ function cookieScope() {
|
|
|
24
27
|
if (url.hostname === "localhost" || url.hostname === "[::1]")
|
|
25
28
|
url.hostname = "127.0.0.1";
|
|
26
29
|
canonical = url.toString().replace(/\/$/, "");
|
|
27
|
-
} catch {
|
|
28
|
-
// Not a URL: the raw string still tells two backends apart.
|
|
29
|
-
}
|
|
30
|
+
} catch {}
|
|
30
31
|
return createHash("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// The session logic the server runs outside any route: renewing a session the
|
|
2
|
-
// backend refused, the session a page carries, and where a signed-out visit
|
|
3
|
-
// goes. The module that owns sign-in provides it through `registerAuth` in its
|
|
4
|
-
// `dms.server.ts`. Without one, a page carries no session and a refused
|
|
5
|
-
// session is not renewed.
|
|
6
|
-
|
|
7
1
|
import { readSession, setRequestSession } from "./auth/session.mjs";
|
|
8
2
|
import { createServerEvent } from "./module-event.mjs";
|
|
9
3
|
import { compilePathPattern } from "./path-patterns.mjs";
|
|
@@ -40,6 +34,12 @@ function hookView(publicSession, session) {
|
|
|
40
34
|
/**
|
|
41
35
|
* The server's session logic for a set of server modules.
|
|
42
36
|
*
|
|
37
|
+
* The session logic the server runs outside any route: renewing a session the
|
|
38
|
+
* backend refused, the session a page carries, and where a signed-out visit
|
|
39
|
+
* goes. The module that owns sign-in provides it through `registerAuth` in its
|
|
40
|
+
* `dms.server.ts`. Without one, a page carries no session and a refused session
|
|
41
|
+
* is not renewed.
|
|
42
|
+
*
|
|
43
43
|
* @param provider The resolved auth hooks, `undefined` when no module
|
|
44
44
|
* registers any
|
|
45
45
|
* @param context `(id) => ({ module, app })`, a module's event context
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// The backend prefixes the frontend server proxies. The renderer hardcodes
|
|
2
|
-
// none: a frontend module declares them in its `dms.server.ts`
|
|
3
|
-
// (`registerBackendProxy`), and a backend module in the frontend manifest
|
|
4
|
-
// (`backendProxies`, next to its `publicEndpoints`). A path no prefix covers
|
|
5
|
-
// is never sent to the backend.
|
|
6
|
-
|
|
7
1
|
import { compilePathPattern } from "./path-patterns.mjs";
|
|
8
2
|
|
|
9
3
|
/** How the proxy authenticates a request to the backend. */
|
|
@@ -60,6 +54,12 @@ function compileProxy(pattern, options, owner) {
|
|
|
60
54
|
/**
|
|
61
55
|
* The deployment's backend proxies, one per pattern.
|
|
62
56
|
*
|
|
57
|
+
* The backend prefixes the frontend server proxies. The renderer hardcodes
|
|
58
|
+
* none: a frontend module declares them in its `dms.server.ts`
|
|
59
|
+
* (`registerBackendProxy`), and a backend module in the frontend manifest
|
|
60
|
+
* (`backendProxies`, next to its `publicEndpoints`). A path no prefix covers is
|
|
61
|
+
* never sent to the backend.
|
|
62
|
+
*
|
|
63
63
|
* @param declared `{ id, pattern, options }` per `registerBackendProxy` call,
|
|
64
64
|
* from the highest-priority module down
|
|
65
65
|
* @param manifest `{ module, path, auth?, maxBodyBytes? }` per backend
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
// Reading a failed DMS backend response for the page a visitor gets instead.
|
|
2
|
-
//
|
|
3
|
-
// Lives apart from server.mjs so the file stays under the size the linter
|
|
4
|
-
// allows.
|
|
5
|
-
|
|
6
1
|
import { GENERIC_ERROR_MESSAGE } from "./auth/backend.mjs";
|
|
7
2
|
import { isFrontendVisit, redirectFrontendVisit } from "./inertia.mjs";
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
/**
|
|
5
|
+
* What a JSON caller gets when the backend fails: the generic i18n key of
|
|
6
|
+
* the API error contract. A page gets the status code alone, which the DMS
|
|
7
|
+
* error page describes in the visitor's language; the technical cause only
|
|
8
|
+
* reaches the server log.
|
|
9
|
+
*/
|
|
13
10
|
export const UNEXPECTED_ERROR_BODY = { message: GENERIC_ERROR_MESSAGE };
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
/**
|
|
12
|
+
* A typed refusal carries a machine-readable i18n key as its body, bare or
|
|
13
|
+
* JSON-encoded, e.g. `saas.errors.workspace.access_blocked`.
|
|
14
|
+
*/
|
|
16
15
|
const REFUSAL_CODE = /^[a-z0-9_-]+(?:\.[a-z0-9_-]+)+$/i;
|
|
17
16
|
const REFUSAL_CODE_MAX_LENGTH = 200;
|
|
18
17
|
const FORBIDDEN = 403;
|
|
@@ -26,6 +25,10 @@ export class BackendResponseError extends Error {
|
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The refusal code in a backend answer: the string or the `message` of its
|
|
30
|
+
* JSON body, or the body itself when it is not JSON (a bare code).
|
|
31
|
+
*/
|
|
29
32
|
async function refusalCode(response) {
|
|
30
33
|
const text = (await response.text().catch(() => "")).trim();
|
|
31
34
|
if (text.length > REFUSAL_CODE_MAX_LENGTH) return undefined;
|
|
@@ -33,9 +36,7 @@ async function refusalCode(response) {
|
|
|
33
36
|
try {
|
|
34
37
|
const data = JSON.parse(text);
|
|
35
38
|
value = typeof data === "string" ? data : data?.message;
|
|
36
|
-
} catch {
|
|
37
|
-
// A bare code.
|
|
38
|
-
}
|
|
39
|
+
} catch {}
|
|
39
40
|
return typeof value === "string" && REFUSAL_CODE.test(value)
|
|
40
41
|
? value
|
|
41
42
|
: undefined;
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// The built files the production server hands to a browser: what the client
|
|
2
|
-
// build wrote to `dist/client`, except a file or directory whose name starts
|
|
3
|
-
// with a dot. Vite keeps its build metadata there (`.vite/manifest.json`, the
|
|
4
|
-
// source path of every module file), and a module's `public/` may hold dot
|
|
5
|
-
// files too. `/.well-known/`, which RFC 8615 reserves for files meant to be
|
|
6
|
-
// public, stays served.
|
|
7
|
-
|
|
8
1
|
import { existsSync, statSync } from "node:fs";
|
|
9
2
|
import { relative, resolve, sep } from "node:path";
|
|
10
3
|
|
|
@@ -14,6 +7,13 @@ const PUBLIC_DOT_DIRECTORY = ".well-known";
|
|
|
14
7
|
* Whether a path relative to the build root may be served: no segment starts
|
|
15
8
|
* with a dot, but a leading `.well-known`. Split on both separators: on
|
|
16
9
|
* Windows, `relative` answers with backslashes, and a decoded `%5C` is one.
|
|
10
|
+
*
|
|
11
|
+
* The built files the production server hands to a browser: what the client
|
|
12
|
+
* build wrote to `dist/client`, except a file or directory whose name starts
|
|
13
|
+
* with a dot. Vite keeps its build metadata there (`.vite/manifest.json`, the
|
|
14
|
+
* source path of every module file), and a module's `public/` may hold dot
|
|
15
|
+
* files too. `/.well-known/`, which RFC 8615 reserves for files meant to be
|
|
16
|
+
* public, stays served.
|
|
17
17
|
*/
|
|
18
18
|
export function isPublicBuildPath(relativePath) {
|
|
19
19
|
return relativePath
|
|
@@ -30,6 +30,9 @@ export function isPublicBuildPath(relativePath) {
|
|
|
30
30
|
* serves none there: the path is malformed, leaves `root`, names a dot file or
|
|
31
31
|
* directory, or is not a regular file. Decoded first, as the browser encoded
|
|
32
32
|
* it, so the file checked is the file served.
|
|
33
|
+
*
|
|
34
|
+
* The prefix uses the native separator: `resolve` answers with backslashes on
|
|
35
|
+
* Windows.
|
|
33
36
|
*/
|
|
34
37
|
export function publicBuildFile(root, pathname) {
|
|
35
38
|
let decoded;
|
|
@@ -40,7 +43,6 @@ export function publicBuildFile(root, pathname) {
|
|
|
40
43
|
}
|
|
41
44
|
const base = resolve(root);
|
|
42
45
|
const file = resolve(base, `.${decoded}`);
|
|
43
|
-
// Native separator: `resolve` answers with backslashes on Windows.
|
|
44
46
|
if (!file.startsWith(`${base}${sep}`)) return undefined;
|
|
45
47
|
if (!isPublicBuildPath(relative(base, file))) return undefined;
|
|
46
48
|
if (!existsSync(file) || !statSync(file).isFile()) return undefined;
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// What the server tells whoever started it. Started by `ajs dms dev` or
|
|
2
|
-
// `ajs dms start`, it reports over IPC and the CLI words what the user reads.
|
|
3
|
-
// Started on its own, it prints the line itself.
|
|
4
|
-
|
|
5
1
|
import { readFileSync } from "node:fs";
|
|
6
2
|
import { resolve } from "node:path";
|
|
7
3
|
import { stripVTControlCharacters } from "node:util";
|
|
@@ -13,7 +9,13 @@ async function report(message, line, write) {
|
|
|
13
9
|
await new Promise((sent) => process.send(message, () => sent()));
|
|
14
10
|
}
|
|
15
11
|
|
|
16
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The server answers at `address`:`port`.
|
|
14
|
+
*
|
|
15
|
+
* What the server tells whoever started it. Started by `ajs dms dev` or `ajs
|
|
16
|
+
* dms start`, it reports over IPC and the CLI words what the user reads.
|
|
17
|
+
* Started on its own, it prints the line itself.
|
|
18
|
+
*/
|
|
17
19
|
export async function reportReady(address, port) {
|
|
18
20
|
const host = WILDCARD_ADDRESSES.includes(address)
|
|
19
21
|
? "localhost"
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
// Answers for requests Node rejects before the request handler runs. Without
|
|
2
|
-
// a `clientError` listener Node answers on its own and logs nothing, so a page
|
|
3
|
-
// refused for its headers left no trace in the server output.
|
|
4
|
-
|
|
5
1
|
const HEADER_OVERFLOW = "HPE_HEADER_OVERFLOW";
|
|
6
2
|
const HEADER_OVERFLOW_RESPONSE =
|
|
7
3
|
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n";
|
|
8
4
|
const BAD_REQUEST_RESPONSE =
|
|
9
5
|
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
/**
|
|
7
|
+
* A browser retries a refused page with the same cookies: one hint per
|
|
8
|
+
* minute is enough to explain them all.
|
|
9
|
+
*/
|
|
12
10
|
const HEADER_OVERFLOW_HINT_INTERVAL_MS = 60_000;
|
|
13
11
|
|
|
14
12
|
let lastHeaderOverflowHint = -Infinity;
|
|
@@ -27,6 +25,13 @@ function warnHeaderOverflow(maxHeaderSize) {
|
|
|
27
25
|
/**
|
|
28
26
|
* The server's `clientError` listener: explains a 431 in the logs, and
|
|
29
27
|
* otherwise answers as Node does without a listener.
|
|
28
|
+
*
|
|
29
|
+
* Answers requests Node rejects before the request handler runs. Without a
|
|
30
|
+
* `clientError` listener Node answers on its own and logs nothing, so a page
|
|
31
|
+
* refused for its headers left no trace in the server output.
|
|
32
|
+
*
|
|
33
|
+
* Once a response went out on this connection, a status line would land in the
|
|
34
|
+
* middle of it.
|
|
30
35
|
*/
|
|
31
36
|
export function handleClientError(error, socket, maxHeaderSize) {
|
|
32
37
|
let response = BAD_REQUEST_RESPONSE;
|
|
@@ -34,8 +39,6 @@ export function handleClientError(error, socket, maxHeaderSize) {
|
|
|
34
39
|
response = HEADER_OVERFLOW_RESPONSE;
|
|
35
40
|
warnHeaderOverflow(maxHeaderSize);
|
|
36
41
|
}
|
|
37
|
-
// Once a response went out on this connection, a status line would land in
|
|
38
|
-
// the middle of it.
|
|
39
42
|
if (socket.writable && socket.bytesWritten === 0) socket.write(response);
|
|
40
43
|
socket.destroy();
|
|
41
44
|
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// Reading the built client manifest and turning the components a server render
|
|
2
|
-
// created into the stylesheet links its HTML needs.
|
|
3
|
-
//
|
|
4
|
-
// Lives apart from server.mjs so the file stays under the size the linter
|
|
5
|
-
// allows. Production-only: in dev, Vite serves these itself.
|
|
6
|
-
|
|
7
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
2
|
import { join } from "node:path";
|
|
9
3
|
import { fileURLToPath } from "node:url";
|
|
@@ -16,11 +10,18 @@ const CLIENT_MANIFEST_PATH = join(
|
|
|
16
10
|
);
|
|
17
11
|
const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Read once and kept: both files are build output and cannot change while
|
|
15
|
+
* the server is up.
|
|
16
|
+
*/
|
|
21
17
|
let clientManifest;
|
|
22
18
|
let productionTemplate;
|
|
23
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Reads the built client manifest and turns the components a server render
|
|
22
|
+
* created into the stylesheet links its HTML needs. Production-only: in dev,
|
|
23
|
+
* Vite serves these itself.
|
|
24
|
+
*/
|
|
24
25
|
export function productionHtmlTemplate() {
|
|
25
26
|
if (productionTemplate) return productionTemplate;
|
|
26
27
|
const templatePath = existsSync(BUILT_TEMPLATE_PATH)
|
|
@@ -15,6 +15,8 @@ const DYNAMIC_BROTLI_QUALITY = 4;
|
|
|
15
15
|
* accepts (Brotli first), or `undefined` when it accepts neither. `streaming`
|
|
16
16
|
* flushes what each write gives at once, for a body written over time: the
|
|
17
17
|
* client gets each part when it is written, not when a block fills.
|
|
18
|
+
*
|
|
19
|
+
* Implicit identity is a fallback, not a preference over compression.
|
|
18
20
|
*/
|
|
19
21
|
export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
20
22
|
const accepted = request.headers["accept-encoding"] ?? "";
|
|
@@ -38,7 +40,6 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
|
38
40
|
const quality = (token) => qualities.get(token) ?? qualities.get("*") ?? 0;
|
|
39
41
|
const br = quality("br");
|
|
40
42
|
const gzip = quality("gzip");
|
|
41
|
-
// Implicit identity is a fallback, not a preference over compression.
|
|
42
43
|
const identity = qualities.get("identity") ?? 0;
|
|
43
44
|
if (identity > Math.max(br, gzip)) return undefined;
|
|
44
45
|
if (br > 0 && br >= gzip)
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
// The files of the workspace the development server never hands to a browser.
|
|
2
|
-
//
|
|
3
|
-
// Vite's root is the workspace root, where the generated server options (the
|
|
4
|
-
// modules' private options: the backend's secrets) and the cached manifest are
|
|
5
|
-
// written, and the modules' server code is materialized under it. Vite serves
|
|
6
|
-
// any file under its root, as is, `?raw`, `?import` or `?url`. Its
|
|
7
|
-
// `server.fs.deny` list covers most of these, but not every query combination
|
|
8
|
-
// (`?import&raw` got through in Vite 7.2; Vite 8.3 refuses it itself), so a middleware ahead of Vite's own
|
|
9
|
-
// answers 403 by path, whatever the query, and a load guard refuses them to
|
|
10
|
-
// any client request that reaches the transform pipeline another way.
|
|
11
|
-
// `ssrLoadModule`, which loads the server modules, is left alone.
|
|
12
|
-
|
|
13
1
|
import { relative, resolve, sep } from "node:path";
|
|
14
2
|
|
|
15
3
|
const PRIVATE_FILE_NAMES = new Set([
|
|
@@ -22,7 +10,8 @@ const FORBIDDEN_STATUS = 403;
|
|
|
22
10
|
|
|
23
11
|
/**
|
|
24
12
|
* Vite's `server.fs.deny` for the development server: Vite's own defaults,
|
|
25
|
-
* which a `deny` list replaces, and the private files
|
|
13
|
+
* which a `deny` list replaces, and the private files: the generated secrets
|
|
14
|
+
* and the modules' server code are never served.
|
|
26
15
|
*/
|
|
27
16
|
export const DEV_FS_DENY = [
|
|
28
17
|
".env",
|
|
@@ -68,7 +57,24 @@ function requestedFile(url, root) {
|
|
|
68
57
|
return resolve(root, `.${sep}${pathname.replace(/^[\\/]+/, "")}`);
|
|
69
58
|
}
|
|
70
59
|
|
|
71
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Answers 403 to private files, and fails a client load of one.
|
|
62
|
+
*
|
|
63
|
+
* The files of the workspace the development server never hands to a
|
|
64
|
+
* browser. Vite's root is the workspace root, where the generated server
|
|
65
|
+
* options (the modules' private options: the backend's secrets) and the
|
|
66
|
+
* cached manifest are written, and the modules' server code is materialized
|
|
67
|
+
* under it. Vite serves any file under its root, as is, `?raw`, `?import` or
|
|
68
|
+
* `?url`. Its `server.fs.deny` list covers most of these, but not every
|
|
69
|
+
* query combination (`?import&raw` got through in Vite 7.2; Vite 8.3 refuses
|
|
70
|
+
* it itself), so a middleware ahead of Vite's own answers 403 by path,
|
|
71
|
+
* whatever the query, and a load guard refuses them to any client request
|
|
72
|
+
* that reaches the transform pipeline another way. `ssrLoadModule`, which
|
|
73
|
+
* loads the server modules, is left alone.
|
|
74
|
+
*
|
|
75
|
+
* The middleware is registered directly, not returned: it runs before Vite's
|
|
76
|
+
* middlewares.
|
|
77
|
+
*/
|
|
72
78
|
export function denyPrivateFiles() {
|
|
73
79
|
let root = process.cwd();
|
|
74
80
|
return {
|
|
@@ -79,7 +85,6 @@ export function denyPrivateFiles() {
|
|
|
79
85
|
root = config.root;
|
|
80
86
|
},
|
|
81
87
|
configureServer(server) {
|
|
82
|
-
// Registered directly, not returned: it runs before Vite's middlewares.
|
|
83
88
|
server.middlewares.use((request, response, next) => {
|
|
84
89
|
const file = requestedFile(request.url, root);
|
|
85
90
|
if (!file || !isPrivateFile(file, root)) return next();
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
// Putting the page's stylesheets in the document the development server
|
|
2
|
-
// returns, instead of letting the client entry inject them from JavaScript
|
|
3
|
-
// once it has executed.
|
|
4
|
-
//
|
|
5
|
-
// Vite serves every stylesheet as a JavaScript module in development, so a
|
|
6
|
-
// plain SSR document carries no styles at all and the browser paints raw HTML
|
|
7
|
-
// until `/main.ts` and its module graph have run. Production has no such gap:
|
|
8
|
-
// the built `index.html` already links the entry stylesheet, which is why this
|
|
9
|
-
// module is development-only and lives beside client-manifest.mjs rather than
|
|
10
|
-
// inside server.mjs, whose size the linter caps.
|
|
11
|
-
//
|
|
12
|
-
// Each tag carries the module id Vite will use for its own copy
|
|
13
|
-
// (`data-vite-dev-id`), under our own attribute name. `main.ts` hands the
|
|
14
|
-
// stylesheet over as soon as Vite injects that copy — see `adoptViteDevStyles`.
|
|
15
|
-
// We deliberately do NOT reuse `data-vite-dev-id` itself: Vite's client adopts
|
|
16
|
-
// any pre-existing link with that attribute and then skips `updateStyle` for
|
|
17
|
-
// it, which would leave CSS edits stranded.
|
|
18
|
-
|
|
19
1
|
import { join, sep } from "node:path";
|
|
20
2
|
import { fileURLToPath } from "node:url";
|
|
21
3
|
import { renderedModuleStyleTags } from "./client-manifest.mjs";
|
|
@@ -47,12 +29,14 @@ function ssrModuleGraph(devServer) {
|
|
|
47
29
|
return devServer.environments?.ssr?.moduleGraph ?? devServer.moduleGraph;
|
|
48
30
|
}
|
|
49
31
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Vite only treats an id as a stylesheet when the language is the last thing
|
|
34
|
+
* in it, and a component's style block ends in `&lang.css`. Appending
|
|
35
|
+
* `&direct` after that makes Vite skip its stylesheet pipeline, so the block
|
|
36
|
+
* comes back as written: `@import "package/dist/style.css"` is left for the
|
|
37
|
+
* browser, which resolves it against the page URL and gets a 404. `direct`
|
|
38
|
+
* therefore goes in front of the trailing language segment.
|
|
39
|
+
*/
|
|
56
40
|
const TRAILING_LANG = /&(lang\.[^&?]*)$/;
|
|
57
41
|
|
|
58
42
|
function directUrl(url) {
|
|
@@ -90,7 +74,20 @@ async function inlineTag(devServer, { url, id }) {
|
|
|
90
74
|
return `<style ${STYLE_ATTRIBUTE}="${id}">${css}</style>`;
|
|
91
75
|
}
|
|
92
76
|
|
|
93
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Style tags for everything the development render touched, entry sheet
|
|
79
|
+
* first.
|
|
80
|
+
*
|
|
81
|
+
* Puts the page's stylesheets in the document the development server
|
|
82
|
+
* returns, instead of letting the client entry inject them from JavaScript
|
|
83
|
+
* once it has executed. Vite serves every stylesheet as a JavaScript module
|
|
84
|
+
* in development, so a plain SSR document carries no styles at all and the
|
|
85
|
+
* browser paints raw HTML until `/main.ts` and its module graph have run.
|
|
86
|
+
* Production has no such gap: the built `index.html` already links the entry
|
|
87
|
+
* stylesheet, which is why this module is development-only and lives beside
|
|
88
|
+
* client-manifest.mjs rather than inside server.mjs, whose size the linter
|
|
89
|
+
* caps.
|
|
90
|
+
*/
|
|
94
91
|
export async function developmentStyleTags(devServer) {
|
|
95
92
|
const modules = styleModules(devServer);
|
|
96
93
|
const links = [
|
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
// Development-only email bundle builder, forked by the frontend server.
|
|
2
|
-
//
|
|
3
|
-
// Runs Vite's email build in watch mode so `dist/server` exists as soon as the
|
|
4
|
-
// dev server does and follows every template edit the layer watchers mirror
|
|
5
|
-
// into the workspace. It lives in its own process so its builds never hold the
|
|
6
|
-
// dev server's event loop, and it reports each build over IPC so the server
|
|
7
|
-
// renders from a finished bundle only. A build empties `dist/server` first, so
|
|
8
|
-
// it asks the server before it starts and waits for the go-ahead, which the
|
|
9
|
-
// server gives once no render reads the previous bundle any more.
|
|
10
|
-
|
|
11
1
|
import { fileURLToPath } from "node:url";
|
|
12
2
|
import { build } from "vite";
|
|
13
3
|
|
|
14
4
|
const PROJECT_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
15
5
|
|
|
16
|
-
// The server owns this process: when its IPC channel closes, however the
|
|
17
|
-
// server ended, there is nobody left to build for.
|
|
18
6
|
process.on("disconnect", () => process.exit(0));
|
|
19
7
|
|
|
20
8
|
let startBuild;
|
|
@@ -10,18 +10,23 @@ const EMAIL_BUNDLE_WATCH_URL = new URL(
|
|
|
10
10
|
"./email-bundle-watch.mjs",
|
|
11
11
|
import.meta.url,
|
|
12
12
|
);
|
|
13
|
-
|
|
13
|
+
/** Bound the request's wait, but never read a bundle being replaced. */
|
|
14
14
|
const EMAIL_BUILD_WAIT_MS = 60_000;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Settled whenever no email build is running. The version changes with each
|
|
18
|
+
* finished build: Node caches an imported module by URL, so the renderer is
|
|
19
|
+
* imported under a new one to pick up rebuilt templates.
|
|
20
|
+
*/
|
|
19
21
|
let emailBuild = Promise.resolve();
|
|
20
22
|
let settleEmailBuild;
|
|
21
23
|
let emailBundleVersion = 0;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* A build empties dist/server before it writes the new bundle, so it never
|
|
26
|
+
* starts while a render still imports from the previous one: the builder
|
|
27
|
+
* asks first, and gets the go-ahead once the renders in progress have
|
|
28
|
+
* finished.
|
|
29
|
+
*/
|
|
25
30
|
let rendersInProgress = 0;
|
|
26
31
|
let startPendingBuild;
|
|
27
32
|
|
|
@@ -56,7 +61,8 @@ function renderFinished() {
|
|
|
56
61
|
* Build the email bundle now and again on every template change, for
|
|
57
62
|
* development: only the production build otherwise produces it. The builder
|
|
58
63
|
* runs in a child process at low priority, so the dev server keeps answering
|
|
59
|
-
* while it works.
|
|
64
|
+
* while it works. Lowering the priority is skipped where the platform does not
|
|
65
|
+
* permit it: the build then runs at normal priority.
|
|
60
66
|
*/
|
|
61
67
|
export function watchEmailBundle() {
|
|
62
68
|
emailBuildStarted();
|
|
@@ -65,9 +71,7 @@ export function watchEmailBundle() {
|
|
|
65
71
|
});
|
|
66
72
|
try {
|
|
67
73
|
setPriority(builder.pid, constants.priority.PRIORITY_BELOW_NORMAL);
|
|
68
|
-
} catch {
|
|
69
|
-
// Not permitted on this platform; the build only runs at normal priority.
|
|
70
|
-
}
|
|
74
|
+
} catch {}
|
|
71
75
|
builder.on("message", (message) => {
|
|
72
76
|
if (message?.state !== "building") {
|
|
73
77
|
emailBuildSettled();
|
|
@@ -85,9 +89,9 @@ export function watchEmailBundle() {
|
|
|
85
89
|
return builder;
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/** Another build may start while a render waits for the previous one. */
|
|
88
93
|
async function waitForEmailBuild() {
|
|
89
94
|
const deadline = Date.now() + EMAIL_BUILD_WAIT_MS;
|
|
90
|
-
// Another build may start while a render waits for the previous one.
|
|
91
95
|
while (settleEmailBuild && Date.now() < deadline) {
|
|
92
96
|
let timer;
|
|
93
97
|
await Promise.race([
|
|
@@ -113,15 +117,16 @@ function loadEmailRenderer() {
|
|
|
113
117
|
* Renders an email template of the application's email bundle, waiting for a
|
|
114
118
|
* development build in progress.
|
|
115
119
|
*
|
|
120
|
+
* The render is counted with no await in between, so a build the builder asks
|
|
121
|
+
* for now waits for this render, which imports templates and reads catalogs
|
|
122
|
+
* from the bundle until it ends.
|
|
123
|
+
*
|
|
116
124
|
* @param template The template's name
|
|
117
125
|
* @param props The template's props
|
|
118
126
|
* @param options `locale`, the language to render in
|
|
119
127
|
*/
|
|
120
128
|
export async function renderEmailTemplate(template, props, options = {}) {
|
|
121
129
|
await waitForEmailBuild();
|
|
122
|
-
// Counted from here, with no await in between, so a build the builder asks
|
|
123
|
-
// for now waits for this render, which imports templates and reads catalogs
|
|
124
|
-
// from the bundle until it ends.
|
|
125
130
|
rendersInProgress += 1;
|
|
126
131
|
try {
|
|
127
132
|
const { renderEmail } = await loadEmailRenderer();
|