@invisibleloop/pulse 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/public/menu.js +3 -1
- package/docs/src/lib/layout.js +1 -1
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/scripts/build.js +3 -1
- package/src/cli/dev.js +6 -3
- package/src/runtime/ssr.js +42 -35
- package/src/server/index.js +34 -43
package/docs/public/menu.js
CHANGED
|
@@ -52,8 +52,10 @@
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// After client-side navigation —
|
|
55
|
+
// After client-side navigation — #pulse-root is replaced, so the original
|
|
56
|
+
// navSidebar reference is detached. Re-query for the fresh sidebar element.
|
|
56
57
|
document.addEventListener('pulse:navigate', function () {
|
|
58
|
+
navSidebar = document.querySelector('.docs-sidebar')
|
|
57
59
|
var newActive = navSidebar && navSidebar.querySelector('.nav-link.active')
|
|
58
60
|
centerNavLink(newActive)
|
|
59
61
|
})
|
package/docs/src/lib/layout.js
CHANGED
|
@@ -14,7 +14,7 @@ let _manifest = {}
|
|
|
14
14
|
export function initLayoutManifest(m) { _manifest = m }
|
|
15
15
|
const asset = href => _manifest[href] || href
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const version = typeof pkg !== 'undefined' ? (pkg?.version ?? '') : ''
|
|
18
18
|
|
|
19
19
|
function esc(s) {
|
|
20
20
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.7.
|
|
1
|
+
0.7.2
|
package/scripts/build.js
CHANGED
|
@@ -476,7 +476,9 @@ const result = await esbuild.build({
|
|
|
476
476
|
const manifest = {}
|
|
477
477
|
|
|
478
478
|
for (const [outFile, meta] of Object.entries(result.metafile.outputs)) {
|
|
479
|
-
|
|
479
|
+
// outFile is relative to process.cwd(), not ROOT — use path.resolve to get the
|
|
480
|
+
// true absolute path before computing the URL-relative bundle path.
|
|
481
|
+
const bundlePath = '/' + path.relative(path.join(ROOT, 'public'), path.resolve(outFile))
|
|
480
482
|
|
|
481
483
|
// Shared runtime chunk — esbuild generates this via splitting, no entryPoint
|
|
482
484
|
if (!meta.entryPoint && path.basename(outFile).startsWith('runtime-')) {
|
package/src/cli/dev.js
CHANGED
|
@@ -187,11 +187,14 @@ const { updateSpecs } = createServer(specs, {
|
|
|
187
187
|
|
|
188
188
|
const serveFile = (filePath) => {
|
|
189
189
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return false
|
|
190
|
-
// Strip
|
|
191
|
-
//
|
|
190
|
+
// Strip server-only imports that can't resolve in the browser:
|
|
191
|
+
// - node:* built-ins (fs, path, etc.)
|
|
192
|
+
// - JSON files imported with `with { type: 'json' }` (e.g. package.json for version)
|
|
193
|
+
// These are only used inside spec.server functions or module-level server init;
|
|
194
|
+
// the client runtime never calls those code paths.
|
|
192
195
|
let content = fs.readFileSync(filePath, 'utf-8')
|
|
193
196
|
content = content.replace(
|
|
194
|
-
/^[ \t]*import\s+(?:\{[^}]*\}|[\w*]+(?:\s+as\s+\w+)?)\s+from\s+['"]node:[^'"]
|
|
197
|
+
/^[ \t]*import\s+(?:\{[^}]*\}|[\w*]+(?:\s+as\s+\w+)?)\s+from\s+['"](?:node:[^'"]+|[^'"]*\.json)['"]\s*(?:with\s*\{[^}]*\})?\s*;?[ \t]*\n?/gm,
|
|
195
198
|
''
|
|
196
199
|
)
|
|
197
200
|
res.writeHead(200, {
|
package/src/runtime/ssr.js
CHANGED
|
@@ -258,9 +258,48 @@ export function renderToNavStream(spec, ctx = {}, resolvedMeta = {}) {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
// ---------------------------------------------------------------------------
|
|
261
|
-
// Page HTML wrapper
|
|
261
|
+
// Page HTML wrapper — shared script builders
|
|
262
262
|
// ---------------------------------------------------------------------------
|
|
263
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Build the inline script that exposes window.__PULSE_NONCE__ and (optionally)
|
|
266
|
+
* window.__PULSE_STORE__ to the client runtime.
|
|
267
|
+
*
|
|
268
|
+
* Only emitted for hydrated pages. Purely server-rendered pages have no JS to
|
|
269
|
+
* read these globals, and omitting the inline script keeps cached HTML nonce-free
|
|
270
|
+
* so it can be served with a nonce-free CSP without a CSP violation.
|
|
271
|
+
*/
|
|
272
|
+
export function buildStoreScript(spec, storeState, nonce) {
|
|
273
|
+
if (!spec.hydrate) return ''
|
|
274
|
+
if (storeState && Object.keys(storeState).length > 0) {
|
|
275
|
+
return `<script nonce="${nonce}">window.__PULSE_NONCE__='${nonce}';window.__PULSE_STORE__=${JSON.stringify(storeState)};window.__updatePulseStore__=function(s){window.__PULSE_STORE__=Object.assign(window.__PULSE_STORE__||{},s);};</script>`
|
|
276
|
+
}
|
|
277
|
+
return `<script nonce="${nonce}">window.__PULSE_NONCE__='${nonce}';</script>`
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Build the hydration <script> tag that mounts the spec client-side.
|
|
282
|
+
*
|
|
283
|
+
* Bundle paths (/dist/) → a single external <script type="module" src="..."> tag.
|
|
284
|
+
* Dev source paths → an inline module block with explicit imports (nonce required).
|
|
285
|
+
*/
|
|
286
|
+
export function buildHydrateScript(spec, storeDef, nonce) {
|
|
287
|
+
if (!spec.hydrate) return ''
|
|
288
|
+
if (isBundle(spec.hydrate)) {
|
|
289
|
+
return `<script type="module" src="${esc(spec.hydrate)}"></script>`
|
|
290
|
+
}
|
|
291
|
+
const storeImport = storeDef?.hydrate ? `\n import store from '${esc(storeDef.hydrate)}'` : ''
|
|
292
|
+
const storeArg = storeDef?.hydrate ? ', { ssr: true, store }' : ', { ssr: true }'
|
|
293
|
+
return `<script type="module" nonce="${nonce}">
|
|
294
|
+
import spec from '${esc(spec.hydrate)}'
|
|
295
|
+
import { mount } from '/@pulse/runtime/index.js'
|
|
296
|
+
import { initNavigation } from '/@pulse/runtime/navigate.js'${storeImport}
|
|
297
|
+
const root = document.getElementById('pulse-root')
|
|
298
|
+
mount(spec, root, window.__PULSE_SERVER__ || {}${storeArg})
|
|
299
|
+
initNavigation(root, mount)
|
|
300
|
+
</script>`
|
|
301
|
+
}
|
|
302
|
+
|
|
264
303
|
/**
|
|
265
304
|
* Wrap rendered content in a full HTML document.
|
|
266
305
|
*
|
|
@@ -320,40 +359,8 @@ export function wrapDocument({ content, spec = {}, serverState = {}, storeState
|
|
|
320
359
|
? `<script nonce="${nonce}">window.__PULSE_SERVER__ = ${JSON.stringify(serverState)};</script>`
|
|
321
360
|
: ''
|
|
322
361
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// singleton with fresh server data on client-side navigations.
|
|
326
|
-
// window.__PULSE_NONCE__ lets the toast runtime inject a nonce'd <style> tag
|
|
327
|
-
// to satisfy the style-src CSP directive.
|
|
328
|
-
const storeStateScript = storeState && Object.keys(storeState).length > 0
|
|
329
|
-
? `<script nonce="${nonce}">window.__PULSE_NONCE__='${nonce}';window.__PULSE_STORE__=${JSON.stringify(storeState)};window.__updatePulseStore__=function(s){window.__PULSE_STORE__=Object.assign(window.__PULSE_STORE__||{},s);};</script>`
|
|
330
|
-
: spec.hydrate
|
|
331
|
-
? `<script nonce="${nonce}">window.__PULSE_NONCE__='${nonce}';</script>`
|
|
332
|
-
: ''
|
|
333
|
-
|
|
334
|
-
// Hydration bootstrap — makes the server-rendered HTML interactive.
|
|
335
|
-
// When hydrate points to a self-executing bundle (/dist/…) a single <script>
|
|
336
|
-
// tag is enough; the bundle imports spec + runtime and calls mount() itself.
|
|
337
|
-
// In dev mode (source file path) we emit the explicit inline import block.
|
|
338
|
-
const storeImport = spec.hydrate && !isBundle(spec.hydrate) && storeDef?.hydrate
|
|
339
|
-
? `\n import store from '${esc(storeDef.hydrate)}'`
|
|
340
|
-
: ''
|
|
341
|
-
const storeArg = spec.hydrate && !isBundle(spec.hydrate) && storeDef?.hydrate
|
|
342
|
-
? ', { ssr: true, store }'
|
|
343
|
-
: ', { ssr: true }'
|
|
344
|
-
|
|
345
|
-
const hydrateScript = spec.hydrate
|
|
346
|
-
? isBundle(spec.hydrate)
|
|
347
|
-
? `<script type="module" src="${esc(spec.hydrate)}"></script>`
|
|
348
|
-
: `<script type="module" nonce="${nonce}">
|
|
349
|
-
import spec from '${esc(spec.hydrate)}'
|
|
350
|
-
import { mount } from '/@pulse/runtime/index.js'
|
|
351
|
-
import { initNavigation } from '/@pulse/runtime/navigate.js'${storeImport}
|
|
352
|
-
const root = document.getElementById('pulse-root')
|
|
353
|
-
mount(spec, root, window.__PULSE_SERVER__ || {}${storeArg})
|
|
354
|
-
initNavigation(root, mount)
|
|
355
|
-
</script>`
|
|
356
|
-
: ''
|
|
362
|
+
const storeStateScript = buildStoreScript(spec, storeState, nonce)
|
|
363
|
+
const hydrateScript = buildHydrateScript(spec, storeDef, nonce)
|
|
357
364
|
|
|
358
365
|
// Server-Timing header value (caller is responsible for setting the header)
|
|
359
366
|
const serverTimingValue = timing.total !== undefined
|
package/src/server/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { createRequire } from 'module'
|
|
|
21
21
|
|
|
22
22
|
const _require = createRequire(import.meta.url)
|
|
23
23
|
export const version = _require('../../package.json').version
|
|
24
|
-
import { renderToString, renderToStream, renderToNavStream, wrapDocument, resolveServerState } from '../runtime/ssr.js'
|
|
24
|
+
import { renderToString, renderToStream, renderToNavStream, wrapDocument, resolveServerState, buildStoreScript, buildHydrateScript } from '../runtime/ssr.js'
|
|
25
25
|
import { validateSpec } from '../spec/schema.js'
|
|
26
26
|
import { validateStore, resolveStoreState } from '../store/index.js'
|
|
27
27
|
|
|
@@ -92,8 +92,8 @@ function buildCsp(nonce, ext = {}) {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* Build the nonce-free CSP for cached responses.
|
|
95
|
-
* Safe because
|
|
96
|
-
*
|
|
95
|
+
* Safe because isCacheablePage() only returns true when spec.server, spec.store,
|
|
96
|
+
* and spec.hydrate are all absent — guaranteeing no inline nonce scripts in the HTML.
|
|
97
97
|
* @param {Record<string,string[]>} [ext] Extra sources to merge in per directive.
|
|
98
98
|
*/
|
|
99
99
|
function buildCachedCsp(ext = {}) {
|
|
@@ -305,6 +305,26 @@ function pageCacheTtl(spec, dev, defaultCache) {
|
|
|
305
305
|
return cfg?.maxAge || 0
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
/** Build the in-process page cache key scoped to route + request parameters. */
|
|
309
|
+
function buildCacheKey(spec, ctx) {
|
|
310
|
+
return spec.route + '\0' + JSON.stringify(ctx.params) + '\0' + JSON.stringify(ctx.query)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* True when a page's rendered HTML is safe to cache in-process.
|
|
315
|
+
*
|
|
316
|
+
* A page is NOT cacheable if it has any of:
|
|
317
|
+
* - spec.server — embeds a nonce'd __PULSE_SERVER__ inline script
|
|
318
|
+
* - spec.store — same
|
|
319
|
+
* - spec.hydrate — embeds a nonce'd __PULSE_NONCE__ inline script (for toasts)
|
|
320
|
+
*
|
|
321
|
+
* In all three cases the cached HTML would contain a nonce that doesn't match
|
|
322
|
+
* the nonce-free CSP used for cached responses, causing a CSP violation.
|
|
323
|
+
*/
|
|
324
|
+
function isCacheablePage(spec) {
|
|
325
|
+
return !spec.server && !spec.store?.length && !spec.hydrate
|
|
326
|
+
}
|
|
327
|
+
|
|
308
328
|
// ---------------------------------------------------------------------------
|
|
309
329
|
// Dev import map — lets browser resolve @invisibleloop/pulse/* bare specifiers
|
|
310
330
|
// ---------------------------------------------------------------------------
|
|
@@ -350,8 +370,7 @@ async function cachedRenderToString(spec, ctx, dev) {
|
|
|
350
370
|
return renderToString(spec, ctx)
|
|
351
371
|
}
|
|
352
372
|
|
|
353
|
-
|
|
354
|
-
const key = spec.route + '\0' + JSON.stringify(ctx.params) + '\0' + JSON.stringify(ctx.query)
|
|
373
|
+
const key = buildCacheKey(spec, ctx)
|
|
355
374
|
|
|
356
375
|
const cached = serverDataCache.get(key)
|
|
357
376
|
if (cached) return cached
|
|
@@ -841,9 +860,8 @@ async function handleNavStreamResponse(spec, ctx, req, res) {
|
|
|
841
860
|
* Checks the in-process page cache before rendering; stores result after.
|
|
842
861
|
*/
|
|
843
862
|
async function handleStringResponse(spec, ctx, req, res, extraBody = '', dev = false, canonicalBase = '', nonce = '', runtimeBundle = '', defaultCache = null, store = null, csp = {}, faviconPath = null) {
|
|
844
|
-
const cacheKey = spec
|
|
845
|
-
|
|
846
|
-
const ttl = (!spec.server && !spec.store?.length) ? pageCacheTtl(spec, dev, defaultCache) : 0
|
|
863
|
+
const cacheKey = buildCacheKey(spec, ctx)
|
|
864
|
+
const ttl = isCacheablePage(spec) ? pageCacheTtl(spec, dev, defaultCache) : 0
|
|
847
865
|
|
|
848
866
|
let html, serverTimingValue, fromCache = false
|
|
849
867
|
|
|
@@ -892,9 +910,8 @@ async function handleStringResponse(spec, ctx, req, res, extraBody = '', dev = f
|
|
|
892
910
|
*/
|
|
893
911
|
async function handleStreamResponse(spec, ctx, req, res, extraBody = '', dev = false, canonicalBase = '', nonce = '', runtimeBundle = '', defaultCache = null, store = null, csp = {}, faviconPath = null) {
|
|
894
912
|
// Serve from in-process page cache when available — skip streaming overhead.
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
const ttl = (!spec.server && !spec.store?.length) ? pageCacheTtl(spec, dev, defaultCache) : 0
|
|
913
|
+
const cacheKey = buildCacheKey(spec, ctx)
|
|
914
|
+
const ttl = isCacheablePage(spec) ? pageCacheTtl(spec, dev, defaultCache) : 0
|
|
898
915
|
const cached = ttl > 0 ? pageHtmlCache.get(cacheKey) : null
|
|
899
916
|
|
|
900
917
|
if (cached) {
|
|
@@ -973,45 +990,19 @@ ${stylePreloads ? stylePreloads + '\n' : ''}${runtimePreload ? runtimePreload +
|
|
|
973
990
|
<body${bodyAttr}>
|
|
974
991
|
<div id="pulse-root">`
|
|
975
992
|
|
|
976
|
-
const
|
|
977
|
-
? `\n import store from '${escHtml(store.hydrate)}'`
|
|
978
|
-
: ''
|
|
979
|
-
const storeArg = !spec.hydrate?.startsWith('/dist/') && store?.hydrate
|
|
980
|
-
? ', { ssr: true, store }'
|
|
981
|
-
: ', { ssr: true }'
|
|
982
|
-
|
|
983
|
-
const hydrateScript = spec.hydrate
|
|
984
|
-
? spec.hydrate.startsWith('/dist/')
|
|
985
|
-
? `\n <script type="module" src="${escHtml(spec.hydrate)}"></script>`
|
|
986
|
-
: `\n <script type="module" nonce="${nonce}">
|
|
987
|
-
import spec from '${escHtml(spec.hydrate)}'
|
|
988
|
-
import { mount } from '/@pulse/runtime/index.js'
|
|
989
|
-
import { initNavigation } from '/@pulse/runtime/navigate.js'${storeImport}
|
|
990
|
-
const root = document.getElementById('pulse-root')
|
|
991
|
-
mount(spec, root, window.__PULSE_SERVER__ || {}${storeArg})
|
|
992
|
-
initNavigation(root, mount)
|
|
993
|
-
</script>`
|
|
994
|
-
: ''
|
|
995
|
-
|
|
996
|
-
const scriptTags = (meta.scripts || [])
|
|
993
|
+
const scriptTags = (meta.scripts || [])
|
|
997
994
|
.map(src => ` <script src="${escHtml(src)}" defer></script>`)
|
|
998
995
|
.join('\n')
|
|
996
|
+
const storeScript = buildStoreScript(spec, ctx.store || null, nonce)
|
|
997
|
+
const hydrateScript = buildHydrateScript(spec, store, nonce)
|
|
999
998
|
|
|
1000
999
|
const resolvedExtraBody = typeof extraBody === 'function' ? extraBody(nonce) : extraBody
|
|
1001
1000
|
|
|
1002
|
-
// Emit window.__PULSE_STORE__ so the client store singleton can be initialised.
|
|
1003
|
-
// Also exposes __updatePulseStore__ for navigate.js to refresh store on navigation.
|
|
1004
|
-
// window.__PULSE_NONCE__ lets the toast runtime inject a nonce'd <style> tag to
|
|
1005
|
-
// satisfy the style-src CSP directive (which disallows uninonce'd inline styles).
|
|
1006
|
-
const storeScript = ctx.store && Object.keys(ctx.store).length > 0
|
|
1007
|
-
? `\n <script nonce="${nonce}">window.__PULSE_NONCE__='${nonce}';window.__PULSE_STORE__=${JSON.stringify(ctx.store)};window.__updatePulseStore__=function(s){window.__PULSE_STORE__=Object.assign(window.__PULSE_STORE__||{},s);};</script>`
|
|
1008
|
-
: spec.hydrate
|
|
1009
|
-
? `\n <script nonce="${nonce}">window.__PULSE_NONCE__='${nonce}';</script>`
|
|
1010
|
-
: ''
|
|
1011
|
-
|
|
1012
1001
|
const docClose = `
|
|
1013
1002
|
</div>
|
|
1014
|
-
${scriptTags}
|
|
1003
|
+
${scriptTags}
|
|
1004
|
+
${storeScript}
|
|
1005
|
+
${hydrateScript}
|
|
1015
1006
|
${resolvedExtraBody}
|
|
1016
1007
|
</body>
|
|
1017
1008
|
</html>`
|