@jasonshimmy/vite-plugin-cer-app 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/commits.txt +1 -1
- package/cypress.config.ts +16 -0
- package/dist/cli/create/index.js +1 -1
- package/dist/cli/create/index.js.map +1 -1
- package/dist/plugin/build-ssg.d.ts +7 -0
- package/dist/plugin/build-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js +2 -1
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +26 -6
- package/dist/plugin/build-ssr.js.map +1 -1
- package/dist/runtime/composables/index.d.ts +1 -1
- package/dist/runtime/composables/index.d.ts.map +1 -1
- package/dist/runtime/composables/index.js +1 -1
- package/dist/runtime/composables/index.js.map +1 -1
- package/dist/runtime/composables/use-head.d.ts.map +1 -1
- package/dist/runtime/composables/use-head.js +12 -8
- package/dist/runtime/composables/use-head.js.map +1 -1
- package/dist/runtime/entry-server-template.d.ts +1 -1
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +14 -4
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/docs/cli.md +2 -0
- package/docs/components.md +57 -0
- package/docs/composables.md +9 -2
- package/docs/data-loading.md +45 -1
- package/docs/getting-started.md +71 -6
- package/docs/head-management.md +6 -0
- package/docs/plugins.md +25 -0
- package/docs/routing.md +48 -6
- package/e2e/cypress/e2e/api.cy.ts +81 -0
- package/e2e/cypress/e2e/data.cy.ts +111 -0
- package/e2e/cypress/e2e/fouc.cy.ts +65 -0
- package/e2e/cypress/e2e/head.cy.ts +89 -0
- package/e2e/cypress/e2e/interactive.cy.ts +122 -0
- package/e2e/cypress/e2e/routes.cy.ts +128 -0
- package/e2e/cypress/support/commands.ts +60 -0
- package/e2e/cypress/support/e2e.ts +10 -0
- package/{src/runtime/app-template.ts → e2e/kitchen-sink/app/app.ts} +43 -49
- package/e2e/kitchen-sink/app/components/ks-badge.ts +8 -0
- package/e2e/kitchen-sink/app/composables/useKsCounter.ts +9 -0
- package/e2e/kitchen-sink/app/error.ts +13 -0
- package/e2e/kitchen-sink/app/layouts/default.ts +21 -0
- package/e2e/kitchen-sink/app/layouts/minimal.ts +7 -0
- package/e2e/kitchen-sink/app/loading.ts +9 -0
- package/e2e/kitchen-sink/app/middleware/auth.ts +13 -0
- package/e2e/kitchen-sink/app/pages/(auth)/login.ts +13 -0
- package/e2e/kitchen-sink/app/pages/(auth)/protected.ts +20 -0
- package/e2e/kitchen-sink/app/pages/404.ts +9 -0
- package/e2e/kitchen-sink/app/pages/about.ts +17 -0
- package/e2e/kitchen-sink/app/pages/blog/[slug].ts +54 -0
- package/e2e/kitchen-sink/app/pages/blog/index.ts +46 -0
- package/e2e/kitchen-sink/app/pages/counter.ts +17 -0
- package/e2e/kitchen-sink/app/pages/head.ts +20 -0
- package/e2e/kitchen-sink/app/pages/index.ts +27 -0
- package/e2e/kitchen-sink/app/pages/items/[id].ts +20 -0
- package/e2e/kitchen-sink/app/plugins/01.setup.ts +7 -0
- package/e2e/kitchen-sink/cer-auto-imports.d.ts +50 -0
- package/e2e/kitchen-sink/cer-env.d.ts +36 -0
- package/e2e/kitchen-sink/cer-tsconfig.json +30 -0
- package/e2e/kitchen-sink/cer.config.ts +6 -0
- package/e2e/kitchen-sink/index.html +12 -0
- package/e2e/kitchen-sink/server/api/health.ts +3 -0
- package/e2e/kitchen-sink/server/api/posts/[slug].ts +11 -0
- package/e2e/kitchen-sink/server/api/posts/index.ts +5 -0
- package/e2e/kitchen-sink/server/data/posts.ts +21 -0
- package/e2e/scripts/clean.mjs +8 -0
- package/package.json +19 -2
- package/src/__tests__/plugin/build-ssg-render.test.ts +110 -0
- package/src/__tests__/plugin/build-ssg.test.ts +47 -1
- package/src/__tests__/plugin/build-ssr.test.ts +93 -1
- package/src/__tests__/plugin/dev-server.test.ts +493 -0
- package/src/__tests__/plugin/scanner.test.ts +15 -1
- package/src/__tests__/plugin/transforms/auto-import.test.ts +63 -0
- package/src/cli/create/index.ts +1 -1
- package/src/cli/create/templates/spa/app/app.ts.tpl +23 -3
- package/src/cli/create/templates/ssg/app/app.ts.tpl +27 -3
- package/src/cli/create/templates/ssg/app/pages/index.ts.tpl +0 -9
- package/src/cli/create/templates/ssr/app/app.ts.tpl +27 -3
- package/src/plugin/build-ssg.ts +2 -1
- package/src/plugin/build-ssr.ts +26 -6
- package/src/runtime/composables/index.ts +1 -1
- package/src/runtime/composables/use-head.ts +12 -8
- package/src/runtime/entry-server-template.ts +14 -4
- package/vitest.config.ts +5 -1
- package/VITE_PLUGIN_FRAMEWORK_PLAN.md +0 -594
- package/dist/runtime/app-template.d.ts +0 -10
- package/dist/runtime/app-template.d.ts.map +0 -1
- package/dist/runtime/app-template.js +0 -149
- package/dist/runtime/app-template.js.map +0 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
interface Post {
|
|
2
|
+
slug: string
|
|
3
|
+
title: string
|
|
4
|
+
body: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
component('page-blog-slug', () => {
|
|
8
|
+
const props = useProps<{ slug: string }>({ slug: '' })
|
|
9
|
+
const ssrData = usePageData<Post>()
|
|
10
|
+
|
|
11
|
+
const title = ref(ssrData?.title ?? '')
|
|
12
|
+
const body = ref(ssrData?.body ?? '')
|
|
13
|
+
|
|
14
|
+
useOnConnected(async () => {
|
|
15
|
+
if (ssrData) return // already hydrated
|
|
16
|
+
if (!props.slug) return
|
|
17
|
+
try {
|
|
18
|
+
const r = await fetch(`/api/posts/${props.slug}`)
|
|
19
|
+
if (r.ok) {
|
|
20
|
+
const post: Post | null = await r.json()
|
|
21
|
+
if (post) { title.value = post.title; body.value = post.body; return }
|
|
22
|
+
}
|
|
23
|
+
} catch { /* no API server (SPA mode) */ }
|
|
24
|
+
// SPA fallback: import post data directly from the source module
|
|
25
|
+
const { posts } = await import('../../../server/data/posts')
|
|
26
|
+
const post = posts.find((p) => p.slug === props.slug)
|
|
27
|
+
if (post) { title.value = post.title; body.value = post.body }
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return html`
|
|
31
|
+
<div>
|
|
32
|
+
<h1 data-cy="post-title">${title.value || props.slug}</h1>
|
|
33
|
+
<p data-cy="post-slug"><em>slug: <code>${props.slug}</code></em></p>
|
|
34
|
+
<div data-cy="post-body">${body.value}</div>
|
|
35
|
+
<p><a href="/blog" data-cy="post-back">← Back to blog</a></p>
|
|
36
|
+
</div>
|
|
37
|
+
`
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const loader = async ({ params }: { params: { slug: string } }) => {
|
|
41
|
+
const { posts } = await import('../../../server/data/posts')
|
|
42
|
+
const post = posts.find((p) => p.slug === params.slug)
|
|
43
|
+
if (!post) throw new Error('Post not found')
|
|
44
|
+
return { slug: post.slug, title: post.title, body: post.body }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const meta = {
|
|
48
|
+
ssg: {
|
|
49
|
+
paths: async () => [
|
|
50
|
+
{ params: { slug: 'first-post' } },
|
|
51
|
+
{ params: { slug: 'second-post' } },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
interface Post {
|
|
2
|
+
slug: string
|
|
3
|
+
title: string
|
|
4
|
+
excerpt: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
component('page-blog', () => {
|
|
8
|
+
useHead({ title: 'Blog — Kitchen Sink' })
|
|
9
|
+
|
|
10
|
+
const ssrData = usePageData<{ posts: Post[] }>()
|
|
11
|
+
const posts = ref<Post[]>(ssrData?.posts ?? [])
|
|
12
|
+
|
|
13
|
+
useOnConnected(async () => {
|
|
14
|
+
if (ssrData) return // already hydrated — skip client fetch
|
|
15
|
+
try {
|
|
16
|
+
const r = await fetch('/api/posts')
|
|
17
|
+
if (r.ok) {
|
|
18
|
+
const data: Post[] = await r.json()
|
|
19
|
+
if (Array.isArray(data)) { posts.value = data; return }
|
|
20
|
+
}
|
|
21
|
+
} catch { /* no API server (SPA mode) */ }
|
|
22
|
+
// SPA fallback: import post data directly from the source module
|
|
23
|
+
const { posts: staticPosts } = await import('../../../server/data/posts')
|
|
24
|
+
posts.value = staticPosts
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return html`
|
|
28
|
+
<div>
|
|
29
|
+
<h1 data-cy="blog-heading">Blog</h1>
|
|
30
|
+
<p>Posts are loaded via a page <strong>loader</strong> (SSR/SSG) or client-side fetch (SPA).</p>
|
|
31
|
+
<ul data-cy="blog-list">
|
|
32
|
+
${posts.value.map(post => html`
|
|
33
|
+
<li data-cy="blog-item">
|
|
34
|
+
<a href="/blog/${post.slug}" data-cy="blog-link-${post.slug}"><strong>${post.title}</strong></a>
|
|
35
|
+
<p>${post.excerpt}</p>
|
|
36
|
+
</li>
|
|
37
|
+
`)}
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
`
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const loader = async () => {
|
|
44
|
+
const { posts } = await import('../../../server/data/posts')
|
|
45
|
+
return { posts }
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Tests auto-imported composable (useKsCounter) + reactive state
|
|
2
|
+
component('page-counter', () => {
|
|
3
|
+
const { count, increment, decrement, reset } = useKsCounter()
|
|
4
|
+
|
|
5
|
+
return html`
|
|
6
|
+
<div>
|
|
7
|
+
<h1 data-cy="counter-heading">Counter</h1>
|
|
8
|
+
<p>Tests auto-imported <code>useKsCounter</code> composable and reactive state.</p>
|
|
9
|
+
<div data-cy="counter-widget">
|
|
10
|
+
<p>Count: <strong data-cy="count">${count.value}</strong></p>
|
|
11
|
+
<button data-cy="decrement" @click="${decrement}">−</button>
|
|
12
|
+
<button data-cy="reset" @click="${reset}">Reset</button>
|
|
13
|
+
<button data-cy="increment" @click="${increment}">+</button>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
`
|
|
17
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
component('page-head', () => {
|
|
2
|
+
useHead({
|
|
3
|
+
title: 'Head Test — Kitchen Sink',
|
|
4
|
+
meta: [
|
|
5
|
+
{ name: 'description', content: 'A test page for useHead().' },
|
|
6
|
+
{ property: 'og:title', content: 'Head Test' },
|
|
7
|
+
],
|
|
8
|
+
link: [
|
|
9
|
+
{ rel: 'canonical', href: 'http://localhost/head' },
|
|
10
|
+
],
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
return html`
|
|
14
|
+
<div>
|
|
15
|
+
<h1 data-cy="head-heading">Head Test</h1>
|
|
16
|
+
<p data-cy="head-description">This page sets document title and meta tags via <code>useHead()</code>.</p>
|
|
17
|
+
<p>Check the page <code><title></code> and <code><meta name="description"></code> tags.</p>
|
|
18
|
+
</div>
|
|
19
|
+
`
|
|
20
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
component('page-index', () => {
|
|
2
|
+
useHead({
|
|
3
|
+
title: 'Home — Kitchen Sink',
|
|
4
|
+
meta: [{ name: 'description', content: 'Kitchen sink test app.' }],
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
return html`
|
|
8
|
+
<div>
|
|
9
|
+
<ks-badge>v1</ks-badge>
|
|
10
|
+
<h1 data-cy="home-heading">Kitchen Sink</h1>
|
|
11
|
+
<p data-cy="home-description">A comprehensive test app for vite-plugin-cer-app.</p>
|
|
12
|
+
<nav data-cy="page-nav">
|
|
13
|
+
<ul>
|
|
14
|
+
<li><a href="/about">About (minimal layout)</a></li>
|
|
15
|
+
<li><a href="/counter">Counter (reactive state + composable)</a></li>
|
|
16
|
+
<li><a href="/head">Head management (useHead)</a></li>
|
|
17
|
+
<li><a href="/blog">Blog (data loader)</a></li>
|
|
18
|
+
<li><a href="/blog/first-post">Blog post (dynamic route)</a></li>
|
|
19
|
+
<li><a href="/items/1">Item detail (route params)</a></li>
|
|
20
|
+
<li><a href="/protected">Protected (middleware)</a></li>
|
|
21
|
+
<li><a href="/login">Login</a></li>
|
|
22
|
+
<li><a href="/not-a-real-page">404 catch-all</a></li>
|
|
23
|
+
</ul>
|
|
24
|
+
</nav>
|
|
25
|
+
</div>
|
|
26
|
+
`
|
|
27
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
component('page-items-id', () => {
|
|
2
|
+
const props = useProps<{ id: string }>({ id: '' })
|
|
3
|
+
|
|
4
|
+
return html`
|
|
5
|
+
<div>
|
|
6
|
+
<h1 data-cy="item-heading">Item Detail</h1>
|
|
7
|
+
<p data-cy="item-id-display">Item ID: <strong data-cy="item-id">${props.id}</strong></p>
|
|
8
|
+
<p><a href="/" data-cy="item-back">← Home</a></p>
|
|
9
|
+
</div>
|
|
10
|
+
`
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const meta = {
|
|
14
|
+
ssg: {
|
|
15
|
+
paths: async () => [
|
|
16
|
+
{ params: { id: '1' } },
|
|
17
|
+
{ params: { id: '2' } },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app
|
|
2
|
+
// Do not edit — regenerated automatically on dev server start and build.
|
|
3
|
+
|
|
4
|
+
export {}
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
const component: typeof import('@jasonshimmy/custom-elements-runtime')['component']
|
|
8
|
+
const html: typeof import('@jasonshimmy/custom-elements-runtime')['html']
|
|
9
|
+
const css: typeof import('@jasonshimmy/custom-elements-runtime')['css']
|
|
10
|
+
const ref: typeof import('@jasonshimmy/custom-elements-runtime')['ref']
|
|
11
|
+
const computed: typeof import('@jasonshimmy/custom-elements-runtime')['computed']
|
|
12
|
+
const watch: typeof import('@jasonshimmy/custom-elements-runtime')['watch']
|
|
13
|
+
const watchEffect: typeof import('@jasonshimmy/custom-elements-runtime')['watchEffect']
|
|
14
|
+
const useProps: typeof import('@jasonshimmy/custom-elements-runtime')['useProps']
|
|
15
|
+
const useEmit: typeof import('@jasonshimmy/custom-elements-runtime')['useEmit']
|
|
16
|
+
const useOnConnected: typeof import('@jasonshimmy/custom-elements-runtime')['useOnConnected']
|
|
17
|
+
const useOnDisconnected: typeof import('@jasonshimmy/custom-elements-runtime')['useOnDisconnected']
|
|
18
|
+
const useOnAttributeChanged: typeof import('@jasonshimmy/custom-elements-runtime')['useOnAttributeChanged']
|
|
19
|
+
const useOnError: typeof import('@jasonshimmy/custom-elements-runtime')['useOnError']
|
|
20
|
+
const useStyle: typeof import('@jasonshimmy/custom-elements-runtime')['useStyle']
|
|
21
|
+
const useDesignTokens: typeof import('@jasonshimmy/custom-elements-runtime')['useDesignTokens']
|
|
22
|
+
const useGlobalStyle: typeof import('@jasonshimmy/custom-elements-runtime')['useGlobalStyle']
|
|
23
|
+
const useExpose: typeof import('@jasonshimmy/custom-elements-runtime')['useExpose']
|
|
24
|
+
const useSlots: typeof import('@jasonshimmy/custom-elements-runtime')['useSlots']
|
|
25
|
+
const provide: typeof import('@jasonshimmy/custom-elements-runtime')['provide']
|
|
26
|
+
const inject: typeof import('@jasonshimmy/custom-elements-runtime')['inject']
|
|
27
|
+
const createComposable: typeof import('@jasonshimmy/custom-elements-runtime')['createComposable']
|
|
28
|
+
const nextTick: typeof import('@jasonshimmy/custom-elements-runtime')['nextTick']
|
|
29
|
+
const defineModel: typeof import('@jasonshimmy/custom-elements-runtime')['defineModel']
|
|
30
|
+
const getCurrentComponentContext: typeof import('@jasonshimmy/custom-elements-runtime')['getCurrentComponentContext']
|
|
31
|
+
const isReactiveState: typeof import('@jasonshimmy/custom-elements-runtime')['isReactiveState']
|
|
32
|
+
const unsafeHTML: typeof import('@jasonshimmy/custom-elements-runtime')['unsafeHTML']
|
|
33
|
+
const decodeEntities: typeof import('@jasonshimmy/custom-elements-runtime')['decodeEntities']
|
|
34
|
+
const useTeleport: typeof import('@jasonshimmy/custom-elements-runtime')['useTeleport']
|
|
35
|
+
|
|
36
|
+
const when: typeof import('@jasonshimmy/custom-elements-runtime/directives')['when']
|
|
37
|
+
const each: typeof import('@jasonshimmy/custom-elements-runtime/directives')['each']
|
|
38
|
+
const match: typeof import('@jasonshimmy/custom-elements-runtime/directives')['match']
|
|
39
|
+
const anchorBlock: typeof import('@jasonshimmy/custom-elements-runtime/directives')['anchorBlock']
|
|
40
|
+
|
|
41
|
+
const useHead: typeof import('@jasonshimmy/vite-plugin-cer-app/composables')['useHead']
|
|
42
|
+
const usePageData: typeof import('@jasonshimmy/vite-plugin-cer-app/composables')['usePageData']
|
|
43
|
+
|
|
44
|
+
const useKsCounter: typeof import('./app/composables/useKsCounter')['useKsCounter']
|
|
45
|
+
|
|
46
|
+
// SSR loader data injected as window.__CER_DATA__ by the server.
|
|
47
|
+
// Consumed once by usePageData() during client hydration.
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
var __CER_DATA__: any
|
|
50
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app
|
|
2
|
+
// Do not edit — regenerated automatically on dev server start and build.
|
|
3
|
+
|
|
4
|
+
declare module 'virtual:cer-routes' {
|
|
5
|
+
import type { Route } from '@jasonshimmy/custom-elements-runtime/router'
|
|
6
|
+
const routes: Route[]
|
|
7
|
+
export default routes
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module 'virtual:cer-layouts' {}
|
|
11
|
+
declare module 'virtual:cer-components' {}
|
|
12
|
+
|
|
13
|
+
declare module 'virtual:cer-plugins' {
|
|
14
|
+
import type { AppPlugin } from '@jasonshimmy/vite-plugin-cer-app/types'
|
|
15
|
+
const plugins: AppPlugin[]
|
|
16
|
+
export default plugins
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module 'virtual:cer-composables' {
|
|
20
|
+
export { useKsCounter } from './app/composables/useKsCounter'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare module 'virtual:cer-middleware' {
|
|
24
|
+
const middleware: Record<string, Function>
|
|
25
|
+
export { middleware }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare module 'virtual:cer-loading' {
|
|
29
|
+
export const hasLoading: boolean
|
|
30
|
+
export const loadingTag: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare module 'virtual:cer-error' {
|
|
34
|
+
export const hasError: boolean
|
|
35
|
+
export const errorTag: string | null
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"paths": {
|
|
4
|
+
"~/*": [
|
|
5
|
+
"./app/*"
|
|
6
|
+
],
|
|
7
|
+
"~/pages/*": [
|
|
8
|
+
"./app/pages/*"
|
|
9
|
+
],
|
|
10
|
+
"~/layouts/*": [
|
|
11
|
+
"./app/layouts/*"
|
|
12
|
+
],
|
|
13
|
+
"~/components/*": [
|
|
14
|
+
"./app/components/*"
|
|
15
|
+
],
|
|
16
|
+
"~/composables/*": [
|
|
17
|
+
"./app/composables/*"
|
|
18
|
+
],
|
|
19
|
+
"~/plugins/*": [
|
|
20
|
+
"./app/plugins/*"
|
|
21
|
+
],
|
|
22
|
+
"~/middleware/*": [
|
|
23
|
+
"./app/middleware/*"
|
|
24
|
+
],
|
|
25
|
+
"~/assets/*": [
|
|
26
|
+
"./app/assets/*"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { posts } from '../../data/posts'
|
|
2
|
+
|
|
3
|
+
export const GET = (req: any, res: any) => {
|
|
4
|
+
const { slug } = req.params
|
|
5
|
+
const post = posts.find((p) => p.slug === slug)
|
|
6
|
+
if (!post) {
|
|
7
|
+
res.statusCode = 404
|
|
8
|
+
return res.json({ error: 'Not found' })
|
|
9
|
+
}
|
|
10
|
+
res.json(post)
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface Post {
|
|
2
|
+
slug: string
|
|
3
|
+
title: string
|
|
4
|
+
excerpt: string
|
|
5
|
+
body: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const posts: Post[] = [
|
|
9
|
+
{
|
|
10
|
+
slug: 'first-post',
|
|
11
|
+
title: 'First Post',
|
|
12
|
+
excerpt: 'The very first post in the kitchen sink.',
|
|
13
|
+
body: 'First post body content. This was loaded via a page loader.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
slug: 'second-post',
|
|
17
|
+
title: 'Second Post',
|
|
18
|
+
excerpt: 'The second post in the kitchen sink.',
|
|
19
|
+
body: 'Second post body content. Dynamic routes and loaders work together.',
|
|
20
|
+
},
|
|
21
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const root = fileURLToPath(new URL('../kitchen-sink', import.meta.url))
|
|
6
|
+
rmSync(join(root, 'dist'), { recursive: true, force: true })
|
|
7
|
+
rmSync(join(root, 'node_modules', '.cer-app-cache'), { recursive: true, force: true })
|
|
8
|
+
console.log('[e2e] Cleaned kitchen-sink/dist and cache')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jasonshimmy/vite-plugin-cer-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -48,7 +48,22 @@
|
|
|
48
48
|
"dev": "tsc -p tsconfig.build.json --watch",
|
|
49
49
|
"test": "vitest run",
|
|
50
50
|
"test:watch": "vitest",
|
|
51
|
-
"test:coverage": "vitest run --coverage"
|
|
51
|
+
"test:coverage": "vitest run --coverage",
|
|
52
|
+
"e2e:clean": "node e2e/scripts/clean.mjs",
|
|
53
|
+
"e2e:build:spa": "npm run e2e:clean && node dist/cli/index.js build --mode spa --root e2e/kitchen-sink",
|
|
54
|
+
"e2e:build:ssr": "npm run e2e:clean && node dist/cli/index.js build --mode ssr --root e2e/kitchen-sink",
|
|
55
|
+
"e2e:build:ssg": "npm run e2e:clean && node dist/cli/index.js build --mode ssg --root e2e/kitchen-sink",
|
|
56
|
+
"e2e:serve:spa": "node dist/cli/index.js preview --root e2e/kitchen-sink --port 4174",
|
|
57
|
+
"e2e:serve:ssr": "node dist/cli/index.js preview --root e2e/kitchen-sink --port 4175 --ssr",
|
|
58
|
+
"e2e:serve:ssg": "node dist/cli/index.js preview --root e2e/kitchen-sink --port 4176",
|
|
59
|
+
"e2e:run:spa": "cypress run --config baseUrl=http://localhost:4174 --env mode=spa",
|
|
60
|
+
"e2e:run:ssr": "cypress run --config baseUrl=http://localhost:4175 --env mode=ssr",
|
|
61
|
+
"e2e:run:ssg": "cypress run --config baseUrl=http://localhost:4176 --env mode=ssg",
|
|
62
|
+
"e2e:spa": "npm run e2e:build:spa && start-server-and-test e2e:serve:spa http://localhost:4174 e2e:run:spa",
|
|
63
|
+
"e2e:ssr": "npm run e2e:build:ssr && start-server-and-test e2e:serve:ssr http://localhost:4175 e2e:run:ssr",
|
|
64
|
+
"e2e:ssg": "npm run e2e:build:ssg && start-server-and-test e2e:serve:ssg http://localhost:4176 e2e:run:ssg",
|
|
65
|
+
"e2e": "npm run e2e:ssr && npm run e2e:ssg && npm run e2e:spa",
|
|
66
|
+
"cypress:open": "cypress open"
|
|
52
67
|
},
|
|
53
68
|
"peerDependencies": {
|
|
54
69
|
"@jasonshimmy/custom-elements-runtime": ">=3.0.0",
|
|
@@ -64,7 +79,9 @@
|
|
|
64
79
|
"@jasonshimmy/custom-elements-runtime": "^3.1.3",
|
|
65
80
|
"@types/node": "^25.5.0",
|
|
66
81
|
"@vitest/coverage-v8": "^4.1.0",
|
|
82
|
+
"cypress": "^15.12.0",
|
|
67
83
|
"happy-dom": "^20.8.4",
|
|
84
|
+
"start-server-and-test": "^2.0.0",
|
|
68
85
|
"typescript": "^5.4.0",
|
|
69
86
|
"vite": "^8.0.0",
|
|
70
87
|
"vitest": "^4.1.0"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the build-ssg renderPath success path.
|
|
3
|
+
*
|
|
4
|
+
* These tests use a real temporary ESM module as the "server bundle" so that
|
|
5
|
+
* the dynamic import() inside renderPath actually resolves, exercising the
|
|
6
|
+
* success branches (writeRenderedPath call, generatedPaths.push, etc.) that
|
|
7
|
+
* are unreachable when the bundle is absent.
|
|
8
|
+
*
|
|
9
|
+
* A separate test file is required so the node:fs/promises mock does NOT
|
|
10
|
+
* shadow the real writeFile/mkdir used to set up the temp bundle.
|
|
11
|
+
*/
|
|
12
|
+
import { vi, describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
13
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import { join } from 'pathe'
|
|
16
|
+
|
|
17
|
+
// Mock buildSSR so the SSG pipeline skips the Vite build step.
|
|
18
|
+
vi.mock('../../plugin/build-ssr.js', () => ({ buildSSR: vi.fn().mockResolvedValue(undefined) }))
|
|
19
|
+
vi.mock('fast-glob', () => ({ default: vi.fn().mockResolvedValue([]) }))
|
|
20
|
+
// Intentionally NOT mocking node:fs or node:fs/promises so real writes work.
|
|
21
|
+
|
|
22
|
+
import type { ResolvedCerConfig } from '../../plugin/dev-server.js'
|
|
23
|
+
|
|
24
|
+
let tmpRoot: string
|
|
25
|
+
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
// Create a minimal "server bundle" that exports a handler.
|
|
28
|
+
// The handler writes a minimal HTML string to res.end so renderPath captures it.
|
|
29
|
+
tmpRoot = join(tmpdir(), `cer-ssg-render-${Date.now()}`)
|
|
30
|
+
const serverDir = join(tmpRoot, 'dist', 'server')
|
|
31
|
+
mkdirSync(serverDir, { recursive: true })
|
|
32
|
+
writeFileSync(
|
|
33
|
+
join(serverDir, 'server.js'),
|
|
34
|
+
// ESM module — works because Vitest runs with Node's native ESM loader.
|
|
35
|
+
`export const handler = async (req, res) => {
|
|
36
|
+
res.setHeader('Content-Type', 'text/html');
|
|
37
|
+
res.end('<!DOCTYPE html><html><head></head><body>Hello from ' + req.url + '</body></html>');
|
|
38
|
+
};
|
|
39
|
+
export const apiRoutes = [];
|
|
40
|
+
export const plugins = [];
|
|
41
|
+
export const layouts = {};
|
|
42
|
+
`,
|
|
43
|
+
'utf-8',
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
rmSync(tmpRoot, { recursive: true, force: true })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConfig {
|
|
52
|
+
return {
|
|
53
|
+
root: tmpRoot,
|
|
54
|
+
srcDir: join(tmpRoot, 'app'),
|
|
55
|
+
pagesDir: join(tmpRoot, 'app', 'pages'),
|
|
56
|
+
mode: 'ssg',
|
|
57
|
+
ssr: { dsd: true },
|
|
58
|
+
ssg: { routes: ['/'], concurrency: 1 },
|
|
59
|
+
...overrides,
|
|
60
|
+
} as unknown as ResolvedCerConfig
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('buildSSG — renderPath success (real server bundle)', () => {
|
|
64
|
+
it('writes rendered HTML for the root path to dist/index.html', async () => {
|
|
65
|
+
// Reset module registry so _serverMod cache is cleared between test runs.
|
|
66
|
+
await vi.resetModules()
|
|
67
|
+
const { buildSSG } = await import('../../plugin/build-ssg.js')
|
|
68
|
+
await buildSSG(makeConfig())
|
|
69
|
+
|
|
70
|
+
const { readFileSync, existsSync } = await import('node:fs')
|
|
71
|
+
const outPath = join(tmpRoot, 'dist', 'index.html')
|
|
72
|
+
expect(existsSync(outPath)).toBe(true)
|
|
73
|
+
const html = readFileSync(outPath, 'utf-8')
|
|
74
|
+
expect(html).toContain('Hello from /')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('records the path in the ssg-manifest paths array', async () => {
|
|
78
|
+
await vi.resetModules()
|
|
79
|
+
const { buildSSG } = await import('../../plugin/build-ssg.js')
|
|
80
|
+
await buildSSG(makeConfig())
|
|
81
|
+
|
|
82
|
+
const { readFileSync } = await import('node:fs')
|
|
83
|
+
const manifest = JSON.parse(readFileSync(join(tmpRoot, 'dist', 'ssg-manifest.json'), 'utf-8'))
|
|
84
|
+
expect(manifest.paths).toContain('/')
|
|
85
|
+
expect(manifest.errors).toHaveLength(0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('renders multiple paths with concurrency > 1', async () => {
|
|
89
|
+
await vi.resetModules()
|
|
90
|
+
const { buildSSG } = await import('../../plugin/build-ssg.js')
|
|
91
|
+
const config = makeConfig({ ssg: { routes: ['/', '/about'], concurrency: 2 } } as Partial<ResolvedCerConfig>)
|
|
92
|
+
await buildSSG(config)
|
|
93
|
+
|
|
94
|
+
const { readFileSync, existsSync } = await import('node:fs')
|
|
95
|
+
expect(existsSync(join(tmpRoot, 'dist', 'index.html'))).toBe(true)
|
|
96
|
+
expect(existsSync(join(tmpRoot, 'dist', 'about', 'index.html'))).toBe(true)
|
|
97
|
+
const manifest = JSON.parse(readFileSync(join(tmpRoot, 'dist', 'ssg-manifest.json'), 'utf-8'))
|
|
98
|
+
expect(manifest.paths).toHaveLength(2)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('uses cached _serverMod on second renderPath call (no double import)', async () => {
|
|
102
|
+
// In a fresh module instance, render two paths sequentially.
|
|
103
|
+
// The second renderPath call hits the !_serverMod === false branch (cache).
|
|
104
|
+
await vi.resetModules()
|
|
105
|
+
const { buildSSG } = await import('../../plugin/build-ssg.js')
|
|
106
|
+
const config = makeConfig({ ssg: { routes: ['/', '/page2'], concurrency: 1 } } as Partial<ResolvedCerConfig>)
|
|
107
|
+
// Should complete without error — second call uses cached module
|
|
108
|
+
await expect(buildSSG(config)).resolves.not.toThrow()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -19,7 +19,7 @@ import fg from 'fast-glob'
|
|
|
19
19
|
import { createServer } from 'vite'
|
|
20
20
|
import { buildSSR } from '../../plugin/build-ssr.js'
|
|
21
21
|
import { buildRouteEntry } from '../../plugin/path-utils.js'
|
|
22
|
-
import { buildSSG } from '../../plugin/build-ssg.js'
|
|
22
|
+
import { buildSSG, writeRenderedPath } from '../../plugin/build-ssg.js'
|
|
23
23
|
import type { ResolvedCerConfig } from '../../plugin/dev-server.js'
|
|
24
24
|
|
|
25
25
|
function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConfig {
|
|
@@ -263,3 +263,49 @@ describe('buildSSG — path collection', () => {
|
|
|
263
263
|
expect(manifest.paths.length + manifest.errors.length).toBe(1)
|
|
264
264
|
})
|
|
265
265
|
})
|
|
266
|
+
|
|
267
|
+
// ─── writeRenderedPath ────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
describe('writeRenderedPath', () => {
|
|
270
|
+
beforeEach(() => {
|
|
271
|
+
vi.mocked(writeFile).mockClear()
|
|
272
|
+
vi.mocked(mkdir).mockClear()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('writes root path to dist/index.html', async () => {
|
|
276
|
+
await writeRenderedPath('/', '<html>home</html>', '/project/dist')
|
|
277
|
+
const [writePath] = vi.mocked(writeFile).mock.calls[0]
|
|
278
|
+
expect(String(writePath)).toMatch(/dist\/index\.html$/)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('writes /about to dist/about/index.html', async () => {
|
|
282
|
+
await writeRenderedPath('/about', '<html>about</html>', '/project/dist')
|
|
283
|
+
const [writePath] = vi.mocked(writeFile).mock.calls[0]
|
|
284
|
+
expect(String(writePath)).toMatch(/dist\/about\/index\.html$/)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('writes nested path to correct subdirectory', async () => {
|
|
288
|
+
await writeRenderedPath('/blog/first-post', '<html>post</html>', '/project/dist')
|
|
289
|
+
const [writePath] = vi.mocked(writeFile).mock.calls[0]
|
|
290
|
+
expect(String(writePath)).toMatch(/dist\/blog\/first-post\/index\.html$/)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('writes the provided html content', async () => {
|
|
294
|
+
const html = '<html><body>Hello</body></html>'
|
|
295
|
+
await writeRenderedPath('/page', html, '/project/dist')
|
|
296
|
+
const [, content] = vi.mocked(writeFile).mock.calls[0]
|
|
297
|
+
expect(content).toBe(html)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('creates the output directory before writing', async () => {
|
|
301
|
+
await writeRenderedPath('/nested/deep', '<html/>', '/project/dist')
|
|
302
|
+
expect(mkdir).toHaveBeenCalledWith(expect.stringContaining('nested/deep'), { recursive: true })
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('strips leading and trailing slashes from path', async () => {
|
|
306
|
+
await writeRenderedPath('/trailing/', '<html/>', '/project/dist')
|
|
307
|
+
const [writePath] = vi.mocked(writeFile).mock.calls[0]
|
|
308
|
+
expect(String(writePath)).toMatch(/trailing\/index\.html$/)
|
|
309
|
+
expect(String(writePath)).not.toMatch(/\/\//)
|
|
310
|
+
})
|
|
311
|
+
})
|