@jasonshimmy/vite-plugin-cer-app 0.4.5 → 0.4.6

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 (47) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/commits.txt +1 -1
  3. package/dist/cli/create/index.js +22 -7
  4. package/dist/cli/create/index.js.map +1 -1
  5. package/dist/cli/create/templates/{ssr → shared}/app/pages/index.ts.tpl +1 -1
  6. package/dist/plugin/build-ssr.d.ts.map +1 -1
  7. package/dist/plugin/build-ssr.js +2 -205
  8. package/dist/plugin/build-ssr.js.map +1 -1
  9. package/dist/runtime/entry-server-template.d.ts +10 -4
  10. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  11. package/dist/runtime/entry-server-template.js +54 -64
  12. package/dist/runtime/entry-server-template.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/__tests__/plugin/build-ssr.test.ts +3 -120
  15. package/src/__tests__/plugin/entry-server-template.test.ts +106 -3
  16. package/src/cli/create/index.ts +21 -7
  17. package/src/cli/create/templates/{spa → shared}/app/pages/index.ts.tpl +1 -1
  18. package/src/plugin/build-ssr.ts +2 -205
  19. package/src/runtime/entry-server-template.ts +54 -64
  20. package/dist/cli/create/templates/spa/app/pages/index.ts.tpl +0 -8
  21. package/dist/cli/create/templates/ssg/app/pages/index.ts.tpl +0 -8
  22. package/dist/cli/create/templates/ssr/.gitignore.tpl +0 -25
  23. package/dist/cli/create/templates/ssr/app/layouts/default.ts.tpl +0 -15
  24. package/dist/cli/create/templates/ssr/index.html.tpl +0 -12
  25. package/dist/cli/create/templates/ssr/tsconfig.json.tpl +0 -3
  26. package/src/cli/create/templates/spa/.gitignore.tpl +0 -25
  27. package/src/cli/create/templates/spa/app/layouts/default.ts.tpl +0 -15
  28. package/src/cli/create/templates/spa/index.html.tpl +0 -12
  29. package/src/cli/create/templates/spa/tsconfig.json.tpl +0 -3
  30. package/src/cli/create/templates/ssg/.gitignore.tpl +0 -25
  31. package/src/cli/create/templates/ssg/app/layouts/default.ts.tpl +0 -15
  32. package/src/cli/create/templates/ssg/app/pages/index.ts.tpl +0 -8
  33. package/src/cli/create/templates/ssg/index.html.tpl +0 -12
  34. package/src/cli/create/templates/ssg/tsconfig.json.tpl +0 -3
  35. package/src/cli/create/templates/ssr/.gitignore.tpl +0 -25
  36. package/src/cli/create/templates/ssr/app/layouts/default.ts.tpl +0 -15
  37. package/src/cli/create/templates/ssr/app/pages/index.ts.tpl +0 -8
  38. package/src/cli/create/templates/ssr/index.html.tpl +0 -12
  39. package/src/cli/create/templates/ssr/tsconfig.json.tpl +0 -3
  40. /package/dist/cli/create/templates/{spa → shared}/.gitignore.tpl +0 -0
  41. /package/dist/cli/create/templates/{spa → shared}/app/layouts/default.ts.tpl +0 -0
  42. /package/dist/cli/create/templates/{spa → shared}/index.html.tpl +0 -0
  43. /package/dist/cli/create/templates/{spa → shared}/tsconfig.json.tpl +0 -0
  44. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/.gitignore.tpl +0 -0
  45. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/app/layouts/default.ts.tpl +0 -0
  46. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/index.html.tpl +0 -0
  47. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/tsconfig.json.tpl +0 -0
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Template string for `entry-server.ts`.
3
3
  *
4
- * This is the Node.js SSR entry point. It imports all virtual modules,
5
- * wires up the routing, and exports a handler compatible with
6
- * Express/Fastify/Node http.
4
+ * This is the Node.js SSR entry point used for both development and production
5
+ * builds. It imports all virtual modules, wires up the routing, and exports a
6
+ * request handler compatible with Express/Fastify/Node http.
7
+ *
8
+ * Key features:
9
+ * - AsyncLocalStorage for race-condition-free concurrent renders (SSG concurrency > 1)
10
+ * - Declarative Shadow DOM via renderToStringWithJITCSSDSD (always on)
11
+ * - useHead() support via beginHeadCollection / endHeadCollection
12
+ * - DSD polyfill injected at end of <body> after client-template merge
7
13
  */
