@jasonshimmy/vite-plugin-cer-app 0.1.1 → 0.1.2

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/commits.txt +1 -1
  3. package/dist/cli/commands/generate.d.ts.map +1 -1
  4. package/dist/cli/commands/generate.js +2 -0
  5. package/dist/cli/commands/generate.js.map +1 -1
  6. package/dist/cli/commands/preview.d.ts.map +1 -1
  7. package/dist/cli/commands/preview.js +21 -2
  8. package/dist/cli/commands/preview.js.map +1 -1
  9. package/dist/cli/index.js +1 -1
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/plugin/build-ssg.d.ts.map +1 -1
  12. package/dist/plugin/build-ssg.js +10 -21
  13. package/dist/plugin/build-ssg.js.map +1 -1
  14. package/dist/plugin/build-ssr.d.ts.map +1 -1
  15. package/dist/plugin/build-ssr.js +150 -27
  16. package/dist/plugin/build-ssr.js.map +1 -1
  17. package/dist/plugin/virtual/routes.d.ts.map +1 -1
  18. package/dist/plugin/virtual/routes.js +11 -1
  19. package/dist/plugin/virtual/routes.js.map +1 -1
  20. package/dist/runtime/app-template.d.ts +1 -1
  21. package/dist/runtime/app-template.d.ts.map +1 -1
  22. package/dist/runtime/app-template.js +6 -0
  23. package/dist/runtime/app-template.js.map +1 -1
  24. package/dist/runtime/composables/use-page-data.d.ts +15 -6
  25. package/dist/runtime/composables/use-page-data.d.ts.map +1 -1
  26. package/dist/runtime/composables/use-page-data.js +30 -9
  27. package/dist/runtime/composables/use-page-data.js.map +1 -1
  28. package/dist/runtime/entry-server-template.d.ts +1 -1
  29. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  30. package/dist/runtime/entry-server-template.js +137 -16
  31. package/dist/runtime/entry-server-template.js.map +1 -1
  32. package/docs/data-loading.md +7 -6
  33. package/docs/getting-started.md +1 -1
  34. package/docs/rendering-modes.md +1 -1
  35. package/docs/server-api.md +9 -0
  36. package/package.json +1 -1
  37. package/src/__tests__/index.test.ts +21 -0
  38. package/src/__tests__/plugin/build-ssg.test.ts +265 -0
  39. package/src/__tests__/plugin/build-ssr.test.ts +180 -0
  40. package/src/__tests__/plugin/cer-app-plugin.test.ts +409 -0
  41. package/src/__tests__/plugin/dts-generator.test.ts +246 -0
  42. package/src/__tests__/plugin/resolve-config.test.ts +158 -0
  43. package/src/__tests__/plugin/virtual/error.test.ts +71 -0
  44. package/src/__tests__/plugin/virtual/loading.test.ts +72 -0
  45. package/src/__tests__/plugin/virtual/server-middleware.test.ts +102 -0
  46. package/src/__tests__/runtime/use-page-data.test.ts +81 -5
  47. package/src/__tests__/types/config.test.ts +23 -0
  48. package/src/cli/commands/generate.ts +2 -0
  49. package/src/cli/commands/preview.ts +21 -2
  50. package/src/cli/index.ts +1 -1
  51. package/src/plugin/build-ssg.ts +9 -22
  52. package/src/plugin/build-ssr.ts +149 -27
  53. package/src/plugin/virtual/routes.ts +12 -1
  54. package/src/runtime/app-template.ts +6 -0
  55. package/src/runtime/composables/use-page-data.ts +31 -9
  56. package/src/runtime/entry-server-template.ts +137 -16
  57. package/tsconfig.build.json +1 -1
  58. package/dist/__tests__/plugin/path-utils.test.d.ts +0 -2
  59. package/dist/__tests__/plugin/path-utils.test.d.ts.map +0 -1
  60. package/dist/__tests__/plugin/path-utils.test.js +0 -305
  61. package/dist/__tests__/plugin/path-utils.test.js.map +0 -1
  62. package/dist/__tests__/plugin/scanner.test.d.ts +0 -2
  63. package/dist/__tests__/plugin/scanner.test.d.ts.map +0 -1
  64. package/dist/__tests__/plugin/scanner.test.js +0 -143
  65. package/dist/__tests__/plugin/scanner.test.js.map +0 -1
  66. package/dist/__tests__/plugin/transforms/auto-import.test.d.ts +0 -2
  67. package/dist/__tests__/plugin/transforms/auto-import.test.d.ts.map +0 -1
  68. package/dist/__tests__/plugin/transforms/auto-import.test.js +0 -151
  69. package/dist/__tests__/plugin/transforms/auto-import.test.js.map +0 -1
  70. package/dist/__tests__/plugin/transforms/head-inject.test.d.ts +0 -2
  71. package/dist/__tests__/plugin/transforms/head-inject.test.d.ts.map +0 -1
  72. package/dist/__tests__/plugin/transforms/head-inject.test.js +0 -151
  73. package/dist/__tests__/plugin/transforms/head-inject.test.js.map +0 -1
  74. package/dist/__tests__/plugin/virtual/components.test.d.ts +0 -2
  75. package/dist/__tests__/plugin/virtual/components.test.d.ts.map +0 -1
  76. package/dist/__tests__/plugin/virtual/components.test.js +0 -47
  77. package/dist/__tests__/plugin/virtual/components.test.js.map +0 -1
  78. package/dist/__tests__/plugin/virtual/composables.test.d.ts +0 -2
  79. package/dist/__tests__/plugin/virtual/composables.test.d.ts.map +0 -1
  80. package/dist/__tests__/plugin/virtual/composables.test.js +0 -48
  81. package/dist/__tests__/plugin/virtual/composables.test.js.map +0 -1
  82. package/dist/__tests__/plugin/virtual/layouts.test.d.ts +0 -2
  83. package/dist/__tests__/plugin/virtual/layouts.test.d.ts.map +0 -1
  84. package/dist/__tests__/plugin/virtual/layouts.test.js +0 -59
  85. package/dist/__tests__/plugin/virtual/layouts.test.js.map +0 -1
  86. package/dist/__tests__/plugin/virtual/middleware.test.d.ts +0 -2
  87. package/dist/__tests__/plugin/virtual/middleware.test.d.ts.map +0 -1
  88. package/dist/__tests__/plugin/virtual/middleware.test.js +0 -58
  89. package/dist/__tests__/plugin/virtual/middleware.test.js.map +0 -1
  90. package/dist/__tests__/plugin/virtual/plugins.test.d.ts +0 -2
  91. package/dist/__tests__/plugin/virtual/plugins.test.d.ts.map +0 -1
  92. package/dist/__tests__/plugin/virtual/plugins.test.js +0 -73
  93. package/dist/__tests__/plugin/virtual/plugins.test.js.map +0 -1
  94. package/dist/__tests__/plugin/virtual/routes.test.d.ts +0 -2
  95. package/dist/__tests__/plugin/virtual/routes.test.d.ts.map +0 -1
  96. package/dist/__tests__/plugin/virtual/routes.test.js +0 -167
  97. package/dist/__tests__/plugin/virtual/routes.test.js.map +0 -1
  98. package/dist/__tests__/plugin/virtual/server-api.test.d.ts +0 -2
  99. package/dist/__tests__/plugin/virtual/server-api.test.d.ts.map +0 -1
  100. package/dist/__tests__/plugin/virtual/server-api.test.js +0 -72
  101. package/dist/__tests__/plugin/virtual/server-api.test.js.map +0 -1
  102. package/dist/__tests__/runtime/use-head.test.d.ts +0 -2
  103. package/dist/__tests__/runtime/use-head.test.d.ts.map +0 -1
  104. package/dist/__tests__/runtime/use-head.test.js +0 -202
  105. package/dist/__tests__/runtime/use-head.test.js.map +0 -1
  106. package/dist/__tests__/runtime/use-page-data.test.d.ts +0 -2
  107. package/dist/__tests__/runtime/use-page-data.test.d.ts.map +0 -1
  108. package/dist/__tests__/runtime/use-page-data.test.js +0 -41
  109. package/dist/__tests__/runtime/use-page-data.test.js.map +0 -1
