arcway 0.1.11 → 0.1.12

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/client/router.js CHANGED
@@ -1,5 +1,3 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import React from "react";
3
1
  import {
4
2
  createContext,
5
3
  useContext,
@@ -9,86 +7,96 @@ import {
9
7
  useMemo,
10
8
  useRef,
11
9
  useTransition,
12
- createElement
13
- } from "react";
10
+ createElement,
11
+ } from 'react';
14
12
  import {
15
13
  readClientManifest,
16
14
  loadPage,
17
15
  matchClientRoute,
18
16
  loadLoadingComponents,
19
- prefetchRoute
20
- } from "./page-loader.js";
21
- import { parseQuery } from "./query.js";
22
- const ROUTER_CTX_KEY = "__router_context__";
23
- const RouterContext = globalThis[ROUTER_CTX_KEY] ??= createContext(null);
17
+ prefetchRoute,
18
+ } from './page-loader.js';
19
+ import { parseQuery } from './query.js';
20
+
21
+ const ROUTER_CTX_KEY = '__router_context__';
22
+ const RouterContext = (globalThis[ROUTER_CTX_KEY] ??= createContext(null));
23
+
24
24
  function wrapInLayouts(element, layouts) {
25
25
  for (let i = layouts.length - 1; i >= 0; i--) {
26
26
  element = createElement(layouts[i], null, element);
27
27
  }
28
28
  return element;
29
29
  }
30
+
30
31
  function useRouter() {
31
32
  const ctx = useContext(RouterContext);
32
33
  if (!ctx) {
33
- throw new Error("useRouter must be used within a <Router> provider");
34
+ throw new Error('useRouter must be used within a <Router> provider');
34
35
  }
36
+
35
37
  return useMemo(
36
38
  () => ({
37
39
  pathname: ctx.pathname,
38
40
  params: ctx.params,
39
- query: typeof window !== "undefined" ? parseQuery(window.location.search) : {},
41
+ query: typeof window !== 'undefined' ? parseQuery(window.location.search) : {},
40
42
  push: (to, options) => ctx.navigate(to, { ...options, replace: false }),
41
43
  replace: (to, options) => ctx.navigate(to, { ...options, replace: true }),
42
44
  back: () => {
43
- if (typeof window !== "undefined") window.history.back();
45
+ if (typeof window !== 'undefined') window.history.back();
44
46
  },
45
47
  forward: () => {
46
- if (typeof window !== "undefined") window.history.forward();
48
+ if (typeof window !== 'undefined') window.history.forward();
47
49
  },
48
50
  refresh: () => {
49
- if (typeof window !== "undefined") window.location.reload();
50
- }
51
+ if (typeof window !== 'undefined') window.location.reload();
52
+ },
51
53
  }),
52
- [ctx.pathname, ctx.params, ctx.navigate]
54
+ [ctx.pathname, ctx.params, ctx.navigate],
53
55
  );
54
56
  }
57
+
55
58
  function usePathname() {
56
59
  return useRouter().pathname;
57
60
  }
61
+
58
62
  function useParams() {
59
63
  return useRouter().params;
60
64
  }
65
+
61
66
  function useSearchParams() {
62
67
  return useRouter().query;
63
68
  }
