@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
package/docs/rendering-modes.md
CHANGED
|
@@ -83,7 +83,7 @@ dist/
|
|
|
83
83
|
|
|
84
84
|
```ts
|
|
85
85
|
export const handler: (req, res) => void // main request handler
|
|
86
|
-
export { apiRoutes,
|
|
86
|
+
export { apiRoutes, plugins, layouts }
|
|
87
87
|
export default handler
|
|
88
88
|
```
|
|
89
89
|
|
|
@@ -93,8 +93,7 @@ export default handler
|
|
|
93
93
|
export default defineConfig({
|
|
94
94
|
mode: 'ssr',
|
|
95
95
|
ssr: {
|
|
96
|
-
dsd: true,
|
|
97
|
-
streaming: false, // true = stream response; false = buffer full HTML
|
|
96
|
+
dsd: true, // Declarative Shadow DOM (eliminates FOUC)
|
|
98
97
|
},
|
|
99
98
|
})
|
|
100
99
|
```
|
|
@@ -242,7 +241,7 @@ Any CDN or static host. Upload the entire `dist/` directory (excluding `dist/ser
|
|
|
242
241
|
| Dynamic routes | Yes | Yes | Requires `ssg.paths` |
|
|
243
242
|
| API routes | Separate deploy | Same process | Separate deploy |
|
|
244
243
|
| `useHead()` SSR injection | No | Yes | Yes |
|
|
245
|
-
| Streaming | No |
|
|
244
|
+
| Streaming | No | No | No |
|
|
246
245
|
|
|
247
246
|
---
|
|
248
247
|
|
package/docs/testing.md
CHANGED
|
@@ -223,12 +223,12 @@ Use in a page:
|
|
|
223
223
|
```ts
|
|
224
224
|
// app/pages/index.ts
|
|
225
225
|
component('page-index', () => {
|
|
226
|
-
const greeting =
|
|
226
|
+
const greeting = useInject<string>('greeting')
|
|
227
227
|
return html`<p>${greeting}</p>`
|
|
228
228
|
})
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
**Expected:** Page shows the injected string.
|
|
231
|
+
**Expected:** Page shows the injected string in all three modes (SPA, SSR, SSG). `useInject` is auto-imported — no explicit import needed.
|
|
232
232
|
|
|
233
233
|
---
|
|
234
234
|
|
|
@@ -428,7 +428,7 @@ cat dist/items/1/index.html # should contain "Item 1"
|
|
|
428
428
|
|
|
429
429
|
## 14. Run the automated test suite
|
|
430
430
|
|
|
431
|
-
The framework ships with
|
|
431
|
+
The framework ships with unit and integration tests. Run them with:
|
|
432
432
|
|
|
433
433
|
```sh
|
|
434
434
|
cd /path/to/@jasonshimmy/vite-plugin-cer-app
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
component('page-protected', () => {
|
|
2
|
-
|
|
3
|
-
// globalThis store for SSG where router-view loads the chunk before
|
|
4
|
-
// cer-layout-view has had a chance to call provide().
|
|
5
|
-
const appProvides = (globalThis as any).__cerPluginProvides as Map<string, unknown> | undefined
|
|
6
|
-
const greeting = inject<string>('ks-greeting') ?? appProvides?.get('ks-greeting') as string | undefined ?? 'No greeting'
|
|
2
|
+
const greeting = useInject<string>('ks-greeting', 'No greeting')
|
|
7
3
|
|
|
8
4
|
return html`
|
|
9
5
|
<div>
|
|
@@ -40,6 +40,7 @@ declare global {
|
|
|
40
40
|
|
|
41
41
|
const useHead: typeof import('@jasonshimmy/vite-plugin-cer-app/composables')['useHead']
|
|
42
42
|
const usePageData: typeof import('@jasonshimmy/vite-plugin-cer-app/composables')['usePageData']
|
|
43
|
+
const useInject: typeof import('@jasonshimmy/vite-plugin-cer-app/composables')['useInject']
|
|
43
44
|
|
|
44
45
|
const useKsCounter: typeof import('./app/composables/useKsCounter')['useKsCounter']
|
|
45
46
|
|
package/package.json
CHANGED
|
@@ -133,6 +133,16 @@ describe('build-ssr generateServerEntryCode (template content)', () => {
|
|
|
133
133
|
it('sets Content-Type header on response', () => {
|
|
134
134
|
expect(src).toContain('text/html; charset=utf-8')
|
|
135
135
|
})
|
|
136
|
+
|
|
137
|
+
it('template initializes plugins and sets globalThis.__cerPluginProvides', () => {
|
|
138
|
+
expect(src).toContain('__cerPluginProvides')
|
|
139
|
+
expect(src).toContain('_pluginProvides')
|
|
140
|
+
expect(src).toContain('_pluginsReady')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('template awaits _pluginsReady before handling each request', () => {
|
|
144
|
+
expect(src).toContain('await _pluginsReady')
|
|
145
|
+
})
|
|
136
146
|
})
|
|
137
147
|
|
|
138
148
|
describe('buildSSR', () => {
|
|
@@ -37,6 +37,7 @@ vi.mock('../../plugin/virtual/error.js', () => ({ generateErrorCode: vi.fn().moc
|
|
|
37
37
|
vi.mock('../../plugin/transforms/auto-import.js', () => ({ autoImportTransform: vi.fn().mockReturnValue(null) }))
|
|
38
38
|
|
|
39
39
|
import { cerApp } from '../../plugin/index.js'
|
|
40
|
+
import { APP_ENTRY_TEMPLATE } from '../../runtime/app-template.js'
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
type TestPlugin = {
|
|
@@ -142,6 +143,12 @@ describe('cerApp plugin — resolveId hook', () => {
|
|
|
142
143
|
expect(plugin.resolveId('virtual:cer-error')).toBe('\0virtual:cer-error')
|
|
143
144
|
})
|
|
144
145
|
|
|
146
|
+
it('resolves /.cer/app.ts to \\0cer-app-entry (virtual, bypasses fs security)', () => {
|
|
147
|
+
const plugin = getCerPlugin()
|
|
148
|
+
plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
|
|
149
|
+
expect(plugin.resolveId('/.cer/app.ts')).toBe('\0cer-app-entry')
|
|
150
|
+
})
|
|
151
|
+
|
|
145
152
|
it('returns undefined for unknown ids', () => {
|
|
146
153
|
const plugin = getCerPlugin()
|
|
147
154
|
plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
|
|
@@ -150,6 +157,14 @@ describe('cerApp plugin — resolveId hook', () => {
|
|
|
150
157
|
})
|
|
151
158
|
|
|
152
159
|
describe('cerApp plugin — load hook', () => {
|
|
160
|
+
it('loads \\0cer-app-entry with APP_ENTRY_TEMPLATE content', async () => {
|
|
161
|
+
const plugin = getCerPlugin()
|
|
162
|
+
plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
|
|
163
|
+
plugin.configResolved(FAKE_RESOLVED)
|
|
164
|
+
const result = await plugin.load('\0cer-app-entry')
|
|
165
|
+
expect(result).toBe(APP_ENTRY_TEMPLATE)
|
|
166
|
+
})
|
|
167
|
+
|
|
153
168
|
it('returns null for unknown resolved ids', async () => {
|
|
154
169
|
const plugin = getCerPlugin()
|
|
155
170
|
plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
|
|
@@ -48,7 +48,7 @@ function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConf
|
|
|
48
48
|
serverApiDir: '/project/server/api',
|
|
49
49
|
serverMiddlewareDir: '/project/server/middleware',
|
|
50
50
|
port: 3000,
|
|
51
|
-
ssr: { dsd: true
|
|
51
|
+
ssr: { dsd: true },
|
|
52
52
|
ssg: { routes: 'auto', concurrency: 2, fallback: false },
|
|
53
53
|
router: {},
|
|
54
54
|
jitCss: { content: [], extendedColors: false },
|
|
@@ -163,6 +163,11 @@ describe('generateAutoImportDts', () => {
|
|
|
163
163
|
expect(dts).toContain("const usePageData: typeof import('@jasonshimmy/vite-plugin-cer-app/composables')['usePageData']")
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
+
it('declares useInject as a framework global', async () => {
|
|
167
|
+
const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
|
|
168
|
+
expect(dts).toContain("const useInject: typeof import('@jasonshimmy/vite-plugin-cer-app/composables')['useInject']")
|
|
169
|
+
})
|
|
170
|
+
|
|
166
171
|
it('declares when directive as a global', async () => {
|
|
167
172
|
const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
|
|
168
173
|
expect(dts).toContain("const when: typeof import('@jasonshimmy/custom-elements-runtime/directives')['when']")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'pathe'
|
|
4
|
+
|
|
5
|
+
const src = readFileSync(
|
|
6
|
+
resolve(import.meta.dirname, '../../runtime/entry-server-template.ts'),
|
|
7
|
+
'utf-8',
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
describe('entry-server-template (ENTRY_SERVER_TEMPLATE content)', () => {
|
|
11
|
+
it('template imports plugins from virtual:cer-plugins', () => {
|
|
12
|
+
expect(src).toContain('virtual:cer-plugins')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('template initializes plugins and sets globalThis.__cerPluginProvides', () => {
|
|
16
|
+
expect(src).toContain('__cerPluginProvides')
|
|
17
|
+
expect(src).toContain('_pluginProvides')
|
|
18
|
+
expect(src).toContain('_pluginsReady')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('template awaits _pluginsReady before handling each request', () => {
|
|
22
|
+
expect(src).toContain('await _pluginsReady')
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -17,7 +17,6 @@ import { existsSync, writeFileSync, mkdirSync, readFileSync, appendFileSync } fr
|
|
|
17
17
|
import {
|
|
18
18
|
GENERATED_DIR_NAME,
|
|
19
19
|
getGeneratedDir,
|
|
20
|
-
resolveAppEntry,
|
|
21
20
|
resolveHtmlEntry,
|
|
22
21
|
generateDefaultHtml,
|
|
23
22
|
writeGeneratedDir,
|
|
@@ -49,18 +48,6 @@ describe('getGeneratedDir', () => {
|
|
|
49
48
|
})
|
|
50
49
|
})
|
|
51
50
|
|
|
52
|
-
describe('resolveAppEntry', () => {
|
|
53
|
-
it('returns user app/app.ts when it exists', () => {
|
|
54
|
-
vi.mocked(existsSync).mockReturnValue(true)
|
|
55
|
-
expect(resolveAppEntry(mockConfig)).toBe(`${ROOT}/app/app.ts`)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('returns .cer/app.ts when user app/app.ts is absent', () => {
|
|
59
|
-
vi.mocked(existsSync).mockReturnValue(false)
|
|
60
|
-
expect(resolveAppEntry(mockConfig)).toBe(`${ROOT}/.cer/app.ts`)
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
51
|
describe('resolveHtmlEntry', () => {
|
|
65
52
|
it('returns user index.html when it exists', () => {
|
|
66
53
|
vi.mocked(existsSync).mockReturnValue(true)
|
|
@@ -74,26 +61,19 @@ describe('resolveHtmlEntry', () => {
|
|
|
74
61
|
})
|
|
75
62
|
|
|
76
63
|
describe('generateDefaultHtml', () => {
|
|
77
|
-
it('references /app
|
|
78
|
-
|
|
79
|
-
const html = generateDefaultHtml(mockConfig)
|
|
80
|
-
expect(html).toContain('/app/app.ts')
|
|
81
|
-
expect(html).not.toContain('/.cer/app.ts')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('references /.cer/app.ts when user entry is absent', () => {
|
|
85
|
-
vi.mocked(existsSync).mockReturnValue(false)
|
|
86
|
-
const html = generateDefaultHtml(mockConfig)
|
|
64
|
+
it('always references /.cer/app.ts', () => {
|
|
65
|
+
const html = generateDefaultHtml()
|
|
87
66
|
expect(html).toContain('/.cer/app.ts')
|
|
67
|
+
expect(html).not.toContain('/app/app.ts')
|
|
88
68
|
})
|
|
89
69
|
|
|
90
70
|
it('includes <cer-layout-view> mount point', () => {
|
|
91
|
-
const html = generateDefaultHtml(
|
|
71
|
+
const html = generateDefaultHtml()
|
|
92
72
|
expect(html).toContain('<cer-layout-view>')
|
|
93
73
|
})
|
|
94
74
|
|
|
95
75
|
it('is valid HTML with doctype', () => {
|
|
96
|
-
const html = generateDefaultHtml(
|
|
76
|
+
const html = generateDefaultHtml()
|
|
97
77
|
expect(html).toContain('<!DOCTYPE html>')
|
|
98
78
|
})
|
|
99
79
|
})
|
|
@@ -112,28 +92,17 @@ describe('writeGeneratedDir', () => {
|
|
|
112
92
|
expect(mkdirSync).not.toHaveBeenCalled()
|
|
113
93
|
})
|
|
114
94
|
|
|
115
|
-
it('writes .cer/app.ts
|
|
116
|
-
// Only the .cer dir check needs to return true to skip mkdirSync — but we
|
|
117
|
-
// want the user entry check to return false. Use a counter.
|
|
118
|
-
let callCount = 0
|
|
119
|
-
vi.mocked(existsSync).mockImplementation(() => {
|
|
120
|
-
callCount++
|
|
121
|
-
// First call: .cer/ dir → true (already exists, skip mkdir)
|
|
122
|
-
// Second call: app/app.ts → false (absent, write template)
|
|
123
|
-
// Subsequent calls: false (no .gitignore)
|
|
124
|
-
return callCount === 1
|
|
125
|
-
})
|
|
95
|
+
it('always writes .cer/app.ts', () => {
|
|
126
96
|
writeGeneratedDir(mockConfig)
|
|
127
97
|
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
128
98
|
expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(true)
|
|
129
99
|
})
|
|
130
100
|
|
|
131
|
-
it('
|
|
132
|
-
// existsSync always returns true — dir exists, user entry exists
|
|
101
|
+
it('always writes .cer/app.ts even when .cer/ dir already exists', () => {
|
|
133
102
|
vi.mocked(existsSync).mockReturnValue(true)
|
|
134
103
|
writeGeneratedDir(mockConfig)
|
|
135
104
|
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
136
|
-
expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(
|
|
105
|
+
expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(true)
|
|
137
106
|
})
|
|
138
107
|
|
|
139
108
|
it('always writes .cer/index.html', () => {
|
|
@@ -84,11 +84,6 @@ describe('resolveConfig', () => {
|
|
|
84
84
|
expect(cfg.ssr.dsd).toBe(false)
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
-
it('defaults ssr.streaming to false', () => {
|
|
88
|
-
const cfg = resolveConfig({}, ROOT)
|
|
89
|
-
expect(cfg.ssr.streaming).toBe(false)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
87
|
it('defaults ssg.routes to "auto"', () => {
|
|
93
88
|
const cfg = resolveConfig({}, ROOT)
|
|
94
89
|
expect(cfg.ssg.routes).toBe('auto')
|
|
@@ -221,6 +221,13 @@ describe('autoImportTransform — framework composable injection', () => {
|
|
|
221
221
|
expect(count).toBe(1)
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
+
it('injects useInject import when useInject is used', () => {
|
|
225
|
+
const code = "component('page-about', () => { const svc = useInject('my-service'); return html`<div></div>` })"
|
|
226
|
+
const result = autoImportTransform(code, '/project/app/pages/about.ts', opts)!
|
|
227
|
+
expect(result).toContain(`from ${FRAMEWORK_PKG}`)
|
|
228
|
+
expect(result).toContain('useInject')
|
|
229
|
+
})
|
|
230
|
+
|
|
224
231
|
it('injects usePageData for root-level convention files (loading.ts, error.ts)', () => {
|
|
225
232
|
const code = "component('page-loading', () => { const d = usePageData(); return html`<div></div>` })"
|
|
226
233
|
const result = autoImportTransform(code, '/project/app/loading.ts', opts)!
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*
|
|
4
|
+
* Tests for useInject — client-side path.
|
|
5
|
+
*
|
|
6
|
+
* Runs in happy-dom so `document` is defined, putting useInject() on the
|
|
7
|
+
* client branch. inject() from the runtime is mocked since it requires a
|
|
8
|
+
* live component context that doesn't exist in unit tests.
|
|
9
|
+
*/
|
|
10
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
11
|
+
|
|
12
|
+
vi.mock('@jasonshimmy/custom-elements-runtime', () => ({
|
|
13
|
+
inject: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
import { inject } from '@jasonshimmy/custom-elements-runtime'
|
|
17
|
+
import { useInject } from '../../runtime/composables/use-inject.js'
|
|
18
|
+
|
|
19
|
+
const _g = globalThis as Record<string, unknown>
|
|
20
|
+
|
|
21
|
+
describe('useInject — client-side', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
delete _g['__cerPluginProvides']
|
|
24
|
+
vi.mocked(inject).mockReturnValue(undefined)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
delete _g['__cerPluginProvides']
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('returns the value from inject() when the component context has it', () => {
|
|
32
|
+
vi.mocked(inject).mockReturnValue('injected-value')
|
|
33
|
+
expect(useInject('my-key')).toBe('injected-value')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('falls back to __cerPluginProvides when inject() returns undefined', () => {
|
|
37
|
+
_g['__cerPluginProvides'] = new Map([['my-key', 'plugin-provided']])
|
|
38
|
+
expect(useInject('my-key')).toBe('plugin-provided')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('inject() result takes priority over __cerPluginProvides', () => {
|
|
42
|
+
vi.mocked(inject).mockReturnValue('inject-wins')
|
|
43
|
+
_g['__cerPluginProvides'] = new Map([['my-key', 'global-value']])
|
|
44
|
+
expect(useInject('my-key')).toBe('inject-wins')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns defaultValue when inject() and provides both miss', () => {
|
|
48
|
+
expect(useInject('missing', 'default')).toBe('default')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns undefined when inject() and provides both miss with no defaultValue', () => {
|
|
52
|
+
expect(useInject('missing')).toBeUndefined()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns defaultValue when inject() returns undefined and key is absent from provides', () => {
|
|
56
|
+
_g['__cerPluginProvides'] = new Map()
|
|
57
|
+
expect(useInject('missing', 42)).toBe(42)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('is generic and preserves the typed shape from inject()', () => {
|
|
61
|
+
interface Service { call(): string }
|
|
62
|
+
const svc: Service = { call: () => 'ok' }
|
|
63
|
+
vi.mocked(inject).mockReturnValue(svc)
|
|
64
|
+
const result = useInject<Service>('svc')
|
|
65
|
+
expect(result?.call()).toBe('ok')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useInject — server-side (SSR/SSG) path.
|
|
3
|
+
*
|
|
4
|
+
* This file runs in the default 'node' environment where `document` is
|
|
5
|
+
* undefined, so useInject() always takes the SSR branch: reading from
|
|
6
|
+
* globalThis.__cerPluginProvides.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
9
|
+
import { useInject } from '../../runtime/composables/use-inject.js'
|
|
10
|
+
|
|
11
|
+
const _g = globalThis as Record<string, unknown>
|
|
12
|
+
|
|
13
|
+
describe('useInject — server-side (SSR/SSG)', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
delete _g['__cerPluginProvides']
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
delete _g['__cerPluginProvides']
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns value from __cerPluginProvides when key is present', () => {
|
|
23
|
+
_g['__cerPluginProvides'] = new Map([['my-service', { greet: () => 'hello' }]])
|
|
24
|
+
const result = useInject<{ greet(): string }>('my-service')
|
|
25
|
+
expect(typeof result?.greet).toBe('function')
|
|
26
|
+
expect(result?.greet()).toBe('hello')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns defaultValue when key is absent from the provides map', () => {
|
|
30
|
+
_g['__cerPluginProvides'] = new Map()
|
|
31
|
+
expect(useInject('missing', 'fallback')).toBe('fallback')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns undefined when key is absent and no defaultValue is given', () => {
|
|
35
|
+
_g['__cerPluginProvides'] = new Map()
|
|
36
|
+
expect(useInject('missing')).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns undefined when __cerPluginProvides is not set at all', () => {
|
|
40
|
+
expect(useInject('my-service')).toBeUndefined()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns defaultValue when __cerPluginProvides is not set and defaultValue is given', () => {
|
|
44
|
+
expect(useInject('my-service', 'default')).toBe('default')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('is generic and preserves the typed shape', () => {
|
|
48
|
+
interface Store { count: number }
|
|
49
|
+
const store: Store = { count: 42 }
|
|
50
|
+
_g['__cerPluginProvides'] = new Map([['store', store]])
|
|
51
|
+
const result = useInject<Store>('store')
|
|
52
|
+
expect(result?.count).toBe(42)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('is safe to call multiple times — does not consume the value', () => {
|
|
56
|
+
_g['__cerPluginProvides'] = new Map([['key', 'value']])
|
|
57
|
+
expect(useInject('key')).toBe('value')
|
|
58
|
+
expect(useInject('key')).toBe('value')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('supports multiple keys independently', () => {
|
|
62
|
+
_g['__cerPluginProvides'] = new Map([['a', 1], ['b', 2]])
|
|
63
|
+
expect(useInject('a')).toBe(1)
|
|
64
|
+
expect(useInject('b')).toBe(2)
|
|
65
|
+
})
|
|
66
|
+
})
|
package/src/cli/create/index.ts
CHANGED
|
@@ -179,6 +179,10 @@ async function generateInlineTemplate(
|
|
|
179
179
|
): Promise<void> {
|
|
180
180
|
await mkdir(join(targetDir, 'app/pages'), { recursive: true })
|
|
181
181
|
await mkdir(join(targetDir, 'app/layouts'), { recursive: true })
|
|
182
|
+
await mkdir(join(targetDir, 'app/components'), { recursive: true })
|
|
183
|
+
await mkdir(join(targetDir, 'app/composables'), { recursive: true })
|
|
184
|
+
await mkdir(join(targetDir, 'app/plugins'), { recursive: true })
|
|
185
|
+
await mkdir(join(targetDir, 'app/middleware'), { recursive: true })
|
|
182
186
|
|
|
183
187
|
// package.json
|
|
184
188
|
await writeFile(
|
|
@@ -215,13 +219,6 @@ async function generateInlineTemplate(
|
|
|
215
219
|
'utf-8',
|
|
216
220
|
)
|
|
217
221
|
|
|
218
|
-
// app/app.ts
|
|
219
|
-
await writeFile(
|
|
220
|
-
join(targetDir, 'app/app.ts'),
|
|
221
|
-
`import '@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 { component, ref, provide, useOnConnected, useOnDisconnected, registerBuiltinComponents } 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\nconst isNavigating = ref(false)\nconst currentError = ref(null)\n;(globalThis as any).resetError = () => { currentError.value = null; router.replace(router.getCurrent().path) }\nconst _push = router.push.bind(router)\nconst _replace = router.replace.bind(router)\nrouter.push = async (path) => { isNavigating.value = true; currentError.value = null; try { await _push(path) } catch (err) { currentError.value = err instanceof Error ? err.message : String(err) } finally { isNavigating.value = false } }\nrouter.replace = async (path) => { isNavigating.value = true; currentError.value = null; try { await _replace(path) } catch (err) { currentError.value = err instanceof Error ? err.message : String(err) } finally { isNavigating.value = false } }\n\nconst _pluginProvides = new Map()\n;(globalThis as any).__cerPluginProvides = _pluginProvides\n\ncomponent('cer-layout-view', () => {\n for (const [key, value] of _pluginProvides) { provide(key, value) }\n const current = ref(router.getCurrent())\n let unsub: (() => void) | undefined\n useOnConnected(() => { unsub = router.subscribe((s: typeof current.value) => { current.value = s }) })\n useOnDisconnected(() => { unsub?.(); unsub = undefined })\n if (currentError.value !== null) {\n if (hasError && errorTag) return { tag: errorTag, props: { attrs: { error: String(currentError.value) } }, children: [] }\n return { tag: 'div', props: { attrs: { style: 'padding:2rem;font-family:monospace' } }, children: [String(currentError.value)] }\n }\n if (isNavigating.value && hasLoading && loadingTag) return { tag: loadingTag, props: {}, children: [] }\n const matched = router.matchRoute(current.value.path)\n const layoutName = (matched?.route as any)?.meta?.layout ?? 'default'\n const layoutTag = (layouts as Record<string, string>)[layoutName]\n const routerView = { tag: 'router-view', props: {}, children: [] }\n return layoutTag ? { tag: layoutTag, props: {}, children: [routerView] } : routerView\n})\n\nfor (const plugin of plugins ?? []) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({ router, provide: (key: string, value: unknown) => { _pluginProvides.set(key, value) }, config: {} })\n }\n}\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\nif (typeof window !== 'undefined') {\n await _replace(window.location.pathname + window.location.search + window.location.hash)\n delete (globalThis as any).__CER_DATA__\n}\n\nexport { router }\n`,
|
|
222
|
-
'utf-8',
|
|
223
|
-
)
|
|
224
|
-
|
|
225
222
|
// app/pages/index.ts
|
|
226
223
|
await writeFile(
|
|
227
224
|
join(targetDir, 'app/pages/index.ts'),
|
|
@@ -236,10 +233,17 @@ async function generateInlineTemplate(
|
|
|
236
233
|
'utf-8',
|
|
237
234
|
)
|
|
238
235
|
|
|
236
|
+
// .gitignore
|
|
237
|
+
await writeFile(
|
|
238
|
+
join(targetDir, '.gitignore'),
|
|
239
|
+
`# Dependencies\nnode_modules/\n\n# Build output\ndist/\n\n# CER App generated directory\n.cer/\n\n# Environment variables\n.env.local\n.env.*.local\n\n# Editor\n.vscode/\n.idea/\n*.suo\n*.sw?\n\n# OS\n.DS_Store\nThumbs.db\n\n# Logs\n*.log\n`,
|
|
240
|
+
'utf-8',
|
|
241
|
+
)
|
|
242
|
+
|
|
239
243
|
// index.html
|
|
240
244
|
await writeFile(
|
|
241
245
|
join(targetDir, 'index.html'),
|
|
242
|
-
`<!DOCTYPE html>\n<html lang="en">\n <head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>${projectName}</title>\n </head>\n <body>\n <cer-layout-view></cer-layout-view>\n <script type="module" src="/app
|
|
246
|
+
`<!DOCTYPE html>\n<html lang="en">\n <head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>${projectName}</title>\n </head>\n <body>\n <cer-layout-view></cer-layout-view>\n <script type="module" src="/.cer/app.ts"></script>\n </body>\n</html>\n`,
|
|
243
247
|
'utf-8',
|
|
244
248
|
)
|
|
245
249
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist/
|
|
6
|
+
|
|
7
|
+
# CER App generated directory
|
|
8
|
+
.cer/
|
|
9
|
+
|
|
10
|
+
# Environment variables
|
|
11
|
+
.env.local
|
|
12
|
+
.env.*.local
|
|
13
|
+
|
|
14
|
+
# Editor
|
|
15
|
+
.vscode/
|
|
16
|
+
.idea/
|
|
17
|
+
*.suo
|
|
18
|
+
*.sw?
|
|
19
|
+
|
|
20
|
+
# OS
|
|
21
|
+
.DS_Store
|
|
22
|
+
Thumbs.db
|
|
23
|
+
|
|
24
|
+
# Logs
|
|
25
|
+
*.log
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist/
|
|
6
|
+
|
|
7
|
+
# CER App generated directory
|
|
8
|
+
.cer/
|
|
9
|
+
|
|
10
|
+
# Environment variables
|
|
11
|
+
.env.local
|
|
12
|
+
.env.*.local
|
|
13
|
+
|
|
14
|
+
# Editor
|
|
15
|
+
.vscode/
|
|
16
|
+
.idea/
|
|
17
|
+
*.suo
|
|
18
|
+
*.sw?
|
|
19
|
+
|
|
20
|
+
# OS
|
|
21
|
+
.DS_Store
|
|
22
|
+
Thumbs.db
|
|
23
|
+
|
|
24
|
+
# Logs
|
|
25
|
+
*.log
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist/
|
|
6
|
+
|
|
7
|
+
# CER App generated directory
|
|
8
|
+
.cer/
|
|
9
|
+
|
|
10
|
+
# Environment variables
|
|
11
|
+
.env.local
|
|
12
|
+
.env.*.local
|
|
13
|
+
|
|
14
|
+
# Editor
|
|
15
|
+
.vscode/
|
|
16
|
+
.idea/
|
|
17
|
+
*.suo
|
|
18
|
+
*.sw?
|
|
19
|
+
|
|
20
|
+
# OS
|
|
21
|
+
.DS_Store
|
|
22
|
+
Thumbs.db
|
|
23
|
+
|
|
24
|
+
# Logs
|
|
25
|
+
*.log
|
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)
|
package/src/plugin/dev-server.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface ResolvedCerConfig {
|
|
|
15
15
|
serverApiDir: string
|
|
16
16
|
serverMiddlewareDir: string
|
|
17
17
|
port: number
|
|
18
|
-
ssr: { dsd: boolean
|
|
18
|
+
ssr: { dsd: boolean }
|
|
19
19
|
ssg: { routes: 'auto' | string[]; concurrency: number; fallback: boolean }
|
|
20
20
|
router: { base?: string; scrollToFragment?: boolean | object }
|
|
21
21
|
jitCss: { content: string[]; extendedColors: boolean }
|