@jasonshimmy/vite-plugin-cer-app 0.3.0 → 0.4.1
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/commits.txt +1 -1
- package/dist/cli/create/index.js +1 -1
- package/dist/cli/create/templates/spa/index.html.tpl +1 -1
- package/dist/cli/create/templates/ssg/index.html.tpl +1 -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/dts-generator.js +1 -1
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/generated-dir.d.ts +1 -1
- package/dist/plugin/generated-dir.js +2 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +21 -0
- 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 +1 -1
- package/dist/runtime/app-template.d.ts.map +1 -1
- package/dist/runtime/app-template.js +10 -10
- 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/docs/composables.md +37 -0
- package/docs/plugins.md +23 -15
- package/docs/rendering-modes.md +1 -1
- package/docs/testing.md +3 -3
- package/e2e/cypress/e2e/interactive.cy.ts +15 -0
- 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 +55 -2
- 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 +2 -2
- 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/cli/create/index.ts +1 -1
- package/src/cli/create/templates/spa/index.html.tpl +1 -1
- package/src/cli/create/templates/ssg/index.html.tpl +1 -1
- package/src/cli/create/templates/ssr/index.html.tpl +1 -1
- package/src/plugin/build-ssr.ts +18 -0
- package/src/plugin/dts-generator.ts +1 -1
- package/src/plugin/generated-dir.ts +2 -2
- package/src/plugin/index.ts +22 -0
- package/src/plugin/transforms/auto-import.ts +2 -2
- package/src/runtime/app-template.ts +10 -10
- 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/plugin/build-ssr.ts
CHANGED
|
@@ -50,6 +50,23 @@ registerBuiltinComponents()
|
|
|
50
50
|
// minimal set (<, >, & …) and re-escapes everything else.
|
|
51
51
|
registerEntityMap(entitiesJson)
|
|
52
52
|
|
|
53
|
+
// Run plugins once at server startup so their provide() values are available
|
|
54
|
+
// to useInject() during every SSR/SSG render pass.
|
|
55
|
+
const _pluginProvides = new Map()
|
|
56
|
+
;(globalThis).__cerPluginProvides = _pluginProvides
|
|
57
|
+
const _pluginsReady = (async () => {
|
|
58
|
+
const _bootstrapRouter = initRouter({ routes })
|
|
59
|
+
for (const plugin of plugins) {
|
|
60
|
+
if (plugin && typeof plugin.setup === 'function') {
|
|
61
|
+
await plugin.setup({
|
|
62
|
+
router: _bootstrapRouter,
|
|
63
|
+
provide: (key, value) => _pluginProvides.set(key, value),
|
|
64
|
+
config: {},
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})()
|
|
69
|
+
|
|
53
70
|
// Load the Vite-built client index.html (dist/client/index.html) so every SSR
|
|
54
71
|
// response includes the client-side scripts needed for hydration and routing.
|
|
55
72
|
// The server bundle lives at dist/server/server.js, so ../client resolves correctly.
|
|
@@ -119,6 +136,7 @@ function _mergeWithClientTemplate(ssrHtml, clientTemplate) {
|
|
|
119
136
|
// synchronously right before renderToStringWithJITCSS — guaranteeing that
|
|
120
137
|
// concurrent renders (SSG concurrency > 1) never race on a shared global.
|
|
121
138
|
const _prepareRequest = async (req) => {
|
|
139
|
+
await _pluginsReady
|
|
122
140
|
const router = initRouter({ routes, initialUrl: req.url ?? '/' })
|
|
123
141
|
const current = router.getCurrent()
|
|
124
142
|
const { route, params } = router.matchRoute(current.path)
|
|
@@ -76,7 +76,7 @@ const RUNTIME_GLOBALS = [
|
|
|
76
76
|
|
|
77
77
|
const DIRECTIVE_GLOBALS = ['when', 'each', 'match', 'anchorBlock']
|
|
78
78
|
|
|
79
|
-
const FRAMEWORK_GLOBALS = ['useHead', 'usePageData']
|
|
79
|
+
const FRAMEWORK_GLOBALS = ['useHead', 'usePageData', 'useInject']
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Scans a composables directory and returns a map of export name → file path.
|
|
@@ -26,7 +26,7 @@ export function resolveHtmlEntry(config: ResolvedCerConfig): string {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Generates the content for the default `.cer/index.html`.
|
|
29
|
-
* Always points to the
|
|
29
|
+
* Always points to the virtual `/@cer/app.ts` entry.
|
|
30
30
|
*/
|
|
31
31
|
export function generateDefaultHtml(): string {
|
|
32
32
|
return `<!DOCTYPE html>
|
|
@@ -38,7 +38,7 @@ export function generateDefaultHtml(): string {
|
|
|
38
38
|
</head>
|
|
39
39
|
<body>
|
|
40
40
|
<cer-layout-view></cer-layout-view>
|
|
41
|
-
<script type="module" src="
|
|
41
|
+
<script type="module" src="/@cer/app.ts"></script>
|
|
42
42
|
</body>
|
|
43
43
|
</html>
|
|
44
44
|
`
|
package/src/plugin/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { autoImportTransform } from './transforms/auto-import.js'
|
|
|
8
8
|
import { scanComposableExports, writeAutoImportDts, writeTsconfigPaths } from './dts-generator.js'
|
|
9
9
|
import { configureCerDevServer } from './dev-server.js'
|
|
10
10
|
import { writeGeneratedDir, getGeneratedDir } from './generated-dir.js'
|
|
11
|
+
import { APP_ENTRY_TEMPLATE } from '../runtime/app-template.js'
|
|
11
12
|
import { generateRoutesCode } from './virtual/routes.js'
|
|
12
13
|
import { generateLayoutsCode } from './virtual/layouts.js'
|
|
13
14
|
import { generateComponentsCode } from './virtual/components.js'
|
|
@@ -40,6 +41,14 @@ const RESOLVED_IDS = Object.fromEntries(
|
|
|
40
41
|
Object.entries(VIRTUAL_IDS).map(([k, v]) => [k, `\0${v}`]),
|
|
41
42
|
) as Record<keyof typeof VIRTUAL_IDS, string>
|
|
42
43
|
|
|
44
|
+
// The app entry is served via a virtual module at /@cer/app.ts.
|
|
45
|
+
// Using /@ avoids Vite's dot-directory fs security restriction that blocks /.cer/
|
|
46
|
+
// from being served through the transform middleware. The physical .cer/app.ts
|
|
47
|
+
// is still written to disk for IDE/TypeScript support, but the browser fetches
|
|
48
|
+
// /@cer/app.ts which resolves to this virtual module.
|
|
49
|
+
const APP_ENTRY_URL = '/@cer/app.ts'
|
|
50
|
+
const RESOLVED_APP_ENTRY = '\0cer-app-entry'
|
|
51
|
+
|
|
43
52
|
/**
|
|
44
53
|
* Fills in default values for all config fields and resolves absolute paths.
|
|
45
54
|
*/
|
|
@@ -211,15 +220,28 @@ export function cerApp(userConfig: CerAppConfig = {}): Plugin[] {
|
|
|
211
220
|
configResolved(resolvedConfig) {
|
|
212
221
|
// Re-resolve with the final root
|
|
213
222
|
config = resolveConfig(userConfig, resolvedConfig.root)
|
|
223
|
+
// Write .cer/ immediately after config is resolved so the physical
|
|
224
|
+
// app.ts exists for IDE/TypeScript support before any Vite hooks fire.
|
|
225
|
+
writeGeneratedDir(config)
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
transformIndexHtml(html: string) {
|
|
229
|
+
// Rewrite any existing /.cer/app.ts src reference (older projects or
|
|
230
|
+
// the scaffold template) to /@cer/app.ts so Vite's transform middleware
|
|
231
|
+
// processes it. Vite blocks /.* paths from the transform pipeline.
|
|
232
|
+
return html.replace(/src=["']\/\.cer\/app\.ts["']/g, 'src="/@cer/app.ts"')
|
|
214
233
|
},
|
|
215
234
|
|
|
216
235
|
resolveId(id: string) {
|
|
236
|
+
if (id === APP_ENTRY_URL) return RESOLVED_APP_ENTRY
|
|
217
237
|
if ((Object.values(VIRTUAL_IDS) as string[]).includes(id)) {
|
|
218
238
|
return `\0${id}`
|
|
219
239
|
}
|
|
220
240
|
},
|
|
221
241
|
|
|
222
242
|
async load(id: string) {
|
|
243
|
+
if (id === RESOLVED_APP_ENTRY) return APP_ENTRY_TEMPLATE
|
|
244
|
+
|
|
223
245
|
const allResolved = Object.values(RESOLVED_IDS) as string[]
|
|
224
246
|
if (!allResolved.includes(id)) return null
|
|
225
247
|
|
|
@@ -11,9 +11,9 @@ const RUNTIME_IMPORTS = `import { component, html, css, ref, computed, watch, wa
|
|
|
11
11
|
|
|
12
12
|
const DIRECTIVE_IMPORTS = `import { when, each, match, anchorBlock } from '@jasonshimmy/custom-elements-runtime/directives';`
|
|
13
13
|
|
|
14
|
-
const FRAMEWORK_IMPORTS = `import { useHead, usePageData } from '@jasonshimmy/vite-plugin-cer-app/composables';`
|
|
14
|
+
const FRAMEWORK_IMPORTS = `import { useHead, usePageData, useInject } from '@jasonshimmy/vite-plugin-cer-app/composables';`
|
|
15
15
|
|
|
16
|
-
const FRAMEWORK_IDENTIFIERS = ['useHead', 'usePageData']
|
|
16
|
+
const FRAMEWORK_IDENTIFIERS = ['useHead', 'usePageData', 'useInject']
|
|
17
17
|
|
|
18
18
|
const RUNTIME_IDENTIFIERS = [
|
|
19
19
|
'component',
|
|
@@ -37,11 +37,11 @@ const router = initRouter({ routes })
|
|
|
37
37
|
const isNavigating = ref(false)
|
|
38
38
|
const currentError = ref(null)
|
|
39
39
|
|
|
40
|
-
const resetError = ()
|
|
40
|
+
const resetError = () => {
|
|
41
41
|
currentError.value = null
|
|
42
42
|
void router.replace(router.getCurrent().path)
|
|
43
43
|
}
|
|
44
|
-
;(globalThis
|
|
44
|
+
;(globalThis).resetError = resetError
|
|
45
45
|
|
|
46
46
|
const _push = router.push.bind(router)
|
|
47
47
|
const _replace = router.replace.bind(router)
|
|
@@ -77,10 +77,10 @@ router.replace = async (path) => {
|
|
|
77
77
|
// Declared BEFORE component('cer-layout-view') to avoid a temporal dead zone
|
|
78
78
|
// ReferenceError: customElements.define() upgrades existing DOM elements
|
|
79
79
|
// synchronously, calling the render function immediately.
|
|
80
|
-
const _pluginProvides = new Map
|
|
80
|
+
const _pluginProvides = new Map()
|
|
81
81
|
// Expose plugin provides globally so page components can read them synchronously
|
|
82
82
|
// regardless of render order.
|
|
83
|
-
;(globalThis
|
|
83
|
+
;(globalThis).__cerPluginProvides = _pluginProvides
|
|
84
84
|
|
|
85
85
|
// ─── <cer-layout-view> ───────────────────────────────────────────────────────
|
|
86
86
|
|
|
@@ -92,10 +92,10 @@ component('cer-layout-view', () => {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const current = ref(router.getCurrent())
|
|
95
|
-
let unsub
|
|
95
|
+
let unsub
|
|
96
96
|
|
|
97
97
|
useOnConnected(() => {
|
|
98
|
-
unsub = router.subscribe((s
|
|
98
|
+
unsub = router.subscribe((s) => { current.value = s })
|
|
99
99
|
})
|
|
100
100
|
useOnDisconnected(() => { unsub?.(); unsub = undefined })
|
|
101
101
|
|
|
@@ -111,9 +111,9 @@ component('cer-layout-view', () => {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const matched = router.matchRoute(current.value.path)
|
|
114
|
-
const routeMeta = matched?.route?.meta
|
|
114
|
+
const routeMeta = matched?.route?.meta
|
|
115
115
|
const layoutName = routeMeta?.layout ?? 'default'
|
|
116
|
-
const layoutTag =
|
|
116
|
+
const layoutTag = layouts[layoutName]
|
|
117
117
|
const routerView = { tag: 'router-view', props: {}, children: [] }
|
|
118
118
|
|
|
119
119
|
if (layoutTag) return { tag: layoutTag, props: {}, children: [routerView] }
|
|
@@ -124,7 +124,7 @@ for (const plugin of plugins) {
|
|
|
124
124
|
if (plugin && typeof plugin.setup === 'function') {
|
|
125
125
|
await plugin.setup({
|
|
126
126
|
router,
|
|
127
|
-
provide: (key
|
|
127
|
+
provide: (key, value) => { _pluginProvides.set(key, value) },
|
|
128
128
|
config: {},
|
|
129
129
|
})
|
|
130
130
|
}
|
|
@@ -151,7 +151,7 @@ if (typeof window !== 'undefined') {
|
|
|
151
151
|
await _replace(window.location.pathname + window.location.search + window.location.hash)
|
|
152
152
|
// Clear SSR loader data after initial navigation so subsequent client-side
|
|
153
153
|
// navigations don't accidentally reuse stale server data.
|
|
154
|
-
delete (globalThis
|
|
154
|
+
delete (globalThis).__CER_DATA__
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
export { router }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { inject } from '@jasonshimmy/custom-elements-runtime'
|
|
2
|
+
|
|
3
|
+
const _g = globalThis as Record<string, unknown>
|
|
4
|
+
const _PROVIDES_KEY = '__cerPluginProvides'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* useInject — reads a value provided by a plugin via plugin.setup()'s provide().
|
|
8
|
+
*
|
|
9
|
+
* Works consistently across all rendering modes:
|
|
10
|
+
*
|
|
11
|
+
* - **SPA/Client**: Uses inject() from the component context tree (established
|
|
12
|
+
* by cer-layout-view calling provide() for each plugin-provided value).
|
|
13
|
+
*
|
|
14
|
+
* - **SSR/SSG**: Reads from globalThis.__cerPluginProvides, populated when the
|
|
15
|
+
* server entry runs plugin.setup() before rendering.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* // In a plugin (app/plugins/my-plugin.ts):
|
|
20
|
+
* export default {
|
|
21
|
+
* name: 'my-plugin',
|
|
22
|
+
* setup({ provide }) {
|
|
23
|
+
* provide('my-service', { greet: () => 'hello' })
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // In a component:
|
|
28
|
+
* component('my-page', () => {
|
|
29
|
+
* const service = useInject<{ greet(): string }>('my-service')
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useInject<T = unknown>(key: string, defaultValue?: T): T | undefined {
|
|
34
|
+
// Server-side (SSR/SSG): read from the global plugin provides map.
|
|
35
|
+
// __cerPluginProvides is populated by the server entry before the render pass.
|
|
36
|
+
if (typeof document === 'undefined') {
|
|
37
|
+
const pluginProvides = _g[_PROVIDES_KEY] as Map<PropertyKey, unknown> | undefined
|
|
38
|
+
const value = pluginProvides?.get(key)
|
|
39
|
+
return value !== undefined ? (value as T) : defaultValue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Client-side: inject() walks the component context tree established by
|
|
43
|
+
// cer-layout-view's provide() calls. Falls back to __cerPluginProvides for
|
|
44
|
+
// reads before cer-layout-view mounts (e.g. during plugin-registered components).
|
|
45
|
+
const value = inject<T>(key)
|
|
46
|
+
if (value !== undefined) return value
|
|
47
|
+
const pluginProvides = _g[_PROVIDES_KEY] as Map<PropertyKey, unknown> | undefined
|
|
48
|
+
return (pluginProvides?.get(key) as T | undefined) ?? defaultValue
|
|
49
|
+
}
|
|
@@ -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)
|