@jasonshimmy/vite-plugin-cer-app 0.4.6 → 0.5.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/.github/copilot-instructions.md +4 -2
- package/CHANGELOG.md +4 -0
- package/IMPLEMENTATION_PLAN.md +52 -10
- package/commits.txt +1 -1
- package/dist/cli/commands/preview-isr.d.ts +51 -0
- package/dist/cli/commands/preview-isr.d.ts.map +1 -0
- package/dist/cli/commands/preview-isr.js +104 -0
- package/dist/cli/commands/preview-isr.js.map +1 -0
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +65 -1
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/plugin/dev-server.d.ts +3 -0
- package/dist/plugin/dev-server.d.ts.map +1 -1
- package/dist/plugin/dev-server.js.map +1 -1
- package/dist/plugin/dts-generator.d.ts.map +1 -1
- package/dist/plugin/dts-generator.js +8 -1
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +9 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/transforms/auto-import.js +2 -2
- 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 +95 -8
- package/dist/plugin/virtual/routes.js.map +1 -1
- package/dist/runtime/app-template.d.ts +1 -1
- package/dist/runtime/app-template.d.ts.map +1 -1
- package/dist/runtime/app-template.js +16 -4
- package/dist/runtime/app-template.js.map +1 -1
- package/dist/runtime/composables/index.d.ts +1 -0
- package/dist/runtime/composables/index.d.ts.map +1 -1
- package/dist/runtime/composables/index.js +1 -0
- package/dist/runtime/composables/index.js.map +1 -1
- package/dist/runtime/composables/use-runtime-config.d.ts +32 -0
- package/dist/runtime/composables/use-runtime-config.d.ts.map +1 -0
- package/dist/runtime/composables/use-runtime-config.js +41 -0
- package/dist/runtime/composables/use-runtime-config.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 +14 -6
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/dist/types/config.d.ts +24 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/page.d.ts +17 -0
- package/dist/types/page.d.ts.map +1 -1
- package/docs/composables.md +36 -0
- package/docs/configuration.md +52 -0
- package/docs/layouts.md +82 -0
- package/docs/rendering-modes.md +52 -11
- package/docs/routing.md +66 -0
- package/e2e/cypress/e2e/isr-nested-runtime.cy.ts +100 -0
- package/e2e/kitchen-sink/app/layouts/admin.ts +13 -0
- package/e2e/kitchen-sink/app/pages/admin/_layout.ts +1 -0
- package/e2e/kitchen-sink/app/pages/admin/dashboard.ts +11 -0
- package/e2e/kitchen-sink/app/pages/blog/[slug].ts +1 -0
- package/e2e/kitchen-sink/app/pages/isr-test.ts +17 -0
- package/e2e/kitchen-sink/cer.config.ts +5 -0
- package/package.json +1 -1
- package/src/__tests__/cli/preview-isr.test.ts +246 -0
- package/src/__tests__/plugin/dts-generator.test.ts +20 -0
- package/src/__tests__/plugin/resolve-config.test.ts +15 -0
- package/src/__tests__/plugin/transforms/auto-import.test.ts +16 -0
- package/src/__tests__/plugin/virtual/routes.test.ts +195 -0
- package/src/__tests__/runtime/use-runtime-config.test.ts +59 -0
- package/src/cli/commands/preview-isr.ts +139 -0
- package/src/cli/commands/preview.ts +71 -2
- package/src/plugin/dev-server.ts +1 -0
- package/src/plugin/dts-generator.ts +8 -1
- package/src/plugin/index.ts +11 -1
- package/src/plugin/transforms/auto-import.ts +2 -2
- package/src/plugin/virtual/routes.ts +106 -9
- package/src/runtime/app-template.ts +16 -4
- package/src/runtime/composables/index.ts +1 -0
- package/src/runtime/composables/use-runtime-config.ts +40 -0
- package/src/runtime/entry-server-template.ts +14 -6
- package/src/types/config.ts +26 -0
- package/src/types/index.ts +1 -1
- package/src/types/page.ts +17 -0
package/dist/types/config.d.ts
CHANGED
|
@@ -14,6 +14,24 @@ export interface AutoImportsConfig {
|
|
|
14
14
|
directives?: boolean;
|
|
15
15
|
runtime?: boolean;
|
|
16
16
|
}
|
|
17
|
+
export interface RuntimePublicConfig {
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface RuntimeConfig {
|
|
21
|
+
/**
|
|
22
|
+
* Public runtime config — available on both server and client via
|
|
23
|
+
* `useRuntimeConfig().public`. Values are serialized into the virtual module
|
|
24
|
+
* at build time, so only use static/env-var values here.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* runtimeConfig: {
|
|
28
|
+
* public: {
|
|
29
|
+
* apiBase: process.env.VITE_API_BASE ?? 'https://api.example.com',
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
public?: RuntimePublicConfig;
|
|
34
|
+
}
|
|
17
35
|
export interface CerAppConfig {
|
|
18
36
|
mode?: 'spa' | 'ssr' | 'ssg';
|
|
19
37
|
srcDir?: string;
|
|
@@ -22,6 +40,12 @@ export interface CerAppConfig {
|
|
|
22
40
|
jitCss?: JitCssConfig;
|
|
23
41
|
autoImports?: AutoImportsConfig;
|
|
24
42
|
port?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Runtime configuration accessible via `useRuntimeConfig()`.
|
|
45
|
+
* Only `public` values are exposed to the client; keep secrets
|
|
46
|
+
* out of `public`.
|
|
47
|
+
*/
|
|
48
|
+
runtimeConfig?: RuntimeConfig;
|
|
25
49
|
}
|
|
26
50
|
export declare function defineConfig(config: CerAppConfig): CerAppConfig;
|
|
27
51
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -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,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;
|
|
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;CAC7B;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":"AAwDA,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig } from './config.js';
|
|
1
|
+
export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig, RuntimeConfig, RuntimePublicConfig } 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';
|
|
@@ -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,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,MAAM,aAAa,CAAA;AAC/H,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,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA"}
|
package/dist/types/page.d.ts
CHANGED
|
@@ -5,12 +5,29 @@ export interface SsgPathsContext {
|
|
|
5
5
|
}
|
|
6
6
|
export interface PageSsgConfig {
|
|
7
7
|
paths?: () => Promise<SsgPathsContext[]> | SsgPathsContext[];
|
|
8
|
+
/**
|
|
9
|
+
* Seconds before a cached SSR response is stale and should be re-rendered.
|
|
10
|
+
* Enables Incremental Static Regeneration (ISR) in the preview server and
|
|
11
|
+
* any production adapter that reads `meta.ssg.revalidate`.
|
|
12
|
+
*
|
|
13
|
+
* @example export const meta = { ssg: { revalidate: 60 } }
|
|
14
|
+
*/
|
|
15
|
+
revalidate?: number;
|
|
8
16
|
}
|
|
9
17
|
export interface PageMeta {
|
|
10
18
|
layout?: string;
|
|
11
19
|
middleware?: string[];
|
|
12
20
|
hydrate?: HydrateStrategy;
|
|
13
21
|
ssg?: PageSsgConfig;
|
|
22
|
+
/**
|
|
23
|
+
* CSS transition name applied to the page during route changes.
|
|
24
|
+
* Set to `true` to use the default 'page' transition name.
|
|
25
|
+
* The framework adds/removes `[data-transition="<name>"]` on the root element
|
|
26
|
+
* so you can target it with CSS animations.
|
|
27
|
+
*
|
|
28
|
+
* @example export const meta = { transition: 'fade' }
|
|
29
|
+
*/
|
|
30
|
+
transition?: string | boolean;
|
|
14
31
|
}
|
|
15
32
|
export interface PageLoaderContext<P extends Record<string, string> = Record<string, string>> {
|
|
16
33
|
params: P;
|
package/dist/types/page.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/types/page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AAElE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,GAAG,eAAe,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/types/page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AAElE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,GAAG,eAAe,EAAE,CAAA;IAC5D;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,GAAG,CAAC,EAAE,aAAa,CAAA;IACnB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAC9B;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAC1F,MAAM,EAAE,CAAC,CAAA;IACT,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,GAAG,EAAE,eAAe,CAAA;CACrB;AAED,MAAM,MAAM,UAAU,CACpB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzD,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACzB,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA"}
|
package/docs/composables.md
CHANGED
|
@@ -141,3 +141,39 @@ import { useInject } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
> **Note:** Prefer `useInject` over the raw `inject()` primitive whenever reading plugin-provided values. Raw `inject()` works in SPA mode but returns `undefined` in SSR and SSG because the server renders components without `<cer-layout-view>`'s provide context.
|
|
144
|
+
|
|
145
|
+
### `useRuntimeConfig()`
|
|
146
|
+
|
|
147
|
+
Returns the `public` runtime configuration object set in `cer.config.ts` under `runtimeConfig.public`. Available in all rendering modes (SPA, SSR, SSG) and on both server and client.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// cer.config.ts
|
|
151
|
+
export default defineConfig({
|
|
152
|
+
runtimeConfig: {
|
|
153
|
+
public: {
|
|
154
|
+
apiBase: process.env.VITE_API_BASE ?? '/api',
|
|
155
|
+
featureFlags: { darkMode: true },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// app/pages/index.ts — auto-imported, no import statement needed
|
|
163
|
+
component('page-index', () => {
|
|
164
|
+
const { public: cfg } = useRuntimeConfig()
|
|
165
|
+
// cfg.apiBase → '/api'
|
|
166
|
+
|
|
167
|
+
return html`<p>API base: ${cfg.apiBase}</p>`
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The config is initialized at app boot (both client and server) by calling `initRuntimeConfig(runtimeConfig)` with the value from `virtual:cer-app-config`. You only need `useRuntimeConfig()` to read it.
|
|
172
|
+
|
|
173
|
+
**Only use `runtimeConfig.public` for values safe to expose to the browser.** Secrets, tokens, and private keys must stay in server-only code (loaders, API handlers, server middleware).
|
|
174
|
+
|
|
175
|
+
If you need it outside auto-imported directories:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { useRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
179
|
+
```
|
package/docs/configuration.md
CHANGED
|
@@ -198,10 +198,60 @@ When `directives: true`, the following are injected if used and not already impo
|
|
|
198
198
|
import { when, each, match, anchorBlock } from '@jasonshimmy/custom-elements-runtime/directives'
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
+
The following framework composables are **always** auto-imported when used, regardless of the `runtime` flag — they come from the plugin package:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { useHead, usePageData, useInject, useRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
205
|
+
```
|
|
206
|
+
|
|
201
207
|
Set any flag to `false` to opt out and manage imports manually.
|
|
202
208
|
|
|
203
209
|
---
|
|
204
210
|
|
|
211
|
+
## `runtimeConfig` options
|
|
212
|
+
|
|
213
|
+
Expose typed, centralized public configuration to both server and client code via `useRuntimeConfig()`.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
export default defineConfig({
|
|
217
|
+
runtimeConfig: {
|
|
218
|
+
public: {
|
|
219
|
+
apiBase: process.env.VITE_API_BASE ?? 'https://api.example.com',
|
|
220
|
+
appVersion: '1.0.0',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### `runtimeConfig.public`
|
|
227
|
+
|
|
228
|
+
**Type:** `Record<string, unknown>`
|
|
229
|
+
**Default:** `{}`
|
|
230
|
+
|
|
231
|
+
Values placed here are serialized into `virtual:cer-app-config` at build time and accessible on both server and client via `useRuntimeConfig().public`.
|
|
232
|
+
|
|
233
|
+
> **Security:** Only put values here that are safe to expose to the browser. Do not put secrets, tokens, or private keys in `public`. Those should be read directly from `process.env` inside server-only code (loaders, server middleware, API handlers).
|
|
234
|
+
|
|
235
|
+
> **Serialization:** Values must be JSON-serializable (strings, numbers, booleans, plain objects, arrays). Functions, class instances, `undefined`, and circular references are not supported and will be lost or throw during the build.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
// Any page, layout, component, or composable
|
|
239
|
+
component('page-index', () => {
|
|
240
|
+
const config = useRuntimeConfig()
|
|
241
|
+
// config.public.apiBase → 'https://api.example.com'
|
|
242
|
+
|
|
243
|
+
return html`<p>API: ${config.public.apiBase}</p>`
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**TypeScript:** Import `RuntimePublicConfig` to type your public config if needed:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import type { RuntimePublicConfig } from '@jasonshimmy/vite-plugin-cer-app/types'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
205
255
|
## Passing config to the Vite plugin directly
|
|
206
256
|
|
|
207
257
|
When using `vite.config.ts` instead of (or alongside) `cer.config.ts`:
|
|
@@ -234,5 +284,7 @@ import type {
|
|
|
234
284
|
SsgConfig,
|
|
235
285
|
JitCssConfig,
|
|
236
286
|
AutoImportsConfig,
|
|
287
|
+
RuntimeConfig,
|
|
288
|
+
RuntimePublicConfig,
|
|
237
289
|
} from '@jasonshimmy/vite-plugin-cer-app/types'
|
|
238
290
|
```
|
package/docs/layouts.md
CHANGED
|
@@ -110,3 +110,85 @@ import layouts from 'virtual:cer-layouts'
|
|
|
110
110
|
## Layout switching and DOM preservation
|
|
111
111
|
|
|
112
112
|
When navigating between pages with different layouts, the framework uses `<cer-keep-alive>` to preserve the layout DOM and avoid unnecessary teardown/remount cycles. This means transitions between pages sharing the same layout are smooth with no layout flash.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 🪆 Nested layouts
|
|
117
|
+
|
|
118
|
+
Place a `_layout.ts` file inside any page subdirectory to add an inner layout that wraps pages in that subtree. The outer layout (from `meta.layout` or the default) wraps the inner layout, which wraps `<router-view>`.
|
|
119
|
+
|
|
120
|
+
### File convention
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
app/
|
|
124
|
+
layouts/
|
|
125
|
+
default.ts # outer layout — full shell (header, footer)
|
|
126
|
+
minimal.ts # outer layout — bare minimum
|
|
127
|
+
sidebar.ts # inner layout — adds a sidebar panel
|
|
128
|
+
pages/
|
|
129
|
+
index.ts # uses 'default' layout only
|
|
130
|
+
admin/
|
|
131
|
+
_layout.ts # ← inner layout override for all /admin/* pages
|
|
132
|
+
index.ts # layout chain: ['default', 'sidebar']
|
|
133
|
+
users.ts # layout chain: ['default', 'sidebar']
|
|
134
|
+
settings.ts # layout chain: ['default', 'sidebar']
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `_layout.ts` syntax
|
|
138
|
+
|
|
139
|
+
Export the inner layout name as a default string:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// app/pages/admin/_layout.ts
|
|
143
|
+
export default 'sidebar'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The value must match a filename (without extension) in `app/layouts/`.
|
|
147
|
+
|
|
148
|
+
### Rendered structure
|
|
149
|
+
|
|
150
|
+
For a page at `app/pages/admin/users.ts` with the above setup:
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<layout-default>
|
|
154
|
+
<layout-sidebar>
|
|
155
|
+
<router-view></router-view>
|
|
156
|
+
</layout-sidebar>
|
|
157
|
+
</layout-default>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Each layout receives the inner content via `<slot>`.
|
|
161
|
+
|
|
162
|
+
### Overriding the outer layout
|
|
163
|
+
|
|
164
|
+
If a page in a nested subtree needs a different outer layout, declare it in `meta.layout` as usual:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// app/pages/admin/login.ts
|
|
168
|
+
export const meta = {
|
|
169
|
+
layout: 'minimal', // overrides outer; chain = ['minimal', 'sidebar']
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Multiple nesting levels
|
|
174
|
+
|
|
175
|
+
Nesting is resolved recursively. Each ancestor directory that contains a `_layout.ts` contributes one level to the chain:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
app/pages/
|
|
179
|
+
admin/
|
|
180
|
+
_layout.ts → 'sidebar'
|
|
181
|
+
settings/
|
|
182
|
+
_layout.ts → 'settings-tabs'
|
|
183
|
+
profile.ts # chain: ['default', 'sidebar', 'settings-tabs']
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `meta.layoutChain` in routes
|
|
187
|
+
|
|
188
|
+
The framework emits the resolved chain as `meta.layoutChain` on the route object at build time. You can read it at runtime:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import routes from 'virtual:cer-routes'
|
|
192
|
+
const adminUsers = routes.find(r => r.path === '/admin/users')
|
|
193
|
+
// adminUsers.meta.layoutChain → ['default', 'sidebar']
|
|
194
|
+
```
|
package/docs/rendering-modes.md
CHANGED
|
@@ -223,19 +223,60 @@ Any CDN or static host. Upload the entire `dist/` directory (excluding `dist/ser
|
|
|
223
223
|
|
|
224
224
|
---
|
|
225
225
|
|
|
226
|
+
## ISR — Incremental Static Regeneration
|
|
227
|
+
|
|
228
|
+
ISR is a per-route cache layer in the SSR preview server. Pages with `meta.ssg.revalidate` set are rendered once, cached, and re-rendered in the background when the TTL expires (stale-while-revalidate).
|
|
229
|
+
|
|
230
|
+
### How it works
|
|
231
|
+
|
|
232
|
+
1. **First request (HIT after fresh render):** Cache miss — render via SSR, store in memory cache with TTL, then serve from the newly-populated cache. `X-Cache: HIT` is set.
|
|
233
|
+
2. **Within TTL (HIT):** Serve directly from cache. `X-Cache: HIT` header is set.
|
|
234
|
+
3. **After TTL expires (STALE):** Serve the stale cached HTML immediately with `X-Cache: STALE`. Kick off a background re-render. When the re-render completes, update the cache.
|
|
235
|
+
4. **While revalidating:** Continue serving stale HTML to new requests.
|
|
236
|
+
|
|
237
|
+
### Configuration
|
|
238
|
+
|
|
239
|
+
Add `revalidate` (seconds) to `meta.ssg` in any page:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// app/pages/blog/[slug].ts
|
|
243
|
+
export const meta = {
|
|
244
|
+
ssg: {
|
|
245
|
+
revalidate: 60, // cache for 60 s; re-render in background after expiry
|
|
246
|
+
paths: async () => {
|
|
247
|
+
const posts = await fetchPosts()
|
|
248
|
+
return posts.map(p => ({ params: { slug: p.slug } }))
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Use cases
|
|
255
|
+
|
|
256
|
+
| TTL | Use case |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `revalidate: 0` | TTL expires immediately — first request is HIT; every subsequent request is STALE with a background re-render |
|
|
259
|
+
| `revalidate: 60` | News articles, dashboards |
|
|
260
|
+
| `revalidate: 3600` | Product pages, documentation |
|
|
261
|
+
| `revalidate: 86400` | Marketing pages, rarely-changing content |
|
|
262
|
+
|
|
263
|
+
### Availability
|
|
264
|
+
|
|
265
|
+
ISR is currently active in the built-in **preview server** (`cer-app preview`). When integrating the server bundle into Express / Hono / Fastify in production, implement the same stale-while-revalidate pattern using `route.meta?.ssg?.revalidate` from the exported `routes` array.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
226
269
|
## Comparing modes
|
|
227
270
|
|
|
228
|
-
| Feature | SPA | SSR | SSG |
|
|
229
|
-
|
|
230
|
-
| Initial HTML | Empty shell | Full HTML | Full HTML |
|
|
231
|
-
| SEO | Poor | Excellent | Excellent |
|
|
232
|
-
| TTFB | Fast | Depends on server | Very fast (CDN) |
|
|
233
|
-
| Server required | No | Yes | No |
|
|
234
|
-
| Data freshness | Real-time | Real-time | Build-time |
|
|
235
|
-
| Dynamic routes | Yes | Yes | Requires `ssg.paths` |
|
|
236
|
-
| API routes | Separate deploy | Same process | Separate deploy |
|
|
237
|
-
| `useHead()` SSR injection | No | Yes | Yes |
|
|
238
|
-
| Streaming | No | No | No |
|
|
271
|
+
| Feature | SPA | SSR | SSG | ISR |
|
|
272
|
+
|---|---|---|---|---|
|
|
273
|
+
| Initial HTML | Empty shell | Full HTML | Full HTML | Full HTML |
|
|
274
|
+
| SEO | Poor | Excellent | Excellent | Excellent |
|
|
275
|
+
| TTFB | Fast | Depends on server | Very fast (CDN) | Very fast after first render |
|
|
276
|
+
| Server required | No | Yes | No | Yes |
|
|
277
|
+
| Data freshness | Real-time | Real-time | Build-time | Configurable TTL |
|
|
278
|
+
| Dynamic routes | Yes | Yes | Requires `ssg.paths` | Yes |
|
|
279
|
+
| API routes | Separate deploy | Same process | Separate deploy | Same process |
|
|
239
280
|
|
|
240
281
|
---
|
|
241
282
|
|
package/docs/routing.md
CHANGED
|
@@ -172,6 +172,72 @@ export const meta = {
|
|
|
172
172
|
}
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
+
### `meta.ssg.revalidate`
|
|
176
|
+
|
|
177
|
+
**Type:** `number` (seconds)
|
|
178
|
+
|
|
179
|
+
Enables Incremental Static Regeneration (ISR) for the route. When set, the preview server caches the rendered HTML and serves it until the TTL expires. After expiry, stale HTML is served immediately while a fresh render runs in the background (stale-while-revalidate).
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// app/pages/blog/[slug].ts
|
|
183
|
+
export const meta = {
|
|
184
|
+
ssg: {
|
|
185
|
+
revalidate: 60, // re-render at most once per minute
|
|
186
|
+
paths: async () => { /* ... */ },
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
See [Rendering Modes — ISR](rendering-modes.md#isr--incremental-static-regeneration) for full details.
|
|
192
|
+
|
|
193
|
+
### `meta.transition`
|
|
194
|
+
|
|
195
|
+
**Type:** `string | boolean`
|
|
196
|
+
|
|
197
|
+
Attaches transition metadata to the route. The value is extracted at build time and emitted as `meta.transition` on the route object — the framework does **not** apply any CSS or DOM changes automatically.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// app/pages/about.ts
|
|
201
|
+
export const meta = {
|
|
202
|
+
transition: 'fade', // stored as route.meta.transition at runtime
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Read the value in your navigation handler or layout to implement transitions yourself:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import routes from 'virtual:cer-routes'
|
|
210
|
+
const about = routes.find(r => r.path === '/about')
|
|
211
|
+
// about.meta.transition → 'fade'
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Example — apply a CSS class during navigation using a plugin:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
// app/plugins/transitions.ts
|
|
218
|
+
export default {
|
|
219
|
+
setup({ router }) {
|
|
220
|
+
router.beforeEach((to) => {
|
|
221
|
+
const name = to.meta?.transition
|
|
222
|
+
if (name) document.documentElement.setAttribute('data-transition', String(name))
|
|
223
|
+
else document.documentElement.removeAttribute('data-transition')
|
|
224
|
+
})
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```css
|
|
230
|
+
[data-transition="fade"] cer-layout-view {
|
|
231
|
+
animation: fadeIn 0.2s ease;
|
|
232
|
+
}
|
|
233
|
+
@keyframes fadeIn {
|
|
234
|
+
from { opacity: 0 }
|
|
235
|
+
to { opacity: 1 }
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Set to `false` to explicitly mark a page as having no transition (useful when a catch-all or default would otherwise apply one).
|
|
240
|
+
|
|
175
241
|
---
|
|
176
242
|
|
|
177
243
|
## Route sorting
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ISR (Incremental Static Regeneration), nested layouts, and
|
|
3
|
+
* runtimeConfig — features added alongside Nuxt/Next parity improvements.
|
|
4
|
+
*
|
|
5
|
+
* ISR and runtimeConfig tests only run in SSR mode (preview server).
|
|
6
|
+
* Nested-layout tests run in all modes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const mode = Cypress.env('mode') as 'spa' | 'ssr' | 'ssg'
|
|
10
|
+
|
|
11
|
+
// ─── ISR (preview server only) ────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
if (mode === 'ssr') {
|
|
14
|
+
describe('ISR — stale-while-revalidate cache', () => {
|
|
15
|
+
it('request to a route with revalidate returns X-Cache: HIT', () => {
|
|
16
|
+
// On first render (cache miss) the server renders, caches, then serves
|
|
17
|
+
// from cache — so all requests within the TTL return HIT.
|
|
18
|
+
cy.request('/blog/first-post').then((response) => {
|
|
19
|
+
expect(response.headers['x-cache']).to.equal('HIT')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('subsequent requests within TTL also return X-Cache: HIT', () => {
|
|
24
|
+
cy.request('/blog/first-post')
|
|
25
|
+
cy.request('/blog/first-post').then((response) => {
|
|
26
|
+
expect(response.headers['x-cache']).to.equal('HIT')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('X-Cache header is absent on non-revalidate routes', () => {
|
|
31
|
+
cy.request('/about').then((response) => {
|
|
32
|
+
expect(response.headers).not.to.have.property('x-cache')
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// /isr-test uses revalidate: 0 — the TTL is always expired after the first
|
|
37
|
+
// render, so the second request is always served stale-while-revalidate.
|
|
38
|
+
it('first request to a revalidate:0 route returns X-Cache: HIT', () => {
|
|
39
|
+
cy.request('/isr-test').then((response) => {
|
|
40
|
+
expect(response.headers['x-cache']).to.equal('HIT')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('second request to a revalidate:0 route returns X-Cache: STALE', () => {
|
|
45
|
+
cy.request('/isr-test') // prime the cache
|
|
46
|
+
cy.request('/isr-test').then((response) => {
|
|
47
|
+
expect(response.headers['x-cache']).to.equal('STALE')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Nested layouts ────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
describe('Nested layouts — admin section', () => {
|
|
56
|
+
it('renders the admin dashboard page', () => {
|
|
57
|
+
cy.visit('/admin/dashboard')
|
|
58
|
+
cy.get('[data-cy=admin-dashboard-heading]').should('contain', 'Admin Dashboard')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('renders the outer default layout (site-header, site-nav)', () => {
|
|
62
|
+
cy.visit('/admin/dashboard')
|
|
63
|
+
cy.get('[data-cy=site-header]').should('exist')
|
|
64
|
+
cy.get('[data-cy=site-nav]').should('exist')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('renders the inner admin layout (admin-sidebar, admin-content)', () => {
|
|
68
|
+
cy.visit('/admin/dashboard')
|
|
69
|
+
cy.get('[data-cy=admin-layout]').should('exist')
|
|
70
|
+
cy.get('[data-cy=admin-sidebar]').should('exist')
|
|
71
|
+
cy.get('[data-cy=admin-content]').should('exist')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('layout-admin is a DOM child of layout-default (layout chain order)', () => {
|
|
75
|
+
// Verifies the chain ['default', 'admin'] is rendered outermost-first.
|
|
76
|
+
// layout-admin must be in the light DOM of layout-default, not the other
|
|
77
|
+
// way around. A CSS descendant selector proves this without needing to
|
|
78
|
+
// pierce slot boundaries.
|
|
79
|
+
cy.visit('/admin/dashboard')
|
|
80
|
+
cy.get('layout-default layout-admin').should('exist')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (mode !== 'spa') {
|
|
84
|
+
it('admin layout is pre-rendered in initial HTML (SSR/SSG)', () => {
|
|
85
|
+
cy.request('/admin/dashboard').then((response) => {
|
|
86
|
+
expect(response.body).to.include('admin-layout')
|
|
87
|
+
expect(response.body).to.include('Admin Dashboard')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ─── runtimeConfig ─────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
describe('runtimeConfig.public', () => {
|
|
96
|
+
it('renders the appName from runtimeConfig on the admin dashboard', () => {
|
|
97
|
+
cy.visit('/admin/dashboard')
|
|
98
|
+
cy.get('[data-cy=admin-app-name]').should('contain', 'Kitchen Sink')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
component('layout-admin', () => {
|
|
2
|
+
return html`
|
|
3
|
+
<div data-cy="admin-layout">
|
|
4
|
+
<aside data-cy="admin-sidebar">
|
|
5
|
+
<p>Admin Panel</p>
|
|
6
|
+
<a href="/admin/dashboard" data-cy="admin-nav-dashboard">Dashboard</a>
|
|
7
|
+
</aside>
|
|
8
|
+
<section data-cy="admin-content">
|
|
9
|
+
<slot></slot>
|
|
10
|
+
</section>
|
|
11
|
+
</div>
|
|
12
|
+
`
|
|
13
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default 'admin'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
component('page-admin-dashboard', () => {
|
|
2
|
+
const config = useRuntimeConfig()
|
|
3
|
+
const appName = config.public?.appName ?? 'Kitchen Sink'
|
|
4
|
+
|
|
5
|
+
return html`
|
|
6
|
+
<div>
|
|
7
|
+
<h1 data-cy="admin-dashboard-heading">Admin Dashboard</h1>
|
|
8
|
+
<p data-cy="admin-app-name">App: ${appName}</p>
|
|
9
|
+
</div>
|
|
10
|
+
`
|
|
11
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
component('page-isr-test', () => {
|
|
2
|
+
return html`
|
|
3
|
+
<div>
|
|
4
|
+
<h1 data-cy="isr-test-heading">ISR Test</h1>
|
|
5
|
+
<p data-cy="isr-test-description">
|
|
6
|
+
This page uses <code>revalidate: 0</code> so every post-first request
|
|
7
|
+
is immediately stale.
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
10
|
+
`
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const meta = {
|
|
14
|
+
ssg: {
|
|
15
|
+
revalidate: 0,
|
|
16
|
+
},
|
|
17
|
+
}
|