@jasonshimmy/vite-plugin-cer-app 0.1.6 → 0.3.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/.github/workflows/publish.yml +56 -5
- package/CHANGELOG.md +8 -0
- package/README.md +2 -0
- package/commits.txt +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +19 -5
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +0 -1
- package/dist/cli/commands/preview.js.map +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-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/plugin/build-ssr.d.ts +10 -0
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +21 -8
- 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/dev-server.js +0 -2
- package/dist/plugin/dev-server.js.map +1 -1
- package/dist/plugin/dts-generator.d.ts +4 -4
- package/dist/plugin/dts-generator.d.ts.map +1 -1
- package/dist/plugin/dts-generator.js +39 -19
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/generated-dir.d.ts +28 -0
- package/dist/plugin/generated-dir.d.ts.map +1 -0
- package/dist/plugin/generated-dir.js +106 -0
- package/dist/plugin/generated-dir.js.map +1 -0
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +27 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/path-utils.js.map +1 -1
- package/dist/plugin/virtual/loading.d.ts.map +1 -1
- package/dist/plugin/virtual/loading.js.map +1 -1
- package/dist/runtime/app-template.d.ts +9 -0
- package/dist/runtime/app-template.d.ts.map +1 -0
- package/dist/runtime/app-template.js +159 -0
- package/dist/runtime/app-template.js.map +1 -0
- 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/configuration.md +2 -11
- package/docs/getting-started.md +2 -100
- package/docs/rendering-modes.md +4 -5
- package/docs/routing.md +1 -1
- package/e2e/kitchen-sink/tsconfig.json +3 -0
- package/eslint.config.ts +22 -0
- package/package.json +6 -1
- package/src/__tests__/plugin/build-ssr.test.ts +24 -10
- package/src/__tests__/plugin/cer-app-plugin.test.ts +35 -0
- package/src/__tests__/plugin/dev-server.test.ts +1 -1
- package/src/__tests__/plugin/dts-generator.test.ts +15 -6
- package/src/__tests__/plugin/generated-dir.test.ts +137 -0
- package/src/__tests__/plugin/resolve-config.test.ts +0 -5
- package/src/__tests__/types/config.test.ts +1 -1
- package/src/cli/commands/build.ts +19 -5
- package/src/cli/commands/dev.ts +2 -2
- package/src/cli/commands/preview.ts +7 -5
- 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-ssg.ts +2 -2
- package/src/plugin/build-ssr.ts +22 -8
- package/src/plugin/dev-server.ts +5 -4
- package/src/plugin/dts-generator.ts +43 -19
- package/src/plugin/generated-dir.ts +115 -0
- package/src/plugin/index.ts +32 -2
- package/src/plugin/path-utils.ts +1 -1
- package/src/plugin/virtual/loading.ts +0 -1
- package/{e2e/kitchen-sink/app/app.ts → src/runtime/app-template.ts} +24 -7
- 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/e2e/kitchen-sink/index.html +0 -12
- 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
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
mkdirSync: vi.fn(),
|
|
10
|
+
readFileSync: vi.fn().mockReturnValue(''),
|
|
11
|
+
appendFileSync: vi.fn(),
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
vi.mock('../../runtime/app-template.js', () => ({ APP_ENTRY_TEMPLATE: '// app template' }))
|
|
15
|
+
|
|
16
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync, appendFileSync } from 'node:fs'
|
|
17
|
+
import {
|
|
18
|
+
GENERATED_DIR_NAME,
|
|
19
|
+
getGeneratedDir,
|
|
20
|
+
resolveHtmlEntry,
|
|
21
|
+
generateDefaultHtml,
|
|
22
|
+
writeGeneratedDir,
|
|
23
|
+
} from '../../plugin/generated-dir.js'
|
|
24
|
+
|
|
25
|
+
const ROOT = '/project'
|
|
26
|
+
const mockConfig = {
|
|
27
|
+
root: ROOT,
|
|
28
|
+
srcDir: `${ROOT}/app`,
|
|
29
|
+
} as Parameters<typeof writeGeneratedDir>[0]
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.mocked(existsSync).mockReturnValue(false)
|
|
33
|
+
vi.mocked(writeFileSync).mockClear()
|
|
34
|
+
vi.mocked(mkdirSync).mockClear()
|
|
35
|
+
vi.mocked(readFileSync).mockReturnValue('')
|
|
36
|
+
vi.mocked(appendFileSync).mockClear()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('GENERATED_DIR_NAME', () => {
|
|
40
|
+
it('is .cer', () => {
|
|
41
|
+
expect(GENERATED_DIR_NAME).toBe('.cer')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('getGeneratedDir', () => {
|
|
46
|
+
it('returns <root>/.cer', () => {
|
|
47
|
+
expect(getGeneratedDir(ROOT)).toBe(`${ROOT}/.cer`)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('resolveHtmlEntry', () => {
|
|
52
|
+
it('returns user index.html when it exists', () => {
|
|
53
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
54
|
+
expect(resolveHtmlEntry(mockConfig)).toBe(`${ROOT}/index.html`)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns .cer/index.html when user index.html is absent', () => {
|
|
58
|
+
vi.mocked(existsSync).mockReturnValue(false)
|
|
59
|
+
expect(resolveHtmlEntry(mockConfig)).toBe(`${ROOT}/.cer/index.html`)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('generateDefaultHtml', () => {
|
|
64
|
+
it('always references /.cer/app.ts', () => {
|
|
65
|
+
const html = generateDefaultHtml()
|
|
66
|
+
expect(html).toContain('/.cer/app.ts')
|
|
67
|
+
expect(html).not.toContain('/app/app.ts')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('includes <cer-layout-view> mount point', () => {
|
|
71
|
+
const html = generateDefaultHtml()
|
|
72
|
+
expect(html).toContain('<cer-layout-view>')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('is valid HTML with doctype', () => {
|
|
76
|
+
const html = generateDefaultHtml()
|
|
77
|
+
expect(html).toContain('<!DOCTYPE html>')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('writeGeneratedDir', () => {
|
|
82
|
+
it('creates the .cer directory when absent', () => {
|
|
83
|
+
// existsSync returns false for everything → dir is created
|
|
84
|
+
writeGeneratedDir(mockConfig)
|
|
85
|
+
expect(mkdirSync).toHaveBeenCalledWith(`${ROOT}/.cer`, { recursive: true })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('does not re-create the directory when it already exists', () => {
|
|
89
|
+
// existsSync returns true → dir already present
|
|
90
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
91
|
+
writeGeneratedDir(mockConfig)
|
|
92
|
+
expect(mkdirSync).not.toHaveBeenCalled()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('always writes .cer/app.ts', () => {
|
|
96
|
+
writeGeneratedDir(mockConfig)
|
|
97
|
+
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
98
|
+
expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('always writes .cer/app.ts even when .cer/ dir already exists', () => {
|
|
102
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
103
|
+
writeGeneratedDir(mockConfig)
|
|
104
|
+
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
105
|
+
expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('always writes .cer/index.html', () => {
|
|
109
|
+
writeGeneratedDir(mockConfig)
|
|
110
|
+
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
111
|
+
expect(paths.some(p => p.endsWith('/.cer/index.html'))).toBe(true)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('creates .gitignore when absent', () => {
|
|
115
|
+
// existsSync returns false → .cer/ dir created, app.ts written, .gitignore created
|
|
116
|
+
writeGeneratedDir(mockConfig)
|
|
117
|
+
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
118
|
+
expect(paths.some(p => p.endsWith('/.gitignore'))).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('appends .cer/ to existing .gitignore that does not contain it', () => {
|
|
122
|
+
// .gitignore exists (readFileSync returns '' — no .cer/ entry)
|
|
123
|
+
vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith('.gitignore'))
|
|
124
|
+
vi.mocked(readFileSync).mockReturnValue('node_modules/\ndist/\n')
|
|
125
|
+
writeGeneratedDir(mockConfig)
|
|
126
|
+
expect(appendFileSync).toHaveBeenCalled()
|
|
127
|
+
const appendArg = vi.mocked(appendFileSync).mock.calls[0][1] as string
|
|
128
|
+
expect(appendArg).toContain('.cer/')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('does not append to .gitignore when .cer/ is already present', () => {
|
|
132
|
+
vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith('.gitignore'))
|
|
133
|
+
vi.mocked(readFileSync).mockReturnValue('node_modules/\n.cer/\ndist/\n')
|
|
134
|
+
writeGeneratedDir(mockConfig)
|
|
135
|
+
expect(appendFileSync).not.toHaveBeenCalled()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -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')
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { build } from 'vite'
|
|
3
|
-
import { resolve } from 'pathe'
|
|
3
|
+
import { resolve, join } from 'pathe'
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
5
|
-
import { existsSync } from 'node:fs'
|
|
5
|
+
import { existsSync, renameSync } from 'node:fs'
|
|
6
6
|
import { cerApp, resolveConfig } from '../../plugin/index.js'
|
|
7
|
-
import { buildSSR } from '../../plugin/build-ssr.js'
|
|
7
|
+
import { buildSSR, resolveClientEntry } from '../../plugin/build-ssr.js'
|
|
8
8
|
import { buildSSG } from '../../plugin/build-ssg.js'
|
|
9
|
+
import { writeGeneratedDir } from '../../plugin/generated-dir.js'
|
|
9
10
|
import type { CerAppConfig } from '../../types/config.js'
|
|
10
11
|
|
|
11
12
|
async function loadCerConfig(root: string): Promise<CerAppConfig> {
|
|
@@ -73,15 +74,28 @@ export function buildCommand(): Command {
|
|
|
73
74
|
|
|
74
75
|
switch (config.mode) {
|
|
75
76
|
case 'spa': {
|
|
76
|
-
//
|
|
77
|
+
// Write .cer/ files BEFORE resolveClientEntry checks for .cer/index.html.
|
|
78
|
+
writeGeneratedDir(config)
|
|
79
|
+
const spaEntry = resolveClientEntry(config)
|
|
80
|
+
const spaOutDir = resolve(root, 'dist')
|
|
77
81
|
await build({
|
|
78
82
|
root,
|
|
79
83
|
plugins: cerApp(userConfig),
|
|
80
84
|
build: {
|
|
81
|
-
outDir:
|
|
85
|
+
outDir: spaOutDir,
|
|
86
|
+
rollupOptions: { input: spaEntry },
|
|
82
87
|
},
|
|
83
88
|
})
|
|
89
|
+
// If the entry was .cer/index.html, Vite outputs it as dist/.cer/index.html.
|
|
90
|
+
// Rename it to dist/index.html so the preview server can find it.
|
|
91
|
+
const generatedHtmlOut = join(spaOutDir, '.cer/index.html')
|
|
92
|
+
const rootHtmlOut = join(spaOutDir, 'index.html')
|
|
93
|
+
if (existsSync(generatedHtmlOut) && !existsSync(rootHtmlOut)) {
|
|
94
|
+
renameSync(generatedHtmlOut, rootHtmlOut)
|
|
95
|
+
}
|
|
84
96
|
console.log('[cer-app] SPA build complete.')
|
|
97
|
+
// Force exit: Vite HTML builds may keep Node timers alive.
|
|
98
|
+
process.exit(0)
|
|
85
99
|
break
|
|
86
100
|
}
|
|
87
101
|
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createServer } from 'vite'
|
|
|
3
3
|
import { resolve } from 'pathe'
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
5
5
|
import { existsSync } from 'node:fs'
|
|
6
|
-
import { cerApp
|
|
6
|
+
import { cerApp } from '../../plugin/index.js'
|
|
7
7
|
import type { CerAppConfig } from '../../types/config.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -28,7 +28,7 @@ async function loadCerConfig(root: string): Promise<CerAppConfig> {
|
|
|
28
28
|
try {
|
|
29
29
|
// Use Vite's build to transpile TS config at runtime
|
|
30
30
|
const { build } = await import('vite')
|
|
31
|
-
|
|
31
|
+
await build({
|
|
32
32
|
build: {
|
|
33
33
|
lib: {
|
|
34
34
|
entry: filePath,
|
|
@@ -107,9 +107,10 @@ export function previewCommand(): Command {
|
|
|
107
107
|
console.log('[cer-app] Starting SSR preview server...')
|
|
108
108
|
|
|
109
109
|
// Load the server bundle
|
|
110
|
+
type SsrHandlerFn = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
|
|
110
111
|
let serverMod: {
|
|
111
|
-
handler?:
|
|
112
|
-
default?:
|
|
112
|
+
handler?: SsrHandlerFn
|
|
113
|
+
default?: SsrHandlerFn
|
|
113
114
|
apiRoutes?: Array<{ path: string; handlers: Record<string, unknown> }>
|
|
114
115
|
}
|
|
115
116
|
try {
|
|
@@ -151,10 +152,11 @@ export function previewCommand(): Command {
|
|
|
151
152
|
}
|
|
152
153
|
augRes.status = function (code) { this.statusCode = code; return this }
|
|
153
154
|
|
|
155
|
+
type ApiHandlerFn = (req: typeof augReq, res: typeof augRes) => void | Promise<void>
|
|
154
156
|
const handlerFn =
|
|
155
|
-
(route.handlers[method.toLowerCase()] as
|
|
156
|
-
(route.handlers[method.toUpperCase()] as
|
|
157
|
-
(route.handlers['default'] as
|
|
157
|
+
(route.handlers[method.toLowerCase()] as ApiHandlerFn | undefined) ??
|
|
158
|
+
(route.handlers[method.toUpperCase()] as ApiHandlerFn | undefined) ??
|
|
159
|
+
(route.handlers['default'] as ApiHandlerFn | undefined)
|
|
158
160
|
|
|
159
161
|
if (typeof handlerFn === 'function') {
|
|
160
162
|
try {
|
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-ssg.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { writeFile, mkdir } from 'node:fs/promises'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import { join } from 'pathe'
|
|
4
|
-
import { createServer,
|
|
4
|
+
import { createServer, type UserConfig } from 'vite'
|
|
5
5
|
import type { ResolvedCerConfig } from './dev-server.js'
|
|
6
6
|
import { buildSSR } from './build-ssr.js'
|
|
7
7
|
import { buildRouteEntry } from './path-utils.js'
|
|
@@ -138,7 +138,7 @@ async function renderPath(
|
|
|
138
138
|
setHeader: () => {},
|
|
139
139
|
end: (body: string) => resolve(body),
|
|
140
140
|
}
|
|
141
|
-
;(handlerFn as
|
|
141
|
+
;(handlerFn as (req: unknown, res: unknown) => Promise<void>)(mockReq, mockRes).catch(reject)
|
|
142
142
|
})
|
|
143
143
|
}
|
|
144
144
|
|
package/src/plugin/build-ssr.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { build, type UserConfig } from 'vite'
|
|
2
2
|
import { join, resolve } from 'pathe'
|
|
3
|
-
import { existsSync } from 'node:fs'
|
|
3
|
+
import { existsSync, renameSync } from 'node:fs'
|
|
4
4
|
import type { ResolvedCerConfig } from './dev-server.js'
|
|
5
|
+
import { getGeneratedDir, writeGeneratedDir } from './generated-dir.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Resolves the client build entry point for an SSR/SSG build.
|
|
8
9
|
*
|
|
9
10
|
* Priority order:
|
|
10
|
-
* 1. `index.html` at the project root —
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* themselves (e.g. a custom Express server).
|
|
15
|
-
* 3. `app/app.ts` — last resort (same bundle, no DSD hydration preamble).
|
|
11
|
+
* 1. `index.html` at the project root — consumer-provided HTML shell.
|
|
12
|
+
* 2. `.cer/index.html` — auto-generated HTML shell (Nuxt-style magic).
|
|
13
|
+
* 3. `app/entry-client.ts` — fallback for projects that manage HTML externally.
|
|
14
|
+
* 4. `app/app.ts` — last resort (same bundle, no DSD hydration preamble).
|
|
16
15
|
*/
|
|
17
|
-
function resolveClientEntry(config: ResolvedCerConfig): string {
|
|
16
|
+
export function resolveClientEntry(config: ResolvedCerConfig): string {
|
|
18
17
|
const indexHtml = resolve(config.root, 'index.html')
|
|
19
18
|
if (existsSync(indexHtml)) return indexHtml
|
|
19
|
+
const cerIndexHtml = join(getGeneratedDir(config.root), 'index.html')
|
|
20
|
+
if (existsSync(cerIndexHtml)) return cerIndexHtml
|
|
20
21
|
const entryClient = resolve(config.srcDir, 'entry-client.ts')
|
|
21
22
|
if (existsSync(entryClient)) return entryClient
|
|
22
23
|
return resolve(config.srcDir, 'app.ts')
|
|
@@ -225,6 +226,10 @@ export async function buildSSR(
|
|
|
225
226
|
const clientOutDir = join(config.root, 'dist/client')
|
|
226
227
|
const serverOutDir = join(config.root, 'dist/server')
|
|
227
228
|
|
|
229
|
+
// Write .cer/ generated files BEFORE resolving the client entry so that
|
|
230
|
+
// .cer/index.html is on disk when resolveClientEntry checks for it.
|
|
231
|
+
writeGeneratedDir(config)
|
|
232
|
+
|
|
228
233
|
// Resolve the client entry — index.html is preferred so Vite writes a
|
|
229
234
|
// processed index.html to dist/client/ for use as the SSG shell template.
|
|
230
235
|
const clientEntry = resolveClientEntry(config)
|
|
@@ -244,6 +249,15 @@ export async function buildSSR(
|
|
|
244
249
|
},
|
|
245
250
|
})
|
|
246
251
|
|
|
252
|
+
// If the client entry was .cer/index.html, Vite outputs it as
|
|
253
|
+
// dist/client/.cer/index.html (preserving relative path). The SSR server
|
|
254
|
+
// template expects dist/client/index.html, so rename it into place.
|
|
255
|
+
const generatedHtmlOut = join(clientOutDir, '.cer/index.html')
|
|
256
|
+
const rootHtmlOut = join(clientOutDir, 'index.html')
|
|
257
|
+
if (existsSync(generatedHtmlOut) && !existsSync(rootHtmlOut)) {
|
|
258
|
+
renameSync(generatedHtmlOut, rootHtmlOut)
|
|
259
|
+
}
|
|
260
|
+
|
|
247
261
|
// Generate server entry source inline via a virtual plugin
|
|
248
262
|
const serverEntryCode = generateServerEntryCode()
|
|
249
263
|
const VIRTUAL_SERVER_ENTRY = 'virtual:cer-server-entry'
|
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 }
|
|
@@ -186,11 +186,12 @@ export function configureCerDevServer(
|
|
|
186
186
|
|
|
187
187
|
// Try to find a handler for the HTTP method. Exports may be GET/POST (uppercase)
|
|
188
188
|
// or get/post (lowercase); try both plus a 'default' fallback.
|
|
189
|
+
type RouteHandlerFn = (req: typeof augmentedReq, res: typeof augmentedRes) => void | Promise<void>
|
|
189
190
|
const handlerKey = method.toLowerCase()
|
|
190
191
|
const handler =
|
|
191
|
-
(route.handlers[handlerKey] as
|
|
192
|
-
(route.handlers[method.toUpperCase()] as
|
|
193
|
-
(route.handlers['default'] as
|
|
192
|
+
(route.handlers[handlerKey] as RouteHandlerFn | undefined) ??
|
|
193
|
+
(route.handlers[method.toUpperCase()] as RouteHandlerFn | undefined) ??
|
|
194
|
+
(route.handlers['default'] as RouteHandlerFn | undefined)
|
|
194
195
|
|
|
195
196
|
if (typeof handler === 'function') {
|
|
196
197
|
try {
|
|
@@ -1,27 +1,46 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs'
|
|
2
|
-
import { readFileSync
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
3
|
import { join, relative } from 'pathe'
|
|
4
4
|
import { scanDirectory } from './scanner.js'
|
|
5
|
+
import { GENERATED_DIR_NAME } from './generated-dir.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Writes
|
|
8
|
-
*
|
|
9
|
-
* { "extends": "
|
|
8
|
+
* Writes `.cer/tsconfig.json` containing path aliases for the `~/` prefix
|
|
9
|
+
* plus include/exclude entries so the consumer's `tsconfig.json` only needs:
|
|
10
|
+
* { "extends": "./.cer/tsconfig.json" }
|
|
10
11
|
*/
|
|
11
12
|
export function writeTsconfigPaths(root: string, srcDir: string): void {
|
|
12
|
-
const
|
|
13
|
+
const cerDir = join(root, GENERATED_DIR_NAME)
|
|
14
|
+
if (!existsSync(cerDir)) {
|
|
15
|
+
mkdirSync(cerDir, { recursive: true })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Paths are relative to .cer/ inside the project root, so prefix with ../
|
|
19
|
+
const srcRel = '../' + relative(root, srcDir).replace(/\\/g, '/')
|
|
13
20
|
const paths: Record<string, string[]> = {
|
|
14
|
-
'~/*': [`${
|
|
15
|
-
'~/pages/*': [`${
|
|
16
|
-
'~/layouts/*': [`${
|
|
17
|
-
'~/components/*': [`${
|
|
18
|
-
'~/composables/*': [`${
|
|
19
|
-
'~/plugins/*': [`${
|
|
20
|
-
'~/middleware/*': [`${
|
|
21
|
-
'~/assets/*': [`${
|
|
21
|
+
'~/*': [`${srcRel}/*`],
|
|
22
|
+
'~/pages/*': [`${srcRel}/pages/*`],
|
|
23
|
+
'~/layouts/*': [`${srcRel}/layouts/*`],
|
|
24
|
+
'~/components/*': [`${srcRel}/components/*`],
|
|
25
|
+
'~/composables/*': [`${srcRel}/composables/*`],
|
|
26
|
+
'~/plugins/*': [`${srcRel}/plugins/*`],
|
|
27
|
+
'~/middleware/*': [`${srcRel}/middleware/*`],
|
|
28
|
+
'~/assets/*': [`${srcRel}/assets/*`],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tsconfig = {
|
|
32
|
+
compilerOptions: { paths },
|
|
33
|
+
include: [
|
|
34
|
+
'../app/**/*.ts',
|
|
35
|
+
'../server/**/*.ts',
|
|
36
|
+
'./**/*.ts',
|
|
37
|
+
'./**/*.d.ts',
|
|
38
|
+
],
|
|
39
|
+
exclude: ['../node_modules', '../dist'],
|
|
22
40
|
}
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
|
|
42
|
+
const content = JSON.stringify(tsconfig, null, 2) + '\n'
|
|
43
|
+
writeFileSync(join(cerDir, 'tsconfig.json'), content, 'utf-8')
|
|
25
44
|
}
|
|
26
45
|
|
|
27
46
|
const RUNTIME_GLOBALS = [
|
|
@@ -199,16 +218,21 @@ export async function generateVirtualModuleDts(
|
|
|
199
218
|
}
|
|
200
219
|
|
|
201
220
|
/**
|
|
202
|
-
* Writes
|
|
221
|
+
* Writes `auto-imports.d.ts` and `env.d.ts` to `.cer/` inside the project root.
|
|
203
222
|
*/
|
|
204
223
|
export async function writeAutoImportDts(
|
|
205
224
|
root: string,
|
|
206
225
|
composablesDir: string,
|
|
207
226
|
composableExports?: Map<string, string>,
|
|
208
227
|
): Promise<void> {
|
|
228
|
+
const cerDir = join(root, GENERATED_DIR_NAME)
|
|
229
|
+
if (!existsSync(cerDir)) {
|
|
230
|
+
mkdirSync(cerDir, { recursive: true })
|
|
231
|
+
}
|
|
232
|
+
|
|
209
233
|
const scanned = composableExports ?? await scanComposableExports(composablesDir)
|
|
210
234
|
const autoImportsContent = await generateAutoImportDts(root, composablesDir, scanned)
|
|
211
235
|
const envContent = await generateVirtualModuleDts(root, composablesDir, scanned)
|
|
212
|
-
writeFileSync(join(
|
|
213
|
-
writeFileSync(join(
|
|
236
|
+
writeFileSync(join(cerDir, 'auto-imports.d.ts'), autoImportsContent, 'utf-8')
|
|
237
|
+
writeFileSync(join(cerDir, 'env.d.ts'), envContent, 'utf-8')
|
|
214
238
|
}
|