@ossido-labs/ossido 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/bin/build-config.js +5 -0
- package/bin/build-prod.js +5 -0
- package/bin/dev-ssr.js +5 -0
- package/bin/run-build-hook.js +6 -0
- package/bin/watch.js +5 -0
- package/dist/esm/build/config/create-json-config.d.ts +10 -0
- package/dist/esm/build/config/create-json-config.js +52 -0
- package/dist/esm/build/config/create-json-config.js.map +1 -0
- package/dist/esm/build/config/index.d.ts +2 -0
- package/dist/esm/build/config/load-config.d.ts +2 -0
- package/dist/esm/build/config/load-config.js +20 -0
- package/dist/esm/build/config/load-config.js.map +1 -0
- package/dist/esm/build/config/normalize-config.d.ts +10 -0
- package/dist/esm/build/config/normalize-config.js +75 -0
- package/dist/esm/build/config/normalize-config.js.map +1 -0
- package/dist/esm/build/constants.d.ts +5 -0
- package/dist/esm/build/constants.js +10 -0
- package/dist/esm/build/constants.js.map +1 -0
- package/dist/esm/build/index.d.ts +15 -0
- package/dist/esm/build/index.js +245 -0
- package/dist/esm/build/index.js.map +1 -0
- package/dist/esm/build/logger.d.ts +11 -0
- package/dist/esm/build/logger.js +76 -0
- package/dist/esm/build/logger.js.map +1 -0
- package/dist/esm/build/types.d.ts +29 -0
- package/dist/esm/build/utils.d.ts +6 -0
- package/dist/esm/build/utils.js +40 -0
- package/dist/esm/build/utils.js.map +1 -0
- package/dist/esm/build-client/index.d.ts +1 -0
- package/dist/esm/build-client/index.js +1 -0
- package/dist/esm/client/index.d.ts +92 -0
- package/dist/esm/client/index.js +38 -0
- package/dist/esm/client/index.js.map +1 -0
- package/dist/esm/config/index.d.ts +1 -0
- package/dist/esm/config/index.js +0 -0
- package/dist/esm/config/types.d.ts +130 -0
- package/dist/esm/constants.d.ts +1 -0
- package/dist/esm/constants.js +6 -0
- package/dist/esm/constants.js.map +1 -0
- package/dist/esm/hydration/browserLogForwarding.d.ts +7 -0
- package/dist/esm/hydration/browserLogForwarding.js +152 -0
- package/dist/esm/hydration/browserLogForwarding.js.map +1 -0
- package/dist/esm/hydration/index.d.ts +4 -0
- package/dist/esm/hydration/index.js +24 -0
- package/dist/esm/hydration/index.js.map +1 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/shared/DevResources.d.ts +7 -0
- package/dist/esm/shared/DevResources.js +45 -0
- package/dist/esm/shared/DevResources.js.map +1 -0
- package/dist/esm/shared/OssidoContext.d.ts +14 -0
- package/dist/esm/shared/OssidoContext.js +28 -0
- package/dist/esm/shared/OssidoContext.js.map +1 -0
- package/dist/esm/shared/OssidoEntryPoint.d.ts +11 -0
- package/dist/esm/shared/OssidoEntryPoint.js +17 -0
- package/dist/esm/shared/OssidoEntryPoint.js.map +1 -0
- package/dist/esm/shared/OssidoScripts.d.ts +2 -0
- package/dist/esm/shared/OssidoScripts.js +34 -0
- package/dist/esm/shared/OssidoScripts.js.map +1 -0
- package/dist/esm/shared/ProdResources.d.ts +7 -0
- package/dist/esm/shared/ProdResources.js +18 -0
- package/dist/esm/shared/ProdResources.js.map +1 -0
- package/dist/esm/shared/RouterContextProviderWrapper.d.ts +14 -0
- package/dist/esm/shared/RouterContextProviderWrapper.js +27 -0
- package/dist/esm/shared/RouterContextProviderWrapper.js.map +1 -0
- package/dist/esm/shared/dynamic/RouteLazyLoading.d.ts +6 -0
- package/dist/esm/shared/dynamic/RouteLazyLoading.js +20 -0
- package/dist/esm/shared/dynamic/RouteLazyLoading.js.map +1 -0
- package/dist/esm/shared/dynamic/dynamic.d.ts +15 -0
- package/dist/esm/shared/dynamic/dynamic.js +57 -0
- package/dist/esm/shared/dynamic/dynamic.js.map +1 -0
- package/dist/esm/shared/dynamic/index.d.ts +2 -0
- package/dist/esm/shared/ossido-context.d.ts +22 -0
- package/dist/esm/shared/ossido-context.js +22 -0
- package/dist/esm/shared/ossido-context.js.map +1 -0
- package/dist/esm/shared/routeHot.d.ts +10 -0
- package/dist/esm/shared/routeHot.js +20 -0
- package/dist/esm/shared/routeHot.js.map +1 -0
- package/dist/esm/ssr/index.d.ts +4 -0
- package/dist/esm/ssr/index.js +6 -0
- package/dist/esm/ssr/polyfills/Event.d.ts +18 -0
- package/dist/esm/ssr/polyfills/Event.js +24 -0
- package/dist/esm/ssr/polyfills/Event.js.map +1 -0
- package/dist/esm/ssr/polyfills/MessageChannel.d.ts +19 -0
- package/dist/esm/ssr/polyfills/MessageChannel.js +50 -0
- package/dist/esm/ssr/polyfills/MessageChannel.js.map +1 -0
- package/dist/esm/ssr/polyfills/MessageEvent.d.ts +12 -0
- package/dist/esm/ssr/polyfills/MessageEvent.js +20 -0
- package/dist/esm/ssr/polyfills/MessageEvent.js.map +1 -0
- package/dist/esm/ssr/polyfills/globalScope.d.ts +30 -0
- package/dist/esm/ssr/polyfills/globalScope.js +5 -0
- package/dist/esm/ssr/polyfills/globalScope.js.map +1 -0
- package/dist/esm/ssr/server.d.ts +40 -0
- package/dist/esm/ssr/server.js +61 -0
- package/dist/esm/ssr/server.js.map +1 -0
- package/dist/esm/ssr/utils.d.ts +19 -0
- package/dist/esm/ssr/utils.js +78 -0
- package/dist/esm/ssr/utils.js.map +1 -0
- package/dist/esm/types.d.ts +34 -0
- package/package.json +110 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/build/logger.ts
|
|
2
|
+
/**
|
|
3
|
+
* `[FE]`-tagged logging for the frontend build/dev tooling (vite, the SSR
|
|
4
|
+
* bundler), matching the Rust `[FE]` format so all Ossido logs read the same:
|
|
5
|
+
* `[FE] LEVEL HH:MM:SS - message`. The `[FE]` tag is cyan; the level is coloured
|
|
6
|
+
* by severity.
|
|
7
|
+
*/
|
|
8
|
+
const RESET = "\x1B[0m";
|
|
9
|
+
const BOLD = "\x1B[1m";
|
|
10
|
+
const DIM = "\x1B[2m";
|
|
11
|
+
const CYAN = "\x1B[36m";
|
|
12
|
+
const RED = "\x1B[31m";
|
|
13
|
+
const YELLOW = "\x1B[33m";
|
|
14
|
+
const GREEN = "\x1B[32m";
|
|
15
|
+
/**
|
|
16
|
+
* Colours are disabled when `NO_COLOR` is set to a non-empty value (matching the
|
|
17
|
+
* Rust side / the `NO_COLOR` convention), so `NO_COLOR=1 ossido dev` is plain.
|
|
18
|
+
*/
|
|
19
|
+
const COLOR_ENABLED = typeof process === "undefined" || !process.env.NO_COLOR;
|
|
20
|
+
function timestamp() {
|
|
21
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false });
|
|
22
|
+
}
|
|
23
|
+
/** Wrap `text` in ANSI `code`…`RESET`, unless colours are disabled. */
|
|
24
|
+
function paint(code, text) {
|
|
25
|
+
return COLOR_ENABLED ? `${code}${text}${RESET}` : text;
|
|
26
|
+
}
|
|
27
|
+
function levelColor(level) {
|
|
28
|
+
if (level === "ERROR") return RED;
|
|
29
|
+
if (level === "WARN") return YELLOW;
|
|
30
|
+
return GREEN;
|
|
31
|
+
}
|
|
32
|
+
function formatLine(level, message) {
|
|
33
|
+
return `${paint(`${BOLD}${CYAN}`, "[FE]")} ${paint(levelColor(level), level)} ${paint(DIM, timestamp())} - ${message}`;
|
|
34
|
+
}
|
|
35
|
+
/** Print a single `[FE]`-tagged log line. */
|
|
36
|
+
function feLog(level, message) {
|
|
37
|
+
console.log(formatLine(level, message));
|
|
38
|
+
}
|
|
39
|
+
const LEVEL_RANK = {
|
|
40
|
+
silent: 0,
|
|
41
|
+
error: 1,
|
|
42
|
+
warn: 2,
|
|
43
|
+
info: 3
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* A vite {@link Logger} that routes vite's own output through the `[FE]` format,
|
|
47
|
+
* honouring `logLevel` (vite does not pre-filter for a custom logger, so the
|
|
48
|
+
* logger must do it — otherwise info/warn noise leaks even at `'error'`).
|
|
49
|
+
*/
|
|
50
|
+
function createOssidoViteLogger(logLevel = "info") {
|
|
51
|
+
const threshold = LEVEL_RANK[logLevel] ?? LEVEL_RANK.info;
|
|
52
|
+
const logger = {
|
|
53
|
+
hasWarned: false,
|
|
54
|
+
info: (message) => {
|
|
55
|
+
if (threshold >= LEVEL_RANK.info) feLog("INFO", message);
|
|
56
|
+
},
|
|
57
|
+
warn: (message) => {
|
|
58
|
+
logger.hasWarned = true;
|
|
59
|
+
if (threshold >= LEVEL_RANK.warn) feLog("WARN", message);
|
|
60
|
+
},
|
|
61
|
+
warnOnce: (message) => {
|
|
62
|
+
if (threshold >= LEVEL_RANK.warn) feLog("WARN", message);
|
|
63
|
+
},
|
|
64
|
+
error: (message) => {
|
|
65
|
+
logger.hasWarned = true;
|
|
66
|
+
if (threshold >= LEVEL_RANK.error) feLog("ERROR", message);
|
|
67
|
+
},
|
|
68
|
+
clearScreen: () => void 0,
|
|
69
|
+
hasErrorLogged: () => false
|
|
70
|
+
};
|
|
71
|
+
return logger;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { createOssidoViteLogger, feLog };
|
|
76
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","names":[],"sources":["../../../src/build/logger.ts"],"sourcesContent":["import { type Logger, type LogLevel } from 'vite';\n\n/**\n * `[FE]`-tagged logging for the frontend build/dev tooling (vite, the SSR\n * bundler), matching the Rust `[FE]` format so all Ossido logs read the same:\n * `[FE] LEVEL HH:MM:SS - message`. The `[FE]` tag is cyan; the level is coloured\n * by severity.\n */\n\nconst RESET = '\\x1b[0m';\nconst BOLD = '\\x1b[1m';\nconst DIM = '\\x1b[2m';\nconst CYAN = '\\x1b[36m';\nconst RED = '\\x1b[31m';\nconst YELLOW = '\\x1b[33m';\nconst GREEN = '\\x1b[32m';\n\n/**\n * Colours are disabled when `NO_COLOR` is set to a non-empty value (matching the\n * Rust side / the `NO_COLOR` convention), so `NO_COLOR=1 ossido dev` is plain.\n */\nconst COLOR_ENABLED = typeof process === 'undefined' || !process.env.NO_COLOR;\n\ntype FeLevel = 'INFO' | 'WARN' | 'ERROR';\n\nfunction timestamp(): string {\n return new Date().toLocaleTimeString('en-GB', { hour12: false });\n}\n\n/** Wrap `text` in ANSI `code`…`RESET`, unless colours are disabled. */\nfunction paint(code: string, text: string): string {\n return COLOR_ENABLED ? `${code}${text}${RESET}` : text;\n}\n\nfunction levelColor(level: FeLevel): string {\n if (level === 'ERROR') return RED;\n if (level === 'WARN') return YELLOW;\n return GREEN;\n}\n\nfunction formatLine(level: FeLevel, message: string): string {\n const tag = paint(`${BOLD}${CYAN}`, '[FE]');\n const lvl = paint(levelColor(level), level);\n const time = paint(DIM, timestamp());\n return `${tag} ${lvl} ${time} - ${message}`;\n}\n\n/** Print a single `[FE]`-tagged log line. */\nexport function feLog(level: FeLevel, message: string): void {\n // eslint-disable-next-line no-console\n console.log(formatLine(level, message));\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = {\n silent: 0,\n error: 1,\n warn: 2,\n info: 3,\n};\n\n/**\n * A vite {@link Logger} that routes vite's own output through the `[FE]` format,\n * honouring `logLevel` (vite does not pre-filter for a custom logger, so the\n * logger must do it — otherwise info/warn noise leaks even at `'error'`).\n */\nexport function createOssidoViteLogger(logLevel: LogLevel = 'info'): Logger {\n const threshold = LEVEL_RANK[logLevel] ?? LEVEL_RANK.info;\n const logger: Logger = {\n hasWarned: false,\n info: (message) => {\n if (threshold >= LEVEL_RANK.info) feLog('INFO', message);\n },\n warn: (message) => {\n logger.hasWarned = true;\n if (threshold >= LEVEL_RANK.warn) feLog('WARN', message);\n },\n warnOnce: (message) => {\n if (threshold >= LEVEL_RANK.warn) feLog('WARN', message);\n },\n error: (message) => {\n logger.hasWarned = true;\n if (threshold >= LEVEL_RANK.error) feLog('ERROR', message);\n },\n // Ossido manages its own console; never clear it from under other processes.\n clearScreen: () => undefined,\n hasErrorLogged: () => false,\n };\n return logger;\n}\n"],"mappings":";;;;;;;AASA,MAAM,QAAQ;AACd,MAAM,OAAO;AACb,MAAM,MAAM;AACZ,MAAM,OAAO;AACb,MAAM,MAAM;AACZ,MAAM,SAAS;AACf,MAAM,QAAQ;;;;;AAMd,MAAM,gBAAgB,OAAO,YAAY,eAAe,CAAC,QAAQ,IAAI;AAIrE,SAAS,YAAoB;CAC3B,wBAAO,IAAI,KAAK,EAAC,CAAC,mBAAmB,SAAS,EAAE,QAAQ,MAAM,CAAC;AACjE;;AAGA,SAAS,MAAM,MAAc,MAAsB;CACjD,OAAO,gBAAgB,GAAG,OAAO,OAAO,UAAU;AACpD;AAEA,SAAS,WAAW,OAAwB;CAC1C,IAAI,UAAU,SAAS,OAAO;CAC9B,IAAI,UAAU,QAAQ,OAAO;CAC7B,OAAO;AACT;AAEA,SAAS,WAAW,OAAgB,SAAyB;CAI3D,OAAO,GAHK,MAAM,GAAG,OAAO,QAAQ,MAGxB,EAAE,GAFF,MAAM,WAAW,KAAK,GAAG,KAElB,EAAE,GADR,MAAM,KAAK,UAAU,CACP,EAAE,KAAK;AACpC;;AAGA,SAAgB,MAAM,OAAgB,SAAuB;CAE3D,QAAQ,IAAI,WAAW,OAAO,OAAO,CAAC;AACxC;AAEA,MAAM,aAAuC;CAC3C,QAAQ;CACR,OAAO;CACP,MAAM;CACN,MAAM;AACR;;;;;;AAOA,SAAgB,uBAAuB,WAAqB,QAAgB;CAC1E,MAAM,YAAY,WAAW,aAAa,WAAW;CACrD,MAAM,SAAiB;EACrB,WAAW;EACX,OAAO,YAAY;GACjB,IAAI,aAAa,WAAW,MAAM,MAAM,QAAQ,OAAO;EACzD;EACA,OAAO,YAAY;GACjB,OAAO,YAAY;GACnB,IAAI,aAAa,WAAW,MAAM,MAAM,QAAQ,OAAO;EACzD;EACA,WAAW,YAAY;GACrB,IAAI,aAAa,WAAW,MAAM,MAAM,QAAQ,OAAO;EACzD;EACA,QAAQ,YAAY;GAClB,OAAO,YAAY;GACnB,IAAI,aAAa,WAAW,OAAO,MAAM,SAAS,OAAO;EAC3D;EAEA,mBAAmB;EACnB,sBAAsB;CACxB;CACA,OAAO;AACT"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OssidoConfig, OssidoConfigServer, OssidoConfigOutput, OssidoLogFormat, OssidoLogLevel } from '../config';
|
|
2
|
+
/** Fully-resolved logging config (defaults filled) written to `config.json`. */
|
|
3
|
+
export interface InternalOssidoConfigLogging {
|
|
4
|
+
format: OssidoLogFormat;
|
|
5
|
+
routeTree: boolean;
|
|
6
|
+
browser: {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
level: OssidoLogLevel;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Fully-resolved dev config (defaults filled). */
|
|
12
|
+
export interface InternalOssidoConfigDev {
|
|
13
|
+
criticalCss: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Fully-resolved SSR config. `renderThreads` is `null` when unset — the Rust
|
|
17
|
+
* runtime then defaults it to the machine's available parallelism.
|
|
18
|
+
*/
|
|
19
|
+
export interface InternalOssidoConfigSsr {
|
|
20
|
+
renderThreads: number | null;
|
|
21
|
+
}
|
|
22
|
+
export interface InternalOssidoConfig extends Omit<OssidoConfig, 'server' | 'logging' | 'dev' | 'ssr' | 'output' | 'viewTransitions'> {
|
|
23
|
+
server: OssidoConfigServer;
|
|
24
|
+
logging: InternalOssidoConfigLogging;
|
|
25
|
+
dev: InternalOssidoConfigDev;
|
|
26
|
+
ssr: InternalOssidoConfigSsr;
|
|
27
|
+
output: OssidoConfigOutput;
|
|
28
|
+
viewTransitions: boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const blockingAsync: (callback: () => Promise<void>) => void;
|
|
2
|
+
/**
|
|
3
|
+
* Every file under `dir`, as paths relative to `dir` (posix-style separators).
|
|
4
|
+
* Returns `[]` if `dir` doesn't exist. Used to build a build hook's `manifest`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const listFilesRecursive: (dir: string) => Array<string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
//#region src/build/utils.ts
|
|
5
|
+
const blockingAsync = (callback) => {
|
|
6
|
+
(async () => {
|
|
7
|
+
try {
|
|
8
|
+
await callback();
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error(error);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Every file under `dir`, as paths relative to `dir` (posix-style separators).
|
|
17
|
+
* Returns `[]` if `dir` doesn't exist. Used to build a build hook's `manifest`.
|
|
18
|
+
*/
|
|
19
|
+
const listFilesRecursive = (dir) => {
|
|
20
|
+
const walk = (current, prefix) => {
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const files = [];
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
30
|
+
if (entry.isDirectory()) files.push(...walk(join(current, entry.name), rel));
|
|
31
|
+
else if (entry.isFile()) files.push(rel);
|
|
32
|
+
}
|
|
33
|
+
return files;
|
|
34
|
+
};
|
|
35
|
+
return walk(dir, "");
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { blockingAsync, listFilesRecursive };
|
|
40
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../../../src/build/utils.ts"],"sourcesContent":["import { readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport const blockingAsync = (callback: () => Promise<void>): void => {\n void (async (): Promise<void> => {\n try {\n await callback();\n } catch (error) {\n // Surface build failures instead of silently swallowing them. A failed\n // SSR bundle build, for example, otherwise degrades to a confusing\n // client-side fallback with hydration mismatches.\n console.error(error);\n process.exitCode = 1;\n }\n })();\n};\n\n/**\n * Every file under `dir`, as paths relative to `dir` (posix-style separators).\n * Returns `[]` if `dir` doesn't exist. Used to build a build hook's `manifest`.\n */\nexport const listFilesRecursive = (dir: string): Array<string> => {\n const walk = (current: string, prefix: string): Array<string> => {\n let entries;\n try {\n entries = readdirSync(current, { withFileTypes: true });\n } catch {\n return []; // missing directory (e.g. no output produced)\n }\n const files: Array<string> = [];\n for (const entry of entries) {\n const rel = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n files.push(...walk(join(current, entry.name), rel));\n } else if (entry.isFile()) {\n files.push(rel);\n }\n }\n return files;\n };\n return walk(dir, '');\n};\n"],"mappings":";;;;AAGA,MAAa,iBAAiB,aAAwC;CACpE,CAAM,YAA2B;EAC/B,IAAI;GACF,MAAM,SAAS;EACjB,SAAS,OAAO;GAId,QAAQ,MAAM,KAAK;GACnB,QAAQ,WAAW;EACrB;CACF,EAAC,CAAE;AACL;;;;;AAMA,MAAa,sBAAsB,QAA+B;CAChE,MAAM,QAAQ,SAAiB,WAAkC;EAC/D,IAAI;EACJ,IAAI;GACF,UAAU,YAAY,SAAS,EAAE,eAAe,KAAK,CAAC;EACxD,QAAQ;GACN,OAAO,CAAC;EACV;EACA,MAAM,QAAuB,CAAC;EAC9B,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,SAAS,MAAM;GACvD,IAAI,MAAM,YAAY,GACpB,MAAM,KAAK,GAAG,KAAK,KAAK,SAAS,MAAM,IAAI,GAAG,GAAG,CAAC;QAC7C,IAAI,MAAM,OAAO,GACtB,MAAM,KAAK,GAAG;EAElB;EACA,OAAO;CACT;CACA,OAAO,KAAK,KAAK,EAAE;AACrB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'vite/client';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "vite/client";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A typed `fetch` client for a project's `#[api(METHOD)]` routes.
|
|
3
|
+
*
|
|
4
|
+
* The HTTP method is the client method name (`apiClient.post(...)`) so the URL,
|
|
5
|
+
* route params, and the `.json()` response are all inferred from the method +
|
|
6
|
+
* path. The route map is generated into `.ossido/types.ts` as an augmentation of
|
|
7
|
+
* the `Register` interface below:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* declare module "@ossido-labs/ossido/client" {
|
|
11
|
+
* interface Register {
|
|
12
|
+
* apiRoutes: {
|
|
13
|
+
* "/api/pokemons/[name]": {
|
|
14
|
+
* GET: { params: { name: string }; response: import("@ossido-labs/ossido/types").Pokemon }
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { apiClient } from '@ossido-labs/ossido/client'
|
|
23
|
+
*
|
|
24
|
+
* const res = await apiClient.get('/api/pokemons/[name]', { params: { name: 'pikachu' } })
|
|
25
|
+
* const pokemon = await res.json() //=> Pokemon
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
type MethodName = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
29
|
+
type HttpMethod = Uppercase<MethodName>;
|
|
30
|
+
/**
|
|
31
|
+
* Augmentation target for the generated route map. Left empty here; the
|
|
32
|
+
* project's generated `.ossido/types.ts` merges an `apiRoutes` member into it.
|
|
33
|
+
*/
|
|
34
|
+
export interface Register {
|
|
35
|
+
}
|
|
36
|
+
interface ApiRouteEntry {
|
|
37
|
+
params: Record<string, string>;
|
|
38
|
+
response: unknown;
|
|
39
|
+
}
|
|
40
|
+
type AnyApiRoutes = Record<string, Partial<Record<HttpMethod, ApiRouteEntry>>>;
|
|
41
|
+
/**
|
|
42
|
+
* The registered routes, or a permissive fallback when a project hasn't
|
|
43
|
+
* generated its types yet (so the client stays usable, just untyped).
|
|
44
|
+
*/
|
|
45
|
+
type ApiRoutes = Register extends {
|
|
46
|
+
apiRoutes: infer R;
|
|
47
|
+
} ? R extends AnyApiRoutes ? R : AnyApiRoutes : AnyApiRoutes;
|
|
48
|
+
/** Paths that serve the given HTTP method. */
|
|
49
|
+
type PathsFor<M extends HttpMethod> = {
|
|
50
|
+
[P in keyof ApiRoutes]: M extends keyof ApiRoutes[P] ? P : never;
|
|
51
|
+
}[keyof ApiRoutes] & string;
|
|
52
|
+
type EntryFor<P extends string, M extends HttpMethod> = P extends keyof ApiRoutes ? M extends keyof ApiRoutes[P] ? ApiRoutes[P][M] extends ApiRouteEntry ? ApiRoutes[P][M] : ApiRouteEntry : ApiRouteEntry : ApiRouteEntry;
|
|
53
|
+
type ParamsOf<P extends string, M extends HttpMethod> = EntryFor<P, M>['params'];
|
|
54
|
+
type ResponseOf<P extends string, M extends HttpMethod> = EntryFor<P, M>['response'];
|
|
55
|
+
/**
|
|
56
|
+
* Whether a params object *requires* any key. `{} extends T` is true only when
|
|
57
|
+
* every property of `T` is optional/absent — so a static route
|
|
58
|
+
* (`Record<string, never>`) is `false` and a dynamic one (`{ id: string }`) is
|
|
59
|
+
* `true`. (`keyof T extends never` would be wrong: `keyof Record<string, never>`
|
|
60
|
+
* is `string`, not `never`.)
|
|
61
|
+
*/
|
|
62
|
+
type HasKeys<T> = {} extends T ? false : true;
|
|
63
|
+
/** A `fetch` {@link Response} whose `json()` is typed to the route's response. */
|
|
64
|
+
export interface TypedResponse<T> extends Response {
|
|
65
|
+
json(): Promise<T>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `fetch` options minus `method` (the method is the client method name), plus
|
|
69
|
+
* `params` to fill the URL's `[segments]` — required only for dynamic routes.
|
|
70
|
+
*/
|
|
71
|
+
export type RequestOptions<Params> = Omit<RequestInit, 'method'> & (HasKeys<Params> extends true ? {
|
|
72
|
+
params: Params;
|
|
73
|
+
} : {
|
|
74
|
+
params?: Record<string, never>;
|
|
75
|
+
});
|
|
76
|
+
type MethodFn<M extends HttpMethod> = <P extends PathsFor<M>>(path: P, ...args: HasKeys<ParamsOf<P, M>> extends true ? [options: RequestOptions<ParamsOf<P, M>>] : [options?: RequestOptions<ParamsOf<P, M>>]) => Promise<TypedResponse<ResponseOf<P, M>>>;
|
|
77
|
+
export type ApiClient = {
|
|
78
|
+
[M in MethodName]: MethodFn<Uppercase<M>>;
|
|
79
|
+
};
|
|
80
|
+
export interface CreateApiClientOptions {
|
|
81
|
+
/** Prepended to every request path (e.g. an absolute origin for SSR fetches). */
|
|
82
|
+
baseUrl?: string;
|
|
83
|
+
/** Default `fetch` options merged into every request. */
|
|
84
|
+
defaults?: Omit<RequestInit, 'method'>;
|
|
85
|
+
/** Custom `fetch` implementation (defaults to the global `fetch`). */
|
|
86
|
+
fetch?: typeof fetch;
|
|
87
|
+
}
|
|
88
|
+
/** Create an {@link ApiClient}, optionally with a base URL and default options. */
|
|
89
|
+
export declare function createApiClient(options?: CreateApiClientOptions): ApiClient;
|
|
90
|
+
/** The default client — global `fetch`, relative paths. */
|
|
91
|
+
export declare const apiClient: ApiClient;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/client/index.ts
|
|
2
|
+
const PARAM_SEGMENT = /\[(\.\.\.)?([^\]]+)\]/g;
|
|
3
|
+
/** Substitute `params` into a path template's `[segments]`. */
|
|
4
|
+
function buildPath(path, params) {
|
|
5
|
+
if (!params) return path;
|
|
6
|
+
return path.replace(PARAM_SEGMENT, (_match, spread, name) => {
|
|
7
|
+
const value = params[name];
|
|
8
|
+
if (value == null) throw new TypeError(`Missing value for route param "${name}" in "${path}"`);
|
|
9
|
+
const str = String(value);
|
|
10
|
+
return spread ? str.split("/").map(encodeURIComponent).join("/") : encodeURIComponent(str);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** Create an {@link ApiClient}, optionally with a base URL and default options. */
|
|
14
|
+
function createApiClient(options = {}) {
|
|
15
|
+
const { baseUrl = "", defaults, fetch: fetchImpl } = options;
|
|
16
|
+
const request = (method) => (path, opts = {}) => {
|
|
17
|
+
const { params, ...init } = opts;
|
|
18
|
+
const url = baseUrl + buildPath(path, params);
|
|
19
|
+
return (fetchImpl ?? fetch)(url, {
|
|
20
|
+
...defaults,
|
|
21
|
+
...init,
|
|
22
|
+
method
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
get: request("GET"),
|
|
27
|
+
post: request("POST"),
|
|
28
|
+
put: request("PUT"),
|
|
29
|
+
patch: request("PATCH"),
|
|
30
|
+
delete: request("DELETE")
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The default client — global `fetch`, relative paths. */
|
|
34
|
+
const apiClient = createApiClient();
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { apiClient, createApiClient };
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/client/index.ts"],"sourcesContent":["/**\n * A typed `fetch` client for a project's `#[api(METHOD)]` routes.\n *\n * The HTTP method is the client method name (`apiClient.post(...)`) so the URL,\n * route params, and the `.json()` response are all inferred from the method +\n * path. The route map is generated into `.ossido/types.ts` as an augmentation of\n * the `Register` interface below:\n *\n * ```ts\n * declare module \"@ossido-labs/ossido/client\" {\n * interface Register {\n * apiRoutes: {\n * \"/api/pokemons/[name]\": {\n * GET: { params: { name: string }; response: import(\"@ossido-labs/ossido/types\").Pokemon }\n * }\n * }\n * }\n * }\n * ```\n *\n * ```ts\n * import { apiClient } from '@ossido-labs/ossido/client'\n *\n * const res = await apiClient.get('/api/pokemons/[name]', { params: { name: 'pikachu' } })\n * const pokemon = await res.json() //=> Pokemon\n * ```\n */\n\ntype MethodName = 'get' | 'post' | 'put' | 'patch' | 'delete';\ntype HttpMethod = Uppercase<MethodName>;\n\n/**\n * Augmentation target for the generated route map. Left empty here; the\n * project's generated `.ossido/types.ts` merges an `apiRoutes` member into it.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface Register {}\n\n// Route params are always URL-segment strings; an empty route has\n// `Record<string, never>` (no fillable segments).\ninterface ApiRouteEntry {\n params: Record<string, string>;\n response: unknown;\n}\ntype AnyApiRoutes = Record<string, Partial<Record<HttpMethod, ApiRouteEntry>>>;\n\n/**\n * The registered routes, or a permissive fallback when a project hasn't\n * generated its types yet (so the client stays usable, just untyped).\n */\ntype ApiRoutes = Register extends { apiRoutes: infer R }\n ? R extends AnyApiRoutes\n ? R\n : AnyApiRoutes\n : AnyApiRoutes;\n\n/** Paths that serve the given HTTP method. */\ntype PathsFor<M extends HttpMethod> = {\n [P in keyof ApiRoutes]: M extends keyof ApiRoutes[P] ? P : never;\n}[keyof ApiRoutes] &\n string;\n\ntype EntryFor<\n P extends string,\n M extends HttpMethod,\n> = P extends keyof ApiRoutes\n ? M extends keyof ApiRoutes[P]\n ? ApiRoutes[P][M] extends ApiRouteEntry\n ? ApiRoutes[P][M]\n : ApiRouteEntry\n : ApiRouteEntry\n : ApiRouteEntry;\n\ntype ParamsOf<P extends string, M extends HttpMethod> = EntryFor<\n P,\n M\n>['params'];\ntype ResponseOf<P extends string, M extends HttpMethod> = EntryFor<\n P,\n M\n>['response'];\n\n/**\n * Whether a params object *requires* any key. `{} extends T` is true only when\n * every property of `T` is optional/absent — so a static route\n * (`Record<string, never>`) is `false` and a dynamic one (`{ id: string }`) is\n * `true`. (`keyof T extends never` would be wrong: `keyof Record<string, never>`\n * is `string`, not `never`.)\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\ntype HasKeys<T> = {} extends T ? false : true;\n\n/** A `fetch` {@link Response} whose `json()` is typed to the route's response. */\nexport interface TypedResponse<T> extends Response {\n json(): Promise<T>;\n}\n\n/**\n * `fetch` options minus `method` (the method is the client method name), plus\n * `params` to fill the URL's `[segments]` — required only for dynamic routes.\n */\nexport type RequestOptions<Params> = Omit<RequestInit, 'method'> &\n (HasKeys<Params> extends true\n ? { params: Params }\n : { params?: Record<string, never> });\n\ntype MethodFn<M extends HttpMethod> = <P extends PathsFor<M>>(\n path: P,\n ...args: HasKeys<ParamsOf<P, M>> extends true\n ? [options: RequestOptions<ParamsOf<P, M>>]\n : [options?: RequestOptions<ParamsOf<P, M>>]\n) => Promise<TypedResponse<ResponseOf<P, M>>>;\n\nexport type ApiClient = { [M in MethodName]: MethodFn<Uppercase<M>> };\n\nexport interface CreateApiClientOptions {\n /** Prepended to every request path (e.g. an absolute origin for SSR fetches). */\n baseUrl?: string;\n /** Default `fetch` options merged into every request. */\n defaults?: Omit<RequestInit, 'method'>;\n /** Custom `fetch` implementation (defaults to the global `fetch`). */\n fetch?: typeof fetch;\n}\n\n// Matches `[name]` and `[...slug]`, capturing the spread marker and the name.\nconst PARAM_SEGMENT = /\\[(\\.\\.\\.)?([^\\]]+)\\]/g;\n\n/** Substitute `params` into a path template's `[segments]`. */\nfunction buildPath(path: string, params?: Record<string, unknown>): string {\n if (!params) return path;\n return path.replace(\n PARAM_SEGMENT,\n (_match, spread: string | undefined, name: string) => {\n const value = params[name];\n if (value == null) {\n throw new TypeError(\n `Missing value for route param \"${name}\" in \"${path}\"`,\n );\n }\n const str = String(value);\n // A catch-all (`[...slug]`) spans multiple segments, so keep its slashes.\n return spread\n ? str.split('/').map(encodeURIComponent).join('/')\n : encodeURIComponent(str);\n },\n );\n}\n\n/** Create an {@link ApiClient}, optionally with a base URL and default options. */\nexport function createApiClient(\n options: CreateApiClientOptions = {},\n): ApiClient {\n const { baseUrl = '', defaults, fetch: fetchImpl } = options;\n\n const request =\n (method: HttpMethod) =>\n (\n path: string,\n opts: RequestInit & { params?: Record<string, unknown> } = {},\n ): Promise<Response> => {\n const { params, ...init } = opts;\n const url = baseUrl + buildPath(path, params);\n return (fetchImpl ?? fetch)(url, { ...defaults, ...init, method });\n };\n\n return {\n get: request('GET'),\n post: request('POST'),\n put: request('PUT'),\n patch: request('PATCH'),\n delete: request('DELETE'),\n } as ApiClient;\n}\n\n/** The default client — global `fetch`, relative paths. */\nexport const apiClient: ApiClient = createApiClient();\n"],"mappings":";AA6HA,MAAM,gBAAgB;;AAGtB,SAAS,UAAU,MAAc,QAA0C;CACzE,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO,KAAK,QACV,gBACC,QAAQ,QAA4B,SAAiB;EACpD,MAAM,QAAQ,OAAO;EACrB,IAAI,SAAS,MACX,MAAM,IAAI,UACR,kCAAkC,KAAK,QAAQ,KAAK,EACtD;EAEF,MAAM,MAAM,OAAO,KAAK;EAExB,OAAO,SACH,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,KAAK,GAAG,IAC/C,mBAAmB,GAAG;CAC5B,CACF;AACF;;AAGA,SAAgB,gBACd,UAAkC,CAAC,GACxB;CACX,MAAM,EAAE,UAAU,IAAI,UAAU,OAAO,cAAc;CAErD,MAAM,WACH,YAEC,MACA,OAA2D,CAAC,MACtC;EACtB,MAAM,EAAE,QAAQ,GAAG,SAAS;EAC5B,MAAM,MAAM,UAAU,UAAU,MAAM,MAAM;EAC5C,QAAQ,aAAa,MAAK,CAAE,KAAK;GAAE,GAAG;GAAU,GAAG;GAAM;EAAO,CAAC;CACnE;CAEF,OAAO;EACL,KAAK,QAAQ,KAAK;EAClB,MAAM,QAAQ,MAAM;EACpB,KAAK,QAAQ,KAAK;EAClB,OAAO,QAAQ,OAAO;EACtB,QAAQ,QAAQ,QAAQ;CAC1B;AACF;;AAGA,MAAa,YAAuB,gBAAgB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { OssidoConfig, OssidoConfigServer, OssidoConfigLogging, OssidoConfigSsr, OssidoConfigOutput, OssidoConfigBuild, OssidoBuildContext, OssidoBuildHook, OssidoLogFormat, OssidoLogLevel, } from './types';
|
|
File without changes
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { AliasOptions, DepOptimizationOptions, PluginOption, CSSOptions } from 'vite';
|
|
2
|
+
export interface OssidoConfigServer {
|
|
3
|
+
host: string;
|
|
4
|
+
origin: string | null;
|
|
5
|
+
port: number;
|
|
6
|
+
}
|
|
7
|
+
/** Output format for the server logs. */
|
|
8
|
+
export type OssidoLogFormat = 'pretty' | 'json';
|
|
9
|
+
/** Log severities, lowest to highest. */
|
|
10
|
+
export type OssidoLogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
|
11
|
+
export interface OssidoConfigLogging {
|
|
12
|
+
/**
|
|
13
|
+
* Output style for the (Rust) server logs.
|
|
14
|
+
* - `'pretty'` (default): human, coloured, single line.
|
|
15
|
+
* - `'json'`: one JSON object per line, GCP Cloud Logging compatible.
|
|
16
|
+
*/
|
|
17
|
+
format?: OssidoLogFormat;
|
|
18
|
+
/**
|
|
19
|
+
* Print the route tree on `ossido dev` start-up. Default `false` — set to
|
|
20
|
+
* `true` to opt in. (`ossido build` always prints it, regardless of this
|
|
21
|
+
* option.)
|
|
22
|
+
*/
|
|
23
|
+
routeTree?: boolean;
|
|
24
|
+
/** Forwarding of browser `console.*` to the dev server console (dev only). */
|
|
25
|
+
browser?: {
|
|
26
|
+
/** Whether to forward browser logs at all. Default `true`. */
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/** Minimum console level to forward. Default `'info'`. */
|
|
29
|
+
level?: OssidoLogLevel;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @see http://ossido.dev/documentation/configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface OssidoConfig {
|
|
36
|
+
server?: Partial<OssidoConfigServer>;
|
|
37
|
+
vite?: {
|
|
38
|
+
alias?: AliasOptions;
|
|
39
|
+
css?: CSSOptions;
|
|
40
|
+
optimizeDeps?: DepOptimizationOptions;
|
|
41
|
+
plugins?: Array<PluginOption>;
|
|
42
|
+
};
|
|
43
|
+
logging?: OssidoConfigLogging;
|
|
44
|
+
dev?: OssidoConfigDev;
|
|
45
|
+
ssr?: OssidoConfigSsr;
|
|
46
|
+
/**
|
|
47
|
+
* Default build output mode. Default `'server'`.
|
|
48
|
+
* - `'server'`: build the SSR server (`ossido build`).
|
|
49
|
+
* - `'static'`: statically generate the site (equivalent to `ossido build
|
|
50
|
+
* --static`).
|
|
51
|
+
*
|
|
52
|
+
* The `--static` / `--server` CLI flags override this per invocation.
|
|
53
|
+
*/
|
|
54
|
+
output?: OssidoConfigOutput;
|
|
55
|
+
/** Build lifecycle hooks (`ossido build`, both output modes; not `dev`). */
|
|
56
|
+
build?: OssidoConfigBuild;
|
|
57
|
+
/**
|
|
58
|
+
* Animate client-side navigations with the browser
|
|
59
|
+
* [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API).
|
|
60
|
+
* Default `false`. When enabled, `push`/`replace`/`<Link>`/back-forward
|
|
61
|
+
* navigations run inside `document.startViewTransition` (a crossfade by
|
|
62
|
+
* default; customise with `view-transition-name` / `::view-transition-*` CSS).
|
|
63
|
+
* No-ops where unsupported or under `prefers-reduced-motion`; override per
|
|
64
|
+
* navigation with `push(path, { viewTransition: false })` or
|
|
65
|
+
* `<Link viewTransition={false}>`.
|
|
66
|
+
*/
|
|
67
|
+
viewTransitions?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** Build output mode. */
|
|
70
|
+
export type OssidoConfigOutput = 'static' | 'server';
|
|
71
|
+
/**
|
|
72
|
+
* Everything a build hook is given about the current `ossido build`.
|
|
73
|
+
*/
|
|
74
|
+
export interface OssidoBuildContext {
|
|
75
|
+
/** The resolved output mode for this build. */
|
|
76
|
+
mode: OssidoConfigOutput;
|
|
77
|
+
/**
|
|
78
|
+
* Absolute path to the deployable output directory: `out/static` for a static
|
|
79
|
+
* build, `out` for a server build. The directory you'd upload/deploy.
|
|
80
|
+
*/
|
|
81
|
+
outputDirectory: string;
|
|
82
|
+
/** Absolute path to the project's source `public/` folder. */
|
|
83
|
+
publicDirectory: string;
|
|
84
|
+
/**
|
|
85
|
+
* Emitted file paths, relative to `outputDirectory`. Populated for
|
|
86
|
+
* `postbuild` (after all artifacts exist); empty for `prebuild`.
|
|
87
|
+
*/
|
|
88
|
+
manifest: Array<string>;
|
|
89
|
+
/** The resolved Ossido config, so hooks can branch on build settings. */
|
|
90
|
+
config: OssidoConfig;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A build lifecycle hook. May be async — the build awaits it, and a thrown
|
|
94
|
+
* error fails the build.
|
|
95
|
+
*/
|
|
96
|
+
export type OssidoBuildHook = (ctx: OssidoBuildContext) => void | Promise<void>;
|
|
97
|
+
/** `prebuild` / `postbuild` hooks, defined under `build` in the config. */
|
|
98
|
+
export interface OssidoConfigBuild {
|
|
99
|
+
/** Runs before the build starts (its `ctx.manifest` is empty). */
|
|
100
|
+
prebuild?: OssidoBuildHook;
|
|
101
|
+
/** Runs after all build artifacts have been produced. */
|
|
102
|
+
postbuild?: OssidoBuildHook;
|
|
103
|
+
}
|
|
104
|
+
/** Server-side rendering options. */
|
|
105
|
+
export interface OssidoConfigSsr {
|
|
106
|
+
/**
|
|
107
|
+
* Number of dedicated V8 render-pool threads. Each holds a warm isolate and
|
|
108
|
+
* renders one request at a time, off the async runtime (which stays free for
|
|
109
|
+
* I/O). Defaults to the machine's available parallelism (CPU cores), resolved
|
|
110
|
+
* at runtime.
|
|
111
|
+
*
|
|
112
|
+
* Lower it to cap memory on constrained hosts (e.g. `1`); raising it past the
|
|
113
|
+
* core count only adds contention for CPU-bound rendering. Overridable at
|
|
114
|
+
* runtime with the `OSSIDO_SSR_THREADS` environment variable.
|
|
115
|
+
*/
|
|
116
|
+
renderThreads?: number;
|
|
117
|
+
}
|
|
118
|
+
/** Development-only tweaks. */
|
|
119
|
+
export interface OssidoConfigDev {
|
|
120
|
+
/**
|
|
121
|
+
* Compute and inject per-route critical CSS during dev navigation, preventing
|
|
122
|
+
* a flash of unstyled content (vite injects CSS via JS, which otherwise
|
|
123
|
+
* arrives a beat late). Default `true`.
|
|
124
|
+
*
|
|
125
|
+
* Computing it walks the route's module/CSS graph, which adds latency to
|
|
126
|
+
* navigation. Set `false` for the snappiest dev navigation, accepting a brief
|
|
127
|
+
* unstyled flash on first visit to a route.
|
|
128
|
+
*/
|
|
129
|
+
criticalCss?: boolean;
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SERVER_PAYLOAD_VARIABLE_NAME = "__OSSIDO_SERVER_PAYLOAD__";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../src/constants.ts"],"sourcesContent":["export const SERVER_PAYLOAD_VARIABLE_NAME = '__OSSIDO_SERVER_PAYLOAD__';\n"],"mappings":";AAAA,MAAa,+BAA+B"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only: forward browser `console.*` (and uncaught errors) to the Ossido dev
|
|
3
|
+
* server console, where they are printed tagged `[FE]`. The server applies the
|
|
4
|
+
* `logging.browser` level/enabled config. The original console output is kept
|
|
5
|
+
* intact.
|
|
6
|
+
*/
|
|
7
|
+
export declare function installBrowserLogForwarding(): void;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//#region src/hydration/browserLogForwarding.ts
|
|
2
|
+
const CONSOLE_METHODS = [
|
|
3
|
+
"log",
|
|
4
|
+
"info",
|
|
5
|
+
"warn",
|
|
6
|
+
"error",
|
|
7
|
+
"debug",
|
|
8
|
+
"trace"
|
|
9
|
+
];
|
|
10
|
+
const LOGS_ENDPOINT = "/__ossido/logs";
|
|
11
|
+
const DATA_ROUTE_PREFIX = "/__ossido/data";
|
|
12
|
+
const FLUSH_DELAY_MS = 60;
|
|
13
|
+
const MAX_FRAMES = 6;
|
|
14
|
+
const MAX_TRACKED_DATA_LOADS = 50;
|
|
15
|
+
/** Mirror the backend's status→level mapping so a failed data load stands out. */
|
|
16
|
+
function levelForStatus(status) {
|
|
17
|
+
if (status >= 500) return "error";
|
|
18
|
+
if (status >= 400) return "warn";
|
|
19
|
+
return "info";
|
|
20
|
+
}
|
|
21
|
+
function formatArg(arg) {
|
|
22
|
+
if (typeof arg === "string") return arg;
|
|
23
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
|
24
|
+
if (arg === void 0) return "undefined";
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(arg);
|
|
27
|
+
} catch {
|
|
28
|
+
return String(arg);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Tidy a browser stack: drop the leading `Name: message` line, trim frames and
|
|
32
|
+
* strip the dev-server origin so paths read like the project source. */
|
|
33
|
+
function cleanStack(stack) {
|
|
34
|
+
if (!stack) return [];
|
|
35
|
+
const origin = typeof location !== "undefined" ? location.origin : "";
|
|
36
|
+
return stack.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("at ") || line.includes("@")).map((line) => line.replaceAll(`${origin}/`, "").replaceAll("vite-server/", "")).slice(0, MAX_FRAMES);
|
|
37
|
+
}
|
|
38
|
+
function toEntry(level, args) {
|
|
39
|
+
const message = args.map(formatArg).join(" ");
|
|
40
|
+
const errorArg = args.find((arg) => arg instanceof Error);
|
|
41
|
+
return {
|
|
42
|
+
level,
|
|
43
|
+
message,
|
|
44
|
+
error: errorArg ? {
|
|
45
|
+
name: errorArg.name,
|
|
46
|
+
message: errorArg.message,
|
|
47
|
+
stack: cleanStack(errorArg.stack)
|
|
48
|
+
} : void 0
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let installed = false;
|
|
52
|
+
function installBrowserLogForwarding() {
|
|
53
|
+
if (installed || typeof window === "undefined") return;
|
|
54
|
+
installed = true;
|
|
55
|
+
const queue = [];
|
|
56
|
+
let scheduled = false;
|
|
57
|
+
const flush = () => {
|
|
58
|
+
scheduled = false;
|
|
59
|
+
if (queue.length === 0) return;
|
|
60
|
+
const body = JSON.stringify(queue.splice(0, queue.length));
|
|
61
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
62
|
+
if (!navigator.sendBeacon?.(LOGS_ENDPOINT, blob)) fetch(LOGS_ENDPOINT, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
body,
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
keepalive: true
|
|
67
|
+
}).catch(() => void 0);
|
|
68
|
+
};
|
|
69
|
+
const schedule = () => {
|
|
70
|
+
if (scheduled) return;
|
|
71
|
+
scheduled = true;
|
|
72
|
+
setTimeout(flush, FLUSH_DELAY_MS);
|
|
73
|
+
};
|
|
74
|
+
const enqueue = (level, args) => {
|
|
75
|
+
try {
|
|
76
|
+
queue.push(toEntry(level, args));
|
|
77
|
+
schedule();
|
|
78
|
+
} catch {}
|
|
79
|
+
};
|
|
80
|
+
const withForwarding = (level) => (original) => (...args) => {
|
|
81
|
+
original(...args);
|
|
82
|
+
enqueue(level, args);
|
|
83
|
+
};
|
|
84
|
+
for (const method of CONSOLE_METHODS) console[method] = withForwarding(method)(console[method].bind(console));
|
|
85
|
+
window.addEventListener("error", (event) => {
|
|
86
|
+
enqueue("error", [event.error ?? event.message]);
|
|
87
|
+
});
|
|
88
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
89
|
+
enqueue("error", [event.reason]);
|
|
90
|
+
});
|
|
91
|
+
const dataLoads = /* @__PURE__ */ new Map();
|
|
92
|
+
const originalFetch = typeof window.fetch === "function" ? window.fetch.bind(window) : void 0;
|
|
93
|
+
if (originalFetch) window.fetch = (input, init) => {
|
|
94
|
+
const request = originalFetch(input, init);
|
|
95
|
+
try {
|
|
96
|
+
const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
97
|
+
const { pathname, search } = new URL(raw, location.origin);
|
|
98
|
+
if (pathname.startsWith(DATA_ROUTE_PREFIX)) {
|
|
99
|
+
const navPath = pathname.slice(14) + search || "/";
|
|
100
|
+
const start = performance.now();
|
|
101
|
+
if (dataLoads.size >= MAX_TRACKED_DATA_LOADS) {
|
|
102
|
+
const oldest = dataLoads.keys().next().value;
|
|
103
|
+
if (oldest !== void 0) dataLoads.delete(oldest);
|
|
104
|
+
}
|
|
105
|
+
dataLoads.set(navPath, request.then((res) => ({
|
|
106
|
+
status: res.status,
|
|
107
|
+
durationMs: performance.now() - start
|
|
108
|
+
}), () => null));
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
return request;
|
|
112
|
+
};
|
|
113
|
+
const emitNavigation = async () => {
|
|
114
|
+
try {
|
|
115
|
+
const path = window.location.pathname + window.location.search;
|
|
116
|
+
const load = dataLoads.get(path);
|
|
117
|
+
dataLoads.delete(path);
|
|
118
|
+
const timing = load ? await load : null;
|
|
119
|
+
queue.push(timing ? {
|
|
120
|
+
level: levelForStatus(timing.status),
|
|
121
|
+
message: path,
|
|
122
|
+
navigation: true,
|
|
123
|
+
status: timing.status,
|
|
124
|
+
durationMs: timing.durationMs
|
|
125
|
+
} : {
|
|
126
|
+
level: "info",
|
|
127
|
+
message: path,
|
|
128
|
+
navigation: true
|
|
129
|
+
});
|
|
130
|
+
schedule();
|
|
131
|
+
} catch {}
|
|
132
|
+
};
|
|
133
|
+
const reportNavigation = () => {
|
|
134
|
+
queueMicrotask(() => {
|
|
135
|
+
emitNavigation();
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
const patchHistory = (method) => {
|
|
139
|
+
const original = history[method].bind(history);
|
|
140
|
+
history[method] = (...args) => {
|
|
141
|
+
original(...args);
|
|
142
|
+
reportNavigation();
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
patchHistory("pushState");
|
|
146
|
+
patchHistory("replaceState");
|
|
147
|
+
window.addEventListener("popstate", reportNavigation);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
export { installBrowserLogForwarding };
|
|
152
|
+
//# sourceMappingURL=browserLogForwarding.js.map
|