@@ -0,0 +1,409 @@
1
+ import { vi, describe, it, expect, beforeEach } from 'vitest'
2
+
3
+ vi.mock('@jasonshimmy/custom-elements-runtime/vite-plugin', () => ({
4
+ cerPlugin: vi.fn().mockReturnValue([{ name: 'cer-runtime-plugin' }]),
5
+ }))
6
+ vi.mock('../../plugin/dev-server.js', () => ({
7
+ configureCerDevServer: vi.fn().mockResolvedValue(undefined),
8
+ }))
9
+ vi.mock('../../plugin/scanner.js', () => ({
10
+ createWatcher: vi.fn().mockReturnValue({ on: vi.fn(), close: vi.fn() }),
11
+ scanDirectory: vi.fn().mockResolvedValue([]),
12
+ }))
13
+ vi.mock('../../plugin/dts-generator.js', () => ({
14
+ scanComposableExports: vi.fn().mockResolvedValue(new Map()),
15
+ writeAutoImportDts: vi.fn().mockResolvedValue(undefined),
16
+ writeTsconfigPaths: vi.fn(),
17
+ }))
18
+ vi.mock('../../plugin/virtual/routes.js', () => ({ generateRoutesCode: vi.fn().mockResolvedValue('// routes') }))
19
+ vi.mock('../../plugin/virtual/layouts.js', () => ({ generateLayoutsCode: vi.fn().mockResolvedValue('// layouts') }))
20
+ vi.mock('../../plugin/virtual/components.js', () => ({ generateComponentsCode: vi.fn().mockResolvedValue('// components') }))
21
+ vi.mock('../../plugin/virtual/composables.js', () => ({ generateComposablesCode: vi.fn().mockResolvedValue('// composables') }))
22
+ vi.mock('../../plugin/virtual/plugins.js', () => ({ generatePluginsCode: vi.fn().mockResolvedValue('// plugins') }))
23
+ vi.mock('../../plugin/virtual/middleware.js', () => ({ generateMiddlewareCode: vi.fn().mockResolvedValue('// middleware') }))
24
+ vi.mock('../../plugin/virtual/server-api.js', () => ({ generateServerApiCode: vi.fn().mockResolvedValue('// server-api') }))
25
+ vi.mock('../../plugin/virtual/server-middleware.js', () => ({ generateServerMiddlewareCode: vi.fn().mockResolvedValue('// server-middleware') }))
26
+ vi.mock('../../plugin/virtual/loading.js', () => ({ generateLoadingCode: vi.fn().mockResolvedValue('// loading') }))
27
+ vi.mock('../../plugin/virtual/error.js', () => ({ generateErrorCode: vi.fn().mockResolvedValue('// error') }))
28
+ vi.mock('../../plugin/transforms/auto-import.js', () => ({ autoImportTransform: vi.fn().mockReturnValue(null) }))
29
+
30
+ import { cerApp } from '../../plugin/index.js'
31
+
32
+
33
+ type TestPlugin = {
34
+ name: string
35
+ config: (viteConfig: Record<string, unknown>, env: Record<string, unknown>) => unknown
36
+ configResolved: (resolved: Record<string, unknown>) => void
37
+ resolveId: (id: string) => string | undefined
38
+ load: (id: string) => Promise<string | null>
39
+ transform: (code: string, id: string) => unknown
40
+ buildStart: () => Promise<void>
41
+ configureServer: (server: unknown) => Promise<void>
42
+ }
43
+
44
+ // Helper to get the cerAppPlugin (first plugin in the returned array)
45
+ function getCerPlugin(userConfig = {}): TestPlugin {
46
+ return cerApp(userConfig)[0] as unknown as TestPlugin
47
+ }
48
+
49
+ // Minimal resolved config that mirrors what Vite passes to configResolved
50
+ const FAKE_RESOLVED = { root: '/project' }
51
+
52
+ describe('cerApp()', () => {
53
+ it('returns an array of plugins', () => {
54
+ const plugins = cerApp()
55
+ expect(Array.isArray(plugins)).toBe(true)
56
+ expect(plugins.length).toBeGreaterThan(0)
57
+ })
58
+
59
+ it('first plugin is named @jasonshimmy/vite-plugin-cer-app', () => {
60
+ const plugin = getCerPlugin()
61
+ expect(plugin.name).toBe('@jasonshimmy/vite-plugin-cer-app')
62
+ })
63
+ })
64
+
65
+ describe('cerApp plugin — config hook', () => {
66
+ it('returns build.target: esnext', () => {
67
+ const plugin = getCerPlugin()
68
+ const result = plugin.config({ root: '/project' }, { command: 'build', mode: 'production' }) as Record<string, unknown>
69
+ expect((result.build as Record<string, unknown>).target).toBe('esnext')
70
+ })
71
+
72
+ it('config hook resolves the root', () => {
73
+ const plugin = getCerPlugin()
74
+ const result = plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
75
+ expect(result).toBeDefined()
76
+ })
77
+ })
78
+
79
+ describe('cerApp plugin — configResolved hook', () => {
80
+ it('does not throw when called', () => {
81
+ const plugin = getCerPlugin()
82
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
83
+ expect(() => plugin.configResolved(FAKE_RESOLVED)).not.toThrow()
84
+ })
85
+ })
86
+
87
+ describe('cerApp plugin — resolveId hook', () => {
88
+ beforeEach(() => {
89
+ const plugin = getCerPlugin()
90
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
91
+ plugin.configResolved(FAKE_RESOLVED)
92
+ })
93
+
94
+ it('resolves virtual:cer-routes to \\0virtual:cer-routes', () => {
95
+ const plugin = getCerPlugin()
96
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
97
+ expect(plugin.resolveId('virtual:cer-routes')).toBe('\0virtual:cer-routes')
98
+ })
99
+
100
+ it('resolves virtual:cer-layouts', () => {
101
+ const plugin = getCerPlugin()
102
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
103
+ expect(plugin.resolveId('virtual:cer-layouts')).toBe('\0virtual:cer-layouts')
104
+ })
105
+
106
+ it('resolves virtual:cer-components', () => {
107
+ const plugin = getCerPlugin()
108
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
109
+ expect(plugin.resolveId('virtual:cer-components')).toBe('\0virtual:cer-components')
110
+ })
111
+
112
+ it('resolves virtual:cer-plugins', () => {
113
+ const plugin = getCerPlugin()
114
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
115
+ expect(plugin.resolveId('virtual:cer-plugins')).toBe('\0virtual:cer-plugins')
116
+ })
117
+
118
+ it('resolves virtual:cer-server-api', () => {
119
+ const plugin = getCerPlugin()
120
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
121
+ expect(plugin.resolveId('virtual:cer-server-api')).toBe('\0virtual:cer-server-api')
122
+ })
123
+
124
+ it('resolves virtual:cer-loading', () => {
125
+ const plugin = getCerPlugin()
126
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
127
+ expect(plugin.resolveId('virtual:cer-loading')).toBe('\0virtual:cer-loading')
128
+ })
129
+
130
+ it('resolves virtual:cer-error', () => {
131
+ const plugin = getCerPlugin()
132
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
133
+ expect(plugin.resolveId('virtual:cer-error')).toBe('\0virtual:cer-error')
134
+ })
135
+
136
+ it('returns undefined for unknown ids', () => {
137
+ const plugin = getCerPlugin()
138
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
139
+ expect(plugin.resolveId('some-unknown-id')).toBeUndefined()
140
+ })
141
+ })
142
+
143
+ describe('cerApp plugin — load hook', () => {
144
+ it('returns null for unknown resolved ids', async () => {
145
+ const plugin = getCerPlugin()
146
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
147
+ plugin.configResolved(FAKE_RESOLVED)
148
+ const result = await plugin.load('\0unknown-module')
149
+ expect(result).toBeNull()
150
+ })
151
+
152
+ it('loads virtual:cer-routes module code', async () => {
153
+ const plugin = getCerPlugin()
154
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
155
+ plugin.configResolved(FAKE_RESOLVED)
156
+ const result = await plugin.load('\0virtual:cer-routes')
157
+ expect(result).toBe('// routes')
158
+ })
159
+
160
+ it('loads virtual:cer-layouts module code', 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('\0virtual:cer-layouts')
165
+ expect(result).toBe('// layouts')
166
+ })
167
+
168
+ it('loads virtual:cer-components module code', async () => {
169
+ const plugin = getCerPlugin()
170
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
171
+ plugin.configResolved(FAKE_RESOLVED)
172
+ const result = await plugin.load('\0virtual:cer-components')
173
+ expect(result).toBe('// components')
174
+ })
175
+
176
+ it('loads virtual:cer-loading module code', async () => {
177
+ const plugin = getCerPlugin()
178
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
179
+ plugin.configResolved(FAKE_RESOLVED)
180
+ const result = await plugin.load('\0virtual:cer-loading')
181
+ expect(result).toBe('// loading')
182
+ })
183
+
184
+ it('loads virtual:cer-error module code', async () => {
185
+ const plugin = getCerPlugin()
186
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
187
+ plugin.configResolved(FAKE_RESOLVED)
188
+ const result = await plugin.load('\0virtual:cer-error')
189
+ expect(result).toBe('// error')
190
+ })
191
+
192
+ it('returns cached code on repeated load calls', async () => {
193
+ const { generateRoutesCode } = await import('../../plugin/virtual/routes.js')
194
+ vi.mocked(generateRoutesCode).mockClear()
195
+
196
+ // Use a SINGLE plugin instance so the module cache persists across calls
197
+ const plugin = getCerPlugin()
198
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
199
+ plugin.configResolved(FAKE_RESOLVED)
200
+
201
+ const first = await plugin.load('\0virtual:cer-routes')
202
+ const second = await plugin.load('\0virtual:cer-routes')
203
+
204
+ expect(first).toBe(second)
205
+ // generateRoutesCode should only be called once (cache hit on second call)
206
+ expect(generateRoutesCode).toHaveBeenCalledTimes(1)
207
+ })
208
+
209
+ it('loads virtual:cer-app-config and exports appConfig', async () => {
210
+ const plugin = getCerPlugin({ mode: 'ssg' })
211
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
212
+ plugin.configResolved(FAKE_RESOLVED)
213
+ const result = await plugin.load('\0virtual:cer-app-config') as string
214
+ expect(result).toContain('appConfig')
215
+ expect(result).toContain('ssg')
216
+ })
217
+ })
218
+
219
+ describe('cerApp plugin — transform hook', () => {
220
+ it('returns null for virtual module ids', async () => {
221
+ const plugin = getCerPlugin()
222
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
223
+ plugin.configResolved(FAKE_RESOLVED)
224
+ const result = plugin.transform('// code', '\0virtual:cer-routes')
225
+ expect(result).toBeNull()
226
+ })
227
+
228
+ it('calls autoImportTransform for regular files', async () => {
229
+ const { autoImportTransform } = await import('../../plugin/transforms/auto-import.js')
230
+ const plugin = getCerPlugin()
231
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
232
+ plugin.configResolved(FAKE_RESOLVED)
233
+
234
+ plugin.transform('const x = 1', '/project/app/pages/index.ts')
235
+ expect(autoImportTransform).toHaveBeenCalled()
236
+ })
237
+
238
+ it('returns null when autoImportTransform returns null', async () => {
239
+ const plugin = getCerPlugin()
240
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
241
+ plugin.configResolved(FAKE_RESOLVED)
242
+ const result = plugin.transform('const x = 1', '/project/app/pages/index.ts')
243
+ expect(result).toBeNull()
244
+ })
245
+
246
+ it('returns { code, map } when autoImportTransform returns a string', async () => {
247
+ const { autoImportTransform } = await import('../../plugin/transforms/auto-import.js')
248
+ vi.mocked(autoImportTransform).mockReturnValueOnce('transformed code')
249
+ const plugin = getCerPlugin()
250
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
251
+ plugin.configResolved(FAKE_RESOLVED)
252
+ const result = plugin.transform('const x = 1', '/project/app/pages/index.ts') as { code: string; map: null }
253
+ expect(result).toEqual({ code: 'transformed code', map: null })
254
+ })
255
+
256
+ it('returns null when autoImports.runtime is false', async () => {
257
+ const plugin = getCerPlugin({ autoImports: { runtime: false } })
258
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
259
+ plugin.configResolved(FAKE_RESOLVED)
260
+ const result = plugin.transform('const x = 1', '/project/app/pages/index.ts')
261
+ expect(result).toBeNull()
262
+ })
263
+ })
264
+
265
+ describe('cerApp plugin — buildStart hook', () => {
266
+ it('calls scanComposableExports on build start', async () => {
267
+ const { scanComposableExports } = await import('../../plugin/dts-generator.js')
268
+ vi.mocked(scanComposableExports).mockClear()
269
+ const plugin = getCerPlugin()
270
+ plugin.config({ root: '/project' }, { command: 'build', mode: 'production' })
271
+ plugin.configResolved(FAKE_RESOLVED)
272
+ await plugin.buildStart()
273
+ expect(scanComposableExports).toHaveBeenCalledTimes(1)
274
+ })
275
+
276
+ it('calls writeAutoImportDts on build start', async () => {
277
+ const { writeAutoImportDts } = await import('../../plugin/dts-generator.js')
278
+ vi.mocked(writeAutoImportDts).mockClear()
279
+ const plugin = getCerPlugin()
280
+ plugin.config({ root: '/project' }, { command: 'build', mode: 'production' })
281
+ plugin.configResolved(FAKE_RESOLVED)
282
+ await plugin.buildStart()
283
+ expect(writeAutoImportDts).toHaveBeenCalledTimes(1)
284
+ })
285
+
286
+ it('calls writeTsconfigPaths on build start', async () => {
287
+ const { writeTsconfigPaths } = await import('../../plugin/dts-generator.js')
288
+ vi.mocked(writeTsconfigPaths).mockClear()
289
+ const plugin = getCerPlugin()
290
+ plugin.config({ root: '/project' }, { command: 'build', mode: 'production' })
291
+ plugin.configResolved(FAKE_RESOLVED)
292
+ await plugin.buildStart()
293
+ expect(writeTsconfigPaths).toHaveBeenCalledTimes(1)
294
+ })
295
+ })
296
+
297
+ describe('cerApp plugin — configureServer hook', () => {
298
+ it('calls scanComposableExports on server configure', async () => {
299
+ const { scanComposableExports } = await import('../../plugin/dts-generator.js')
300
+ vi.mocked(scanComposableExports).mockClear()
301
+ const plugin = getCerPlugin()
302
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
303
+ plugin.configResolved(FAKE_RESOLVED)
304
+
305
+ const mockServer = {
306
+ watcher: { on: vi.fn() },
307
+ moduleGraph: { getModuleById: vi.fn().mockReturnValue(null), invalidateModule: vi.fn() },
308
+ ws: { send: vi.fn() },
309
+ }
310
+ await plugin.configureServer(mockServer)
311
+ expect(scanComposableExports).toHaveBeenCalled()
312
+ })
313
+
314
+ it('calls configureCerDevServer on server configure', async () => {
315
+ const { configureCerDevServer } = await import('../../plugin/dev-server.js')
316
+ vi.mocked(configureCerDevServer).mockClear()
317
+ const plugin = getCerPlugin()
318
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
319
+ plugin.configResolved(FAKE_RESOLVED)
320
+
321
+ const mockServer = {
322
+ watcher: { on: vi.fn() },
323
+ moduleGraph: { getModuleById: vi.fn().mockReturnValue(null), invalidateModule: vi.fn() },
324
+ ws: { send: vi.fn() },
325
+ }
326
+ await plugin.configureServer(mockServer)
327
+ expect(configureCerDevServer).toHaveBeenCalled()
328
+ })
329
+
330
+ it('invokes the file-change watcher callback on add event', async () => {
331
+ const { createWatcher } = await import('../../plugin/scanner.js')
332
+ const { scanComposableExports } = await import('../../plugin/dts-generator.js')
333
+ vi.mocked(scanComposableExports).mockClear()
334
+
335
+ let capturedCallback: ((event: string, file: string) => void) | null = null
336
+ vi.mocked(createWatcher).mockImplementationOnce((_watcher, _dirs, cb) => {
337
+ capturedCallback = cb
338
+ return { on: vi.fn(), close: vi.fn() } as unknown as ReturnType<typeof createWatcher>
339
+ })
340
+
341
+ const plugin = getCerPlugin()
342
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
343
+ plugin.configResolved(FAKE_RESOLVED)
344
+
345
+ const mockServer = {
346
+ watcher: { on: vi.fn() },
347
+ moduleGraph: { getModuleById: vi.fn().mockReturnValue(null), invalidateModule: vi.fn() },
348
+ ws: { send: vi.fn() },
349
+ }
350
+ await plugin.configureServer(mockServer)
351
+
352
+ // Simulate an 'add' event on a pages file — covers getDirtyVirtualIds and watcher callback
353
+ await capturedCallback!('add', '/project/app/pages/new-page.ts')
354
+ expect((mockServer.ws.send as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith({ type: 'full-reload' })
355
+ })
356
+
357
+ it('re-scans composables when a composable file is added', async () => {
358
+ const { createWatcher } = await import('../../plugin/scanner.js')
359
+ const { scanComposableExports } = await import('../../plugin/dts-generator.js')
360
+ vi.mocked(scanComposableExports).mockClear()
361
+
362
+ let capturedCallback: ((event: string, file: string) => void) | null = null
363
+ vi.mocked(createWatcher).mockImplementationOnce((_watcher, _dirs, cb) => {
364
+ capturedCallback = cb
365
+ return { on: vi.fn(), close: vi.fn() } as unknown as ReturnType<typeof createWatcher>
366
+ })
367
+
368
+ const plugin = getCerPlugin()
369
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
370
+ plugin.configResolved(FAKE_RESOLVED)
371
+
372
+ const mockServer = {
373
+ watcher: { on: vi.fn() },
374
+ moduleGraph: { getModuleById: vi.fn().mockReturnValue(null), invalidateModule: vi.fn() },
375
+ ws: { send: vi.fn() },
376
+ }
377
+ await plugin.configureServer(mockServer)
378
+
379
+ const callsBeforeEvent = vi.mocked(scanComposableExports).mock.calls.length
380
+ await capturedCallback!('add', '/project/app/composables/use-new.ts')
381
+ expect(vi.mocked(scanComposableExports).mock.calls.length).toBeGreaterThan(callsBeforeEvent)
382
+ })
383
+
384
+ it('does not trigger HMR on non-add/unlink events', async () => {
385
+ const { createWatcher } = await import('../../plugin/scanner.js')
386
+
387
+ let capturedCallback: ((event: string, file: string) => void) | null = null
388
+ vi.mocked(createWatcher).mockImplementationOnce((_watcher, _dirs, cb) => {
389
+ capturedCallback = cb
390
+ return { on: vi.fn(), close: vi.fn() } as unknown as ReturnType<typeof createWatcher>
391
+ })
392
+
393
+ const plugin = getCerPlugin()
394
+ plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
395
+ plugin.configResolved(FAKE_RESOLVED)
396
+
397
+ const wsSend = vi.fn()
398
+ const mockServer = {
399
+ watcher: { on: vi.fn() },
400
+ moduleGraph: { getModuleById: vi.fn().mockReturnValue(null), invalidateModule: vi.fn() },
401
+ ws: { send: wsSend },
402
+ }
403
+ await plugin.configureServer(mockServer)
404
+ wsSend.mockClear()
405
+
406
+ await capturedCallback!('change', '/project/app/pages/index.ts')
407
+ expect(wsSend).not.toHaveBeenCalled()
408
+ })
409
+ })
@@ -0,0 +1,246 @@
1
+ import { vi, describe, it, expect, beforeEach } from 'vitest'
2
+
3
+ vi.mock('node:fs', async (importOriginal) => {
4
+ const actual = await importOriginal<typeof import('node:fs')>()
5
+ return {
6
+ ...actual,
7
+ existsSync: vi.fn().mockReturnValue(false),
8
+ writeFileSync: vi.fn(),
9
+ readFileSync: vi.fn().mockReturnValue(''),
10
+ }
11
+ })
12
+ vi.mock('../../plugin/scanner.js', () => ({ scanDirectory: vi.fn().mockResolvedValue([]) }))
13
+
14
+ import { existsSync, writeFileSync, readFileSync } from 'node:fs'
15
+ import { scanDirectory } from '../../plugin/scanner.js'
16
+ import {
17
+ writeTsconfigPaths,
18
+ scanComposableExports,
19
+ generateAutoImportDts,
20
+ generateVirtualModuleDts,
21
+ writeAutoImportDts,
22
+ } from '../../plugin/dts-generator.js'
23
+
24
+ const ROOT = '/project'
25
+ const COMPOSABLES_DIR = '/project/app/composables'
26
+
27
+ beforeEach(() => {
28
+ vi.mocked(existsSync).mockReturnValue(false)
29
+ vi.mocked(writeFileSync).mockClear()
30
+ vi.mocked(scanDirectory).mockResolvedValue([])
31
+ vi.mocked(readFileSync).mockReturnValue('')
32
+ })
33
+
34
+ describe('writeTsconfigPaths', () => {
35
+ it('writes cer-tsconfig.json to the root directory', () => {
36
+ writeTsconfigPaths(ROOT, `${ROOT}/app`)
37
+ expect(writeFileSync).toHaveBeenCalledWith(
38
+ `${ROOT}/cer-tsconfig.json`,
39
+ expect.any(String),
40
+ 'utf-8',
41
+ )
42
+ })
43
+
44
+ it('includes ~/\\* path alias in tsconfig', () => {
45
+ writeTsconfigPaths(ROOT, `${ROOT}/app`)
46
+ const content = vi.mocked(writeFileSync).mock.calls[0][1] as string
47
+ expect(content).toContain('~/*')
48
+ })
49
+
50
+ it('includes ~/pages/* path alias', () => {
51
+ writeTsconfigPaths(ROOT, `${ROOT}/app`)
52
+ const content = vi.mocked(writeFileSync).mock.calls[0][1] as string
53
+ expect(content).toContain('~/pages/*')
54
+ })
55
+
56
+ it('generates valid JSON', () => {
57
+ writeTsconfigPaths(ROOT, `${ROOT}/app`)
58
+ const content = vi.mocked(writeFileSync).mock.calls[0][1] as string
59
+ expect(() => JSON.parse(content)).not.toThrow()
60
+ })
61
+
62
+ it('wraps paths in compilerOptions', () => {
63
+ writeTsconfigPaths(ROOT, `${ROOT}/app`)
64
+ const content = vi.mocked(writeFileSync).mock.calls[0][1] as string
65
+ const json = JSON.parse(content)
66
+ expect(json).toHaveProperty('compilerOptions.paths')
67
+ })
68
+ })
69
+
70
+ describe('scanComposableExports', () => {
71
+ it('returns empty map when composablesDir does not exist', async () => {
72
+ const result = await scanComposableExports(COMPOSABLES_DIR)
73
+ expect(result.size).toBe(0)
74
+ })
75
+
76
+ it('returns empty map when no files found', async () => {
77
+ vi.mocked(existsSync).mockReturnValue(true)
78
+ const result = await scanComposableExports(COMPOSABLES_DIR)
79
+ expect(result.size).toBe(0)
80
+ })
81
+
82
+ it('finds exported functions', async () => {
83
+ vi.mocked(existsSync).mockReturnValue(true)
84
+ vi.mocked(scanDirectory).mockResolvedValue([`${COMPOSABLES_DIR}/use-counter.ts`])
85
+ vi.mocked(readFileSync).mockReturnValue('export function useCounter() {}')
86
+ const result = await scanComposableExports(COMPOSABLES_DIR)
87
+ expect(result.has('useCounter')).toBe(true)
88
+ expect(result.get('useCounter')).toBe(`${COMPOSABLES_DIR}/use-counter.ts`)
89
+ })
90
+
91
+ it('finds exported const', async () => {
92
+ vi.mocked(existsSync).mockReturnValue(true)
93
+ vi.mocked(scanDirectory).mockResolvedValue([`${COMPOSABLES_DIR}/use-state.ts`])
94
+ vi.mocked(readFileSync).mockReturnValue('export const useState = () => {}')
95
+ const result = await scanComposableExports(COMPOSABLES_DIR)
96
+ expect(result.has('useState')).toBe(true)
97
+ })
98
+
99
+ it('finds multiple exports from a single file', async () => {
100
+ vi.mocked(existsSync).mockReturnValue(true)
101
+ vi.mocked(scanDirectory).mockResolvedValue([`${COMPOSABLES_DIR}/utils.ts`])
102
+ vi.mocked(readFileSync).mockReturnValue(`
103
+ export function useFoo() {}
104
+ export const useBar = () => {}
105
+ `)
106
+ const result = await scanComposableExports(COMPOSABLES_DIR)
107
+ expect(result.has('useFoo')).toBe(true)
108
+ expect(result.has('useBar')).toBe(true)
109
+ })
110
+
111
+ it('handles multiple files', async () => {
112
+ vi.mocked(existsSync).mockReturnValue(true)
113
+ vi.mocked(scanDirectory).mockResolvedValue([
114
+ `${COMPOSABLES_DIR}/a.ts`,
115
+ `${COMPOSABLES_DIR}/b.ts`,
116
+ ])
117
+ vi.mocked(readFileSync)
118
+ .mockReturnValueOnce('export function useA() {}')
119
+ .mockReturnValueOnce('export function useB() {}')
120
+ const result = await scanComposableExports(COMPOSABLES_DIR)
121
+ expect(result.has('useA')).toBe(true)
122
+ expect(result.has('useB')).toBe(true)
123
+ })
124
+ })
125
+
126
+ describe('generateAutoImportDts', () => {
127
+ it('includes AUTO-GENERATED comment', async () => {
128
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
129
+ expect(dts).toContain('AUTO-GENERATED')
130
+ })
131
+
132
+ it('declares component as a global', async () => {
133
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
134
+ expect(dts).toContain("const component: typeof import('@jasonshimmy/custom-elements-runtime')['component']")
135
+ })
136
+
137
+ it('declares html as a global', async () => {
138
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
139
+ expect(dts).toContain("const html: typeof import('@jasonshimmy/custom-elements-runtime')['html']")
140
+ })
141
+
142
+ it('declares ref as a global', async () => {
143
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
144
+ expect(dts).toContain("const ref: typeof import('@jasonshimmy/custom-elements-runtime')['ref']")
145
+ })
146
+
147
+ it('declares useHead as a framework global', async () => {
148
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
149
+ expect(dts).toContain("const useHead: typeof import('@jasonshimmy/vite-plugin-cer-app/composables')['useHead']")
150
+ })
151
+
152
+ it('declares usePageData as a framework global', async () => {
153
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
154
+ expect(dts).toContain("const usePageData: typeof import('@jasonshimmy/vite-plugin-cer-app/composables')['usePageData']")
155
+ })
156
+
157
+ it('declares when directive as a global', async () => {
158
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
159
+ expect(dts).toContain("const when: typeof import('@jasonshimmy/custom-elements-runtime/directives')['when']")
160
+ })
161
+
162
+ it('declares __CER_DATA__ global variable', async () => {
163
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
164
+ expect(dts).toContain('var __CER_DATA__')
165
+ })
166
+
167
+ it('wraps declarations in declare global block', async () => {
168
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR)
169
+ expect(dts).toContain('declare global {')
170
+ expect(dts).toContain('}')
171
+ })
172
+
173
+ it('includes user composable exports when provided', async () => {
174
+ const exports = new Map([['useMyThing', `${COMPOSABLES_DIR}/my-thing.ts`]])
175
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR, exports)
176
+ expect(dts).toContain('useMyThing')
177
+ })
178
+
179
+ it('uses relative path for user composables', async () => {
180
+ const exports = new Map([['useFoo', `${ROOT}/app/composables/foo.ts`]])
181
+ const dts = await generateAutoImportDts(ROOT, COMPOSABLES_DIR, exports)
182
+ // Path should be relative from root
183
+ expect(dts).toContain('./app/composables/foo')
184
+ })
185
+ })
186
+
187
+ describe('generateVirtualModuleDts', () => {
188
+ it('includes AUTO-GENERATED comment', async () => {
189
+ const dts = await generateVirtualModuleDts(ROOT, COMPOSABLES_DIR)
190
+ expect(dts).toContain('AUTO-GENERATED')
191
+ })
192
+
193
+ it('declares virtual:cer-routes module', async () => {
194
+ const dts = await generateVirtualModuleDts(ROOT, COMPOSABLES_DIR)
195
+ expect(dts).toContain("declare module 'virtual:cer-routes'")
196
+ })
197
+
198
+ it('declares virtual:cer-layouts module', async () => {
199
+ const dts = await generateVirtualModuleDts(ROOT, COMPOSABLES_DIR)
200
+ expect(dts).toContain("declare module 'virtual:cer-layouts'")
201
+ })
202
+
203
+ it('declares virtual:cer-plugins module', async () => {
204
+ const dts = await generateVirtualModuleDts(ROOT, COMPOSABLES_DIR)
205
+ expect(dts).toContain("declare module 'virtual:cer-plugins'")
206
+ })
207
+
208
+ it('declares virtual:cer-loading module with hasLoading and loadingTag', async () => {
209
+ const dts = await generateVirtualModuleDts(ROOT, COMPOSABLES_DIR)
210
+ expect(dts).toContain("declare module 'virtual:cer-loading'")
211
+ expect(dts).toContain('hasLoading')
212
+ expect(dts).toContain('loadingTag')
213
+ })
214
+
215
+ it('declares virtual:cer-error module with hasError and errorTag', async () => {
216
+ const dts = await generateVirtualModuleDts(ROOT, COMPOSABLES_DIR)
217
+ expect(dts).toContain("declare module 'virtual:cer-error'")
218
+ expect(dts).toContain('hasError')
219
+ expect(dts).toContain('errorTag')
220
+ })
221
+
222
+ it('includes user composable re-exports in virtual:cer-composables', async () => {
223
+ const exports = new Map([['useMyThing', `${ROOT}/app/composables/my-thing.ts`]])
224
+ const dts = await generateVirtualModuleDts(ROOT, COMPOSABLES_DIR, exports)
225
+ expect(dts).toContain('useMyThing')
226
+ })
227
+ })
228
+
229
+ describe('writeAutoImportDts', () => {
230
+ it('writes cer-auto-imports.d.ts to root', async () => {
231
+ await writeAutoImportDts(ROOT, COMPOSABLES_DIR)
232
+ const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
233
+ expect(paths.some(p => p.includes('cer-auto-imports.d.ts'))).toBe(true)
234
+ })
235
+
236
+ it('writes cer-env.d.ts to root', async () => {
237
+ await writeAutoImportDts(ROOT, COMPOSABLES_DIR)
238
+ const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
239
+ expect(paths.some(p => p.includes('cer-env.d.ts'))).toBe(true)
240
+ })
241
+
242
+ it('writes exactly two files', async () => {
243
+ await writeAutoImportDts(ROOT, COMPOSABLES_DIR)
244
+ expect(writeFileSync).toHaveBeenCalledTimes(2)
245
+ })
246
+ })