@ape-egg/vibe 4.0.1 → 4.1.3
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/README.md +3 -1
- package/boot.js +10 -9
- package/component.js +0 -29
- package/hot-module-refresh.js +0 -0
- package/index.js +0 -28
- package/package.json +3 -2
- package/runtime/_vibe-compiled-iteration-batch.js +0 -56
- package/runtime/affected.js +6 -189
- package/runtime/cleanup.js +22 -71
- package/runtime/component-cache.js +0 -53
- package/runtime/component.js +6 -407
- package/runtime/conditionals.js +1 -143
- package/runtime/constants.js +19 -82
- package/runtime/debug.js +22 -47
- package/runtime/dispatch.js +0 -46
- package/runtime/hydrate.js +11 -130
- package/runtime/index.js +9 -382
- package/runtime/inert.js +18 -0
- package/runtime/iterate.js +7 -595
- package/runtime/iteration-utils.js +18 -71
- package/runtime/loop-scope.js +0 -58
- package/runtime/manifest.js +0 -27
- package/runtime/parse.js +2 -116
- package/runtime/pre-compiled-iterations.js +3 -51
- package/runtime/pre-compiled-manifest.js +6 -169
- package/runtime/raw-html.js +0 -5
- package/runtime/reconcile.js +4 -159
- package/runtime/staging.js +0 -57
- package/runtime/state.js +0 -61
- package/runtime/this-scope.js +0 -17
- package/runtime/tracking.js +0 -65
- package/runtime/utils.js +1 -144
- package/runtime/vibe-css.js +37 -0
- package/spa.js +0 -76
- package/vibe.css +5 -44
package/spa.js
CHANGED
|
@@ -1,31 +1,5 @@
|
|
|
1
|
-
// @ape-egg/vibe/spa
|
|
2
|
-
//
|
|
3
|
-
// Standalone SPA router — the tier between a handrolled router and compiler
|
|
4
|
-
// SPA mode. Maintains one contract: $.page = { path, route, params, src,
|
|
5
|
-
// name }, which a reactive component src (<component src="@[page.src]">)
|
|
6
|
-
// turns into a route outlet.
|
|
7
|
-
//
|
|
8
|
-
// SOFT DEPENDENCY: nothing in Vibe's runtime imports this, and deleting it
|
|
9
|
-
// leaves a working framework. Its one import is the standalone template cache
|
|
10
|
-
// (itself dependency-free), so navigation prefetches ride the same in-flight
|
|
11
|
-
// promise the outlet's mount consumes — one request per fragment. The default
|
|
12
|
-
// onNavigate writes to the runtime global window.$ at call time; pass a
|
|
13
|
-
// custom onNavigate and the module stays a pure router (parse/claim/history)
|
|
14
|
-
// over that cache.
|
|
15
|
-
//
|
|
16
|
-
// Route grammar (shared with the compiler's route table and route scanners):
|
|
17
|
-
// /static/path literal segments
|
|
18
|
-
// /brawlers/:index :param captures one segment
|
|
19
|
-
// /docs/:rest* trailing :name* captures zero or more segments
|
|
20
|
-
// * declared no-match fallback — deep links and popstate
|
|
21
|
-
// only, never claims a click
|
|
22
|
-
// Tables are pre-sorted most-specific-first; first match wins.
|
|
23
|
-
|
|
24
1
|
import { fetchComponentTemplate } from './runtime/component-cache.js';
|
|
25
2
|
|
|
26
|
-
// Captured params arrive percent-encoded from the URL bar; hand authors the
|
|
27
|
-
// decoded value. A malformed escape (user-typed %zz) keeps the raw segment —
|
|
28
|
-
// URLs are external input, not authored code.
|
|
29
3
|
const decode = (segment) => {
|
|
30
4
|
try {
|
|
31
5
|
return decodeURIComponent(segment);
|
|
@@ -34,9 +8,6 @@ const decode = (segment) => {
|
|
|
34
8
|
}
|
|
35
9
|
};
|
|
36
10
|
|
|
37
|
-
// Match one pathname against one route template. Returns the params object
|
|
38
|
-
// (possibly empty) on match, null otherwise. Empty segments are dropped, so
|
|
39
|
-
// trailing slashes resolve to the same route.
|
|
40
11
|
const matchRoute = (path, route) => {
|
|
41
12
|
const routeSegments = route.split('/').filter(Boolean);
|
|
42
13
|
const pathSegments = path.split('/').filter(Boolean);
|
|
@@ -54,21 +25,12 @@ const matchRoute = (path, route) => {
|
|
|
54
25
|
return pathSegments.length === routeSegments.length ? params : null;
|
|
55
26
|
};
|
|
56
27
|
|
|
57
|
-
// Route slug: segments joined with dashes, params flattened to their bare
|
|
58
|
-
// name — '/' → 'home', '/pve/:id' → 'pve-id', '/docs/:rest*' → 'docs-rest'.
|
|
59
|
-
// Stable across param values, so markup hangs page-scoped attributes and
|
|
60
|
-
// active checks on it: <page @[page.name]>, page.name.startsWith('pve').
|
|
61
28
|
const routeName = (route) =>
|
|
62
29
|
route
|
|
63
30
|
.replace(/^\/+|\/+$/g, '')
|
|
64
31
|
.replace(/:(\w+)\*?/g, '$1')
|
|
65
32
|
.replace(/\//g, '-') || 'home';
|
|
66
33
|
|
|
67
|
-
// Resolve a location (anything with a .pathname, or a bare path string)
|
|
68
|
-
// against a route table → { path, route, params, src, name, title? } | null.
|
|
69
|
-
// Pure: the compiled shell seeds initial $.page with it, unit tests drive it
|
|
70
|
-
// directly. '*' is skipped during matching and applied only when nothing
|
|
71
|
-
// real matched — it is a declared fallback, not a positional route.
|
|
72
34
|
export const resolve = (location, routes) => {
|
|
73
35
|
const path = location.pathname ?? location;
|
|
74
36
|
for (const entry of routes) {
|
|
@@ -85,29 +47,16 @@ export const resolve = (location, routes) => {
|
|
|
85
47
|
};
|
|
86
48
|
}
|
|
87
49
|
const fallback = routes.find((entry) => entry.route === '*');
|
|
88
|
-
// 'not-found', not '*': the slug feeds name bindings (<page @[page.name]>)
|
|
89
|
-
// and setAttribute('*') throws — the fallback must be attribute-safe.
|
|
90
50
|
return fallback
|
|
91
51
|
? { path, route: '*', params: {}, src: fallback.src, name: 'not-found', title: fallback.title }
|
|
92
52
|
: null;
|
|
93
53
|
};
|
|
94
54
|
|
|
95
|
-
// Programmatic navigation without holding the setupSpa return value: app code
|
|
96
|
-
// imports this and calls it like a link click. With no router active (MPA
|
|
97
|
-
// output, or before setupSpa runs) it falls back to a native load, so the same
|
|
98
|
-
// call site works in both output modes.
|
|
99
55
|
let activeRouter = null;
|
|
100
56
|
export const navigate = (path) =>
|
|
101
57
|
activeRouter ? activeRouter.navigate(path) : location.assign(new URL(path, location.origin));
|
|
102
58
|
|
|
103
|
-
// Wire the router: one document-level click listener + popstate. Claiming
|
|
104
|
-
// rule: same-origin, unmodified, untargeted clicks whose pathname matches a
|
|
105
|
-
// REAL route — '*' never claims, so unrouted paths navigate natively (which
|
|
106
|
-
// is what makes mixed MPA/SPA output work). Returns { navigate, dispose }.
|
|
107
59
|
export const setupSpa = ({ routes, onNavigate }) => {
|
|
108
|
-
// Default Vibe binding: one fresh-object assignment so bindings diff
|
|
109
|
-
// cleanly, reading window.$ at call time (page scripts boot vibe before
|
|
110
|
-
// setupSpa runs). A route-supplied title swaps document.title.
|
|
111
60
|
const apply = onNavigate ?? (({ path, route, params, src, name, title }) => {
|
|
112
61
|
window.$.page = { path, route, params, src, name };
|
|
113
62
|
if (title) document.title = title;
|
|
@@ -118,14 +67,6 @@ export const setupSpa = ({ routes, onNavigate }) => {
|
|
|
118
67
|
return resolved && resolved.route !== '*' ? resolved : null;
|
|
119
68
|
};
|
|
120
69
|
|
|
121
|
-
// Two-phase commit: the URL flips at interaction time, but route state —
|
|
122
|
-
// and with it every binding the outgoing page still renders — flips only
|
|
123
|
-
// once the incoming fragment's template is in hand. The old view never
|
|
124
|
-
// re-renders against the new route, and the prefetch IS the mount's fetch
|
|
125
|
-
// (same cache entry), so a navigation still costs one request. A failed
|
|
126
|
-
// prefetch resolves anyway: the flip proceeds and the mount path owns the
|
|
127
|
-
// error, exactly as before. The token makes rapid navigations last-wins —
|
|
128
|
-
// a superseded prefetch never applies a stale flip.
|
|
129
70
|
let navigationToken = 0;
|
|
130
71
|
const commit = (resolved, { scroll, hash }) => {
|
|
131
72
|
const token = ++navigationToken;
|
|
@@ -135,12 +76,6 @@ export const setupSpa = ({ routes, onNavigate }) => {
|
|
|
135
76
|
prefetch.then(() => {
|
|
136
77
|
if (token !== navigationToken) return;
|
|
137
78
|
apply(resolved);
|
|
138
|
-
// A cross-page hash link scrolls to its anchor once the fragment's
|
|
139
|
-
// mount settles — late ready fires exactly then. Hashless navigations
|
|
140
|
-
// start at the top. getElementById + decode, not querySelector: a URL
|
|
141
|
-
// fragment is an element id, not a CSS selector — digit-leading ids
|
|
142
|
-
// are valid HTML but invalid selectors, and fragments arrive
|
|
143
|
-
// percent-encoded.
|
|
144
79
|
if (hash)
|
|
145
80
|
window.$?.on?.('ready', () =>
|
|
146
81
|
document.getElementById(decodeURIComponent(hash.slice(1)))?.scrollIntoView(),
|
|
@@ -160,30 +95,19 @@ export const setupSpa = ({ routes, onNavigate }) => {
|
|
|
160
95
|
if (anchor.target || anchor.hasAttribute('download')) return;
|
|
161
96
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button) return;
|
|
162
97
|
if (event.defaultPrevented) return;
|
|
163
|
-
// Same-page hash links keep the native anchor jump.
|
|
164
98
|
if (anchor.hash && anchor.pathname === location.pathname) return;
|
|
165
99
|
const resolved = claim(anchor);
|
|
166
100
|
if (!resolved) return;
|
|
167
101
|
event.preventDefault();
|
|
168
|
-
// Same-URL click: claimed (no reload), but no duplicate history entry —
|
|
169
|
-
// Back must keep meaning "the previous page". The WHOLE url: a link to
|
|
170
|
-
// the same pathname with a different query string is a real navigation.
|
|
171
102
|
if (anchor.href === location.href) return;
|
|
172
103
|
push(anchor.href, resolved, anchor.hash || null);
|
|
173
104
|
};
|
|
174
105
|
|
|
175
|
-
// History traversal re-resolves WITH the '*' fallback — symmetric with
|
|
176
|
-
// deep-link entry, whose URL may itself be a '*' page. No scroll: the
|
|
177
|
-
// browser restores scroll position on popstate. Same two-phase commit:
|
|
178
|
-
// going back re-renders the old view only when the target is ready.
|
|
179
106
|
const onPopstate = () => {
|
|
180
107
|
const resolved = resolve(location, routes);
|
|
181
108
|
if (resolved) commit(resolved, { scroll: false });
|
|
182
109
|
};
|
|
183
110
|
|
|
184
|
-
// Programmatic navigation, claiming like a link click: real route match →
|
|
185
|
-
// SPA navigation, anything else → native load (the server rewrite serves
|
|
186
|
-
// the shell, whose deep-link resolution may then mount '*').
|
|
187
111
|
const navigate = (path) => {
|
|
188
112
|
const url = new URL(path, location.origin);
|
|
189
113
|
const resolved = claim(url);
|
package/vibe.css
CHANGED
|
@@ -1,46 +1,7 @@
|
|
|
1
|
-
[vibe-fouc],
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
1
|
+
[vibe-fouc], .vibe-fouc { visibility: hidden }
|
|
2
|
+
[vibe-staged] { display: none }
|
|
3
|
+
[vibe-fouc], .vibe-fouc, [vibe-fouc] *, .vibe-fouc * { transition: none !important; animation: none !important }
|
|
5
4
|
|
|
6
|
-
/*
|
|
7
|
-
* outgoing page (which keeps its place, visible and styled) until the commit
|
|
8
|
-
* swaps them in one paint. display: none keeps the staged content out of
|
|
9
|
-
* layout so the two never occupy the document together. */
|
|
10
|
-
[vibe-staged] {
|
|
11
|
-
display: none;
|
|
12
|
-
}
|
|
5
|
+
/* vibe:optional — everything below is skipped by disableVibeCss */
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
* CSS animations — a covered element still animates and still fires
|
|
16
|
-
* animationend, reaching inline handlers before the module chain defines
|
|
17
|
-
* them. Suppressed under the cover, entrance animations instead play on
|
|
18
|
-
* reveal, matching what the user actually sees. */
|
|
19
|
-
[vibe-fouc],
|
|
20
|
-
.vibe-fouc,
|
|
21
|
-
[vibe-fouc] *,
|
|
22
|
-
.vibe-fouc * {
|
|
23
|
-
transition: none !important;
|
|
24
|
-
animation: none !important;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/* Vibe Dehydrate - Skip Reactive Processing
|
|
28
|
-
* Elements with [vibe-dehydrate] attribute or .vibe-dehydrate class are skipped during
|
|
29
|
-
* Vibe's parsing and hydration. Useful for displaying literal @[variable] syntax in
|
|
30
|
-
* documentation, examples, or code snippets without triggering reactivity.
|
|
31
|
-
*/
|
|
32
|
-
[vibe-dehydrate],
|
|
33
|
-
.vibe-dehydrate {
|
|
34
|
-
/* No styles applied - marker only */
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/* Component Wrappers - Layout Transparent
|
|
38
|
-
* Component wrappers (<component> or <div class="component">) use display: contents
|
|
39
|
-
* to make the wrapper invisible in the layout. Children render as if the wrapper doesn't exist.
|
|
40
|
-
*/
|
|
41
|
-
component,
|
|
42
|
-
div.component,
|
|
43
|
-
slot,
|
|
44
|
-
div.slot {
|
|
45
|
-
display: contents;
|
|
46
|
-
}
|
|
7
|
+
component, div.component, slot, div.slot { display: contents }
|