@webjsdev/core 0.7.1
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 +55 -0
- package/index.d.ts +28 -0
- package/index.js +39 -0
- package/package.json +46 -0
- package/src/component.d.ts +120 -0
- package/src/component.js +1061 -0
- package/src/context.js +327 -0
- package/src/css.js +54 -0
- package/src/directives.js +454 -0
- package/src/escape.js +15 -0
- package/src/expose.js +90 -0
- package/src/html.js +30 -0
- package/src/lazy-loader.js +107 -0
- package/src/nav.js +45 -0
- package/src/registry.js +190 -0
- package/src/render-client.js +1717 -0
- package/src/render-server.js +1128 -0
- package/src/repeat.js +37 -0
- package/src/rich-fetch.js +70 -0
- package/src/router-client.js +1434 -0
- package/src/serialize.js +452 -0
- package/src/signal.js +684 -0
- package/src/slot.js +860 -0
- package/src/suspense.js +46 -0
- package/src/task.js +386 -0
- package/src/testing.js +157 -0
- package/src/webjs-frame.js +65 -0
- package/src/websocket-client.js +111 -0
package/src/nav.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side navigation helpers, modelled after NextJs.
|
|
3
|
+
*
|
|
4
|
+
* Usage (inside a server-only module: page, layout, API route, server action):
|
|
5
|
+
*
|
|
6
|
+
* import { notFound, redirect } from '@webjsdev/core';
|
|
7
|
+
* if (!post) notFound();
|
|
8
|
+
* if (user.banned) redirect('/goodbye');
|
|
9
|
+
*
|
|
10
|
+
* Both throw a sentinel that the SSR pipeline catches. Never catch them
|
|
11
|
+
* yourself: let them propagate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const NOT_FOUND = Symbol.for('webjs.notFound');
|
|
15
|
+
const REDIRECT = Symbol.for('webjs.redirect');
|
|
16
|
+
|
|
17
|
+
/** @returns {never} */
|
|
18
|
+
export function notFound() {
|
|
19
|
+
const err = new Error('webjs: notFound()');
|
|
20
|
+
/** @type any */ (err).__webjs = NOT_FOUND;
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} url
|
|
26
|
+
* @param {number} [status] 307 (temp) by default; pass 308 for permanent
|
|
27
|
+
* @returns {never}
|
|
28
|
+
*/
|
|
29
|
+
export function redirect(url, status = 307) {
|
|
30
|
+
const err = new Error(`webjs: redirect(${url})`);
|
|
31
|
+
/** @type any */ (err).__webjs = REDIRECT;
|
|
32
|
+
/** @type any */ (err).url = url;
|
|
33
|
+
/** @type any */ (err).status = status;
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @param {unknown} e */
|
|
38
|
+
export function isNotFound(e) {
|
|
39
|
+
return !!e && /** @type any */ (e).__webjs === NOT_FOUND;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @param {unknown} e */
|
|
43
|
+
export function isRedirect(e) {
|
|
44
|
+
return !!e && /** @type any */ (e).__webjs === REDIRECT;
|
|
45
|
+
}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isomorphic custom element registry.
|
|
3
|
+
*
|
|
4
|
+
* The authoring pattern is the web-standard one:
|
|
5
|
+
*
|
|
6
|
+
* class Counter extends WebComponent { … }
|
|
7
|
+
* customElements.define('my-counter', Counter);
|
|
8
|
+
*
|
|
9
|
+
* On the browser `customElements` is the native registry; we attach a
|
|
10
|
+
* thin wrapper that mirrors each registration into our own map so SSR
|
|
11
|
+
* / router / lazy-loading code can look up `tag → Class` without
|
|
12
|
+
* dipping into the platform registry.
|
|
13
|
+
*
|
|
14
|
+
* On the server there is no native `customElements`; we install a
|
|
15
|
+
* minimal shim on `globalThis` that just records the registration.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {{ cls: typeof import('./component.js').WebComponent, moduleUrl: string | null, lazy: boolean, tag: string }} RegistryEntry
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/* ------------------------------------------------------------------
|
|
21
|
+
* Shared registry on globalThis.
|
|
22
|
+
*
|
|
23
|
+
* In a typical install, the dev server (`@webjsdev/server`) and the
|
|
24
|
+
* user's app modules each import `@webjsdev/core`. When the cli is
|
|
25
|
+
* installed globally (or hoisted at a different level than the user's
|
|
26
|
+
* app), Node resolves the bare specifier from each importer's location
|
|
27
|
+
* and may end up loading TWO instances of `@webjsdev/core`: one for
|
|
28
|
+
* the server, one for the user's app. Each instance would otherwise
|
|
29
|
+
* have its own private `registry` Map, so:
|
|
30
|
+
*
|
|
31
|
+
* user-app: ThemeToggle.register('theme-toggle') → instance-A registry
|
|
32
|
+
* server: lookup('theme-toggle') → instance-B registry (empty)
|
|
33
|
+
*
|
|
34
|
+
* SSR's `injectDSD` would then skip the tag and emit bare
|
|
35
|
+
* `<theme-toggle></theme-toggle>` even though the user registered it
|
|
36
|
+
* correctly. Symptom: components render with no children server-side
|
|
37
|
+
* and only "appear" after the browser hydrates.
|
|
38
|
+
*
|
|
39
|
+
* We side-step this by keying both maps off `globalThis` with a stable
|
|
40
|
+
* `Symbol.for(...)`. Every loaded instance of this module finds the
|
|
41
|
+
* same maps regardless of which copy of `@webjsdev/core` it lives in.
|
|
42
|
+
* @ts-ignore */
|
|
43
|
+
const REGISTRY_KEY = Symbol.for('webjs:registry');
|
|
44
|
+
/** @ts-ignore */
|
|
45
|
+
const CLASS_TO_TAG_KEY = Symbol.for('webjs:classToTag');
|
|
46
|
+
|
|
47
|
+
const _g = /** @type {any} */ (globalThis);
|
|
48
|
+
/** @type {Map<string, RegistryEntry>} */
|
|
49
|
+
const registry = _g[REGISTRY_KEY] || (_g[REGISTRY_KEY] = new Map());
|
|
50
|
+
/** @type {WeakMap<Function, string>} */
|
|
51
|
+
const classToTag = _g[CLASS_TO_TAG_KEY] || (_g[CLASS_TO_TAG_KEY] = new WeakMap());
|
|
52
|
+
|
|
53
|
+
const isBrowser =
|
|
54
|
+
typeof window !== 'undefined' && typeof customElements !== 'undefined';
|
|
55
|
+
|
|
56
|
+
/* ------------------------------------------------------------------
|
|
57
|
+
* define(tag, cls): called internally by the server-side customElements
|
|
58
|
+
* shim AND by the browser wrapper below. Populates our bookkeeping and,
|
|
59
|
+
* on the browser, delegates to the native customElements.define.
|
|
60
|
+
* ------------------------------------------------------------------ */
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} tag
|
|
64
|
+
* @param {typeof import('./component.js').WebComponent} cls
|
|
65
|
+
*/
|
|
66
|
+
function registerInternal(tag, cls) {
|
|
67
|
+
if (!tag || typeof tag !== 'string' || !tag.includes('-')) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`customElements.define: tag "${tag}" must contain a hyphen (HTML spec)`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const lazy = /** @type {any} */ (cls).lazy === true;
|
|
73
|
+
const entry = registry.get(tag);
|
|
74
|
+
if (entry) {
|
|
75
|
+
entry.cls = cls;
|
|
76
|
+
entry.lazy = lazy;
|
|
77
|
+
} else {
|
|
78
|
+
registry.set(tag, { cls, moduleUrl: null, lazy, tag });
|
|
79
|
+
}
|
|
80
|
+
classToTag.set(cls, tag);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ------------------------------------------------------------------
|
|
84
|
+
* Browser: wrap native customElements.define so every registration
|
|
85
|
+
* also lands in our map. The native behaviour (upgrading matching
|
|
86
|
+
* elements + preventing double-define) is preserved.
|
|
87
|
+
* ------------------------------------------------------------------ */
|
|
88
|
+
|
|
89
|
+
if (isBrowser) {
|
|
90
|
+
const native = customElements.define.bind(customElements);
|
|
91
|
+
/** @type {any} */ (customElements).define = function (tag, cls, options) {
|
|
92
|
+
if (!customElements.get(tag)) native(tag, cls, options);
|
|
93
|
+
registerInternal(tag, cls);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ------------------------------------------------------------------
|
|
98
|
+
* Server: install a minimal customElements shim on globalThis so
|
|
99
|
+
* `customElements.define('x', X)` at the bottom of a component module
|
|
100
|
+
* is a legal no-op that populates our registry. Idempotent.
|
|
101
|
+
* ------------------------------------------------------------------ */
|
|
102
|
+
|
|
103
|
+
if (!isBrowser) {
|
|
104
|
+
const g = /** @type {any} */ (globalThis);
|
|
105
|
+
if (!g.customElements) {
|
|
106
|
+
g.customElements = {
|
|
107
|
+
define(tag, cls) {
|
|
108
|
+
registerInternal(tag, cls);
|
|
109
|
+
},
|
|
110
|
+
get(tag) {
|
|
111
|
+
return registry.get(tag)?.cls;
|
|
112
|
+
},
|
|
113
|
+
/** No-op: server never upgrades real elements. */
|
|
114
|
+
upgrade() {},
|
|
115
|
+
/** Resolves immediately: SSR never waits for upgrade. */
|
|
116
|
+
whenDefined(tag) {
|
|
117
|
+
const e = registry.get(tag);
|
|
118
|
+
return Promise.resolve(e?.cls);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ------------------------------------------------------------------
|
|
125
|
+
* Public API: unchanged signature surface.
|
|
126
|
+
* ------------------------------------------------------------------ */
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @deprecated Low-level internal: prefer `customElements.define(tag, cls)`.
|
|
130
|
+
* Kept as a minimal wrapper for back-compat with any framework code that
|
|
131
|
+
* calls it directly.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} tag
|
|
134
|
+
* @param {typeof import('./component.js').WebComponent} cls
|
|
135
|
+
*/
|
|
136
|
+
export function register(tag, cls) {
|
|
137
|
+
if (isBrowser) {
|
|
138
|
+
/** @type any */ (customElements).define(tag, cls);
|
|
139
|
+
} else {
|
|
140
|
+
registerInternal(tag, cls);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Server-side: record the browser-visible URL for a component's module
|
|
146
|
+
* BEFORE the module is imported. Populated at server boot by the
|
|
147
|
+
* component scanner so `lookupModuleUrl` works for modulepreload hints
|
|
148
|
+
* without forcing every component file to be eagerly imported.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} tag
|
|
151
|
+
* @param {string} moduleUrl
|
|
152
|
+
*/
|
|
153
|
+
export function primeModuleUrl(tag, moduleUrl) {
|
|
154
|
+
if (isBrowser) return;
|
|
155
|
+
const entry = registry.get(tag);
|
|
156
|
+
if (entry) {
|
|
157
|
+
entry.moduleUrl = moduleUrl;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
registry.set(tag, {
|
|
161
|
+
cls: /** @type any */ (null),
|
|
162
|
+
moduleUrl,
|
|
163
|
+
lazy: false,
|
|
164
|
+
tag,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @param {string} tag */
|
|
169
|
+
export function lookup(tag) {
|
|
170
|
+
return registry.get(tag)?.cls;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** @param {string} tag */
|
|
174
|
+
export function lookupModuleUrl(tag) {
|
|
175
|
+
return registry.get(tag)?.moduleUrl || null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @param {string} tag */
|
|
179
|
+
export function isLazy(tag) {
|
|
180
|
+
return registry.get(tag)?.lazy === true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Reverse lookup: class → tag. Used for framework warnings / logs. */
|
|
184
|
+
export function tagOf(cls) {
|
|
185
|
+
return classToTag.get(cls);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function allTags() {
|
|
189
|
+
return [...registry.keys()];
|
|
190
|
+
}
|