8
14
  export const ENTRY_SERVER_TEMPLATE = `// Server-side entry — AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app
9
15
  import { readFileSync, existsSync } from 'node:fs'
@@ -16,10 +22,10 @@ import layouts from 'virtual:cer-layouts'
16
22
  import plugins from 'virtual:cer-plugins'
17
23
  import apiRoutes from 'virtual:cer-server-api'
18
24
  import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
19
- import { registerEntityMap, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
25
+ import { registerEntityMap, renderToStringWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
20
26
  import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
21
27
  import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
22
- import { createSSRHandler } from '@jasonshimmy/custom-elements-runtime/ssr-middleware'
28
+ import { beginHeadCollection, endHeadCollection, serializeHeadTags } from '@jasonshimmy/vite-plugin-cer-app/composables'
23
29
 
24
30
  registerBuiltinComponents()
25
31
 
@@ -29,9 +35,8 @@ registerBuiltinComponents()
29
35
  registerEntityMap(entitiesJson)
30
36
 
31
37
  // Run plugins once at server startup so their provide() values are available
32
- // to useInject() during every SSR render pass. Stored on globalThis so all
33
- // dynamically-imported page chunks share the same reference (same pattern as
34
- // __CER_HEAD_COLLECTOR__ and __CER_DATA_STORE__).
38
+ // to useInject() during every SSR/SSG render pass. Stored on globalThis so all
39
+ // dynamically-imported page chunks share the same reference.
35
40
  const _pluginProvides = new Map()
36
41
  ;(globalThis).__cerPluginProvides = _pluginProvides
37
42
  const _pluginsReady = (async () => {
@@ -63,8 +68,8 @@ const _clientTemplate = existsSync(_clientTemplatePath)
63
68
  ? readFileSync(_clientTemplatePath, 'utf-8')
64
69
  : null
65
70
 
66
- // Merge the SSR handler's full HTML document with the Vite client shell so the
67
- // final page contains both pre-rendered DSD content and the client bundle scripts.
71
+ // Merge the SSR rendered body with the Vite client shell so the final page
72
+ // contains both pre-rendered DSD content and the client bundle scripts.
68
73
  function _mergeWithClientTemplate(ssrHtml, clientTemplate) {
69
74
  const headTag = '<head>', headCloseTag = '</head>'
70
75
  const bodyTag = '<body>', bodyCloseTag = '</body>'
@@ -116,18 +121,11 @@ function _mergeWithClientTemplate(ssrHtml, clientTemplate) {
116
121
  return merged
117
122
  }
118
123
 
119
- /**
120
- * Per-request VNode factory initializes a fresh router at the request URL,
121
- * resolves the active layout from the matched route's meta, pre-loads the
122
- * matched page component (bypassing the async router-view so DSD renders
123
- * synchronously), calls the route's data loader (if any), and injects the
124
- * serialized result into the document head as window.__CER_DATA__ for
125
- * client-side hydration.
126
- *
127
- * createStreamingSSRHandler threads the router through each component's SSR
128
- * context so concurrent renders never share state.
129
- */
130
- const vnodeFactory = async (req) => {
124
+ // Per-request async setup: initialize a fresh router, resolve the matched
125
+ // route and layout, pre-load the page module, and call the data loader.
126
+ // Loader data is scoped to the current AsyncLocalStorage context via enterWith()
127
+ // so concurrent renders never share state.
128
+ const _prepareRequest = async (req) => {
131
129
  await _pluginsReady
132
130
  const router = initRouter({ routes, initialUrl: req.url ?? '/' })
133
131
  const current = router.getCurrent()
@@ -151,7 +149,6 @@ const vnodeFactory = async (req) => {
151
149
  const query = current.query ?? {}
152
150
  const data = await mod.loader({ params, query, req })
153
151
  if (data !== undefined && data !== null) {
154
- // Make data available to usePageData() during the SSR render pass.
155
152
  // enterWith() scopes the value to the current async context so
156
153
  // concurrent renders (SSG concurrency > 1) never share data.
157
154
  _cerDataStore.enterWith(data)
@@ -170,51 +167,44 @@ const vnodeFactory = async (req) => {
170
167
  return { vnode, router, head }
171
168
  }
172
169
 
173
- // Capture the raw SSR handler and wrap it to merge the response with the
174
- // Vite client template before sending — this injects the JS/CSS asset bundles
175
- // so the browser can hydrate and enable client-side routing.
176
- const _rawHandler = createSSRHandler(vnodeFactory, {
177
- render: { dsd: true, dsdPolyfill: false },
178
- })
179
-
180
- /**
181
- * The main request handler.
182
- * Compatible with Express, Fastify, and Node's raw http.createServer.
183
- *
184
- * Each request is run inside a fresh _cerDataStore.run() context so that
185
- * concurrent renders (e.g. SSG with concurrency > 1) get isolated stores.
186
- * vnodeFactory calls _cerDataStore.enterWith(loaderData) from within this
187
- * context, making the data visible to usePageData() during SSR rendering
188
- * without any global-state races.
189
- */
190
170
  export const handler = async (req, res) => {
191
- if (!_clientTemplate) {
192
- // No client template run handler normally, then inject DSD polyfill.
193
- let _html = ''
194
- await _cerDataStore.run(null, async () => {
195
- await _rawHandler(req, { setHeader: () => {}, end: (body) => { _html = body } })
171
+ await _cerDataStore.run(null, async () => {
172
+ const { vnode, router, head } = await _prepareRequest(req)
173
+
174
+ // Begin collecting useHead() calls made during the synchronous render pass.
175
+ beginHeadCollection()
176
+
177
+ // dsdPolyfill: false — we inject the polyfill manually after merging so it
178
+ // lands at the end of <body>, not inside <cer-layout-view> light DOM where
179
+ // scripts may not execute.
180
+ const { htmlWithStyles } = renderToStringWithJITCSSDSD(vnode, {
181
+ dsdPolyfill: false,
182
+ router,
196
183
  })
197
- // Inject DSD polyfill at end of <body>, outside any custom element light DOM.
198
- const _final = _html.includes('</body>')
199
- ? _html.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')
200
- : _html + DSD_POLYFILL_SCRIPT
184
+
185
+ // Collect and serialize any useHead() calls from the rendered components.
186
+ const headTags = serializeHeadTags(endHeadCollection())
187
+
188
+ // Merge loader data script + useHead() tags into the document head.
189
+ const headContent = [head, headTags].filter(Boolean).join('\\n')
190
+
191
+ // Wrap the rendered body in a full HTML document and inject the head additions
192
+ // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.
193
+ const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${htmlWithStyles}</body></html>\`
194
+
195
+ let finalHtml = _clientTemplate
196
+ ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)
197
+ : ssrHtml
198
+
199
+ // Inject DSD polyfill at end of <body>, outside <cer-layout-view>, so the
200
+ // browser runs it after parsing the declarative shadow roots.
201
+ finalHtml = finalHtml.includes('</body>')
202
+ ? finalHtml.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')
203
+ : finalHtml + DSD_POLYFILL_SCRIPT
204
+
201
205
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
202
- return res.end(_final)
203
- }
204
- let _capturedHtml = ''
205
- // Wrap _rawHandler in an isolated async-local-storage context so that
206
- // vnodeFactory's enterWith() call is scoped to this request only.
207
- await _cerDataStore.run(null, async () => {
208
- // Omit write() to force the non-streaming collect-then-end code path.
209
- await _rawHandler(req, { setHeader: () => {}, end: (body) => { _capturedHtml = body } })
206
+ res.end(finalHtml)
210
207
  })
211
- let _merged = _mergeWithClientTemplate(_capturedHtml, _clientTemplate)
212
- // Inject DSD polyfill at end of <body>, outside <cer-layout-view> light DOM.
213
- _merged = _merged.includes('</body>')
214
- ? _merged.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')
215
- : _merged + DSD_POLYFILL_SCRIPT
216
- res.setHeader('Content-Type', 'text/html; charset=utf-8')
217
- res.end(_merged)
218
208
  }
219
209
 
220
210
  export { apiRoutes, plugins, layouts, routes }
@@ -1,8 +0,0 @@
1
- component('page-index', () => {
2
- return html`
3
- <div>
4
- <h1>Welcome to {{projectName}}</h1>
5
- <p>Edit <code>app/pages/index.ts</code> to get started.</p>
6
- </div>
7
- `
8
- })
@@ -1,8 +0,0 @@
1
- component('page-index', () => {
2
- return html`
3
- <div>
4
- <h1>Welcome to {{projectName}}</h1>
5
- <p>Edit <code>app/pages/index.ts</code> to get started.</p>
6
- </div>
7
- `
8
- })
@@ -1,25 +0,0 @@
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
@@ -1,15 +0,0 @@
1
- component('layout-default', () => {
2
- return html`
3
- <header>
4
- <nav>
5
- <router-link to="/">Home</router-link>
6
- </nav>
7
- </header>
8
- <main>
9
- <slot></slot>
10
- </main>
11
- <footer>
12
- <p>Built with CER App</p>
13
- </footer>
14
- `
15
- })
@@ -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>{{projectName}}</title>
7
- </head>
8
- <body>
9
- <cer-layout-view></cer-layout-view>
10
- <script type="module" src="/@cer/app.ts"></script>
11
- </body>
12
- </html>
@@ -1,3 +0,0 @@
1
- {
2
- "extends": "./.cer/tsconfig.json"
3
- }
@@ -1,25 +0,0 @@
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
@@ -1,15 +0,0 @@
1
- component('layout-default', () => {
2
- return html`
3
- <header>
4
- <nav>
5
- <router-link to="/">Home</router-link>
6
- </nav>
7
- </header>
8
- <main>
9
- <slot></slot>
10
- </main>
11
- <footer>
12
- <p>Built with CER App</p>
13
- </footer>
14
- `
15
- })
@@ -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>{{projectName}}</title>
7
- </head>
8
- <body>
9
- <cer-layout-view></cer-layout-view>
10
- <script type="module" src="/@cer/app.ts"></script>
11
- </body>
12
- </html>
@@ -1,3 +0,0 @@
1
- {
2
- "extends": "./.cer/tsconfig.json"
3
- }
@@ -1,25 +0,0 @@
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
@@ -1,15 +0,0 @@
1
- component('layout-default', () => {
2
- return html`
3
- <header>
4
- <nav>
5
- <router-link to="/">Home</router-link>
6
- </nav>
7
- </header>
8
- <main>
9
- <slot></slot>
10
- </main>
11
- <footer>
12
- <p>Built with CER App</p>
13
- </footer>
14
- `
15
- })
@@ -1,8 +0,0 @@
1
- component('page-index', () => {
2
- return html`
3
- <div>
4
- <h1>Welcome to {{projectName}}</h1>
5
- <p>Edit <code>app/pages/index.ts</code> to get started.</p>
6
- </div>
7
- `
8
- })
@@ -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>{{projectName}}</title>
7
- </head>
8
- <body>
9
- <cer-layout-view></cer-layout-view>
10
- <script type="module" src="/@cer/app.ts"></script>
11
- </body>
12
- </html>
@@ -1,3 +0,0 @@
1
- {
2
- "extends": "./.cer/tsconfig.json"
3
- }
@@ -1,25 +0,0 @@
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
@@ -1,15 +0,0 @@
1
- component('layout-default', () => {
2
- return html`
3
- <header>
4
- <nav>
5
- <router-link to="/">Home</router-link>
6
- </nav>
7
- </header>
8
- <main>
9
- <slot></slot>
10
- </main>
11
- <footer>
12
- <p>Built with CER App</p>
13
- </footer>
14
- `
15
- })
@@ -1,8 +0,0 @@
1
- component('page-index', () => {
2
- return html`
3
- <div>
4
- <h1>Welcome to {{projectName}}</h1>
5
- <p>Edit <code>app/pages/index.ts</code> to get started.</p>
6
- </div>
7
- `
8
- })
@@ -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>{{projectName}}</title>
7
- </head>
8
- <body>
9
- <cer-layout-view></cer-layout-view>
10
- <script type="module" src="/@cer/app.ts"></script>
11
- </body>
12
- </html>
@@ -1,3 +0,0 @@
1
- {
2
- "extends": "./.cer/tsconfig.json"
3
- }