@jasonshimmy/vite-plugin-cer-app 0.1.0 → 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 (172) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/IMPLEMENTATION_PLAN.md +1 -1
  3. package/README.md +7 -7
  4. package/VITE_PLUGIN_FRAMEWORK_PLAN.md +12 -12
  5. package/commits.txt +1 -3
  6. package/dist/cli/commands/generate.d.ts.map +1 -1
  7. package/dist/cli/commands/generate.js +2 -0
  8. package/dist/cli/commands/generate.js.map +1 -1
  9. package/dist/cli/commands/preview.d.ts.map +1 -1
  10. package/dist/cli/commands/preview.js +21 -2
  11. package/dist/cli/commands/preview.js.map +1 -1
  12. package/dist/cli/create/index.js +2 -2
  13. package/dist/cli/create/index.js.map +1 -1
  14. package/dist/cli/index.js +1 -1
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/plugin/build-ssg.d.ts.map +1 -1
  17. package/dist/plugin/build-ssg.js +10 -21
  18. package/dist/plugin/build-ssg.js.map +1 -1
  19. package/dist/plugin/build-ssr.d.ts.map +1 -1
  20. package/dist/plugin/build-ssr.js +151 -28
  21. package/dist/plugin/build-ssr.js.map +1 -1
  22. package/dist/plugin/dts-generator.js +4 -4
  23. package/dist/plugin/dts-generator.js.map +1 -1
  24. package/dist/plugin/index.js +2 -2
  25. package/dist/plugin/index.js.map +1 -1
  26. package/dist/plugin/transforms/auto-import.js +3 -3
  27. package/dist/plugin/transforms/auto-import.js.map +1 -1
  28. package/dist/plugin/virtual/components.js +3 -3
  29. package/dist/plugin/virtual/components.js.map +1 -1
  30. package/dist/plugin/virtual/composables.js +3 -3
  31. package/dist/plugin/virtual/composables.js.map +1 -1
  32. package/dist/plugin/virtual/error.js +2 -2
  33. package/dist/plugin/virtual/error.js.map +1 -1
  34. package/dist/plugin/virtual/layouts.js +3 -3
  35. package/dist/plugin/virtual/layouts.js.map +1 -1
  36. package/dist/plugin/virtual/loading.js +2 -2
  37. package/dist/plugin/virtual/loading.js.map +1 -1
  38. package/dist/plugin/virtual/middleware.js +3 -3
  39. package/dist/plugin/virtual/middleware.js.map +1 -1
  40. package/dist/plugin/virtual/plugins.js +3 -3
  41. package/dist/plugin/virtual/plugins.js.map +1 -1
  42. package/dist/plugin/virtual/routes.d.ts.map +1 -1
  43. package/dist/plugin/virtual/routes.js +14 -4
  44. package/dist/plugin/virtual/routes.js.map +1 -1
  45. package/dist/plugin/virtual/server-api.js +3 -3
  46. package/dist/plugin/virtual/server-api.js.map +1 -1
  47. package/dist/plugin/virtual/server-middleware.js +3 -3
  48. package/dist/plugin/virtual/server-middleware.js.map +1 -1
  49. package/dist/runtime/app-template.d.ts +1 -1
  50. package/dist/runtime/app-template.d.ts.map +1 -1
  51. package/dist/runtime/app-template.js +6 -0
  52. package/dist/runtime/app-template.js.map +1 -1
  53. package/dist/runtime/composables/use-page-data.d.ts +15 -6
  54. package/dist/runtime/composables/use-page-data.d.ts.map +1 -1
  55. package/dist/runtime/composables/use-page-data.js +30 -9
  56. package/dist/runtime/composables/use-page-data.js.map +1 -1
  57. package/dist/runtime/entry-server-template.d.ts +1 -1
  58. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  59. package/dist/runtime/entry-server-template.js +138 -17
  60. package/dist/runtime/entry-server-template.js.map +1 -1
  61. package/docs/cli.md +2 -2
  62. package/docs/configuration.md +4 -4
  63. package/docs/data-loading.md +8 -7
  64. package/docs/getting-started.md +5 -5
  65. package/docs/head-management.md +3 -3
  66. package/docs/middleware.md +2 -2
  67. package/docs/plugins.md +1 -1
  68. package/docs/rendering-modes.md +1 -1
  69. package/docs/routing.md +1 -1
  70. package/docs/server-api.md +10 -1
  71. package/docs/testing.md +4 -4
  72. package/package.json +1 -1
  73. package/src/__tests__/index.test.ts +21 -0
  74. package/src/__tests__/plugin/build-ssg.test.ts +265 -0
  75. package/src/__tests__/plugin/build-ssr.test.ts +180 -0
  76. package/src/__tests__/plugin/cer-app-plugin.test.ts +409 -0
  77. package/src/__tests__/plugin/dts-generator.test.ts +246 -0
  78. package/src/__tests__/plugin/resolve-config.test.ts +158 -0
  79. package/src/__tests__/plugin/transforms/auto-import.test.ts +1 -1
  80. package/src/__tests__/plugin/virtual/components.test.ts +1 -1
  81. package/src/__tests__/plugin/virtual/composables.test.ts +1 -1
  82. package/src/__tests__/plugin/virtual/error.test.ts +71 -0
  83. package/src/__tests__/plugin/virtual/layouts.test.ts +1 -1
  84. package/src/__tests__/plugin/virtual/loading.test.ts +72 -0
  85. package/src/__tests__/plugin/virtual/middleware.test.ts +1 -1
  86. package/src/__tests__/plugin/virtual/plugins.test.ts +1 -1
  87. package/src/__tests__/plugin/virtual/routes.test.ts +1 -1
  88. package/src/__tests__/plugin/virtual/server-api.test.ts +1 -1
  89. package/src/__tests__/plugin/virtual/server-middleware.test.ts +102 -0
  90. package/src/__tests__/runtime/use-page-data.test.ts +81 -5
  91. package/src/__tests__/types/config.test.ts +23 -0
  92. package/src/cli/commands/generate.ts +2 -0
  93. package/src/cli/commands/preview.ts +21 -2
  94. package/src/cli/create/index.ts +2 -2
  95. package/src/cli/create/templates/spa/cer.config.ts.tpl +1 -1
  96. package/src/cli/create/templates/spa/package.json.tpl +1 -1
  97. package/src/cli/create/templates/ssg/cer.config.ts.tpl +1 -1
  98. package/src/cli/create/templates/ssg/package.json.tpl +1 -1
  99. package/src/cli/create/templates/ssr/cer.config.ts.tpl +1 -1
  100. package/src/cli/create/templates/ssr/package.json.tpl +1 -1
  101. package/src/cli/index.ts +1 -1
  102. package/src/plugin/build-ssg.ts +9 -22
  103. package/src/plugin/build-ssr.ts +150 -28
  104. package/src/plugin/dts-generator.ts +4 -4
  105. package/src/plugin/index.ts +2 -2
  106. package/src/plugin/transforms/auto-import.ts +3 -3
  107. package/src/plugin/virtual/components.ts +3 -3
  108. package/src/plugin/virtual/composables.ts +3 -3
  109. package/src/plugin/virtual/error.ts +2 -2
  110. package/src/plugin/virtual/layouts.ts +3 -3
  111. package/src/plugin/virtual/loading.ts +2 -2
  112. package/src/plugin/virtual/middleware.ts +3 -3
  113. package/src/plugin/virtual/plugins.ts +3 -3
  114. package/src/plugin/virtual/routes.ts +15 -4
  115. package/src/plugin/virtual/server-api.ts +3 -3
  116. package/src/plugin/virtual/server-middleware.ts +3 -3
  117. package/src/runtime/app-template.ts +6 -0
  118. package/src/runtime/composables/use-page-data.ts +31 -9
  119. package/src/runtime/entry-server-template.ts +138 -17
  120. package/tsconfig.build.json +1 -1
  121. package/dist/__tests__/plugin/path-utils.test.d.ts +0 -2
  122. package/dist/__tests__/plugin/path-utils.test.d.ts.map +0 -1
  123. package/dist/__tests__/plugin/path-utils.test.js +0 -305
  124. package/dist/__tests__/plugin/path-utils.test.js.map +0 -1
  125. package/dist/__tests__/plugin/scanner.test.d.ts +0 -2
  126. package/dist/__tests__/plugin/scanner.test.d.ts.map +0 -1
  127. package/dist/__tests__/plugin/scanner.test.js +0 -143
  128. package/dist/__tests__/plugin/scanner.test.js.map +0 -1
  129. package/dist/__tests__/plugin/transforms/auto-import.test.d.ts +0 -2
  130. package/dist/__tests__/plugin/transforms/auto-import.test.d.ts.map +0 -1
  131. package/dist/__tests__/plugin/transforms/auto-import.test.js +0 -151
  132. package/dist/__tests__/plugin/transforms/auto-import.test.js.map +0 -1
  133. package/dist/__tests__/plugin/transforms/head-inject.test.d.ts +0 -2
  134. package/dist/__tests__/plugin/transforms/head-inject.test.d.ts.map +0 -1
  135. package/dist/__tests__/plugin/transforms/head-inject.test.js +0 -151
  136. package/dist/__tests__/plugin/transforms/head-inject.test.js.map +0 -1
  137. package/dist/__tests__/plugin/virtual/components.test.d.ts +0 -2
  138. package/dist/__tests__/plugin/virtual/components.test.d.ts.map +0 -1
  139. package/dist/__tests__/plugin/virtual/components.test.js +0 -47
  140. package/dist/__tests__/plugin/virtual/components.test.js.map +0 -1
  141. package/dist/__tests__/plugin/virtual/composables.test.d.ts +0 -2
  142. package/dist/__tests__/plugin/virtual/composables.test.d.ts.map +0 -1
  143. package/dist/__tests__/plugin/virtual/composables.test.js +0 -48
  144. package/dist/__tests__/plugin/virtual/composables.test.js.map +0 -1
  145. package/dist/__tests__/plugin/virtual/layouts.test.d.ts +0 -2
  146. package/dist/__tests__/plugin/virtual/layouts.test.d.ts.map +0 -1
  147. package/dist/__tests__/plugin/virtual/layouts.test.js +0 -59
  148. package/dist/__tests__/plugin/virtual/layouts.test.js.map +0 -1
  149. package/dist/__tests__/plugin/virtual/middleware.test.d.ts +0 -2
  150. package/dist/__tests__/plugin/virtual/middleware.test.d.ts.map +0 -1
  151. package/dist/__tests__/plugin/virtual/middleware.test.js +0 -58
  152. package/dist/__tests__/plugin/virtual/middleware.test.js.map +0 -1
  153. package/dist/__tests__/plugin/virtual/plugins.test.d.ts +0 -2
  154. package/dist/__tests__/plugin/virtual/plugins.test.d.ts.map +0 -1
  155. package/dist/__tests__/plugin/virtual/plugins.test.js +0 -73
  156. package/dist/__tests__/plugin/virtual/plugins.test.js.map +0 -1
  157. package/dist/__tests__/plugin/virtual/routes.test.d.ts +0 -2
  158. package/dist/__tests__/plugin/virtual/routes.test.d.ts.map +0 -1
  159. package/dist/__tests__/plugin/virtual/routes.test.js +0 -167
  160. package/dist/__tests__/plugin/virtual/routes.test.js.map +0 -1
  161. package/dist/__tests__/plugin/virtual/server-api.test.d.ts +0 -2
  162. package/dist/__tests__/plugin/virtual/server-api.test.d.ts.map +0 -1
  163. package/dist/__tests__/plugin/virtual/server-api.test.js +0 -72
  164. package/dist/__tests__/plugin/virtual/server-api.test.js.map +0 -1
  165. package/dist/__tests__/runtime/use-head.test.d.ts +0 -2
  166. package/dist/__tests__/runtime/use-head.test.d.ts.map +0 -1
  167. package/dist/__tests__/runtime/use-head.test.js +0 -202
  168. package/dist/__tests__/runtime/use-head.test.js.map +0 -1
  169. package/dist/__tests__/runtime/use-page-data.test.d.ts +0 -2
  170. package/dist/__tests__/runtime/use-page-data.test.d.ts.map +0 -1
  171. package/dist/__tests__/runtime/use-page-data.test.js +0 -41
  172. package/dist/__tests__/runtime/use-page-data.test.js.map +0 -1
