@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/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
+ }
@@ -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
+ }