@invisibleloop/pulse 0.20.57 → 0.20.59
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/package.json +2 -2
- package/public/.pulse-ui-version +1 -1
- package/public/pulse-ui.css +6 -0
- package/scripts/css-purge.js +6 -1
- package/src/cli/dev.js +10 -0
- package/src/cli/discover.js +2 -1
- package/src/cli/reload-hooks.js +35 -0
- package/src/cli/scaffold.archive.js +3 -3
- package/src/runtime/ssr.js +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@invisibleloop/pulse",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.59",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI-first frontend framework. The spec is the source of truth.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"scripts": {
|
|
77
77
|
"dev": "node --watch src/cli/dev.js",
|
|
78
78
|
"build": "node scripts/build.js",
|
|
79
|
-
"test": "node src/spec/schema.test.js && node src/runtime/runtime.test.js && node src/runtime/navigate.test.js && node src/runtime/morph.test.js && node src/runtime/ssr.test.js && node src/runtime/image.test.js && node src/server/server.test.js && node src/cli/cli.test.js && node src/cli/validate.test.js && node src/cli/diagnose.test.js && node src/cli/check-bundles.test.js && node src/cli/review.test.js && node src/cli/process-utils.test.js && node src/cli/stamp.test.js && node src/cli/feedback.test.js && node src/cli/update.test.js && node src/cli/dev-host.test.js && node src/ui/ui.test.js && node src/testing/testing.test.js && node src/store/store.test.js && node src/md/md.test.js && node scripts/strip-server.test.js && node scripts/css-purge.test.js && node scripts/release-version.test.js && node src/mcp/mcp.test.js && node src/mcp/guards.test.js",
|
|
79
|
+
"test": "node src/spec/schema.test.js && node src/runtime/runtime.test.js && node src/runtime/navigate.test.js && node src/runtime/morph.test.js && node src/runtime/ssr.test.js && node src/runtime/image.test.js && node src/server/server.test.js && node src/cli/cli.test.js && node src/cli/validate.test.js && node src/cli/diagnose.test.js && node src/cli/check-bundles.test.js && node src/cli/review.test.js && node src/cli/process-utils.test.js && node src/cli/stamp.test.js && node src/cli/feedback.test.js && node src/cli/update.test.js && node src/cli/dev-host.test.js && node src/cli/dev-reload.test.js && node src/ui/ui.test.js && node src/testing/testing.test.js && node src/store/store.test.js && node src/md/md.test.js && node scripts/strip-server.test.js && node scripts/css-purge.test.js && node scripts/release-version.test.js && node src/mcp/mcp.test.js && node src/mcp/guards.test.js",
|
|
80
80
|
"typecheck": "tsc --noEmit",
|
|
81
81
|
"docs": "node --watch src/cli/dev.js --root ../../docs --port 4000",
|
|
82
82
|
"docs:start": "node ../../docs/server.js"
|
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.20.
|
|
1
|
+
0.20.59
|
package/public/pulse-ui.css
CHANGED
|
@@ -3026,3 +3026,9 @@ fieldset[disabled] .ui-rating-stars .ui-rating-star:hover ~ .ui-rating-star { co
|
|
|
3026
3026
|
line-height: 0;
|
|
3027
3027
|
}
|
|
3028
3028
|
|
|
3029
|
+
/* ─── Framework skip link ───────────────────────────────────────────────────
|
|
3030
|
+
* Injected by the server's buffered render path (404s, POST re-renders, stream: false).
|
|
3031
|
+
* Hidden until keyboard focus, like the one in pulse.css — pages that load only
|
|
3032
|
+
* pulse-ui.css must not see it as a plain link at the top of the page. */
|
|
3033
|
+
.pulse-skip-link{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}
|
|
3034
|
+
.pulse-skip-link:focus{position:fixed;left:1rem;top:1rem;width:auto;height:auto;padding:.5rem 1rem;background:#000;color:#fff;font-size:1rem;font-weight:600;border-radius:4px;text-decoration:none;z-index:9999;outline:3px solid #fff;outline-offset:2px}
|
package/scripts/css-purge.js
CHANGED
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
// CSS class extractor
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
|
|
12
|
+
// Classes the framework itself writes into the document (src/runtime/ssr.js wrapDocument),
|
|
13
|
+
// outside any page's view, so nothing the build scans ever contains them. Always kept:
|
|
14
|
+
// without this a built app loses the rules that hide the injected skip link (#161).
|
|
15
|
+
export const FRAMEWORK_INJECTED_CLASSES = ['pulse-skip-link']
|
|
16
|
+
|
|
12
17
|
export function extractUsedClasses(htmlContents, jsContents, safelist = []) {
|
|
13
|
-
const used = new Set()
|
|
18
|
+
const used = new Set(FRAMEWORK_INJECTED_CLASSES)
|
|
14
19
|
|
|
15
20
|
// Extract from class="..." in rendered HTML (no template expressions to worry about)
|
|
16
21
|
for (const content of htmlContents) {
|
package/src/cli/dev.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import path from 'path'
|
|
12
12
|
import fs from 'fs'
|
|
13
|
+
import { register } from 'node:module'
|
|
14
|
+
import { pathToFileURL } from 'node:url'
|
|
13
15
|
import { createServer } from '../server/index.js'
|
|
14
16
|
import { loadPages, loadStore } from './discover.js'
|
|
15
17
|
import * as log from './logger.js'
|
|
@@ -31,6 +33,14 @@ const ROOT = rootArg !== -1
|
|
|
31
33
|
// resolve to the project root, not wherever the parent process was launched from.
|
|
32
34
|
if (process.cwd() !== ROOT) process.chdir(ROOT)
|
|
33
35
|
|
|
36
|
+
// Hot reload re-imports each page with a cache-busting stamp; this hook passes the stamp on
|
|
37
|
+
// to the project modules the page imports, so editing src/lib or src/components takes
|
|
38
|
+
// effect too (see reload-hooks.js). realpath: Node reports resolved URLs without symlinks.
|
|
39
|
+
register('./reload-hooks.js', {
|
|
40
|
+
parentURL: import.meta.url,
|
|
41
|
+
data: { rootUrl: pathToFileURL(fs.realpathSync(ROOT)).href + '/' },
|
|
42
|
+
})
|
|
43
|
+
|
|
34
44
|
let _config = {}
|
|
35
45
|
const configPath = path.join(ROOT, 'pulse.config.js')
|
|
36
46
|
if (fs.existsSync(configPath)) {
|
package/src/cli/discover.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import fs from 'fs'
|
|
16
16
|
import path from 'path'
|
|
17
|
+
import { pathToFileURL } from 'url'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Derive a route from a file path relative to the pages directory.
|
|
@@ -136,7 +137,7 @@ export async function loadStore(projectRoot, cacheBust = null) {
|
|
|
136
137
|
const storePath = path.join(projectRoot, 'pulse.store.js')
|
|
137
138
|
if (!fs.existsSync(storePath)) return null
|
|
138
139
|
|
|
139
|
-
const url =
|
|
140
|
+
const url = pathToFileURL(storePath).href + (cacheBust ? `?t=${cacheBust}` : '')
|
|
140
141
|
const mod = await import(url)
|
|
141
142
|
const def = mod.default
|
|
142
143
|
if (!def || typeof def !== 'object') {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — module-loader hook for dev hot reload
|
|
3
|
+
*
|
|
4
|
+
* `pulse dev` re-imports each page with `?t=<stamp>` to dodge Node's ESM cache, but that
|
|
5
|
+
* only refreshes the page file: everything the page imports (src/lib, src/components, ...)
|
|
6
|
+
* resolves to the same URL as before and comes back stale from the cache. This hook gives
|
|
7
|
+
* a project module imported by a stamped module the same stamp, so the whole project
|
|
8
|
+
* graph is re-evaluated on a reload.
|
|
9
|
+
*
|
|
10
|
+
* Only the project's own files are stamped. Anything in node_modules, the framework
|
|
11
|
+
* itself, and `node:` builtins keep their URL, so packages (and framework singletons)
|
|
12
|
+
* are evaluated once rather than once per edit.
|
|
13
|
+
*
|
|
14
|
+
* Limits: CommonJS files (.cjs) are cached by path, so the stamp cannot refresh them, and
|
|
15
|
+
* modules outside the project root (e.g. a sibling workspace package) are neither stamped
|
|
16
|
+
* nor watched.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
let rootUrl = ''
|
|
20
|
+
|
|
21
|
+
export function initialize(data) {
|
|
22
|
+
rootUrl = data.rootUrl
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
26
|
+
const result = await nextResolve(specifier, context)
|
|
27
|
+
const url = result.url
|
|
28
|
+
if (!rootUrl || !url.startsWith(rootUrl) || url.includes('?')) return result
|
|
29
|
+
if (url.slice(rootUrl.length).includes('node_modules/')) return result // below the root: a project may itself live under a node_modules folder
|
|
30
|
+
if (!context.parentURL?.startsWith(rootUrl)) return result
|
|
31
|
+
|
|
32
|
+
const stamp = new URL(context.parentURL).searchParams.get('t')
|
|
33
|
+
if (!stamp || !/^\d+$/.test(stamp)) return result
|
|
34
|
+
return { ...result, url: `${url}?t=${stamp}` }
|
|
35
|
+
}
|
|
@@ -605,7 +605,7 @@ Rules:
|
|
|
605
605
|
Every page must be fully navigable by keyboard alone.
|
|
606
606
|
|
|
607
607
|
**Structure**
|
|
608
|
-
- Wrap page content in \`<main id="main-content">\` —
|
|
608
|
+
- Wrap page content in \`<main id="main-content">\` — a skip link targets this id (add your own to the layout; the framework only injects a fallback on 404s and form re-renders)
|
|
609
609
|
- Use semantic elements: \`<nav>\`, \`<main>\`, \`<header>\`, \`<footer>\`, \`<section>\`, \`<article>\`, \`<aside>\`
|
|
610
610
|
- One \`<h1>\` per page — the primary heading that describes the current page
|
|
611
611
|
|
|
@@ -896,7 +896,7 @@ Threshold merge order: **global config → environment override → per-route ov
|
|
|
896
896
|
- **Redirect loops** — if \`/dashboard\` guards redirect to \`/login\`, and \`/login\` guards redirect authenticated users to \`/dashboard\`, test both directions to confirm there is no loop.
|
|
897
897
|
|
|
898
898
|
### Performance & Lighthouse
|
|
899
|
-
- **Missing \`<main id="main-content">\`** —
|
|
899
|
+
- **Missing \`<main id="main-content">\`** — skip links target \`#main-content\`. A missing element breaks the skip link and fails Lighthouse Accessibility.
|
|
900
900
|
- **Multiple \`<h1>\` elements** — one \`<h1>\` per page. Multiple at the same level fail Lighthouse Accessibility.
|
|
901
901
|
- **Images without \`width\` and \`height\`** — omitting dimensions prevents the browser from reserving space, causing layout shift and a non-zero CLS. Always provide both.
|
|
902
902
|
- **Colour contrast** — \`#888\` on \`#111\` is the practical minimum for muted text that passes WCAG AA. Lighter greys will fail. Check new colour tokens before committing.
|
|
@@ -909,7 +909,7 @@ Threshold merge order: **global config → environment override → per-route ov
|
|
|
909
909
|
## Rules the agent must follow
|
|
910
910
|
|
|
911
911
|
- **Never set \`hydrate\`** — the framework sets it automatically.
|
|
912
|
-
- **Always wrap page content in \`<main id="main-content">\`** —
|
|
912
|
+
- **Always wrap page content in \`<main id="main-content">\`** — skip links target this id.
|
|
913
913
|
- **Never use \`data-event\` on text inputs** to mirror value into state. It destroys focus on every keystroke. Use uncontrolled inputs and read values from \`FormData\` in \`action.onStart\` instead. For client-side filtering/search, render all items in the HTML and use an inline \`<script>\` to show/hide elements — no state or re-render needed.
|
|
914
914
|
- **Never add \`<script>\` tags manually** — hydration is handled by the framework.
|
|
915
915
|
- **Never add external npm packages** for client-side behaviour — express it in the spec.
|
package/src/runtime/ssr.js
CHANGED
|
@@ -343,6 +343,10 @@ export function wrapDocument({ content, spec = {}, serverState = {}, storeState
|
|
|
343
343
|
const meta = spec.meta || {}
|
|
344
344
|
const title = meta.title || 'Pulse'
|
|
345
345
|
|
|
346
|
+
// The fallback skip link is only for pages that have none: a page (or its layout) that
|
|
347
|
+
// already links to #main-content supplies its own, and two would be a redundant tab stop.
|
|
348
|
+
const ownSkipLink = /href=["']#main-content["']/.test(content)
|
|
349
|
+
|
|
346
350
|
const bodyAttr = [
|
|
347
351
|
meta.theme ? `data-theme="${esc(meta.theme)}"` : '',
|
|
348
352
|
meta.vibe ? `data-vibe="${esc(meta.vibe)}"` : '',
|
|
@@ -419,7 +423,7 @@ export function wrapDocument({ content, spec = {}, serverState = {}, storeState
|
|
|
419
423
|
${schemaScript}
|
|
420
424
|
</head>
|
|
421
425
|
<body${bodyAttr}>
|
|
422
|
-
<a href="#main-content" class="pulse-skip-link">Skip to main content</a>
|
|
426
|
+
${ownSkipLink ? '' : '<a href="#main-content" class="pulse-skip-link">Skip to main content</a>'}
|
|
423
427
|
<div id="pulse-root">
|
|
424
428
|
${content}
|
|
425
429
|
</div>
|