@@ -0,0 +1,180 @@
1
+ import { vi, describe, it, expect, beforeEach } from 'vitest'
2
+ import { readFileSync } from 'node:fs'
3
+ import { resolve } from 'pathe'
4
+
5
+ // We test the server entry code generation by importing just that function.
6
+ // The `buildSSR` function itself invokes Vite's `build` API which we don't
7
+ // need to exercise in unit tests (it's an integration concern).
8
+ vi.mock('vite', () => ({ build: vi.fn().mockResolvedValue(undefined) }))
9
+ // Partial mock: keep the real readFileSync/existsSync but allow overrides in
10
+ // individual describe blocks if needed.
11
+ vi.mock('node:fs', async (importOriginal) => {
12
+ const actual = await importOriginal<typeof import('node:fs')>()
13
+ return { ...actual, existsSync: vi.fn().mockReturnValue(true) }
14
+ })
15
+
16
+ import type { ResolvedCerConfig } from '../../plugin/dev-server.js'
17
+
18
+ // Build a minimal ResolvedCerConfig so we can call generateServerEntryCode
19
+ // without spinning up a real Vite build.
20
+ function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConfig {
21
+ return {
22
+ root: '/project',
23
+ srcDir: '/project/app',
24
+ mode: 'ssr',
25
+ ssr: { dsd: true },
26
+ ssg: { paths: [], concurrency: 4 },
27
+ ...overrides,
28
+ } as unknown as ResolvedCerConfig
29
+ }
30
+
31
+ describe('build-ssr generateServerEntryCode (template content)', () => {
32
+ // Read the source of build-ssr.ts to assert it contains the expected
33
+ // generated code strings. This is intentionally coarse-grained:
34
+ // we check that the template emits the right imports, exports, and
35
+ // structural elements rather than testing every character.
36
+ const src = readFileSync(
37
+ resolve(import.meta.dirname, '../../plugin/build-ssr.ts'),
38
+ 'utf-8',
39
+ )
40
+
41
+ it('template imports registerBuiltinComponents from custom-elements-runtime', () => {
42
+ expect(src).toContain('registerBuiltinComponents')
43
+ })
44
+
45
+ it('template imports renderToStringWithJITCSS from ssr subpath', () => {
46
+ expect(src).toContain('renderToStringWithJITCSS')
47
+ expect(src).toContain('custom-elements-runtime/ssr')
48
+ })
49
+
50
+ it('template imports initRouter from router subpath', () => {
51
+ expect(src).toContain('initRouter')
52
+ expect(src).toContain('custom-elements-runtime/router')
53
+ })
54
+
55
+ it('template loads client index.html for merging', () => {
56
+ expect(src).toContain('_clientTemplate')
57
+ expect(src).toContain('../client/index.html')
58
+ })
59
+
60
+ it('template defines _mergeWithClientTemplate helper', () => {
61
+ expect(src).toContain('_mergeWithClientTemplate')
62
+ })
63
+
64
+ it('template defines _prepareRequest async function', () => {
65
+ expect(src).toContain('_prepareRequest')
66
+ })
67
+
68
+ it('template exports handler as both named and default export', () => {
69
+ expect(src).toContain('export const handler')
70
+ expect(src).toContain('export default handler')
71
+ })
72
+
73
+ it('template exports apiRoutes, plugins, and layouts', () => {
74
+ expect(src).toContain('export { apiRoutes, plugins, layouts }')
75
+ })
76
+
77
+ it('template sets globalThis.__CER_DATA__ synchronously before render', () => {
78
+ expect(src).toContain('globalThis).__CER_DATA__ = loaderData')
79
+ })
80
+
81
+ it('template deletes __CER_DATA__ after render', () => {
82
+ expect(src).toContain('delete (globalThis).__CER_DATA__')
83
+ })
84
+
85
+ it('template uses renderToStringWithJITCSSDSD (dsd always on)', () => {
86
+ expect(src).toContain('renderToStringWithJITCSSDSD')
87
+ })
88
+
89
+ it('template passes dsdPolyfill: false to suppress inline polyfill', () => {
90
+ expect(src).toContain('dsdPolyfill: false')
91
+ })
92
+
93
+ it('template calls registerEntityMap with entities.json', () => {
94
+ expect(src).toContain('registerEntityMap(entitiesJson)')
95
+ expect(src).toContain('entities.json')
96
+ })
97
+
98
+ it('template imports DSD_POLYFILL_SCRIPT and injects before </body>', () => {
99
+ expect(src).toContain('DSD_POLYFILL_SCRIPT')
100
+ expect(src).toContain("finalHtml.replace('</body>'")
101
+ })
102
+
103
+ it('template merges SSR html with client template when available', () => {
104
+ expect(src).toContain('_clientTemplate')
105
+ expect(src).toContain('_mergeWithClientTemplate(ssrHtml, _clientTemplate)')
106
+ })
107
+
108
+ it('template reads virtual:cer-routes', () => {
109
+ expect(src).toContain('virtual:cer-routes')
110
+ })
111
+
112
+ it('template reads virtual:cer-layouts', () => {
113
+ expect(src).toContain('virtual:cer-layouts')
114
+ })
115
+
116
+ it('template reads virtual:cer-plugins', () => {
117
+ expect(src).toContain('virtual:cer-plugins')
118
+ })
119
+
120
+ it('template reads virtual:cer-server-api', () => {
121
+ expect(src).toContain('virtual:cer-server-api')
122
+ })
123
+
124
+ it('template reads virtual:cer-components', () => {
125
+ expect(src).toContain('virtual:cer-components')
126
+ })
127
+
128
+ it('sets Content-Type header on response', () => {
129
+ expect(src).toContain('text/html; charset=utf-8')
130
+ })
131
+ })
132
+
133
+ describe('buildSSR', () => {
134
+ let buildMock: ReturnType<typeof vi.fn>
135
+ let buildSSR: (config: ResolvedCerConfig, userConfig?: Record<string, unknown>) => Promise<void>
136
+
137
+ beforeEach(async () => {
138
+ const { build } = await import('vite')
139
+ buildMock = vi.mocked(build)
140
+ buildMock.mockClear()
141
+ buildMock.mockResolvedValue(undefined as never)
142
+ ;({ buildSSR } = await import('../../plugin/build-ssr.js'))
143
+ })
144
+
145
+ it('calls vite build twice (client then server)', async () => {
146
+ await buildSSR(makeConfig())
147
+ expect(buildMock).toHaveBeenCalledTimes(2)
148
+ })
149
+
150
+ it('first build targets dist/client output dir', async () => {
151
+ await buildSSR(makeConfig())
152
+ const firstCall = buildMock.mock.calls[0][0] as Record<string, unknown>
153
+ expect((firstCall.build as Record<string, unknown>).outDir).toContain('dist/client')
154
+ })
155
+
156
+ it('second build targets dist/server output dir', async () => {
157
+ await buildSSR(makeConfig())
158
+ const secondCall = buildMock.mock.calls[1][0] as Record<string, unknown>
159
+ expect((secondCall.build as Record<string, unknown>).outDir).toContain('dist/server')
160
+ })
161
+
162
+ it('second build has ssr:true', async () => {
163
+ await buildSSR(makeConfig())
164
+ const secondCall = buildMock.mock.calls[1][0] as Record<string, unknown>
165
+ expect((secondCall.build as Record<string, unknown>).ssr).toBe(true)
166
+ })
167
+
168
+ it('server bundle entry file is named server.js', async () => {
169
+ await buildSSR(makeConfig())
170
+ const secondCall = buildMock.mock.calls[1][0] as Record<string, unknown>
171
+ const rollup = (secondCall.build as Record<string, unknown>).rollupOptions as Record<string, unknown>
172
+ expect((rollup.output as Record<string, unknown>).entryFileNames).toBe('server.js')
173
+ })
174
+
175
+ it('merges user viteUserConfig into client build', async () => {
176
+ await buildSSR(makeConfig(), { define: { MY_FLAG: 'true' } })
177
+ const firstCall = buildMock.mock.calls[0][0] as Record<string, unknown>
178
+ expect(firstCall.define).toEqual({ MY_FLAG: 'true' })
179
+ })
180
+ })
@@ -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
+ })