@jasonshimmy/vite-plugin-cer-app 0.1.6 → 0.2.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.
Files changed (61) hide show
  1. package/.github/workflows/publish.yml +56 -5
  2. package/CHANGELOG.md +4 -0
  3. package/commits.txt +1 -1
  4. package/dist/cli/commands/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.js +19 -5
  6. package/dist/cli/commands/build.js.map +1 -1
  7. package/dist/cli/commands/dev.js +1 -1
  8. package/dist/cli/commands/dev.js.map +1 -1
  9. package/dist/cli/commands/preview.d.ts.map +1 -1
  10. package/dist/cli/commands/preview.js +0 -1
  11. package/dist/cli/commands/preview.js.map +1 -1
  12. package/dist/plugin/build-ssg.d.ts.map +1 -1
  13. package/dist/plugin/build-ssg.js.map +1 -1
  14. package/dist/plugin/build-ssr.d.ts +10 -0
  15. package/dist/plugin/build-ssr.d.ts.map +1 -1
  16. package/dist/plugin/build-ssr.js +21 -8
  17. package/dist/plugin/build-ssr.js.map +1 -1
  18. package/dist/plugin/dev-server.d.ts.map +1 -1
  19. package/dist/plugin/dev-server.js +0 -2
  20. package/dist/plugin/dev-server.js.map +1 -1
  21. package/dist/plugin/dts-generator.d.ts +4 -4
  22. package/dist/plugin/dts-generator.d.ts.map +1 -1
  23. package/dist/plugin/dts-generator.js +39 -19
  24. package/dist/plugin/dts-generator.js.map +1 -1
  25. package/dist/plugin/generated-dir.d.ts +34 -0
  26. package/dist/plugin/generated-dir.d.ts.map +1 -0
  27. package/dist/plugin/generated-dir.js +94 -0
  28. package/dist/plugin/generated-dir.js.map +1 -0
  29. package/dist/plugin/index.d.ts.map +1 -1
  30. package/dist/plugin/index.js +27 -0
  31. package/dist/plugin/index.js.map +1 -1
  32. package/dist/plugin/path-utils.js.map +1 -1
  33. package/dist/plugin/virtual/loading.d.ts.map +1 -1
  34. package/dist/plugin/virtual/loading.js.map +1 -1
  35. package/dist/runtime/app-template.d.ts +8 -0
  36. package/dist/runtime/app-template.d.ts.map +1 -0
  37. package/dist/runtime/app-template.js +158 -0
  38. package/dist/runtime/app-template.js.map +1 -0
  39. package/docs/configuration.md +2 -2
  40. package/docs/rendering-modes.md +2 -2
  41. package/docs/routing.md +1 -1
  42. package/e2e/kitchen-sink/tsconfig.json +3 -0
  43. package/eslint.config.ts +22 -0
  44. package/package.json +6 -1
  45. package/src/__tests__/plugin/build-ssr.test.ts +24 -10
  46. package/src/__tests__/plugin/cer-app-plugin.test.ts +35 -0
  47. package/src/__tests__/plugin/dts-generator.test.ts +15 -6
  48. package/src/__tests__/plugin/generated-dir.test.ts +168 -0
  49. package/src/cli/commands/build.ts +19 -5
  50. package/src/cli/commands/dev.ts +2 -2
  51. package/src/cli/commands/preview.ts +7 -5
  52. package/src/plugin/build-ssg.ts +2 -2
  53. package/src/plugin/build-ssr.ts +22 -8
  54. package/src/plugin/dev-server.ts +4 -3
  55. package/src/plugin/dts-generator.ts +43 -19
  56. package/src/plugin/generated-dir.ts +102 -0
  57. package/src/plugin/index.ts +32 -1
  58. package/src/plugin/path-utils.ts +1 -1
  59. package/src/plugin/virtual/loading.ts +0 -1
  60. package/{e2e/kitchen-sink/app/app.ts → src/runtime/app-template.ts} +23 -7
  61. package/e2e/kitchen-sink/index.html +0 -12