69
+
64
70
  function Router({
65
71
  initialPath,
66
72
  initialParams,
67
73
  initialComponent,
68
74
  initialLayouts,
69
75
  initialLoadings,
70
- children
76
+ children,
71
77
  }) {
72
78
  const [pathname, setPathname] = useState(
73
- () => initialPath ?? (typeof window !== "undefined" ? window.location.pathname : "/")
79
+ () => initialPath ?? (typeof window !== 'undefined' ? window.location.pathname : '/'),
74
80
  );
75
81
  const [pageState, setPageState] = useState({
76
82
  component: initialComponent ?? null,
77
83
  layouts: initialLayouts ?? [],
78
84
  loadings: initialLoadings ?? [],
79
- params: initialParams ?? {}
85
+ params: initialParams ?? {},
80
86
  });
81
87
  const [isNavigating, setIsNavigating] = useState(false);
82
88
  const [isPending, startTransition] = useTransition();
83
89
  const manifestRef = useRef(null);
90
+
84
91
  useEffect(() => {
85
92
  manifestRef.current = readClientManifest();
86
93
  const onManifestUpdate = () => {
87
94
  manifestRef.current = readClientManifest();
88
95
  };
89
- window.addEventListener("manifest-update", onManifestUpdate);
90
- return () => window.removeEventListener("manifest-update", onManifestUpdate);
96
+ window.addEventListener('manifest-update', onManifestUpdate);
97
+ return () => window.removeEventListener('manifest-update', onManifestUpdate);
91
98
  }, []);
99
+
92
100
  const applyLoaded = useCallback((loaded, newPath) => {
93
101
  startTransition(() => {
94
102
  setPathname(newPath);
@@ -96,67 +104,78 @@ function Router({
96
104
  component: loaded.component,
97
105
  layouts: loaded.layouts,
98
106
  loadings: loaded.loadings,
99
- params: loaded.params
107
+ params: loaded.params,
100
108
  });
101
109
  setIsNavigating(false);
102
110
  });
103
111
  }, []);
112
+
104
113
  const navigateToPage = useCallback(
105
114
  async (to, options) => {
106
115
  const scroll = options?.scroll !== false;
107
116
  const replace = options?.replace === true;
108
117
  if (to === pathname) return;
118
+
109
119
  const manifest = manifestRef.current;
110
120
  if (!manifest) {
111
121
  window.location.href = to;
112
122
  return;
113
123
  }
124
+
114
125
  const matched = matchClientRoute(manifest, to);
115
126
  if (!matched) {
116
127
  window.location.href = to;
117
128
  return;
118
129
  }
130
+
119
131
  setIsNavigating(true);
120
132
  if (replace) {
121
- window.history.replaceState(null, "", to);
133
+ window.history.replaceState(null, '', to);
122
134
  } else {
123
- window.history.pushState(null, "", to);
135
+ window.history.pushState(null, '', to);
124
136
  }
137
+
125
138
  const targetLoadings = await loadLoadingComponents(matched.route);
126
139
  if (targetLoadings.length > 0) {
127
140
  setPathname(to);
128
141
  setPageState((prev) => ({
129
142
  ...prev,
130
143
  loadings: targetLoadings,
131
- params: matched.params
144
+ params: matched.params,
132
145
  }));
133
146
  }
147
+
134
148
  try {
135
149
  const loaded = await loadPage(manifest, to);
136
150
  if (!loaded) {
137
151
  window.location.href = to;
138
152
  return;
139
153
  }
154
+
140
155
  applyLoaded(loaded, to);
156
+
141
157
  if (scroll) {
142
158
  window.scrollTo(0, 0);
143
159
  }
144
160
  } catch (err) {
145
- console.error("Client navigation failed, falling back to full page load:", err);
161
+ console.error('Client navigation failed, falling back to full page load:', err);
146
162
  window.location.href = to;
147
163
  }
148
164
  },
149
- [pathname, applyLoaded]
165
+ [pathname, applyLoaded],
150
166
  );
167
+
151
168
  useEffect(() => {
152
169
  async function onPopState() {
153
170
  const newPath = window.location.pathname;
154
171
  const manifest = manifestRef.current;
172
+
155
173
  if (!manifest) {
156
174
  setPathname(newPath);
157
175
  setPageState((prev) => ({ ...prev, params: {} }));
158
176
  return;
159
177
  }
178
+
160
179
  try {
161
180
  const loaded = await loadPage(manifest, newPath);
162
181
  if (loaded) {
@@ -168,60 +187,71 @@ function Router({
168
187
  window.location.reload();
169
188
  }
170
189
  }
171
- window.addEventListener("popstate", onPopState);
172
- return () => window.removeEventListener("popstate", onPopState);
190
+
191
+ window.addEventListener('popstate', onPopState);
192
+ return () => window.removeEventListener('popstate', onPopState);
173
193
  }, [applyLoaded]);
194
+
174
195
  const { component: PageComponent, layouts, loadings, params } = pageState;
196
+
175
197
  let content;
176
198
  if (PageComponent) {
177
- const inner = isNavigating && loadings.length > 0 ? createElement(loadings.at(-1)) : createElement(PageComponent, params);
199
+ const inner =
200
+ isNavigating && loadings.length > 0
201
+ ? createElement(loadings.at(-1))
202
+ : createElement(PageComponent, params);
178
203
  content = wrapInLayouts(inner, layouts);
179
204
  } else {
180
205
  content = children;
181
206
  }
182
- return /* @__PURE__ */ jsxs(
207
+
208
+ const progressBar =
209
+ isNavigating && loadings.length === 0 && !isPending
210
+ ? createElement('div', {
211
+ style: {
212
+ position: 'fixed',
213
+ top: 0,
214
+ left: 0,
215
+ width: '100%',
216
+ height: '2px',
217
+ backgroundColor: '#0070f3',
218
+ zIndex: 99999,
219
+ animation: 'nav-progress 1s ease-in-out infinite',
220
+ },
221
+ })
222
+ : null;
223
+
224
+ return createElement(
183
225
  RouterContext.Provider,
184
226
  {
185
227
  value: {
186
228
  pathname,
187
229
  params,
188
- navigate: navigateToPage
230
+ navigate: navigateToPage,
189
231
  },
190
- children: [
191
- content,
192
- isNavigating && loadings.length === 0 && !isPending && /* @__PURE__ */ jsx(
193
- "div",
194
- {
195
- style: {
196
- position: "fixed",
197
- top: 0,
198
- left: 0,
199
- width: "100%",
200
- height: "2px",
201
- backgroundColor: "#0070f3",
202
- zIndex: 99999,
203
- animation: "nav-progress 1s ease-in-out infinite"
204
- }
205
- }
206
- )
207
- ]
208
- }
232
+ },
233
+ content,
234
+ progressBar,
209
235
  );
210
236
  }
211
- function Link({ href, children, onClick, scroll, replace, prefetch = "hover", ...rest }) {
237
+
238
+ function Link({ href, children, onClick, scroll, replace, prefetch = 'hover', ...rest }) {
212
239
  const router = useContext(RouterContext);
213
240
  const linkRef = useRef(null);
241
+
214
242
  const handleMouseEnter = useCallback(() => {
215
- if (prefetch !== "hover") return;
243
+ if (prefetch !== 'hover') return;
216
244
  const manifest = readClientManifest();
217
245
  if (manifest) {
218
246
  prefetchRoute(manifest, href);
219
247
  }
220
248
  }, [href, prefetch]);
249
+
221
250
  useEffect(() => {
222
- if (prefetch !== "viewport") return;
251
+ if (prefetch !== 'viewport') return;
223
252
  const el = linkRef.current;
224
- if (!el || typeof IntersectionObserver === "undefined") return;
253
+ if (!el || typeof IntersectionObserver === 'undefined') return;
254
+
225
255
  const observer = new IntersectionObserver(
226
256
  (entries) => {
227
257
  for (const entry of entries) {
@@ -235,40 +265,42 @@ function Link({ href, children, onClick, scroll, replace, prefetch = "hover", ..
235
265
  }
236
266
  }
237
267
  },
238
- { rootMargin: "200px" }
268
+ { rootMargin: '200px' },
239
269
  );
270
+
240
271
  observer.observe(el);
241
272
  return () => observer.disconnect();
242
273
  }, [href, prefetch]);
274
+
243
275
  function handleClick(e) {
244
276
  if (onClick) onClick(e);
245
277
  if (e.defaultPrevented) return;
246
278
  if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
247
279
  if (e.button !== 0) return;
248
- if (rest.target === "_blank" || rest.download !== void 0) return;
280
+ if (rest.target === '_blank' || rest.download !== undefined) return;
281
+
249
282
  try {
250
283
  const url = new URL(href, window.location.origin);
251
284
  if (url.origin !== window.location.origin) return;
252
285
  } catch {
253
286
  return;
254
287
  }
288
+
255
289
  e.preventDefault();
256
290
  if (router) {
257
291
  router.navigate(href, { scroll, replace });
258
292
  } else {
259
- window.history.pushState(null, "", href);
260
- window.dispatchEvent(new PopStateEvent("popstate"));
293
+ window.history.pushState(null, '', href);
294
+ window.dispatchEvent(new PopStateEvent('popstate'));
261
295
  }
262
296
  }
263
- return /* @__PURE__ */ jsx("a", { ref: linkRef, href, onClick: handleClick, onMouseEnter: handleMouseEnter, ...rest, children });
297
+
298
+ return createElement(
299
+ 'a',
300
+ { ref: linkRef, href, onClick: handleClick, onMouseEnter: handleMouseEnter, ...rest },
301
+ children,
302
+ );
264
303
  }
304
+
265
305
  const SoloRouter = Router;
266
- export {
267
- Link,
268
- Router,
269
- SoloRouter,
270
- useParams,
271
- usePathname,
272
- useRouter,
273
- useSearchParams
274
- };
306
+ export { Link, Router, SoloRouter, useParams, usePathname, useRouter, useSearchParams };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcway",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,6 @@
29
29
  "./ui/style-mira.css": "./client/ui/style-mira.css"
30
30
  },
31
31
  "scripts": {
32
- "prepare": "esbuild client/router.jsx --outfile=client/router.js --format=esm --jsx=automatic",
33
32
  "test": "vitest run --project=unit",
34
33
  "test:storybook": "TMPDIR=~/tmp PLAYWRIGHT_BROWSERS_PATH=~/.cache/playwright vitest run --project=storybook",
35
34
  "test:all": "TMPDIR=~/tmp PLAYWRIGHT_BROWSERS_PATH=~/.cache/playwright vitest run",
package/client/router.jsx DELETED
@@ -1,303 +0,0 @@
1
- import React from 'react';
2
- import {
3
- createContext,
4
- useContext,
5
- useState,
6
- useEffect,
7
- useCallback,
8
- useMemo,
9
- useRef,
10
- useTransition,
11
- createElement,
12
- } from 'react';
13
- import {
14
- readClientManifest,
15
- loadPage,
16
- matchClientRoute,
17
- loadLoadingComponents,
18
- prefetchRoute,
19
- } from './page-loader.js';
20
- import { parseQuery } from './query.js';
21
-
22
- const ROUTER_CTX_KEY = '__router_context__';
23
- const RouterContext = (globalThis[ROUTER_CTX_KEY] ??= createContext(null));
24
-
25
- function wrapInLayouts(element, layouts) {
26
- for (let i = layouts.length - 1; i >= 0; i--) {
27
- element = createElement(layouts[i], null, element);
28
- }
29
- return element;
30
- }
31
-
32
- function useRouter() {
33
- const ctx = useContext(RouterContext);
34
- if (!ctx) {
35
- throw new Error('useRouter must be used within a <Router> provider');
36
- }
37
-
38
- return useMemo(
39
- () => ({
40
- pathname: ctx.pathname,
41
- params: ctx.params,
42
- query: typeof window !== 'undefined' ? parseQuery(window.location.search) : {},
43
- push: (to, options) => ctx.navigate(to, { ...options, replace: false }),
44
- replace: (to, options) => ctx.navigate(to, { ...options, replace: true }),
45
- back: () => {
46
- if (typeof window !== 'undefined') window.history.back();
47
- },
48
- forward: () => {
49
- if (typeof window !== 'undefined') window.history.forward();
50
- },
51
- refresh: () => {
52
- if (typeof window !== 'undefined') window.location.reload();
53
- },
54
- }),
55
- [ctx.pathname, ctx.params, ctx.navigate],
56
- );
57
- }
58
-
59
- function usePathname() {
60
- return useRouter().pathname;
61
- }
62
-
63
- function useParams() {
64
- return useRouter().params;
65
- }
66
-
67
- function useSearchParams() {
68
- return useRouter().query;
69
- }
70
-
71
- function Router({
72
- initialPath,
73
- initialParams,
74
- initialComponent,
75
- initialLayouts,
76
- initialLoadings,
77
- children,
78
- }) {
79
- const [pathname, setPathname] = useState(
80
- () => initialPath ?? (typeof window !== 'undefined' ? window.location.pathname : '/'),
81
- );
82
- const [pageState, setPageState] = useState({
83
- component: initialComponent ?? null,
84
- layouts: initialLayouts ?? [],
85
- loadings: initialLoadings ?? [],
86
- params: initialParams ?? {},
87
- });
88
- const [isNavigating, setIsNavigating] = useState(false);
89
- const [isPending, startTransition] = useTransition();
90
- const manifestRef = useRef(null);
91
-
92
- useEffect(() => {
93
- manifestRef.current = readClientManifest();
94
- const onManifestUpdate = () => {
95
- manifestRef.current = readClientManifest();
96
- };
97
- window.addEventListener('manifest-update', onManifestUpdate);
98
- return () => window.removeEventListener('manifest-update', onManifestUpdate);
99
- }, []);
100
-
101
- const applyLoaded = useCallback((loaded, newPath) => {
102
- startTransition(() => {
103
- setPathname(newPath);
104
- setPageState({
105
- component: loaded.component,
106
- layouts: loaded.layouts,
107
- loadings: loaded.loadings,
108
- params: loaded.params,
109
- });
110
- setIsNavigating(false);
111
- });
112
- }, []);
113
-
114
- const navigateToPage = useCallback(
115
- async (to, options) => {
116
- const scroll = options?.scroll !== false;
117
- const replace = options?.replace === true;
118
- if (to === pathname) return;
119
-
120
- const manifest = manifestRef.current;
121
- if (!manifest) {
122
- window.location.href = to;
123
- return;
124
- }
125
-
126
- const matched = matchClientRoute(manifest, to);
127
- if (!matched) {
128
- window.location.href = to;
129
- return;
130
- }
131
-
132
- setIsNavigating(true);
133
- if (replace) {
134
- window.history.replaceState(null, '', to);
135
- } else {
136
- window.history.pushState(null, '', to);
137
- }
138
-
139
- const targetLoadings = await loadLoadingComponents(matched.route);
140
- if (targetLoadings.length > 0) {
141
- setPathname(to);
142
- setPageState((prev) => ({
143
- ...prev,
144
- loadings: targetLoadings,
145
- params: matched.params,
146
- }));
147
- }
148
-
149
- try {
150
- const loaded = await loadPage(manifest, to);
151
- if (!loaded) {
152
- window.location.href = to;
153
- return;
154
- }
155
-
156
- applyLoaded(loaded, to);
157
-
158
- if (scroll) {
159
- window.scrollTo(0, 0);
160
- }
161
- } catch (err) {
162
- console.error('Client navigation failed, falling back to full page load:', err);
163
- window.location.href = to;
164
- }
165
- },
166
- [pathname, applyLoaded],
167
- );
168
-
169
- useEffect(() => {
170
- async function onPopState() {
171
- const newPath = window.location.pathname;
172
- const manifest = manifestRef.current;
173
-
174
- if (!manifest) {
175
- setPathname(newPath);
176
- setPageState((prev) => ({ ...prev, params: {} }));
177
- return;
178
- }
179
-
180
- try {
181
- const loaded = await loadPage(manifest, newPath);
182
- if (loaded) {
183
- applyLoaded(loaded, newPath);
184
- } else {
185
- window.location.reload();
186
- }
187
- } catch {
188
- window.location.reload();
189
- }
190
- }
191
-
192
- window.addEventListener('popstate', onPopState);
193
- return () => window.removeEventListener('popstate', onPopState);
194
- }, [applyLoaded]);
195
-
196
- const { component: PageComponent, layouts, loadings, params } = pageState;
197
-
198
- let content;
199
- if (PageComponent) {
200
- const inner = isNavigating && loadings.length > 0
201
- ? createElement(loadings.at(-1))
202
- : createElement(PageComponent, params);
203
- content = wrapInLayouts(inner, layouts);
204
- } else {
205
- content = children;
206
- }
207
-
208
- return (
209
- <RouterContext.Provider
210
- value={{
211
- pathname,
212
- params,
213
- navigate: navigateToPage,
214
- }}
215
- >
216
- {content}
217
- {isNavigating && loadings.length === 0 && !isPending && (
218
- <div
219
- style={{
220
- position: 'fixed',
221
- top: 0,
222
- left: 0,
223
- width: '100%',
224
- height: '2px',
225
- backgroundColor: '#0070f3',
226
- zIndex: 99999,
227
- animation: 'nav-progress 1s ease-in-out infinite',
228
- }}
229
- />
230
- )}
231
- </RouterContext.Provider>
232
- );
233
- }
234
-
235
- function Link({ href, children, onClick, scroll, replace, prefetch = 'hover', ...rest }) {
236
- const router = useContext(RouterContext);
237
- const linkRef = useRef(null);
238
-
239
- const handleMouseEnter = useCallback(() => {
240
- if (prefetch !== 'hover') return;
241
- const manifest = readClientManifest();
242
- if (manifest) {
243
- prefetchRoute(manifest, href);
244
- }
245
- }, [href, prefetch]);
246
-
247
- useEffect(() => {
248
- if (prefetch !== 'viewport') return;
249
- const el = linkRef.current;
250
- if (!el || typeof IntersectionObserver === 'undefined') return;
251
-
252
- const observer = new IntersectionObserver(
253
- (entries) => {
254
- for (const entry of entries) {
255
- if (entry.isIntersecting) {
256
- const manifest = readClientManifest();
257
- if (manifest) {
258
- prefetchRoute(manifest, href);
259
- }
260
- observer.unobserve(el);
261
- break;
262
- }
263
- }
264
- },
265
- { rootMargin: '200px' },
266
- );
267
-
268
- observer.observe(el);
269
- return () => observer.disconnect();
270
- }, [href, prefetch]);
271
-
272
- function handleClick(e) {
273
- if (onClick) onClick(e);
274
- if (e.defaultPrevented) return;
275
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
276
- if (e.button !== 0) return;
277
- if (rest.target === '_blank' || rest.download !== undefined) return;
278
-
279
- try {
280
- const url = new URL(href, window.location.origin);
281
- if (url.origin !== window.location.origin) return;
282
- } catch {
283
- return;
284
- }
285
-
286
- e.preventDefault();
287
- if (router) {
288
- router.navigate(href, { scroll, replace });
289
- } else {
290
- window.history.pushState(null, '', href);
291
- window.dispatchEvent(new PopStateEvent('popstate'));
292
- }
293
- }
294
-
295
- return (
296
- <a ref={linkRef} href={href} onClick={handleClick} onMouseEnter={handleMouseEnter} {...rest}>
297
- {children}
298
- </a>
299
- );
300
- }
301
-
302
- const SoloRouter = Router;
303
- export { Link, Router, SoloRouter, useParams, usePathname, useRouter, useSearchParams };