@jasonshimmy/vite-plugin-cer-app 0.2.0 → 0.4.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/README.md +2 -0
- package/commits.txt +1 -1
- package/dist/cli/create/index.js +7 -3
- package/dist/cli/create/index.js.map +1 -1
- package/dist/cli/create/templates/spa/.gitignore.tpl +25 -0
- package/dist/cli/create/templates/spa/index.html.tpl +1 -1
- package/dist/cli/create/templates/ssg/.gitignore.tpl +25 -0
- package/dist/cli/create/templates/ssg/index.html.tpl +1 -1
- package/dist/cli/create/templates/ssr/.gitignore.tpl +25 -0
- package/dist/cli/create/templates/ssr/cer.config.ts.tpl +0 -1
- package/dist/cli/create/templates/ssr/index.html.tpl +1 -1
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +18 -0
- package/dist/plugin/build-ssr.js.map +1 -1
- package/dist/plugin/dev-server.d.ts +0 -1
- package/dist/plugin/dev-server.d.ts.map +1 -1
- package/dist/plugin/dts-generator.js +1 -1
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/generated-dir.d.ts +5 -11
- package/dist/plugin/generated-dir.d.ts.map +1 -1
- package/dist/plugin/generated-dir.js +43 -31
- package/dist/plugin/generated-dir.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/runtime/app-template.d.ts +5 -4
- package/dist/runtime/app-template.d.ts.map +1 -1
- package/dist/runtime/app-template.js +6 -5
- 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-inject.d.ts +29 -0
- package/dist/runtime/composables/use-inject.d.ts.map +1 -0
- package/dist/runtime/composables/use-inject.js +48 -0
- package/dist/runtime/composables/use-inject.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 +20 -0
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/dist/types/config.d.ts +0 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/docs/cli.md +1 -1
- package/docs/composables.md +37 -0
- package/docs/configuration.md +2 -11
- package/docs/getting-started.md +2 -100
- package/docs/plugins.md +23 -15
- package/docs/rendering-modes.md +3 -4
- package/docs/testing.md +3 -3
- package/e2e/kitchen-sink/app/pages/(auth)/protected.ts +1 -5
- package/e2e/kitchen-sink/cer-auto-imports.d.ts +1 -0
- package/package.json +1 -1
- package/src/__tests__/plugin/build-ssr.test.ts +10 -0
- package/src/__tests__/plugin/cer-app-plugin.test.ts +15 -0
- package/src/__tests__/plugin/dev-server.test.ts +1 -1
- package/src/__tests__/plugin/dts-generator.test.ts +5 -0
- package/src/__tests__/plugin/entry-server-template.test.ts +24 -0
- package/src/__tests__/plugin/generated-dir.test.ts +8 -39
- package/src/__tests__/plugin/resolve-config.test.ts +0 -5
- package/src/__tests__/plugin/transforms/auto-import.test.ts +7 -0
- package/src/__tests__/runtime/use-inject-client.test.ts +67 -0
- package/src/__tests__/runtime/use-inject.test.ts +66 -0
- package/src/__tests__/types/config.test.ts +1 -1
- package/src/cli/create/index.ts +12 -8
- package/src/cli/create/templates/spa/.gitignore.tpl +25 -0
- package/src/cli/create/templates/spa/index.html.tpl +1 -1
- package/src/cli/create/templates/ssg/.gitignore.tpl +25 -0
- package/src/cli/create/templates/ssg/index.html.tpl +1 -1
- package/src/cli/create/templates/ssr/.gitignore.tpl +25 -0
- package/src/cli/create/templates/ssr/cer.config.ts.tpl +0 -1
- package/src/cli/create/templates/ssr/index.html.tpl +1 -1
- package/src/plugin/build-ssr.ts +18 -0
- package/src/plugin/dev-server.ts +1 -1
- package/src/plugin/dts-generator.ts +1 -1
- package/src/plugin/generated-dir.ts +44 -31
- package/src/plugin/index.ts +9 -1
- package/src/plugin/transforms/auto-import.ts +2 -2
- package/src/runtime/app-template.ts +6 -5
- package/src/runtime/composables/index.ts +1 -0
- package/src/runtime/composables/use-inject.ts +49 -0
- package/src/runtime/entry-server-template.ts +20 -0
- package/src/types/config.ts +0 -1
- package/dist/cli/create/templates/spa/app/app.ts.tpl +0 -93
- package/dist/cli/create/templates/ssg/app/app.ts.tpl +0 -97
- package/dist/cli/create/templates/ssr/app/app.ts.tpl +0 -97
- package/src/cli/create/templates/spa/app/app.ts.tpl +0 -93
- package/src/cli/create/templates/ssg/app/app.ts.tpl +0 -97
- package/src/cli/create/templates/ssr/app/app.ts.tpl +0 -97
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-import.js","sourceRoot":"","sources":["../../../src/plugin/transforms/auto-import.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQjC,MAAM,eAAe,GAAG,0ZAA0Z,CAAA;AAElb,MAAM,iBAAiB,GAAG,mGAAmG,CAAA;AAE7H,MAAM,iBAAiB,GAAG,
|
|
1
|
+
{"version":3,"file":"auto-import.js","sourceRoot":"","sources":["../../../src/plugin/transforms/auto-import.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Template string for
|
|
2
|
+
* Template string for `.cer/app.ts` — the framework client entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Always written to `.cer/app.ts` on every dev/build so consumers
|
|
5
|
+
* automatically receive the latest bootstrap code on plugin update.
|
|
6
|
+
* This file is gitignored and should never be edited directly.
|
|
6
7
|
*/
|
|
7
|
-
export declare const APP_ENTRY_TEMPLATE = "// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app\
|
|
8
|
+
export declare const APP_ENTRY_TEMPLATE = "// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app \u2014 do not edit.\n// Regenerated automatically on every dev server start and build.\n\nimport '@jasonshimmy/custom-elements-runtime/css'\nimport 'virtual:cer-jit-css'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport { hasLoading, loadingTag } from 'virtual:cer-loading'\nimport { hasError, errorTag } from 'virtual:cer-error'\nimport {\n component,\n ref,\n provide,\n useOnConnected,\n useOnDisconnected,\n registerBuiltinComponents,\n} from '@jasonshimmy/custom-elements-runtime'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { enableJITCSS } from '@jasonshimmy/custom-elements-runtime/jit-css'\n\nregisterBuiltinComponents()\nenableJITCSS()\n\nconst router = initRouter({ routes })\n\n// \u2500\u2500\u2500 Navigation state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst isNavigating = ref(false)\nconst currentError = ref(null)\n\nconst resetError = (): void => {\n currentError.value = null\n void router.replace(router.getCurrent().path)\n}\n;(globalThis as Record<string, unknown>).resetError = resetError\n\nconst _push = router.push.bind(router)\nconst _replace = router.replace.bind(router)\n\nrouter.push = async (path) => {\n isNavigating.value = true\n currentError.value = null\n try {\n await _push(path)\n } catch (err) {\n currentError.value = err instanceof Error ? err.message : String(err)\n } finally {\n isNavigating.value = false\n }\n}\n\nrouter.replace = async (path) => {\n isNavigating.value = true\n currentError.value = null\n try {\n await _replace(path)\n } catch (err) {\n currentError.value = err instanceof Error ? err.message : String(err)\n } finally {\n isNavigating.value = false\n }\n}\n\n// \u2500\u2500\u2500 Plugins \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Collect plugin-provided values so cer-layout-view can forward them into\n// the component context tree via the real provide() hook (which inject() walks).\n// Declared BEFORE component('cer-layout-view') to avoid a temporal dead zone\n// ReferenceError: customElements.define() upgrades existing DOM elements\n// synchronously, calling the render function immediately.\nconst _pluginProvides = new Map<string, unknown>()\n// Expose plugin provides globally so page components can read them synchronously\n// regardless of render order.\n;(globalThis as Record<string, unknown>).__cerPluginProvides = _pluginProvides\n\n// \u2500\u2500\u2500 <cer-layout-view> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ncomponent('cer-layout-view', () => {\n // Forward plugin-provided values into the component context so inject() in\n // any descendant component can resolve them by walking up the DOM tree.\n for (const [key, value] of _pluginProvides) {\n provide(key, value)\n }\n\n const current = ref(router.getCurrent())\n let unsub: (() => void) | undefined\n\n useOnConnected(() => {\n unsub = router.subscribe((s: typeof current.value) => { current.value = s })\n })\n useOnDisconnected(() => { unsub?.(); unsub = undefined })\n\n if (currentError.value !== null) {\n if (hasError && errorTag) {\n return { tag: errorTag, props: { attrs: { error: String(currentError.value) } }, children: [] }\n }\n return { tag: 'div', props: { attrs: { style: 'padding:2rem;font-family:monospace' } }, children: String(currentError.value) }\n }\n\n if (isNavigating.value && hasLoading && loadingTag) {\n return { tag: loadingTag, props: {}, children: [] }\n }\n\n const matched = router.matchRoute(current.value.path)\n const routeMeta = matched?.route?.meta as { layout?: string } | undefined\n const layoutName = routeMeta?.layout ?? 'default'\n const layoutTag = (layouts as Record<string, string>)[layoutName]\n const routerView = { tag: 'router-view', props: {}, children: [] }\n\n if (layoutTag) return { tag: layoutTag, props: {}, children: [routerView] }\n return routerView\n})\n\nfor (const plugin of plugins) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({\n router,\n provide: (key: string, value: unknown) => { _pluginProvides.set(key, value) },\n config: {},\n })\n }\n}\n\n// \u2500\u2500\u2500 Pre-load initial route \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Download the current page's route chunk AFTER plugins run so that\n// cer-layout-view's first render (which calls provide()) completes before\n// page components are defined and their renders are scheduled. This ensures\n// inject() in child components can find values stored by provide().\n\nif (typeof window !== 'undefined') {\n const _initMatch = router.matchRoute(window.location.pathname)\n if (_initMatch?.route?.load) {\n try { await _initMatch.route.load() } catch { /* non-fatal */ }\n }\n}\n\n// \u2500\u2500\u2500 Initial navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nif (typeof window !== 'undefined') {\n // Use the original (unwrapped) replace so isNavigating stays false during\n // the initial paint \u2014 the loading component must not flash over pre-rendered content.\n await _replace(window.location.pathname + window.location.search + window.location.hash)\n // Clear SSR loader data after initial navigation so subsequent client-side\n // navigations don't accidentally reuse stale server data.\n delete (globalThis as Record<string, unknown>).__CER_DATA__\n}\n\nexport { router }\n";
|
|
8
9
|
//# sourceMappingURL=app-template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-template.d.ts","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"app-template.d.ts","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,u3NAsJ9B,CAAA"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Template string for
|
|
2
|
+
* Template string for `.cer/app.ts` — the framework client entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Always written to `.cer/app.ts` on every dev/build so consumers
|
|
5
|
+
* automatically receive the latest bootstrap code on plugin update.
|
|
6
|
+
* This file is gitignored and should never be edited directly.
|
|
6
7
|
*/
|
|
7
|
-
export const APP_ENTRY_TEMPLATE = `// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app
|
|
8
|
-
//
|
|
8
|
+
export const APP_ENTRY_TEMPLATE = `// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app — do not edit.
|
|
9
|
+
// Regenerated automatically on every dev server start and build.
|
|
9
10
|
|
|
10
11
|
import '@jasonshimmy/custom-elements-runtime/css'
|
|
11
12
|
import 'virtual:cer-jit-css'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-template.js","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"app-template.js","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsJjC,CAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { useHead, beginHeadCollection, endHeadCollection, serializeHeadTags } from './use-head.js';
|
|
2
2
|
export type { HeadInput } from './use-head.js';
|
|
3
3
|
export { usePageData } from './use-page-data.js';
|
|
4
|
+
export { useInject } from './use-inject.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAClG,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAClG,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/runtime/composables/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAElG,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/runtime/composables/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAElG,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInject — reads a value provided by a plugin via plugin.setup()'s provide().
|
|
3
|
+
*
|
|
4
|
+
* Works consistently across all rendering modes:
|
|
5
|
+
*
|
|
6
|
+
* - **SPA/Client**: Uses inject() from the component context tree (established
|
|
7
|
+
* by cer-layout-view calling provide() for each plugin-provided value).
|
|
8
|
+
*
|
|
9
|
+
* - **SSR/SSG**: Reads from globalThis.__cerPluginProvides, populated when the
|
|
10
|
+
* server entry runs plugin.setup() before rendering.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // In a plugin (app/plugins/my-plugin.ts):
|
|
15
|
+
* export default {
|
|
16
|
+
* name: 'my-plugin',
|
|
17
|
+
* setup({ provide }) {
|
|
18
|
+
* provide('my-service', { greet: () => 'hello' })
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* // In a component:
|
|
23
|
+
* component('my-page', () => {
|
|
24
|
+
* const service = useInject<{ greet(): string }>('my-service')
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function useInject<T = unknown>(key: string, defaultValue?: T): T | undefined;
|
|
29
|
+
//# sourceMappingURL=use-inject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-inject.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/use-inject.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,SAAS,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,SAAS,CAgBnF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { inject } from '@jasonshimmy/custom-elements-runtime';
|
|
2
|
+
const _g = globalThis;
|
|
3
|
+
const _PROVIDES_KEY = '__cerPluginProvides';
|
|
4
|
+
/**
|
|
5
|
+
* useInject — reads a value provided by a plugin via plugin.setup()'s provide().
|
|
6
|
+
*
|
|
7
|
+
* Works consistently across all rendering modes:
|
|
8
|
+
*
|
|
9
|
+
* - **SPA/Client**: Uses inject() from the component context tree (established
|
|
10
|
+
* by cer-layout-view calling provide() for each plugin-provided value).
|
|
11
|
+
*
|
|
12
|
+
* - **SSR/SSG**: Reads from globalThis.__cerPluginProvides, populated when the
|
|
13
|
+
* server entry runs plugin.setup() before rendering.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // In a plugin (app/plugins/my-plugin.ts):
|
|
18
|
+
* export default {
|
|
19
|
+
* name: 'my-plugin',
|
|
20
|
+
* setup({ provide }) {
|
|
21
|
+
* provide('my-service', { greet: () => 'hello' })
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* // In a component:
|
|
26
|
+
* component('my-page', () => {
|
|
27
|
+
* const service = useInject<{ greet(): string }>('my-service')
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useInject(key, defaultValue) {
|
|
32
|
+
// Server-side (SSR/SSG): read from the global plugin provides map.
|
|
33
|
+
// __cerPluginProvides is populated by the server entry before the render pass.
|
|
34
|
+
if (typeof document === 'undefined') {
|
|
35
|
+
const pluginProvides = _g[_PROVIDES_KEY];
|
|
36
|
+
const value = pluginProvides?.get(key);
|
|
37
|
+
return value !== undefined ? value : defaultValue;
|
|
38
|
+
}
|
|
39
|
+
// Client-side: inject() walks the component context tree established by
|
|
40
|
+
// cer-layout-view's provide() calls. Falls back to __cerPluginProvides for
|
|
41
|
+
// reads before cer-layout-view mounts (e.g. during plugin-registered components).
|
|
42
|
+
const value = inject(key);
|
|
43
|
+
if (value !== undefined)
|
|
44
|
+
return value;
|
|
45
|
+
const pluginProvides = _g[_PROVIDES_KEY];
|
|
46
|
+
return pluginProvides?.get(key) ?? defaultValue;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=use-inject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-inject.js","sourceRoot":"","sources":["../../../src/runtime/composables/use-inject.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sCAAsC,CAAA;AAE7D,MAAM,EAAE,GAAG,UAAqC,CAAA;AAChD,MAAM,aAAa,GAAG,qBAAqB,CAAA;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,SAAS,CAAc,GAAW,EAAE,YAAgB;IAClE,mEAAmE;IACnE,+EAA+E;IAC/E,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,MAAM,cAAc,GAAG,EAAE,CAAC,aAAa,CAA0C,CAAA;QACjF,MAAM,KAAK,GAAG,cAAc,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;QACtC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAE,KAAW,CAAC,CAAC,CAAC,YAAY,CAAA;IAC1D,CAAC;IAED,wEAAwE;IACxE,2EAA2E;IAC3E,kFAAkF;IAClF,MAAM,KAAK,GAAG,MAAM,CAAI,GAAG,CAAC,CAAA;IAC5B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACrC,MAAM,cAAc,GAAG,EAAE,CAAC,aAAa,CAA0C,CAAA;IACjF,OAAQ,cAAc,EAAE,GAAG,CAAC,GAAG,CAAmB,IAAI,YAAY,CAAA;AACpE,CAAC"}
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* wires up the routing, and exports a handler compatible with
|
|
6
6
|
* Express/Fastify/Node http.
|
|
7
7
|
*/
|
|
8
|
-
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 { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'\nimport { registerEntityMap, 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 { createSSRHandler } from '@jasonshimmy/custom-elements-runtime/ssr-middleware'\n\nregisterBuiltinComponents()\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// 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 handler's full HTML document with the Vite client shell so the\n// final page 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/**\n * Per-request VNode factory \u2014 initializes a fresh router at the request URL,\n * resolves the active layout from the matched route's meta, pre-loads the\n * matched page component (bypassing the async router-view so DSD renders\n * synchronously), calls the route's data loader (if any), and injects the\n * serialized result into the document head as window.__CER_DATA__ for\n * client-side hydration.\n *\n * createStreamingSSRHandler threads the router through each component's SSR\n * context so concurrent renders never share state.\n */\nconst vnodeFactory = async (req) => {\n const router = initRouter({ routes, initialUrl: req.url ?? '/' })\n const current = router.getCurrent()\n const { route, params } = router.matchRoute(current.path)\n const layoutName = route?.meta?.layout ?? 'default'\n const layoutTag = layouts[layoutName]\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 // Make data available to usePageData() during the SSR render pass.\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 {\n // Non-fatal: loader errors fall back to an empty page; client will refetch.\n }\n }\n\n const vnode = layoutTag\n ? { tag: layoutTag, props: {}, children: [pageVnode] }\n : pageVnode\n\n return { vnode, router, head }\n}\n\n// Capture the raw SSR handler and wrap it to merge the response with the\n// Vite client template before sending \u2014 this injects the JS/CSS asset bundles\n// so the browser can hydrate and enable client-side routing.\nconst _rawHandler = createSSRHandler(vnodeFactory, {\n render: { dsd: true, dsdPolyfill: false },\n})\n\n/**\n * The main request handler.\n * Compatible with Express, Fastify, and Node's raw http.createServer.\n *\n * Each request is run inside a fresh _cerDataStore.run() context so that\n * concurrent renders (e.g. SSG with concurrency > 1) get isolated stores.\n * vnodeFactory calls _cerDataStore.enterWith(loaderData) from within this\n * context, making the data visible to usePageData() during SSR rendering\n * without any global-state races.\n */\nexport const handler = async (req, res) => {\n if (!_clientTemplate) {\n // No client template \u2014 run handler normally, then inject DSD polyfill.\n let _html = ''\n await _cerDataStore.run(null, async () => {\n await _rawHandler(req, { setHeader: () => {}, end: (body) => { _html = body } })\n })\n // Inject DSD polyfill at end of <body>, outside any custom element light DOM.\n const _final = _html.includes('</body>')\n ? _html.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')\n : _html + DSD_POLYFILL_SCRIPT\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n return res.end(_final)\n }\n let _capturedHtml = ''\n // Wrap _rawHandler in an isolated async-local-storage context so that\n // vnodeFactory's enterWith() call is scoped to this request only.\n await _cerDataStore.run(null, async () => {\n // Omit write() to force the non-streaming collect-then-end code path.\n await _rawHandler(req, { setHeader: () => {}, end: (body) => { _capturedHtml = body } })\n })\n let _merged = _mergeWithClientTemplate(_capturedHtml, _clientTemplate)\n // Inject DSD polyfill at end of <body>, outside <cer-layout-view> light DOM.\n _merged = _merged.includes('</body>')\n ? _merged.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')\n : _merged + DSD_POLYFILL_SCRIPT\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(_merged)\n}\n\nexport { apiRoutes, plugins, layouts, routes }\nexport default handler\n";
|
|
8
|
+
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 { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'\nimport { registerEntityMap, 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 { createSSRHandler } from '@jasonshimmy/custom-elements-runtime/ssr-middleware'\n\nregisterBuiltinComponents()\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 render pass. Stored on globalThis so all\n// dynamically-imported page chunks share the same reference (same pattern as\n// __CER_HEAD_COLLECTOR__ and __CER_DATA_STORE__).\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 handler's full HTML document with the Vite client shell so the\n// final page 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/**\n * Per-request VNode factory \u2014 initializes a fresh router at the request URL,\n * resolves the active layout from the matched route's meta, pre-loads the\n * matched page component (bypassing the async router-view so DSD renders\n * synchronously), calls the route's data loader (if any), and injects the\n * serialized result into the document head as window.__CER_DATA__ for\n * client-side hydration.\n *\n * createStreamingSSRHandler threads the router through each component's SSR\n * context so concurrent renders never share state.\n */\nconst vnodeFactory = 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 const layoutName = route?.meta?.layout ?? 'default'\n const layoutTag = layouts[layoutName]\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 // Make data available to usePageData() during the SSR render pass.\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 {\n // Non-fatal: loader errors fall back to an empty page; client will refetch.\n }\n }\n\n const vnode = layoutTag\n ? { tag: layoutTag, props: {}, children: [pageVnode] }\n : pageVnode\n\n return { vnode, router, head }\n}\n\n// Capture the raw SSR handler and wrap it to merge the response with the\n// Vite client template before sending \u2014 this injects the JS/CSS asset bundles\n// so the browser can hydrate and enable client-side routing.\nconst _rawHandler = createSSRHandler(vnodeFactory, {\n render: { dsd: true, dsdPolyfill: false },\n})\n\n/**\n * The main request handler.\n * Compatible with Express, Fastify, and Node's raw http.createServer.\n *\n * Each request is run inside a fresh _cerDataStore.run() context so that\n * concurrent renders (e.g. SSG with concurrency > 1) get isolated stores.\n * vnodeFactory calls _cerDataStore.enterWith(loaderData) from within this\n * context, making the data visible to usePageData() during SSR rendering\n * without any global-state races.\n */\nexport const handler = async (req, res) => {\n if (!_clientTemplate) {\n // No client template \u2014 run handler normally, then inject DSD polyfill.\n let _html = ''\n await _cerDataStore.run(null, async () => {\n await _rawHandler(req, { setHeader: () => {}, end: (body) => { _html = body } })\n })\n // Inject DSD polyfill at end of <body>, outside any custom element light DOM.\n const _final = _html.includes('</body>')\n ? _html.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')\n : _html + DSD_POLYFILL_SCRIPT\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n return res.end(_final)\n }\n let _capturedHtml = ''\n // Wrap _rawHandler in an isolated async-local-storage context so that\n // vnodeFactory's enterWith() call is scoped to this request only.\n await _cerDataStore.run(null, async () => {\n // Omit write() to force the non-streaming collect-then-end code path.\n await _rawHandler(req, { setHeader: () => {}, end: (body) => { _capturedHtml = body } })\n })\n let _merged = _mergeWithClientTemplate(_capturedHtml, _clientTemplate)\n // Inject DSD polyfill at end of <body>, outside <cer-layout-view> light DOM.\n _merged = _merged.includes('</body>')\n ? _merged.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')\n : _merged + DSD_POLYFILL_SCRIPT\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(_merged)\n}\n\nexport { apiRoutes, plugins, layouts, routes }\nexport default handler\n";
|
|
9
9
|
//# 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;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"entry-server-template.d.ts","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,m6TAsNjC,CAAA"}
|
|
@@ -28,6 +28,25 @@ registerBuiltinComponents()
|
|
|
28
28
|
// minimal set (<, >, & …) and re-escapes everything else.
|
|
29
29
|
registerEntityMap(entitiesJson)
|
|
30
30
|
|
|
31
|
+
// Run plugins once at server startup so their provide() values are available
|
|
32
|
+
// to useInject() during every SSR render pass. Stored on globalThis so all
|
|
33
|
+
// dynamically-imported page chunks share the same reference (same pattern as
|
|
34
|
+
// __CER_HEAD_COLLECTOR__ and __CER_DATA_STORE__).
|
|
35
|
+
const _pluginProvides = new Map()
|
|
36
|
+
;(globalThis).__cerPluginProvides = _pluginProvides
|
|
37
|
+
const _pluginsReady = (async () => {
|
|
38
|
+
const _bootstrapRouter = initRouter({ routes })
|
|
39
|
+
for (const plugin of plugins) {
|
|
40
|
+
if (plugin && typeof plugin.setup === 'function') {
|
|
41
|
+
await plugin.setup({
|
|
42
|
+
router: _bootstrapRouter,
|
|
43
|
+
provide: (key, value) => _pluginProvides.set(key, value),
|
|
44
|
+
config: {},
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})()
|
|
49
|
+
|
|
31
50
|
// Async-local storage for request-scoped SSR loader data.
|
|
32
51
|
// Using AsyncLocalStorage ensures concurrent SSR renders (e.g. SSG with
|
|
33
52
|
// concurrency > 1) never see each other's data — each request's async chain
|
|
@@ -109,6 +128,7 @@ function _mergeWithClientTemplate(ssrHtml, clientTemplate) {
|
|
|
109
128
|
* context so concurrent renders never share state.
|
|
110
129
|
*/
|
|
111
130
|
const vnodeFactory = async (req) => {
|
|
131
|
+
await _pluginsReady
|
|
112
132
|
const router = initRouter({ routes, initialUrl: req.url ?? '/' })
|
|
113
133
|
const current = router.getCurrent()
|
|
114
134
|
const { route, params } = router.matchRoute(current.path)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-server-template.js","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;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;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsNpC,CAAA"}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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,SAAS;IACxB,GAAG,CAAC,EAAE,OAAO,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,SAAS;IACxB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd;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,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;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":"AAmCA,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/docs/cli.md
CHANGED
|
@@ -187,7 +187,7 @@ my-app/
|
|
|
187
187
|
| Mode | `cer.config.ts` | `package.json` scripts |
|
|
188
188
|
|---|---|---|
|
|
189
189
|
| SPA | `mode: 'spa'` | `dev`, `build`, `preview` |
|
|
190
|
-
| SSR | `mode: 'ssr'`, `ssr.
|
|
190
|
+
| SSR | `mode: 'ssr'`, `ssr.dsd: true` | `dev`, `build`, `preview --ssr` |
|
|
191
191
|
| SSG | `mode: 'ssg'`, `ssg.routes: 'auto'` | `dev`, `build`, `preview`, `generate` |
|
|
192
192
|
|
|
193
193
|
---
|
package/docs/composables.md
CHANGED
|
@@ -104,3 +104,40 @@ export function useSession() { return session }
|
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
Use `useOnConnected` or lazy initialization inside the function body for side effects.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Built-in framework composables
|
|
111
|
+
|
|
112
|
+
These composables are provided by the framework and auto-imported alongside the runtime. They do **not** live in `app/composables/` — they are injected from `@jasonshimmy/vite-plugin-cer-app/composables`.
|
|
113
|
+
|
|
114
|
+
### `useHead(input)`
|
|
115
|
+
|
|
116
|
+
Sets document head tags (`<title>`, `<meta>`, `<link>`, etc.). Works in SPA, SSR, and SSG modes. See [head-management.md](./head-management.md).
|
|
117
|
+
|
|
118
|
+
### `usePageData<T>()`
|
|
119
|
+
|
|
120
|
+
Returns the serialized loader data for the current page, hydrated from `window.__CER_DATA__` on the client or from the per-request `AsyncLocalStorage` context during SSR/SSG. See [data-loading.md](./data-loading.md).
|
|
121
|
+
|
|
122
|
+
### `useInject<T>(key, defaultValue?)`
|
|
123
|
+
|
|
124
|
+
Reads a value provided by a plugin via `app.provide(key, value)`. Works consistently in all rendering modes:
|
|
125
|
+
|
|
126
|
+
- **SPA / client** — resolves via `inject()` from the component context tree.
|
|
127
|
+
- **SSR / SSG** — reads from `globalThis.__cerPluginProvides`, populated by the server entry before the first render.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// app/pages/dashboard.ts
|
|
131
|
+
component('page-dashboard', () => {
|
|
132
|
+
const store = useInject<Store>('store')
|
|
133
|
+
return html`<p>Count: ${store?.state.count ?? 0}</p>`
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If you need it outside auto-imported directories, import explicitly:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { useInject } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
141
|
+
```
|
|
142
|
+
|
|
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.
|
package/docs/configuration.md
CHANGED
|
@@ -13,7 +13,6 @@ export default defineConfig({
|
|
|
13
13
|
|
|
14
14
|
ssr: {
|
|
15
15
|
dsd: true,
|
|
16
|
-
streaming: false,
|
|
17
16
|
},
|
|
18
17
|
|
|
19
18
|
ssg: {
|
|
@@ -87,8 +86,7 @@ Controls SSR rendering behavior.
|
|
|
87
86
|
|
|
88
87
|
```ts
|
|
89
88
|
ssr: {
|
|
90
|
-
dsd: true,
|
|
91
|
-
streaming: false // Reserved for future use
|
|
89
|
+
dsd: true, // Emit Declarative Shadow DOM
|
|
92
90
|
}
|
|
93
91
|
```
|
|
94
92
|
|
|
@@ -99,13 +97,6 @@ ssr: {
|
|
|
99
97
|
|
|
100
98
|
When `true`, renders components with [Declarative Shadow DOM](https://developer.chrome.com/docs/css-ui/declarative-shadow-dom) markup. This eliminates Flash of Unstyled Content (FOUC) because styles are embedded directly in the HTML.
|
|
101
99
|
|
|
102
|
-
### `ssr.streaming`
|
|
103
|
-
|
|
104
|
-
**Type:** `boolean`
|
|
105
|
-
**Default:** `false`
|
|
106
|
-
|
|
107
|
-
Reserved for future use. Currently, the SSR renderer always collects the full HTML string before sending the response.
|
|
108
|
-
|
|
109
100
|
---
|
|
110
101
|
|
|
111
102
|
## `ssg` options
|
|
@@ -247,7 +238,7 @@ export default defineConfig({
|
|
|
247
238
|
plugins: [
|
|
248
239
|
cerApp({
|
|
249
240
|
mode: 'ssr',
|
|
250
|
-
ssr: { dsd: true
|
|
241
|
+
ssr: { dsd: true },
|
|
251
242
|
}),
|
|
252
243
|
],
|
|
253
244
|
})
|
package/docs/getting-started.md
CHANGED
|
@@ -128,110 +128,12 @@ component('layout-default', () => {
|
|
|
128
128
|
</head>
|
|
129
129
|
<body>
|
|
130
130
|
<cer-layout-view></cer-layout-view>
|
|
131
|
-
<script type="module" src="/app
|
|
131
|
+
<script type="module" src="/.cer/app.ts"></script>
|
|
132
132
|
</body>
|
|
133
133
|
</html>
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
The framework generates this file when you scaffold a new project. It bootstraps the router, registers all auto-discovered components, runs plugins, and mounts the app:
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
// app/app.ts
|
|
142
|
-
import '@jasonshimmy/custom-elements-runtime/css'
|
|
143
|
-
import 'virtual:cer-jit-css'
|
|
144
|
-
import 'virtual:cer-components'
|
|
145
|
-
import routes from 'virtual:cer-routes'
|
|
146
|
-
import layouts from 'virtual:cer-layouts'
|
|
147
|
-
import plugins from 'virtual:cer-plugins'
|
|
148
|
-
import { hasLoading, loadingTag } from 'virtual:cer-loading'
|
|
149
|
-
import { hasError, errorTag } from 'virtual:cer-error'
|
|
150
|
-
import {
|
|
151
|
-
component, ref, provide,
|
|
152
|
-
useOnConnected, useOnDisconnected,
|
|
153
|
-
registerBuiltinComponents,
|
|
154
|
-
} from '@jasonshimmy/custom-elements-runtime'
|
|
155
|
-
import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
|
|
156
|
-
import { enableJITCSS } from '@jasonshimmy/custom-elements-runtime/jit-css'
|
|
157
|
-
|
|
158
|
-
registerBuiltinComponents()
|
|
159
|
-
enableJITCSS()
|
|
160
|
-
|
|
161
|
-
const router = initRouter({ routes })
|
|
162
|
-
|
|
163
|
-
const isNavigating = ref(false)
|
|
164
|
-
const currentError = ref(null)
|
|
165
|
-
;(globalThis as any).resetError = () => {
|
|
166
|
-
currentError.value = null
|
|
167
|
-
router.replace(router.getCurrent().path)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const _push = router.push.bind(router)
|
|
171
|
-
const _replace = router.replace.bind(router)
|
|
172
|
-
router.push = async (path) => {
|
|
173
|
-
isNavigating.value = true; currentError.value = null
|
|
174
|
-
try { await _push(path) } catch (err) { currentError.value = err instanceof Error ? err.message : String(err) } finally { isNavigating.value = false }
|
|
175
|
-
}
|
|
176
|
-
router.replace = async (path) => {
|
|
177
|
-
isNavigating.value = true; currentError.value = null
|
|
178
|
-
try { await _replace(path) } catch (err) { currentError.value = err instanceof Error ? err.message : String(err) } finally { isNavigating.value = false }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// _pluginProvides is populated by plugin setup and forwarded into the component
|
|
182
|
-
// context tree via provide() inside cer-layout-view so inject() works in all modes.
|
|
183
|
-
// Also exposed on globalThis for the SSG timing edge case — see docs/plugins.md.
|
|
184
|
-
const _pluginProvides = new Map<string, unknown>()
|
|
185
|
-
;(globalThis as any).__cerPluginProvides = _pluginProvides
|
|
186
|
-
|
|
187
|
-
component('cer-layout-view', () => {
|
|
188
|
-
for (const [key, value] of _pluginProvides) { provide(key, value) }
|
|
189
|
-
|
|
190
|
-
const current = ref(router.getCurrent())
|
|
191
|
-
let unsub: (() => void) | undefined
|
|
192
|
-
useOnConnected(() => { unsub = router.subscribe((s) => { current.value = s }) })
|
|
193
|
-
useOnDisconnected(() => { unsub?.(); unsub = undefined })
|
|
194
|
-
|
|
195
|
-
if (currentError.value !== null) {
|
|
196
|
-
if (hasError && errorTag) return { tag: errorTag, props: { attrs: { error: String(currentError.value) } }, children: [] }
|
|
197
|
-
return { tag: 'div', props: { attrs: { style: 'padding:2rem;font-family:monospace' } }, children: String(currentError.value) }
|
|
198
|
-
}
|
|
199
|
-
if (isNavigating.value && hasLoading && loadingTag) return { tag: loadingTag, props: {}, children: [] }
|
|
200
|
-
|
|
201
|
-
const matched = router.matchRoute(current.value.path)
|
|
202
|
-
const layoutName = (matched?.route as any)?.meta?.layout ?? 'default'
|
|
203
|
-
const layoutTag = (layouts as Record<string, string>)[layoutName]
|
|
204
|
-
const routerView = { tag: 'router-view', props: {}, children: [] }
|
|
205
|
-
return layoutTag ? { tag: layoutTag, props: {}, children: [routerView] } : routerView
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
// Plugins run AFTER cer-layout-view is defined so provide() calls from plugins
|
|
209
|
-
// are forwarded into the component tree on the very first render.
|
|
210
|
-
for (const plugin of plugins ?? []) {
|
|
211
|
-
if (plugin && typeof plugin.setup === 'function') {
|
|
212
|
-
await plugin.setup({ router, provide: (key, value) => { _pluginProvides.set(key, value) }, config: {} })
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Pre-load the current page's route chunk AFTER plugins run.
|
|
217
|
-
// This ensures cer-layout-view's first render (and its provide() calls) completes
|
|
218
|
-
// before page component modules are imported and their renders are scheduled.
|
|
219
|
-
if (typeof window !== 'undefined') {
|
|
220
|
-
const _initMatch = router.matchRoute(window.location.pathname)
|
|
221
|
-
if (_initMatch?.route?.load) {
|
|
222
|
-
try { await _initMatch.route.load() } catch { /* non-fatal */ }
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (typeof window !== 'undefined') {
|
|
227
|
-
await _replace(window.location.pathname + window.location.search + window.location.hash)
|
|
228
|
-
delete (globalThis as any).__CER_DATA__
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export { router }
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
> **Note:** Do not move the plugin loop before `component('cer-layout-view', …)`. The layout component must be defined first so that when plugins call `app.provide()`, the values are available to the component tree from the very first render. See [Plugins](plugins.md) for details.
|
|
136
|
+
> **Note:** The framework bootstrap lives in `.cer/app.ts` and is regenerated automatically on every dev server start and build. You never edit or own this file — updates to the plugin propagate to it immediately, just like Nuxt's `.nuxt/` directory.
|
|
235
137
|
|
|
236
138
|
---
|
|
237
139
|
|
package/docs/plugins.md
CHANGED
|
@@ -31,9 +31,9 @@ interface AppPlugin {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
interface AppContext {
|
|
34
|
-
provide(key:
|
|
34
|
+
provide(key: PropertyKey, value: unknown): void
|
|
35
35
|
router: Router
|
|
36
|
-
config:
|
|
36
|
+
config: CerAppConfig
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -75,8 +75,8 @@ export default {
|
|
|
75
75
|
```ts
|
|
76
76
|
// app/pages/index.ts
|
|
77
77
|
component('page-index', () => {
|
|
78
|
-
const store =
|
|
79
|
-
const count = computed(() => store
|
|
78
|
+
const store = useInject<Store>('store')
|
|
79
|
+
const count = computed(() => store?.state.count ?? 0)
|
|
80
80
|
|
|
81
81
|
return html`<p>Count: ${count}</p>`
|
|
82
82
|
})
|
|
@@ -120,20 +120,14 @@ export default {
|
|
|
120
120
|
|
|
121
121
|
---
|
|
122
122
|
|
|
123
|
-
##
|
|
123
|
+
## Reading provided values with `useInject`
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
To write pages that work correctly in **all three modes** (SPA, SSR, SSG), use `globalThis.__cerPluginProvides` as a synchronous fallback when `inject()` returns `undefined`:
|
|
125
|
+
To read values provided by a plugin, use `useInject` instead of the raw `inject()` from the runtime. `useInject` works correctly in **all three modes** — SPA, SSR, and SSG:
|
|
128
126
|
|
|
129
127
|
```ts
|
|
130
128
|
// app/pages/dashboard.ts
|
|
131
129
|
component('page-dashboard', () => {
|
|
132
|
-
|
|
133
|
-
// In SSG the page chunk may render before cer-layout-view calls provide(),
|
|
134
|
-
// so fall back to the global map that app/app.ts populates before any render.
|
|
135
|
-
const pluginProvides = (globalThis as any).__cerPluginProvides as Map<string, unknown> | undefined
|
|
136
|
-
const store = inject<Store>('store') ?? pluginProvides?.get('store') as Store | undefined
|
|
130
|
+
const store = useInject<Store>('store')
|
|
137
131
|
|
|
138
132
|
if (!store) return html`<p>Loading…</p>`
|
|
139
133
|
|
|
@@ -141,7 +135,21 @@ component('page-dashboard', () => {
|
|
|
141
135
|
})
|
|
142
136
|
```
|
|
143
137
|
|
|
144
|
-
|
|
138
|
+
`useInject` is auto-imported in `app/pages/`, `app/layouts/`, and `app/components/` — no explicit import needed.
|
|
139
|
+
|
|
140
|
+
**Why not raw `inject()`?** In SSR and SSG modes the server renders the component tree directly, without `<cer-layout-view>` establishing the Vue-style provide context. `useInject` bridges this gap:
|
|
141
|
+
|
|
142
|
+
| Mode | How the value is resolved |
|
|
143
|
+
|------|--------------------------|
|
|
144
|
+
| SPA / client | `inject()` walks the component tree (provided by `<cer-layout-view>`) |
|
|
145
|
+
| SSR (dev & prod) | reads from `globalThis.__cerPluginProvides` (set by the server entry at startup) |
|
|
146
|
+
| SSG | same as SSR |
|
|
147
|
+
|
|
148
|
+
If you need `useInject` outside of auto-imported directories, import it explicitly:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { useInject } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
152
|
+
```
|
|
145
153
|
|
|
146
154
|
---
|
|
147
155
|
|
|
@@ -154,7 +162,7 @@ import plugins from 'virtual:cer-plugins'
|
|
|
154
162
|
// plugins is an array of AppPlugin objects in load order
|
|
155
163
|
```
|
|
156
164
|
|
|
157
|
-
In
|
|
165
|
+
In `.cer/app.ts` (the auto-generated framework entry), plugins are executed sequentially before the router initializes:
|
|
158
166
|
|
|
159
167
|
```ts
|
|
160
168
|
for (const plugin of plugins) {
|