@jasonshimmy/vite-plugin-cer-app 0.7.0 → 0.9.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/CHANGELOG.md +8 -0
- package/ROADMAP.md +278 -0
- package/commits.txt +1 -1
- package/dist/cli/commands/preview-isr.d.ts +6 -0
- package/dist/cli/commands/preview-isr.d.ts.map +1 -1
- package/dist/cli/commands/preview-isr.js +12 -0
- package/dist/cli/commands/preview-isr.js.map +1 -1
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +66 -6
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/plugin/dev-server.d.ts +1 -0
- package/dist/plugin/dev-server.d.ts.map +1 -1
- package/dist/plugin/dts-generator.d.ts.map +1 -1
- package/dist/plugin/dts-generator.js +4 -2
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +30 -12
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
- package/dist/plugin/transforms/auto-import.js +5 -4
- package/dist/plugin/transforms/auto-import.js.map +1 -1
- package/dist/plugin/virtual/routes.d.ts.map +1 -1
- package/dist/plugin/virtual/routes.js +7 -1
- package/dist/plugin/virtual/routes.js.map +1 -1
- package/dist/runtime/composables/define-middleware.d.ts +15 -0
- package/dist/runtime/composables/define-middleware.d.ts.map +1 -0
- package/dist/runtime/composables/define-middleware.js +16 -0
- package/dist/runtime/composables/define-middleware.js.map +1 -0
- package/dist/runtime/composables/index.d.ts +7 -1
- package/dist/runtime/composables/index.d.ts.map +1 -1
- package/dist/runtime/composables/index.js +4 -1
- package/dist/runtime/composables/index.js.map +1 -1
- package/dist/runtime/composables/use-cookie.d.ts +38 -0
- package/dist/runtime/composables/use-cookie.d.ts.map +1 -0
- package/dist/runtime/composables/use-cookie.js +104 -0
- package/dist/runtime/composables/use-cookie.js.map +1 -0
- package/dist/runtime/composables/use-runtime-config.d.ts +32 -14
- package/dist/runtime/composables/use-runtime-config.d.ts.map +1 -1
- package/dist/runtime/composables/use-runtime-config.js +42 -8
- package/dist/runtime/composables/use-runtime-config.js.map +1 -1
- package/dist/runtime/composables/use-seo-meta.d.ts +42 -0
- package/dist/runtime/composables/use-seo-meta.d.ts.map +1 -0
- package/dist/runtime/composables/use-seo-meta.js +58 -0
- package/dist/runtime/composables/use-seo-meta.js.map +1 -0
- package/dist/runtime/entry-server-template.d.ts +1 -1
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +15 -3
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/dist/types/config.d.ts +14 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/middleware.d.ts +8 -2
- package/dist/types/middleware.d.ts.map +1 -1
- package/docs/cli.md +5 -0
- package/docs/composables.md +165 -7
- package/docs/configuration.md +53 -3
- package/docs/middleware.md +53 -25
- package/e2e/cypress/e2e/cookie.cy.ts +68 -0
- package/e2e/cypress/e2e/middleware.cy.ts +45 -0
- package/e2e/cypress/e2e/preview-hardening.cy.ts +79 -0
- package/e2e/cypress/e2e/seo-meta.cy.ts +108 -0
- package/e2e/kitchen-sink/app/middleware/auth.ts +3 -7
- package/e2e/kitchen-sink/app/pages/cookie-test.ts +22 -0
- package/e2e/kitchen-sink/app/pages/seo-test.ts +23 -0
- package/package.json +1 -1
- package/src/__tests__/cli/preview-hardening.test.ts +175 -0
- package/src/__tests__/cli/preview-isr.test.ts +30 -0
- package/src/__tests__/plugin/cer-app-plugin.test.ts +50 -0
- package/src/__tests__/plugin/entry-server-template.test.ts +21 -0
- package/src/__tests__/plugin/resolve-config.test.ts +18 -0
- package/src/__tests__/plugin/transforms/auto-import.test.ts +39 -0
- package/src/__tests__/plugin/virtual/middleware.test.ts +15 -0
- package/src/__tests__/plugin/virtual/routes.test.ts +32 -0
- package/src/__tests__/runtime/define-middleware.test.ts +43 -0
- package/src/__tests__/runtime/use-cookie.test.ts +218 -0
- package/src/__tests__/runtime/use-runtime-config.test.ts +86 -2
- package/src/__tests__/runtime/use-seo-meta.test.ts +109 -0
- package/src/cli/commands/preview-isr.ts +14 -0
- package/src/cli/commands/preview.ts +78 -6
- package/src/index.ts +3 -1
- package/src/plugin/dev-server.ts +1 -1
- package/src/plugin/dts-generator.ts +4 -2
- package/src/plugin/index.ts +32 -11
- package/src/plugin/transforms/auto-import.ts +5 -4
- package/src/plugin/virtual/routes.ts +7 -1
- package/src/runtime/composables/define-middleware.ts +17 -0
- package/src/runtime/composables/index.ts +7 -1
- package/src/runtime/composables/use-cookie.ts +128 -0
- package/src/runtime/composables/use-runtime-config.ts +67 -11
- package/src/runtime/composables/use-seo-meta.ts +75 -0
- package/src/runtime/entry-server-template.ts +15 -3
- package/src/types/config.ts +15 -0
- package/src/types/index.ts +2 -2
- package/src/types/middleware.ts +8 -6
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Returns the
|
|
3
|
-
* `runtimeConfig.public`. Available on both server and client.
|
|
2
|
+
* Returns the runtime configuration set in `cer.config.ts`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* - `public` — available on both server and client.
|
|
5
|
+
* - `private` — available on the server only (resolved from `process.env` at
|
|
6
|
+
* startup). Never present on the client.
|
|
8
7
|
*
|
|
9
8
|
* @example
|
|
10
9
|
* // cer.config.ts
|
|
11
10
|
* export default defineConfig({
|
|
12
11
|
* runtimeConfig: {
|
|
13
12
|
* public: { apiBase: process.env.VITE_API_BASE ?? '/api' },
|
|
13
|
+
* private: { dbUrl: '', secretKey: '' },
|
|
14
14
|
* },
|
|
15
15
|
* })
|
|
16
16
|
*
|
|
17
|
-
* // app/pages/index.ts
|
|
18
|
-
* const
|
|
19
|
-
*
|
|
17
|
+
* // app/pages/index.ts (loader — server-only)
|
|
18
|
+
* const { private: priv } = useRuntimeConfig()
|
|
19
|
+
* const rows = await db.query(priv.dbUrl)
|
|
20
20
|
*/
|
|
21
21
|
export function useRuntimeConfig() {
|
|
22
22
|
// Dynamic import resolved at runtime — avoids a static circular dependency
|
|
@@ -38,4 +38,38 @@ export function initRuntimeConfig(config) {
|
|
|
38
38
|
;
|
|
39
39
|
globalThis.__cerRuntimeConfig = config;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Converts a camelCase or mixed key to UPPER_SNAKE_CASE for env var lookup.
|
|
43
|
+
* Examples: `dbUrl` → `DB_URL`, `secretKey` → `SECRET_KEY`, `API_KEY` → `API_KEY`.
|
|
44
|
+
*/
|
|
45
|
+
function toUpperSnakeCase(key) {
|
|
46
|
+
return key
|
|
47
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
48
|
+
.toUpperCase();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolves a private config object by looking up each key in the supplied
|
|
52
|
+
* environment variable map, with the following precedence:
|
|
53
|
+
*
|
|
54
|
+
* 1. `env[key]` — exact case (e.g. `process.env.dbUrl`)
|
|
55
|
+
* 2. `env[UPPER_SNAKE_CASE(key)]` — conventional env var form (e.g. `process.env.DB_URL`)
|
|
56
|
+
* 3. `defaultValue` — the declared default from `cer.config.ts`
|
|
57
|
+
*
|
|
58
|
+
* Accepts an optional `env` parameter so the function is unit-testable
|
|
59
|
+
* without mutating `process.env`.
|
|
60
|
+
*/
|
|
61
|
+
export function resolvePrivateConfig(defaults, env = process.env) {
|
|
62
|
+
return Object.fromEntries(Object.entries(defaults).map(([key, defaultValue]) => {
|
|
63
|
+
const envKey = toUpperSnakeCase(key);
|
|
64
|
+
const resolved = env[key] ?? env[envKey] ?? defaultValue;
|
|
65
|
+
// Warn when no env var was found and the declared default is an empty string.
|
|
66
|
+
// An empty-string default is the conventional way to declare a required secret
|
|
67
|
+
// (the key exists for typing purposes but has no safe default value).
|
|
68
|
+
if (resolved === '' && env[key] === undefined && env[envKey] === undefined) {
|
|
69
|
+
console.warn(`[cer-app] runtimeConfig.private: "${key}" is an empty string — ` +
|
|
70
|
+
`set ${envKey} in the environment to provide a value.`);
|
|
71
|
+
}
|
|
72
|
+
return [key, resolved];
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
41
75
|
//# sourceMappingURL=use-runtime-config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-runtime-config.js","sourceRoot":"","sources":["../../../src/runtime/composables/use-runtime-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-runtime-config.js","sourceRoot":"","sources":["../../../src/runtime/composables/use-runtime-config.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,gBAAgB;IAC9B,2EAA2E;IAC3E,iDAAiD;IACjD,8DAA8D;IAC9D,MAAM,GAAG,GAAI,UAAkB,CAAC,kBAAkB,CAAA;IAClD,IAAI,GAAG;QAAE,OAAO,GAA0B,CAAA;IAE1C,iFAAiF;IACjF,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAA2B;IAC3D,8DAA8D;IAC9D,CAAC;IAAC,UAAkB,CAAC,kBAAkB,GAAG,MAAM,CAAA;AAClD,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,OAAO,GAAG;SACP,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC;SACnC,WAAW,EAAE,CAAA;AAClB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAAgC,EAChC,MAA0C,OAAO,CAAC,GAAyC;IAE3F,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,EAAE;QACnD,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAA;QACpC,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,YAAY,CAAA;QACxD,8EAA8E;QAC9E,+EAA+E;QAC/E,sEAAsE;QACtE,IAAI,QAAQ,KAAK,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,SAAS,EAAE,CAAC;YAC3E,OAAO,CAAC,IAAI,CACV,qCAAqC,GAAG,yBAAyB;gBACjE,OAAO,MAAM,yCAAyC,CACvD,CAAA;QACH,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IACxB,CAAC,CAAC,CACH,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface SeoMetaInput {
|
|
2
|
+
/** Sets the document title. */
|
|
3
|
+
title?: string;
|
|
4
|
+
/** Sets the meta description. */
|
|
5
|
+
description?: string;
|
|
6
|
+
ogTitle?: string;
|
|
7
|
+
ogDescription?: string;
|
|
8
|
+
ogImage?: string;
|
|
9
|
+
ogUrl?: string;
|
|
10
|
+
/** e.g. `'website'`, `'article'`. No tag is emitted when omitted. */
|
|
11
|
+
ogType?: string;
|
|
12
|
+
ogSiteName?: string;
|
|
13
|
+
/** e.g. `'summary'`, `'summary_large_image'`. No tag is emitted when omitted. */
|
|
14
|
+
twitterCard?: string;
|
|
15
|
+
twitterTitle?: string;
|
|
16
|
+
twitterDescription?: string;
|
|
17
|
+
twitterImage?: string;
|
|
18
|
+
/** Twitter/X site handle, e.g. '@mysite'. */
|
|
19
|
+
twitterSite?: string;
|
|
20
|
+
/** Canonical URL injected as <link rel="canonical">. */
|
|
21
|
+
canonical?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thin wrapper over `useHead()` for common SEO tags.
|
|
25
|
+
*
|
|
26
|
+
* Sets the page title, meta description, Open Graph tags, Twitter Card tags,
|
|
27
|
+
* and a canonical link element. Only tags with a non-undefined value are emitted.
|
|
28
|
+
*
|
|
29
|
+
* It is auto-imported, so you don't need to import it manually.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* useSeoMeta({
|
|
34
|
+
* title: 'My page',
|
|
35
|
+
* description: 'A great page.',
|
|
36
|
+
* ogImage: 'https://example.com/og.png',
|
|
37
|
+
* canonical: 'https://example.com/my-page',
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function useSeoMeta(input: SeoMetaInput): void;
|
|
42
|
+
//# sourceMappingURL=use-seo-meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-seo-meta.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/use-seo-meta.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CA6BpD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useHead } from './use-head.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thin wrapper over `useHead()` for common SEO tags.
|
|
4
|
+
*
|
|
5
|
+
* Sets the page title, meta description, Open Graph tags, Twitter Card tags,
|
|
6
|
+
* and a canonical link element. Only tags with a non-undefined value are emitted.
|
|
7
|
+
*
|
|
8
|
+
* It is auto-imported, so you don't need to import it manually.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* useSeoMeta({
|
|
13
|
+
* title: 'My page',
|
|
14
|
+
* description: 'A great page.',
|
|
15
|
+
* ogImage: 'https://example.com/og.png',
|
|
16
|
+
* canonical: 'https://example.com/my-page',
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useSeoMeta(input) {
|
|
21
|
+
const meta = [];
|
|
22
|
+
const link = [];
|
|
23
|
+
if (input.description !== undefined)
|
|
24
|
+
meta.push({ name: 'description', content: input.description });
|
|
25
|
+
// Open Graph
|
|
26
|
+
if (input.ogTitle !== undefined)
|
|
27
|
+
meta.push({ property: 'og:title', content: input.ogTitle });
|
|
28
|
+
if (input.ogDescription !== undefined)
|
|
29
|
+
meta.push({ property: 'og:description', content: input.ogDescription });
|
|
30
|
+
if (input.ogImage !== undefined)
|
|
31
|
+
meta.push({ property: 'og:image', content: input.ogImage });
|
|
32
|
+
if (input.ogUrl !== undefined)
|
|
33
|
+
meta.push({ property: 'og:url', content: input.ogUrl });
|
|
34
|
+
if (input.ogType !== undefined)
|
|
35
|
+
meta.push({ property: 'og:type', content: input.ogType });
|
|
36
|
+
if (input.ogSiteName !== undefined)
|
|
37
|
+
meta.push({ property: 'og:site_name', content: input.ogSiteName });
|
|
38
|
+
// Twitter / X
|
|
39
|
+
if (input.twitterCard !== undefined)
|
|
40
|
+
meta.push({ name: 'twitter:card', content: input.twitterCard });
|
|
41
|
+
if (input.twitterTitle !== undefined)
|
|
42
|
+
meta.push({ name: 'twitter:title', content: input.twitterTitle });
|
|
43
|
+
if (input.twitterDescription !== undefined)
|
|
44
|
+
meta.push({ name: 'twitter:description', content: input.twitterDescription });
|
|
45
|
+
if (input.twitterImage !== undefined)
|
|
46
|
+
meta.push({ name: 'twitter:image', content: input.twitterImage });
|
|
47
|
+
if (input.twitterSite !== undefined)
|
|
48
|
+
meta.push({ name: 'twitter:site', content: input.twitterSite });
|
|
49
|
+
// Canonical link
|
|
50
|
+
if (input.canonical !== undefined)
|
|
51
|
+
link.push({ rel: 'canonical', href: input.canonical });
|
|
52
|
+
useHead({
|
|
53
|
+
title: input.title,
|
|
54
|
+
meta: meta.length > 0 ? meta : undefined,
|
|
55
|
+
link: link.length > 0 ? link : undefined,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=use-seo-meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-seo-meta.js","sourceRoot":"","sources":["../../../src/runtime/composables/use-seo-meta.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AA2BvC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,UAAU,CAAC,KAAmB;IAC5C,MAAM,IAAI,GAAkC,EAAE,CAAA;IAC9C,MAAM,IAAI,GAAkC,EAAE,CAAA;IAE9C,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;IAEnG,aAAa;IACb,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IAC5F,IAAI,KAAK,CAAC,aAAa,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,KAAK,CAAC,aAAa,EAAE,CAAC,CAAA;IAC9G,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IAC5F,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAA;IACtF,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAA;IACzF,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAA;IAEtG,cAAc;IACd,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;IACpG,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC,CAAA;IACvG,IAAI,KAAK,CAAC,kBAAkB,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,KAAK,CAAC,kBAAkB,EAAE,CAAC,CAAA;IACzH,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC,CAAA;IACvG,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;IAEpG,iBAAiB;IACjB,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAA;IAEzF,OAAO,CAAC;QACN,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QACxC,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;KACzC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
* - useHead() support via beginHeadCollection / endHeadCollection
|
|
12
12
|
* - DSD polyfill injected at end of <body> after client-template merge
|
|
13
13
|
*/
|
|
14
|
-
export declare const ENTRY_SERVER_TEMPLATE = "// Server-side entry \u2014 AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app\nimport { readFileSync, existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { AsyncLocalStorage } from 'node:async_hooks'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport apiRoutes from 'virtual:cer-server-api'\nimport { runtimeConfig } from 'virtual:cer-app-config'\nimport { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'\nimport { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'\nimport entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'\nimport { errorTag } from 'virtual:cer-error'\nimport { createIsrHandler } from '@jasonshimmy/vite-plugin-cer-app/isr'\n\nregisterBuiltinComponents()\ninitRuntimeConfig(runtimeConfig)\n\n// Pre-load the full HTML entity map so named entities like — decode\n// correctly during SSR. Without this the bundled runtime falls back to a\n// minimal set (<, >, & \u2026) and re-escapes everything else.\nregisterEntityMap(entitiesJson)\n\n// Run plugins once at server startup so their provide() values are available\n// to useInject() during every SSR/SSG render pass. Stored on globalThis so all\n// dynamically-imported page chunks share the same reference.\nconst _pluginProvides = new Map()\n;(globalThis).__cerPluginProvides = _pluginProvides\nconst _pluginsReady = (async () => {\n const _bootstrapRouter = initRouter({ routes })\n for (const plugin of plugins) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({\n router: _bootstrapRouter,\n provide: (key, value) => _pluginProvides.set(key, value),\n config: {},\n })\n }\n }\n})()\n\n// Async-local storage for request-scoped SSR loader data.\n// Using AsyncLocalStorage ensures concurrent SSR renders (e.g. SSG with\n// concurrency > 1) never see each other's data \u2014 each request's async chain\n// carries its own store value, so usePageData() is always race-condition-free.\nconst _cerDataStore = new AsyncLocalStorage()\n// Expose the store so the usePageData() composable can read it server-side.\n;(globalThis).__CER_DATA_STORE__ = _cerDataStore\n\n// Load the Vite-built client index.html (dist/client/index.html) so every SSR\n// response includes the client-side scripts needed for hydration and routing.\n// The server bundle lives at dist/server/server.js, so ../client resolves correctly.\nconst _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')\nconst _clientTemplate = existsSync(_clientTemplatePath)\n ? readFileSync(_clientTemplatePath, 'utf-8')\n : null\n\n// Merge the SSR rendered body with the Vite client shell so the final page\n// contains both pre-rendered DSD content and the client bundle scripts.\nfunction _mergeWithClientTemplate(ssrHtml, clientTemplate) {\n const headTag = '<head>', headCloseTag = '</head>'\n const bodyTag = '<body>', bodyCloseTag = '</body>'\n const headStart = ssrHtml.indexOf(headTag)\n const headEnd = ssrHtml.indexOf(headCloseTag)\n const bodyStart = ssrHtml.indexOf(bodyTag)\n const bodyEnd = ssrHtml.lastIndexOf(bodyCloseTag)\n const ssrHead = headStart >= 0 && headEnd > headStart\n ? ssrHtml.slice(headStart + headTag.length, headEnd).trim() : ''\n const ssrBody = bodyStart >= 0 && bodyEnd > bodyStart\n ? ssrHtml.slice(bodyStart + bodyTag.length, bodyEnd).trim() : ssrHtml\n // Hoist only top-level <style id=...> elements (cer-ssr-jit, cer-ssr-global)\n // from the SSR body into the document <head>. Plain <style> blocks without\n // an id attribute belong to shadow DOM templates and must stay in place \u2014\n // hoisting them to <head> breaks shadow DOM style encapsulation (document\n // styles do not pierce shadow roots), which is the root cause of FOUC.\n const headParts = ssrHead ? [ssrHead] : []\n let ssrBodyContent = ssrBody\n let pos = 0\n while (pos < ssrBodyContent.length) {\n const styleOpen = ssrBodyContent.indexOf('<style id=', pos)\n if (styleOpen < 0) break\n const styleClose = ssrBodyContent.indexOf('</style>', styleOpen)\n if (styleClose < 0) break\n headParts.push(ssrBodyContent.slice(styleOpen, styleClose + 8))\n ssrBodyContent = ssrBodyContent.slice(0, styleOpen) + ssrBodyContent.slice(styleClose + 8)\n pos = styleOpen\n }\n ssrBodyContent = ssrBodyContent.trim()\n // Inject the pre-rendered layout+page as light DOM of the app mount element\n // so it is visible before JS boots, then the client router takes over.\n let merged = clientTemplate\n if (merged.includes('<cer-layout-view></cer-layout-view>')) {\n merged = merged.replace('<cer-layout-view></cer-layout-view>',\n '<cer-layout-view>' + ssrBodyContent + '</cer-layout-view>')\n } else if (merged.includes('<div id=\"app\"></div>')) {\n merged = merged.replace('<div id=\"app\"></div>',\n '<div id=\"app\">' + ssrBodyContent + '</div>')\n }\n const headAdditions = headParts.filter(Boolean).join('\\n')\n if (headAdditions) {\n // If SSR provides a <title>, replace the client template's <title> so the\n // SSR title wins (client template title is the fallback default).\n if (headAdditions.includes('<title>')) {\n merged = merged.replace(/<title>[^<]*<\\/title>/, '')\n }\n merged = merged.replace('</head>', headAdditions + '\\n</head>')\n }\n return merged\n}\n\n// Per-request async setup: initialize a fresh router, resolve the matched\n// route and layout, pre-load the page module, and call the data loader.\n// Loader data is scoped to the current AsyncLocalStorage context via enterWith()\n// so concurrent renders never share state.\nconst _prepareRequest = async (req) => {\n await _pluginsReady\n const router = initRouter({ routes, initialUrl: req.url ?? '/' })\n const current = router.getCurrent()\n const { route, params } = router.matchRoute(current.path)\n\n // Pre-load the page module so we can embed the component tag directly.\n // This avoids the async router-view (which injects content via script tags\n // and breaks Declarative Shadow DOM on initial parse).\n let pageVnode = { tag: 'div', props: {}, children: [] }\n let head\n if (route?.load) {\n try {\n const mod = await route.load()\n const pageTag = mod.default\n if (pageTag) {\n pageVnode = { tag: pageTag, props: { attrs: { ...params } }, children: [] }\n }\n if (typeof mod.loader === 'function') {\n const query = current.query ?? {}\n const data = await mod.loader({ params, query, req })\n if (data !== undefined && data !== null) {\n // enterWith() scopes the value to the current async context so\n // concurrent renders (SSG concurrency > 1) never share data.\n _cerDataStore.enterWith(data)\n head = `<script>window.__CER_DATA__ = ${JSON.stringify(data)}</script>`\n }\n }\n } catch (err) {\n // Loader threw \u2014 render the error page server-side if app/error.ts exists.\n const status = (err && typeof err === 'object' && 'status' in err && typeof err.status === 'number')\n ? err.status : 500\n const message = (err instanceof Error) ? err.message : String(err)\n if (!errorTag) {\n console.error('[cer-app] Loader error (no app/error.ts defined):', err)\n }\n const errVnode = errorTag\n ? { tag: errorTag, props: { attrs: { error: message, status: String(status) } }, children: [] }\n : { tag: 'div', props: {}, children: [] }\n return { vnode: errVnode, router, head: undefined, status }\n }\n }\n\n // Resolve layout chain: nested layouts (meta.layoutChain) or single layout.\n const chain = route?.meta?.layoutChain\n ? route.meta.layoutChain\n : [route?.meta?.layout ?? 'default']\n\n // Wrap pageVnode in the layout chain from innermost to outermost.\n let vnode = pageVnode\n for (let i = chain.length - 1; i >= 0; i--) {\n const tag = layouts[chain[i]]\n if (tag) vnode = { tag, props: {}, children: [vnode] }\n }\n\n return { vnode, router, head, status: null }\n}\n\nexport const handler = async (req, res) => {\n await _cerDataStore.run(null, async () => {\n const { vnode, router, head, status } = await _prepareRequest(req)\n if (status != null) res.statusCode = status\n\n // Begin collecting useHead() calls made during the synchronous render pass.\n // IMPORTANT: the stream's start() function runs synchronously on construction,\n // so ALL useHead() calls happen before the stream object is returned. We must\n // call endHeadCollection() immediately \u2014 before any await \u2014 to avoid a race\n // window where a concurrent request (e.g. SSG concurrency > 1) resets the\n // shared globalThis collector while this handler is suspended at an await.\n beginHeadCollection()\n\n // dsdPolyfill: false \u2014 we inject the polyfill manually after merging so it\n // lands at the end of <body>, not inside <cer-layout-view> light DOM where\n // scripts may not execute.\n // The first chunk from the stream is the full synchronous render. Subsequent\n // chunks are async component swap scripts streamed as they resolve.\n const stream = renderToStreamWithJITCSSDSD(vnode, { dsdPolyfill: false, router })\n\n // Collect head tags synchronously \u2014 all useHead() calls have already fired\n // inside the stream constructor's start() before it returned.\n const headTags = serializeHeadTags(endHeadCollection())\n\n const reader = stream.getReader()\n\n // Read the first (synchronous) chunk.\n const { value: firstChunk = '' } = await reader.read()\n\n // Merge loader data script + useHead() tags into the document head.\n const headContent = [head, headTags].filter(Boolean).join('\\n')\n\n // Wrap the rendered body in a full HTML document and inject the head additions\n // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.\n const ssrHtml = `<!DOCTYPE html><html><head>${headContent}</head><body>${firstChunk}</body></html>`\n\n const merged = _clientTemplate\n ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)\n : ssrHtml\n\n // Split at </body> so async swap scripts and the DSD polyfill can be streamed\n // in before the document is closed.\n const bodyCloseIdx = merged.lastIndexOf('</body>')\n const beforeBodyClose = bodyCloseIdx >= 0 ? merged.slice(0, bodyCloseIdx) : merged\n const fromBodyClose = bodyCloseIdx >= 0 ? merged.slice(bodyCloseIdx) : ''\n\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.setHeader('Transfer-Encoding', 'chunked')\n res.write(beforeBodyClose)\n\n // Stream async component swap scripts through as-is.\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n res.write(value)\n }\n\n // Inject DSD polyfill immediately before </body>, then close the document.\n res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)\n })\n}\n\n// ISR-wrapped handler for production integrations (Express, Hono, Fastify).\n// Routes with meta.ssg.revalidate are served stale-while-revalidate.\nexport const isrHandler = createIsrHandler(routes, handler)\n\nexport { apiRoutes, plugins, layouts, routes }\nexport default handler\n";
|
|
14
|
+
export declare const ENTRY_SERVER_TEMPLATE = "// Server-side entry \u2014 AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app\nimport { readFileSync, existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { AsyncLocalStorage } from 'node:async_hooks'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport apiRoutes from 'virtual:cer-server-api'\nimport { runtimeConfig, _runtimePrivateDefaults } from 'virtual:cer-app-config'\nimport { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'\nimport { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'\nimport entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig, resolvePrivateConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'\nimport { errorTag } from 'virtual:cer-error'\nimport { createIsrHandler } from '@jasonshimmy/vite-plugin-cer-app/isr'\n\nregisterBuiltinComponents()\n\n// Resolve private config from environment variables at server startup.\n// Each key declared in runtimeConfig.private is looked up in process.env,\n// first as-is, then as ALL_CAPS. The declared default is used as fallback.\ninitRuntimeConfig({ ...runtimeConfig, private: resolvePrivateConfig(_runtimePrivateDefaults ?? {}) })\n\n// Pre-load the full HTML entity map so named entities like — decode\n// correctly during SSR. Without this the bundled runtime falls back to a\n// minimal set (<, >, & \u2026) and re-escapes everything else.\nregisterEntityMap(entitiesJson)\n\n// Run plugins once at server startup so their provide() values are available\n// to useInject() during every SSR/SSG render pass. Stored on globalThis so all\n// dynamically-imported page chunks share the same reference.\nconst _pluginProvides = new Map()\n;(globalThis).__cerPluginProvides = _pluginProvides\nconst _pluginsReady = (async () => {\n const _bootstrapRouter = initRouter({ routes })\n for (const plugin of plugins) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({\n router: _bootstrapRouter,\n provide: (key, value) => _pluginProvides.set(key, value),\n config: {},\n })\n }\n }\n})()\n\n// Async-local storage for request-scoped SSR loader data.\n// Using AsyncLocalStorage ensures concurrent SSR renders (e.g. SSG with\n// concurrency > 1) never see each other's data \u2014 each request's async chain\n// carries its own store value, so usePageData() is always race-condition-free.\nconst _cerDataStore = new AsyncLocalStorage()\n// Expose the store so the usePageData() composable can read it server-side.\n;(globalThis).__CER_DATA_STORE__ = _cerDataStore\n\n// Async-local storage for request-scoped req/res access.\n// Allows isomorphic composables (e.g. useCookie) to read/write HTTP headers\n// without prop-drilling the request context through the component tree.\nconst _cerReqStore = new AsyncLocalStorage()\n;(globalThis).__CER_REQ_STORE__ = _cerReqStore\n\n// Load the Vite-built client index.html (dist/client/index.html) so every SSR\n// response includes the client-side scripts needed for hydration and routing.\n// The server bundle lives at dist/server/server.js, so ../client resolves correctly.\nconst _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')\nconst _clientTemplate = existsSync(_clientTemplatePath)\n ? readFileSync(_clientTemplatePath, 'utf-8')\n : null\n\n// Merge the SSR rendered body with the Vite client shell so the final page\n// contains both pre-rendered DSD content and the client bundle scripts.\nfunction _mergeWithClientTemplate(ssrHtml, clientTemplate) {\n const headTag = '<head>', headCloseTag = '</head>'\n const bodyTag = '<body>', bodyCloseTag = '</body>'\n const headStart = ssrHtml.indexOf(headTag)\n const headEnd = ssrHtml.indexOf(headCloseTag)\n const bodyStart = ssrHtml.indexOf(bodyTag)\n const bodyEnd = ssrHtml.lastIndexOf(bodyCloseTag)\n const ssrHead = headStart >= 0 && headEnd > headStart\n ? ssrHtml.slice(headStart + headTag.length, headEnd).trim() : ''\n const ssrBody = bodyStart >= 0 && bodyEnd > bodyStart\n ? ssrHtml.slice(bodyStart + bodyTag.length, bodyEnd).trim() : ssrHtml\n // Hoist only top-level <style id=...> elements (cer-ssr-jit, cer-ssr-global)\n // from the SSR body into the document <head>. Plain <style> blocks without\n // an id attribute belong to shadow DOM templates and must stay in place \u2014\n // hoisting them to <head> breaks shadow DOM style encapsulation (document\n // styles do not pierce shadow roots), which is the root cause of FOUC.\n const headParts = ssrHead ? [ssrHead] : []\n let ssrBodyContent = ssrBody\n let pos = 0\n while (pos < ssrBodyContent.length) {\n const styleOpen = ssrBodyContent.indexOf('<style id=', pos)\n if (styleOpen < 0) break\n const styleClose = ssrBodyContent.indexOf('</style>', styleOpen)\n if (styleClose < 0) break\n headParts.push(ssrBodyContent.slice(styleOpen, styleClose + 8))\n ssrBodyContent = ssrBodyContent.slice(0, styleOpen) + ssrBodyContent.slice(styleClose + 8)\n pos = styleOpen\n }\n ssrBodyContent = ssrBodyContent.trim()\n // Inject the pre-rendered layout+page as light DOM of the app mount element\n // so it is visible before JS boots, then the client router takes over.\n let merged = clientTemplate\n if (merged.includes('<cer-layout-view></cer-layout-view>')) {\n merged = merged.replace('<cer-layout-view></cer-layout-view>',\n '<cer-layout-view>' + ssrBodyContent + '</cer-layout-view>')\n } else if (merged.includes('<div id=\"app\"></div>')) {\n merged = merged.replace('<div id=\"app\"></div>',\n '<div id=\"app\">' + ssrBodyContent + '</div>')\n }\n const headAdditions = headParts.filter(Boolean).join('\\n')\n if (headAdditions) {\n // If SSR provides a <title>, replace the client template's <title> so the\n // SSR title wins (client template title is the fallback default).\n if (headAdditions.includes('<title>')) {\n merged = merged.replace(/<title>[^<]*<\\/title>/, '')\n }\n merged = merged.replace('</head>', headAdditions + '\\n</head>')\n }\n return merged\n}\n\n// Per-request async setup: initialize a fresh router, resolve the matched\n// route and layout, pre-load the page module, and call the data loader.\n// Loader data is scoped to the current AsyncLocalStorage context via enterWith()\n// so concurrent renders never share state.\nconst _prepareRequest = async (req) => {\n await _pluginsReady\n const router = initRouter({ routes, initialUrl: req.url ?? '/' })\n const current = router.getCurrent()\n const { route, params } = router.matchRoute(current.path)\n\n // Pre-load the page module so we can embed the component tag directly.\n // This avoids the async router-view (which injects content via script tags\n // and breaks Declarative Shadow DOM on initial parse).\n let pageVnode = { tag: 'div', props: {}, children: [] }\n let head\n if (route?.load) {\n try {\n const mod = await route.load()\n const pageTag = mod.default\n if (pageTag) {\n pageVnode = { tag: pageTag, props: { attrs: { ...params } }, children: [] }\n }\n if (typeof mod.loader === 'function') {\n const query = current.query ?? {}\n const data = await mod.loader({ params, query, req })\n if (data !== undefined && data !== null) {\n // enterWith() scopes the value to the current async context so\n // concurrent renders (SSG concurrency > 1) never share data.\n _cerDataStore.enterWith(data)\n head = `<script>window.__CER_DATA__ = ${JSON.stringify(data)}</script>`\n }\n }\n } catch (err) {\n // Loader threw \u2014 render the error page server-side if app/error.ts exists.\n const status = (err && typeof err === 'object' && 'status' in err && typeof err.status === 'number')\n ? err.status : 500\n const message = (err instanceof Error) ? err.message : String(err)\n if (!errorTag) {\n console.error('[cer-app] Loader error (no app/error.ts defined):', err)\n }\n const errVnode = errorTag\n ? { tag: errorTag, props: { attrs: { error: message, status: String(status) } }, children: [] }\n : { tag: 'div', props: {}, children: [] }\n return { vnode: errVnode, router, head: undefined, status }\n }\n }\n\n // Resolve layout chain: nested layouts (meta.layoutChain) or single layout.\n const chain = route?.meta?.layoutChain\n ? route.meta.layoutChain\n : [route?.meta?.layout ?? 'default']\n\n // Wrap pageVnode in the layout chain from innermost to outermost.\n let vnode = pageVnode\n for (let i = chain.length - 1; i >= 0; i--) {\n const tag = layouts[chain[i]]\n if (tag) vnode = { tag, props: {}, children: [vnode] }\n }\n\n return { vnode, router, head, status: null }\n}\n\nexport const handler = async (req, res) => {\n await _cerReqStore.run({ req, res }, async () => {\n await _cerDataStore.run(null, async () => {\n const { vnode, router, head, status } = await _prepareRequest(req)\n if (status != null) res.statusCode = status\n\n // Begin collecting useHead() calls made during the synchronous render pass.\n // IMPORTANT: the stream's start() function runs synchronously on construction,\n // so ALL useHead() calls happen before the stream object is returned. We must\n // call endHeadCollection() immediately \u2014 before any await \u2014 to avoid a race\n // window where a concurrent request (e.g. SSG concurrency > 1) resets the\n // shared globalThis collector while this handler is suspended at an await.\n beginHeadCollection()\n\n // dsdPolyfill: false \u2014 we inject the polyfill manually after merging so it\n // lands at the end of <body>, not inside <cer-layout-view> light DOM where\n // scripts may not execute.\n // The first chunk from the stream is the full synchronous render. Subsequent\n // chunks are async component swap scripts streamed as they resolve.\n const stream = renderToStreamWithJITCSSDSD(vnode, { dsdPolyfill: false, router })\n\n // Collect head tags synchronously \u2014 all useHead() calls have already fired\n // inside the stream constructor's start() before it returned.\n const headTags = serializeHeadTags(endHeadCollection())\n\n const reader = stream.getReader()\n\n // Read the first (synchronous) chunk.\n const { value: firstChunk = '' } = await reader.read()\n\n // Merge loader data script + useHead() tags into the document head.\n const headContent = [head, headTags].filter(Boolean).join('\\n')\n\n // Wrap the rendered body in a full HTML document and inject the head additions\n // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.\n const ssrHtml = `<!DOCTYPE html><html><head>${headContent}</head><body>${firstChunk}</body></html>`\n\n const merged = _clientTemplate\n ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)\n : ssrHtml\n\n // Split at </body> so async swap scripts and the DSD polyfill can be streamed\n // in before the document is closed.\n const bodyCloseIdx = merged.lastIndexOf('</body>')\n const beforeBodyClose = bodyCloseIdx >= 0 ? merged.slice(0, bodyCloseIdx) : merged\n const fromBodyClose = bodyCloseIdx >= 0 ? merged.slice(bodyCloseIdx) : ''\n\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.setHeader('Transfer-Encoding', 'chunked')\n res.write(beforeBodyClose)\n\n // Stream async component swap scripts through as-is.\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n res.write(value)\n }\n\n // Inject DSD polyfill immediately before </body>, then close the document.\n res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)\n })\n })\n}\n\n// ISR-wrapped handler for production integrations (Express, Hono, Fastify).\n// Routes with meta.ssg.revalidate are served stale-while-revalidate.\nexport const isrHandler = createIsrHandler(routes, handler)\n\nexport { apiRoutes, plugins, layouts, routes }\nexport default handler\n";
|
|
15
15
|
//# sourceMappingURL=entry-server-template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-server-template.d.ts","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"entry-server-template.d.ts","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,osYAgQjC,CAAA"}
|
|
@@ -21,17 +21,21 @@ import routes from 'virtual:cer-routes'
|
|
|
21
21
|
import layouts from 'virtual:cer-layouts'
|
|
22
22
|
import plugins from 'virtual:cer-plugins'
|
|
23
23
|
import apiRoutes from 'virtual:cer-server-api'
|
|
24
|
-
import { runtimeConfig } from 'virtual:cer-app-config'
|
|
24
|
+
import { runtimeConfig, _runtimePrivateDefaults } from 'virtual:cer-app-config'
|
|
25
25
|
import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
|
|
26
26
|
import { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
|
|
27
27
|
import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
|
|
28
28
|
import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
|
|
29
|
-
import { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
29
|
+
import { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig, resolvePrivateConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
30
30
|
import { errorTag } from 'virtual:cer-error'
|
|
31
31
|
import { createIsrHandler } from '@jasonshimmy/vite-plugin-cer-app/isr'
|
|
32
32
|
|
|
33
33
|
registerBuiltinComponents()
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
// Resolve private config from environment variables at server startup.
|
|
36
|
+
// Each key declared in runtimeConfig.private is looked up in process.env,
|
|
37
|
+
// first as-is, then as ALL_CAPS. The declared default is used as fallback.
|
|
38
|
+
initRuntimeConfig({ ...runtimeConfig, private: resolvePrivateConfig(_runtimePrivateDefaults ?? {}) })
|
|
35
39
|
|
|
36
40
|
// Pre-load the full HTML entity map so named entities like — decode
|
|
37
41
|
// correctly during SSR. Without this the bundled runtime falls back to a
|
|
@@ -64,6 +68,12 @@ const _cerDataStore = new AsyncLocalStorage()
|
|
|
64
68
|
// Expose the store so the usePageData() composable can read it server-side.
|
|
65
69
|
;(globalThis).__CER_DATA_STORE__ = _cerDataStore
|
|
66
70
|
|
|
71
|
+
// Async-local storage for request-scoped req/res access.
|
|
72
|
+
// Allows isomorphic composables (e.g. useCookie) to read/write HTTP headers
|
|
73
|
+
// without prop-drilling the request context through the component tree.
|
|
74
|
+
const _cerReqStore = new AsyncLocalStorage()
|
|
75
|
+
;(globalThis).__CER_REQ_STORE__ = _cerReqStore
|
|
76
|
+
|
|
67
77
|
// Load the Vite-built client index.html (dist/client/index.html) so every SSR
|
|
68
78
|
// response includes the client-side scripts needed for hydration and routing.
|
|
69
79
|
// The server bundle lives at dist/server/server.js, so ../client resolves correctly.
|
|
@@ -188,6 +198,7 @@ const _prepareRequest = async (req) => {
|
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
export const handler = async (req, res) => {
|
|
201
|
+
await _cerReqStore.run({ req, res }, async () => {
|
|
191
202
|
await _cerDataStore.run(null, async () => {
|
|
192
203
|
const { vnode, router, head, status } = await _prepareRequest(req)
|
|
193
204
|
if (status != null) res.statusCode = status
|
|
@@ -247,6 +258,7 @@ export const handler = async (req, res) => {
|
|
|
247
258
|
// Inject DSD polyfill immediately before </body>, then close the document.
|
|
248
259
|
res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)
|
|
249
260
|
})
|
|
261
|
+
})
|
|
250
262
|
}
|
|
251
263
|
|
|
252
264
|
// ISR-wrapped handler for production integrations (Express, Hono, Fastify).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-server-template.js","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG
|
|
1
|
+
{"version":3,"file":"entry-server-template.js","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgQpC,CAAA"}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ export interface AutoImportsConfig {
|
|
|
17
17
|
export interface RuntimePublicConfig {
|
|
18
18
|
[key: string]: unknown;
|
|
19
19
|
}
|
|
20
|
+
export interface RuntimePrivateConfig {
|
|
21
|
+
[key: string]: string;
|
|
22
|
+
}
|
|
20
23
|
export interface RuntimeConfig {
|
|
21
24
|
/**
|
|
22
25
|
* Public runtime config — available on both server and client via
|
|
@@ -31,6 +34,17 @@ export interface RuntimeConfig {
|
|
|
31
34
|
* }
|
|
32
35
|
*/
|
|
33
36
|
public?: RuntimePublicConfig;
|
|
37
|
+
/**
|
|
38
|
+
* Server-only secrets — never serialized into the client bundle.
|
|
39
|
+
* Declare keys with empty-string defaults here; at server startup each key
|
|
40
|
+
* is resolved from `process.env[KEY]` (case-insensitive, ALL_CAPS preferred).
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* runtimeConfig: {
|
|
44
|
+
* private: { dbUrl: '', secretKey: '' },
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
private?: RuntimePrivateConfig;
|
|
34
48
|
}
|
|
35
49
|
export interface CerAppConfig {
|
|
36
50
|
mode?: 'spa' | 'ssr' | 'ssg';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6CAA6C,CAAA;AAE/E,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6CAA6C,CAAA;AAE/E,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,oBAAoB,CAAA;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,CAAC,CAAA;IACxD,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAE/D"}
|
package/dist/types/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAuEA,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig, RuntimeConfig, RuntimePublicConfig } from './config.js';
|
|
1
|
+
export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig, RuntimeConfig, RuntimePublicConfig, RuntimePrivateConfig } from './config.js';
|
|
2
2
|
export { defineConfig } from './config.js';
|
|
3
3
|
export type { HydrateStrategy, SsgPathsContext, PageSsgConfig, PageMeta, PageLoaderContext, PageLoader } from './page.js';
|
|
4
4
|
export type { ApiRequest, ApiResponse, ApiHandler, ApiContext } from './api.js';
|
|
5
5
|
export type { AppContext, AppPlugin } from './plugin.js';
|
|
6
|
-
export type {
|
|
6
|
+
export type { MiddlewareFn, GuardResult, ServerMiddleware } from './middleware.js';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACrJ,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACzH,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC/E,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACxD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { RouteState } from '@jasonshimmy/custom-elements-runtime/router';
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Return value from a route middleware function:
|
|
5
|
+
* - `true` — allow navigation
|
|
6
|
+
* - `false` — block navigation
|
|
7
|
+
* - `string` — redirect to that path
|
|
8
|
+
*/
|
|
9
|
+
export type GuardResult = boolean | string | Promise<boolean | string>;
|
|
10
|
+
export type MiddlewareFn = (to: RouteState, from: RouteState | null) => GuardResult;
|
|
5
11
|
export type ServerMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void | Promise<void>;
|
|
6
12
|
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/types/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6CAA6C,CAAA;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/types/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6CAA6C,CAAA;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,CAAA;AAEtE,MAAM,MAAM,YAAY,GAAG,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,KAAK,WAAW,CAAA;AAEnF,MAAM,MAAM,gBAAgB,GAAG,CAC7B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,MAAM,IAAI,KACb,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA"}
|
package/docs/cli.md
CHANGED
|
@@ -110,6 +110,11 @@ cer-app preview --port 8080
|
|
|
110
110
|
- Static assets from `dist/client/` are served first; HTML requests fall through to the SSR handler
|
|
111
111
|
- Otherwise, starts a static file server with SPA fallback (serves `index.html` for unknown paths)
|
|
112
112
|
- Returns 404 for paths not found in `dist/` (static mode only)
|
|
113
|
+
- **Path traversal protection:** all file requests are validated against the `dist/` root — requests attempting to escape it (e.g. `GET /../../../../etc/passwd`) receive a `400` response
|
|
114
|
+
- **Security headers:** every response includes `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, and `Referrer-Policy: strict-origin-when-cross-origin`
|
|
115
|
+
- **Smart Cache-Control:** Vite content-hashes assets placed in `/assets/` — these are served with `Cache-Control: public, max-age=31536000, immutable`. All other files (HTML, etc.) use `Cache-Control: no-cache` so browsers always revalidate
|
|
116
|
+
- **Graceful shutdown:** on `SIGTERM` or `SIGINT`, the server stops accepting new connections and waits for in-flight requests to finish before exiting. A 10-second timeout triggers a forced exit if connections do not drain
|
|
117
|
+
- **Request timeouts:** `headersTimeout` (10 s) aborts connections that stall while sending headers; `requestTimeout` (30 s) limits the total time allowed per request/response cycle, protecting against slow-client attacks
|
|
113
118
|
|
|
114
119
|
---
|
|
115
120
|
|
package/docs/composables.md
CHANGED
|
@@ -144,7 +144,9 @@ import { useInject } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
|
144
144
|
|
|
145
145
|
### `useRuntimeConfig()`
|
|
146
146
|
|
|
147
|
-
Returns the
|
|
147
|
+
Returns the runtime configuration set in `cer.config.ts`. Returns `{ public, private? }`:
|
|
148
|
+
- `public` — available everywhere (server and client)
|
|
149
|
+
- `private` — server-only secrets resolved from `process.env` at startup; `undefined` on the client
|
|
148
150
|
|
|
149
151
|
```ts
|
|
150
152
|
// cer.config.ts
|
|
@@ -152,28 +154,184 @@ export default defineConfig({
|
|
|
152
154
|
runtimeConfig: {
|
|
153
155
|
public: {
|
|
154
156
|
apiBase: process.env.VITE_API_BASE ?? '/api',
|
|
155
|
-
|
|
157
|
+
},
|
|
158
|
+
private: {
|
|
159
|
+
dbUrl: '', // resolved from process.env.DB_URL at server startup
|
|
156
160
|
},
|
|
157
161
|
},
|
|
158
162
|
})
|
|
159
163
|
```
|
|
160
164
|
|
|
161
165
|
```ts
|
|
162
|
-
// app/pages/index.ts —
|
|
166
|
+
// app/pages/index.ts — public config, works on client and server
|
|
163
167
|
component('page-index', () => {
|
|
164
168
|
const { public: cfg } = useRuntimeConfig()
|
|
165
|
-
// cfg.apiBase → '/api'
|
|
166
|
-
|
|
167
169
|
return html`<p>API base: ${cfg.apiBase}</p>`
|
|
168
170
|
})
|
|
169
171
|
```
|
|
170
172
|
|
|
171
|
-
|
|
173
|
+
```ts
|
|
174
|
+
// app/pages/data.ts — private config, server-only (loader)
|
|
175
|
+
export const loader = async () => {
|
|
176
|
+
const { private: priv } = useRuntimeConfig()
|
|
177
|
+
const rows = await db.query(priv!.dbUrl)
|
|
178
|
+
return { rows }
|
|
179
|
+
}
|
|
180
|
+
```
|
|
172
181
|
|
|
173
|
-
**Only use `runtimeConfig.public` for values safe to expose to the browser.**
|
|
182
|
+
**Only use `runtimeConfig.public` for values safe to expose to the browser.** Use `runtimeConfig.private` for secrets — they are never sent to the client.
|
|
183
|
+
|
|
184
|
+
Keys declared in `runtimeConfig.private` with an empty-string default are treated as **required** secrets. If the corresponding environment variable is not set at server startup, a warning is logged:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
[cer-app] runtimeConfig.private: "dbUrl" is an empty string — set DB_URL in the environment to provide a value.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
If the default is a non-empty string it is used as a genuine fallback — no warning is emitted.
|
|
174
191
|
|
|
175
192
|
If you need it outside auto-imported directories:
|
|
176
193
|
|
|
177
194
|
```ts
|
|
178
195
|
import { useRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
179
196
|
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### `useSeoMeta(input)`
|
|
201
|
+
|
|
202
|
+
Thin wrapper over `useHead()` for the most common SEO tags — Open Graph, Twitter Card, meta description, and canonical URL. Works in SPA, SSR, and SSG modes.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// app/pages/about.ts
|
|
206
|
+
component('page-about', () => {
|
|
207
|
+
useSeoMeta({
|
|
208
|
+
title: 'About Us',
|
|
209
|
+
description: 'Learn more about our team.',
|
|
210
|
+
ogTitle: 'About Us — My Site',
|
|
211
|
+
ogDescription: 'Learn more about our team.',
|
|
212
|
+
ogImage: 'https://example.com/og/about.png',
|
|
213
|
+
ogUrl: 'https://example.com/about',
|
|
214
|
+
ogType: 'website',
|
|
215
|
+
ogSiteName: 'My Site',
|
|
216
|
+
twitterCard: 'summary_large_image',
|
|
217
|
+
twitterSite: '@mysite',
|
|
218
|
+
canonical: 'https://example.com/about',
|
|
219
|
+
})
|
|
220
|
+
return html`<h1>About Us</h1>`
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Only properties you set are emitted — passing `undefined` (or omitting a property entirely) skips that tag.
|
|
225
|
+
|
|
226
|
+
#### `SeoMetaInput` fields
|
|
227
|
+
|
|
228
|
+
| Field | Tag emitted |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `title` | `<title>` |
|
|
231
|
+
| `description` | `<meta name="description">` |
|
|
232
|
+
| `ogTitle` | `<meta property="og:title">` |
|
|
233
|
+
| `ogDescription` | `<meta property="og:description">` |
|
|
234
|
+
| `ogImage` | `<meta property="og:image">` |
|
|
235
|
+
| `ogUrl` | `<meta property="og:url">` |
|
|
236
|
+
| `ogType` | `<meta property="og:type">` |
|
|
237
|
+
| `ogSiteName` | `<meta property="og:site_name">` |
|
|
238
|
+
| `twitterCard` | `<meta name="twitter:card">` |
|
|
239
|
+
| `twitterTitle` | `<meta name="twitter:title">` |
|
|
240
|
+
| `twitterDescription` | `<meta name="twitter:description">` |
|
|
241
|
+
| `twitterImage` | `<meta name="twitter:image">` |
|
|
242
|
+
| `twitterSite` | `<meta name="twitter:site">` |
|
|
243
|
+
| `canonical` | `<link rel="canonical">` |
|
|
244
|
+
|
|
245
|
+
If you need it outside auto-imported directories:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { useSeoMeta } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
TypeScript types:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import type { SeoMetaInput } from '@jasonshimmy/vite-plugin-cer-app/types'
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### `useCookie(name, options?)`
|
|
260
|
+
|
|
261
|
+
Isomorphic cookie composable. Reads and writes cookies transparently on both server and client:
|
|
262
|
+
|
|
263
|
+
- **Server (SSR/SSG):** reads `req.headers.cookie`; writes `Set-Cookie` response headers via `res.setHeader`.
|
|
264
|
+
- **Client:** reads and writes `document.cookie`.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// app/pages/profile.ts
|
|
268
|
+
component('page-profile', () => {
|
|
269
|
+
const session = useCookie('session')
|
|
270
|
+
|
|
271
|
+
// Read
|
|
272
|
+
console.log(session.value) // 'abc123' | undefined
|
|
273
|
+
|
|
274
|
+
// Write
|
|
275
|
+
session.set('abc123', { httpOnly: true, sameSite: 'Strict' })
|
|
276
|
+
|
|
277
|
+
// Remove
|
|
278
|
+
session.remove()
|
|
279
|
+
|
|
280
|
+
return html`<p>Session: ${session.value ?? 'none'}</p>`
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### `CookieRef`
|
|
285
|
+
|
|
286
|
+
| Member | Type | Description |
|
|
287
|
+
|---|---|---|
|
|
288
|
+
| `value` | `string \| undefined` | Current cookie value (read at call time) |
|
|
289
|
+
| `set(value, options?)` | `void` | Write the cookie |
|
|
290
|
+
| `remove(options?)` | `void` | Delete the cookie (sets `Max-Age=0`) |
|
|
291
|
+
|
|
292
|
+
#### `CookieOptions`
|
|
293
|
+
|
|
294
|
+
| Option | Type | Description |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| `path` | `string` | Cookie path (defaults to `/` when setting/removing) |
|
|
297
|
+
| `domain` | `string` | Cookie domain |
|
|
298
|
+
| `maxAge` | `number` | Max age in seconds |
|
|
299
|
+
| `expires` | `Date` | Expiry date |
|
|
300
|
+
| `httpOnly` | `boolean` | Set `HttpOnly` flag |
|
|
301
|
+
| `secure` | `boolean` | Set `Secure` flag |
|
|
302
|
+
| `sameSite` | `'Strict' \| 'Lax' \| 'None'` | `SameSite` attribute |
|
|
303
|
+
|
|
304
|
+
Default options can be passed as the second argument to `useCookie` — they are merged with options passed to `set()`/`remove()`:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const auth = useCookie('auth', { httpOnly: true, secure: true, sameSite: 'Strict' })
|
|
308
|
+
auth.set('tok') // inherits httpOnly, secure, sameSite automatically
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
If you need it outside auto-imported directories:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
import { useCookie } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
TypeScript types:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import type { CookieOptions, CookieRef } from '@jasonshimmy/vite-plugin-cer-app/types'
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
### `defineMiddleware(fn)`
|
|
326
|
+
|
|
327
|
+
Identity helper that gives TypeScript the correct `MiddlewareFn` type. Auto-imported — no import needed in `app/middleware/` files.
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
// app/middleware/auth.ts
|
|
331
|
+
export default defineMiddleware(async (to, from) => {
|
|
332
|
+
const isLoggedIn = !!localStorage.getItem('token')
|
|
333
|
+
return isLoggedIn ? true : '/login'
|
|
334
|
+
})
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
See [Middleware](./middleware.md) for full documentation.
|