@@ -0,0 +1,102 @@
1
+ import { writeFileSync, existsSync, mkdirSync, readFileSync, appendFileSync } from 'node:fs'
2
+ import { join } from 'pathe'
3
+ import type { ResolvedCerConfig } from './dev-server.js'
4
+ import { APP_ENTRY_TEMPLATE } from '../runtime/app-template.js'
5
+
6
+ /** The name of the generated directory relative to the project root. */
7
+ export const GENERATED_DIR_NAME = '.cer'
8
+
9
+ /**
10
+ * Returns the absolute path to the .cer/ generated directory.
11
+ */
12
+ export function getGeneratedDir(root: string): string {
13
+ return join(root, GENERATED_DIR_NAME)
14
+ }
15
+
16
+ /**
17
+ * Returns the app entry file path to use for builds and the dev server.
18
+ * Prefers the consumer's `app/app.ts` when it exists; falls back to `.cer/app.ts`.
19
+ */
20
+ export function resolveAppEntry(config: ResolvedCerConfig): string {
21
+ const userEntry = join(config.srcDir, 'app.ts')
22
+ if (existsSync(userEntry)) return userEntry
23
+ return join(getGeneratedDir(config.root), 'app.ts')
24
+ }
25
+
26
+ /**
27
+ * Returns the HTML entry path to use for builds.
28
+ * Prefers the consumer's root-level `index.html` when it exists;
29
+ * falls back to `.cer/index.html`.
30
+ */
31
+ export function resolveHtmlEntry(config: ResolvedCerConfig): string {
32
+ const userHtml = join(config.root, 'index.html')
33
+ if (existsSync(userHtml)) return userHtml
34
+ return join(getGeneratedDir(config.root), 'index.html')
35
+ }
36
+
37
+ /**
38
+ * Generates the content for a default `index.html`.
39
+ * The script src points to the consumer's `app/app.ts` if it exists,
40
+ * otherwise to the generated `.cer/app.ts`.
41
+ */
42
+ export function generateDefaultHtml(config: ResolvedCerConfig): string {
43
+ const userEntry = join(config.srcDir, 'app.ts')
44
+ const scriptSrc = existsSync(userEntry) ? '/app/app.ts' : '/.cer/app.ts'
45
+ return `<!DOCTYPE html>
46
+ <html lang="en">
47
+ <head>
48
+ <meta charset="UTF-8" />
49
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
50
+ <title>CER App</title>
51
+ </head>
52
+ <body>
53
+ <cer-layout-view></cer-layout-view>
54
+ <script type="module" src="${scriptSrc}"></script>
55
+ </body>
56
+ </html>
57
+ `
58
+ }
59
+
60
+ /**
61
+ * Ensures `.cer/` is listed in the project's `.gitignore`.
62
+ * Creates `.gitignore` if it does not exist.
63
+ */
64
+ function ensureGitignore(root: string): void {
65
+ const gitignorePath = join(root, '.gitignore')
66
+ const entry = `${GENERATED_DIR_NAME}/`
67
+
68
+ if (existsSync(gitignorePath)) {
69
+ const content = readFileSync(gitignorePath, 'utf-8')
70
+ if (!content.includes(entry) && !content.includes(`${GENERATED_DIR_NAME}\n`)) {
71
+ appendFileSync(gitignorePath, `\n# CER App generated directory\n${entry}\n`)
72
+ }
73
+ } else {
74
+ writeFileSync(gitignorePath, `# CER App generated directory\n${entry}\n`)
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Writes all generated files to `.cer/`:
80
+ * - `.cer/app.ts` — default entry (only when `app/app.ts` does not exist)
81
+ * - `.cer/index.html` — default HTML shell
82
+ * - `.cer/tsconfig.json` — written by dts-generator via writeTsconfigPaths
83
+ *
84
+ * Also ensures `.cer/` is listed in `.gitignore`.
85
+ */
86
+ export function writeGeneratedDir(config: ResolvedCerConfig): void {
87
+ const dir = getGeneratedDir(config.root)
88
+ if (!existsSync(dir)) {
89
+ mkdirSync(dir, { recursive: true })
90
+ }
91
+
92
+ // Write default app.ts only when the consumer has not provided one.
93
+ const userEntry = join(config.srcDir, 'app.ts')
94
+ if (!existsSync(userEntry)) {
95
+ writeFileSync(join(dir, 'app.ts'), APP_ENTRY_TEMPLATE, 'utf-8')
96
+ }
97
+
98
+ // Always write the default index.html so builds and the dev server can use it.
99
+ writeFileSync(join(dir, 'index.html'), generateDefaultHtml(config), 'utf-8')
100
+
101
+ ensureGitignore(config.root)
102
+ }
@@ -1,11 +1,13 @@
1
1
  import { resolve, join } from 'pathe'
2
- import type { Plugin, ViteDevServer, ModuleNode } from 'vite'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import type { Plugin, ViteDevServer } from 'vite'
3
4
  import type { CerAppConfig } from '../types/config.js'
4
5
  import type { ResolvedCerConfig } from './dev-server.js'
5
6
  import { cerPlugin } from '@jasonshimmy/custom-elements-runtime/vite-plugin'
6
7
  import { autoImportTransform } from './transforms/auto-import.js'
7
8
  import { scanComposableExports, writeAutoImportDts, writeTsconfigPaths } from './dts-generator.js'
8
9
  import { configureCerDevServer } from './dev-server.js'
10
+ import { writeGeneratedDir, getGeneratedDir } from './generated-dir.js'
9
11
  import { generateRoutesCode } from './virtual/routes.js'
10
12
  import { generateLayoutsCode } from './virtual/layouts.js'
11
13
  import { generateComponentsCode } from './virtual/components.js'
@@ -256,11 +258,38 @@ export function cerApp(userConfig: CerAppConfig = {}): Plugin[] {
256
258
  // config might not be set yet; resolve with cwd
257
259
  config = resolveConfig(userConfig, process.cwd())
258
260
  }
261
+
262
+ // Write .cer/ generated files (app.ts fallback, index.html, .gitignore)
263
+ writeGeneratedDir(config)
264
+
259
265
  // Scan composables and write .d.ts + tsconfig paths on dev server start
260
266
  composableExports = await scanComposableExports(config.composablesDir)
261
267
  await writeAutoImportDts(config.root, config.composablesDir, composableExports)
262
268
  writeTsconfigPaths(config.root, config.srcDir)
263
269
 
270
+ // Serve a generated index.html for HTML requests when the consumer has
271
+ // not provided one. This runs BEFORE configureCerDevServer so that the
272
+ // Vite HTML pipeline (HMR injection, module preprocessing) is applied.
273
+ const userHtml = resolve(config.root, 'index.html')
274
+ if (!existsSync(userHtml)) {
275
+ const cerHtmlPath = join(getGeneratedDir(config.root), 'index.html')
276
+ server.middlewares.use(async (req, res, next) => {
277
+ const url = (req as { url?: string }).url ?? '/'
278
+ const isHtmlRequest =
279
+ url === '/' ||
280
+ url === '/index.html' ||
281
+ (!url.includes('.') && !url.startsWith('/api/'))
282
+ if (isHtmlRequest && existsSync(cerHtmlPath)) {
283
+ const rawHtml = readFileSync(cerHtmlPath, 'utf-8')
284
+ const transformed = await server.transformIndexHtml(url, rawHtml)
285
+ res.setHeader('Content-Type', 'text/html; charset=utf-8')
286
+ res.end(transformed)
287
+ return
288
+ }
289
+ next()
290
+ })
291
+ }
292
+
264
293
  // Watch app/ and server/ directories for file changes
265
294
  const watchDirs = [
266
295
  config.pagesDir,
@@ -302,6 +331,8 @@ export function cerApp(userConfig: CerAppConfig = {}): Plugin[] {
302
331
  if (!config) {
303
332
  config = resolveConfig(userConfig, process.cwd())
304
333
  }
334
+ // Write .cer/ generated files before the build begins
335
+ writeGeneratedDir(config)
305
336
  // Scan composables and generate type declarations + tsconfig paths
306
337
  composableExports = await scanComposableExports(config.composablesDir)
307
338
  await writeAutoImportDts(config.root, config.composablesDir, composableExports)
@@ -1,4 +1,4 @@
1
- import { basename, dirname, join, relative } from 'pathe'
1
+ import { basename, relative } from 'pathe'
2
2
 
3
3
  export interface RouteEntry {
4
4
  filePath: string
@@ -1,6 +1,5 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { join } from 'pathe'
3
- import { scanDirectory } from '../scanner.js'
4
3
 
5
4
  /**
6
5
  * Generates the virtual:cer-loading module code.
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Template string for the default `.cer/app.ts` client entry point.
3
+ *
4
+ * Written to `.cer/app.ts` when the consumer does not provide `app/app.ts`.
5
+ * Consumers can override by creating their own `app/app.ts`.
6
+ */
7
+ export const APP_ENTRY_TEMPLATE = `// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app
8
+ // This is the default client entry point. Create app/app.ts to override it.
9
+
1
10
  import '@jasonshimmy/custom-elements-runtime/css'
2
11
  import 'virtual:cer-jit-css'
3
12
  import 'virtual:cer-components'
@@ -27,10 +36,11 @@ const router = initRouter({ routes })
27
36
  const isNavigating = ref(false)
28
37
  const currentError = ref(null)
29
38
 
30
- ;(globalThis as any).resetError = () => {
39
+ const resetError = (): void => {
31
40
  currentError.value = null
32
- router.replace(router.getCurrent().path)
41
+ void router.replace(router.getCurrent().path)
33
42
  }
43
+ ;(globalThis as Record<string, unknown>).resetError = resetError
34
44
 
35
45
  const _push = router.push.bind(router)
36
46
  const _replace = router.replace.bind(router)
@@ -68,8 +78,8 @@ router.replace = async (path) => {
68
78
  // synchronously, calling the render function immediately.
69
79
  const _pluginProvides = new Map<string, unknown>()
70
80
  // Expose plugin provides globally so page components can read them synchronously
71
- // regardless of render order (inject/provide has timing issues in SSG mode).
72
- ;(globalThis as any).__cerPluginProvides = _pluginProvides
81
+ // regardless of render order.
82
+ ;(globalThis as Record<string, unknown>).__cerPluginProvides = _pluginProvides
73
83
 
74
84
  // ─── <cer-layout-view> ───────────────────────────────────────────────────────
75
85
 
@@ -100,7 +110,8 @@ component('cer-layout-view', () => {
100
110
  }
101
111
 
102
112
  const matched = router.matchRoute(current.value.path)
103
- const layoutName = (matched?.route as any)?.meta?.layout ?? 'default'
113
+ const routeMeta = matched?.route?.meta as { layout?: string } | undefined
114
+ const layoutName = routeMeta?.layout ?? 'default'
104
115
  const layoutTag = (layouts as Record<string, string>)[layoutName]
105
116
  const routerView = { tag: 'router-view', props: {}, children: [] }
106
117
 
@@ -110,7 +121,11 @@ component('cer-layout-view', () => {
110
121
 
111
122
  for (const plugin of plugins) {
112
123
  if (plugin && typeof plugin.setup === 'function') {
113
- await plugin.setup({ router, provide: (key: string, value: unknown) => { _pluginProvides.set(key, value) }, config: {} })
124
+ await plugin.setup({
125
+ router,
126
+ provide: (key: string, value: unknown) => { _pluginProvides.set(key, value) },
127
+ config: {},
128
+ })
114
129
  }
115
130
  }
116
131
 
@@ -135,7 +150,8 @@ if (typeof window !== 'undefined') {
135
150
  await _replace(window.location.pathname + window.location.search + window.location.hash)
136
151
  // Clear SSR loader data after initial navigation so subsequent client-side
137
152
  // navigations don't accidentally reuse stale server data.
138
- delete (globalThis as any).__CER_DATA__
153
+ delete (globalThis as Record<string, unknown>).__CER_DATA__
139
154
  }
140
155
 
141
156
  export { router }
157
+ `
@@ -1,12 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Kitchen Sink</title>
7
- </head>
8
- <body>
9
- <cer-layout-view></cer-layout-view>
10
- <script type="module" src="/app/app.ts"></script>
11
- </body>
12
- </html>