arcway 0.1.1 → 0.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/client/index.js CHANGED
@@ -18,7 +18,7 @@ import { useGraphQL, useGraphQLMutation } from './hooks/use-graphql.js';
18
18
  import { WsManager } from './ws.js';
19
19
  import useLocalStorage from './hooks/web/use-local-storage.js';
20
20
  import useClickOutside from './hooks/web/use-click-outside.js';
21
- import { Link, Router, SoloRouter, useRouter, usePathname, useParams, useSearchParams } from './router.jsx';
21
+ import { Link, Router, SoloRouter, useRouter, usePathname, useParams, useSearchParams } from './router.js';
22
22
  import { Head, setSSRHeadData, clearSSRHeadData, renderHeadToString } from './head.js';
23
23
  import { useEnv, env, collectPublicEnv, buildEnvScriptTag } from './env.js';
24
24
 
@@ -0,0 +1,274 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ useEffect,
8
+ useCallback,
9
+ useMemo,
10
+ useRef,
11
+ useTransition,
12
+ createElement
13
+ } from "react";
14
+ import {
15
+ readClientManifest,
16
+ loadPage,
17
+ matchClientRoute,
18
+ 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);
24
+ function wrapInLayouts(element, layouts) {
25
+ for (let i = layouts.length - 1; i >= 0; i--) {
26
+ element = createElement(layouts[i], null, element);
27
+ }
28
+ return element;
29
+ }
30
+ function useRouter() {
31
+ const ctx = useContext(RouterContext);
32
+ if (!ctx) {
33
+ throw new Error("useRouter must be used within a <Router> provider");
34
+ }
35
+ return useMemo(
36
+ () => ({
37
+ pathname: ctx.pathname,
38
+ params: ctx.params,
39
+ query: typeof window !== "undefined" ? parseQuery(window.location.search) : {},
40
+ push: (to, options) => ctx.navigate(to, { ...options, replace: false }),
41
+ replace: (to, options) => ctx.navigate(to, { ...options, replace: true }),
42
+ back: () => {
43
+ if (typeof window !== "undefined") window.history.back();
44
+ },
45
+ forward: () => {
46
+ if (typeof window !== "undefined") window.history.forward();
47
+ },
48
+ refresh: () => {
49
+ if (typeof window !== "undefined") window.location.reload();
50
+ }
51
+ }),
52
+ [ctx.pathname, ctx.params, ctx.navigate]
53
+ );
54
+ }
55
+ function usePathname() {
56
+ return useRouter().pathname;
57
+ }
58
+ function useParams() {
59
+ return useRouter().params;
60
+ }
61
+ function useSearchParams() {
62
+ return useRouter().query;
63
+ }
64
+ function Router({
65
+ initialPath,
66
+ initialParams,
67
+ initialComponent,
68
+ initialLayouts,
69
+ initialLoadings,
70
+ children
71
+ }) {
72
+ const [pathname, setPathname] = useState(
73
+ () => initialPath ?? (typeof window !== "undefined" ? window.location.pathname : "/")
74
+ );
75
+ const [pageState, setPageState] = useState({
76
+ component: initialComponent ?? null,
77
+ layouts: initialLayouts ?? [],
78
+ loadings: initialLoadings ?? [],
79
+ params: initialParams ?? {}
80
+ });
81
+ const [isNavigating, setIsNavigating] = useState(false);
82
+ const [isPending, startTransition] = useTransition();
83
+ const manifestRef = useRef(null);
84
+ useEffect(() => {
85
+ manifestRef.current = readClientManifest();
86
+ const onManifestUpdate = () => {
87
+ manifestRef.current = readClientManifest();
88
+ };
89
+ window.addEventListener("manifest-update", onManifestUpdate);
90
+ return () => window.removeEventListener("manifest-update", onManifestUpdate);
91
+ }, []);
92
+ const applyLoaded = useCallback((loaded, newPath) => {
93
+ startTransition(() => {
94
+ setPathname(newPath);
95
+ setPageState({
96
+ component: loaded.component,
97
+ layouts: loaded.layouts,
98
+ loadings: loaded.loadings,
99
+ params: loaded.params
100
+ });
101
+ setIsNavigating(false);
102
+ });
103
+ }, []);
104
+ const navigateToPage = useCallback(
105
+ async (to, options) => {
106
+ const scroll = options?.scroll !== false;
107
+ const replace = options?.replace === true;
108
+ if (to === pathname) return;
109
+ const manifest = manifestRef.current;
110
+ if (!manifest) {
111
+ window.location.href = to;
112
+ return;
113
+ }
114
+ const matched = matchClientRoute(manifest, to);
115
+ if (!matched) {
116
+ window.location.href = to;
117
+ return;
118
+ }
119
+ setIsNavigating(true);
120
+ if (replace) {
121
+ window.history.replaceState(null, "", to);
122
+ } else {
123
+ window.history.pushState(null, "", to);
124
+ }
125
+ const targetLoadings = await loadLoadingComponents(matched.route);
126
+ if (targetLoadings.length > 0) {
127
+ setPathname(to);
128
+ setPageState((prev) => ({
129
+ ...prev,
130
+ loadings: targetLoadings,
131
+ params: matched.params
132
+ }));
133
+ }
134
+ try {
135
+ const loaded = await loadPage(manifest, to);
136
+ if (!loaded) {
137
+ window.location.href = to;
138
+ return;
139
+ }
140
+ applyLoaded(loaded, to);
141
+ if (scroll) {
142
+ window.scrollTo(0, 0);
143
+ }
144
+ } catch (err) {
145
+ console.error("Client navigation failed, falling back to full page load:", err);
146
+ window.location.href = to;
147
+ }
148
+ },
149
+ [pathname, applyLoaded]
150
+ );
151
+ useEffect(() => {
152
+ async function onPopState() {
153
+ const newPath = window.location.pathname;
154
+ const manifest = manifestRef.current;
155
+ if (!manifest) {
156
+ setPathname(newPath);
157
+ setPageState((prev) => ({ ...prev, params: {} }));
158
+ return;
159
+ }
160
+ try {
161
+ const loaded = await loadPage(manifest, newPath);
162
+ if (loaded) {
163
+ applyLoaded(loaded, newPath);
164
+ } else {
165
+ window.location.reload();
166
+ }
167
+ } catch {
168
+ window.location.reload();
169
+ }
170
+ }
171
+ window.addEventListener("popstate", onPopState);
172
+ return () => window.removeEventListener("popstate", onPopState);
173
+ }, [applyLoaded]);
174
+ const { component: PageComponent, layouts, loadings, params } = pageState;
175
+ let content;
176
+ if (PageComponent) {
177
+ const inner = isNavigating && loadings.length > 0 ? createElement(loadings.at(-1)) : createElement(PageComponent, params);
178
+ content = wrapInLayouts(inner, layouts);
179
+ } else {
180
+ content = children;
181
+ }
182
+ return /* @__PURE__ */ jsxs(
183
+ RouterContext.Provider,
184
+ {
185
+ value: {
186
+ pathname,
187
+ params,
188
+ navigate: navigateToPage
189
+ },
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
+ }
209
+ );
210
+ }
211
+ function Link({ href, children, onClick, scroll, replace, prefetch = "hover", ...rest }) {
212
+ const router = useContext(RouterContext);
213
+ const linkRef = useRef(null);
214
+ const handleMouseEnter = useCallback(() => {
215
+ if (prefetch !== "hover") return;
216
+ const manifest = readClientManifest();
217
+ if (manifest) {
218
+ prefetchRoute(manifest, href);
219
+ }
220
+ }, [href, prefetch]);
221
+ useEffect(() => {
222
+ if (prefetch !== "viewport") return;
223
+ const el = linkRef.current;
224
+ if (!el || typeof IntersectionObserver === "undefined") return;
225
+ const observer = new IntersectionObserver(
226
+ (entries) => {
227
+ for (const entry of entries) {
228
+ if (entry.isIntersecting) {
229
+ const manifest = readClientManifest();
230
+ if (manifest) {
231
+ prefetchRoute(manifest, href);
232
+ }
233
+ observer.unobserve(el);
234
+ break;
235
+ }
236
+ }
237
+ },
238
+ { rootMargin: "200px" }
239
+ );
240
+ observer.observe(el);
241
+ return () => observer.disconnect();
242
+ }, [href, prefetch]);
243
+ function handleClick(e) {
244
+ if (onClick) onClick(e);
245
+ if (e.defaultPrevented) return;
246
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
247
+ if (e.button !== 0) return;
248
+ if (rest.target === "_blank" || rest.download !== void 0) return;
249
+ try {
250
+ const url = new URL(href, window.location.origin);
251
+ if (url.origin !== window.location.origin) return;
252
+ } catch {
253
+ return;
254
+ }
255
+ e.preventDefault();
256
+ if (router) {
257
+ router.navigate(href, { scroll, replace });
258
+ } else {
259
+ window.history.pushState(null, "", href);
260
+ window.dispatchEvent(new PopStateEvent("popstate"));
261
+ }
262
+ }
263
+ return /* @__PURE__ */ jsx("a", { ref: linkRef, href, onClick: handleClick, onMouseEnter: handleMouseEnter, ...rest, children });
264
+ }
265
+ const SoloRouter = Router;
266
+ export {
267
+ Link,
268
+ Router,
269
+ SoloRouter,
270
+ useParams,
271
+ usePathname,
272
+ useRouter,
273
+ useSearchParams
274
+ };
package/client/ws.js CHANGED
@@ -1,5 +1,3 @@
1
- import { io } from 'socket.io-client';
2
-
3
1
  class WsManager {
4
2
  socket = null;
5
3
  socketId = null;
@@ -25,10 +23,11 @@ class WsManager {
25
23
  return !!this.socket?.connected && this.socketId !== null;
26
24
  }
27
25
 
28
- connect() {
26
+ async connect() {
29
27
  if (this.socket) return;
30
28
 
31
29
  try {
30
+ const { io } = await import('socket.io-client');
32
31
  const parsed = new URL(this.url);
33
32
  this.socket = io(parsed.origin, {
34
33
  path: this.wsPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcway",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,7 @@ function serverExternalsPlugin() {
12
12
  const pkgName =
13
13
  args.path.startsWith('@') && parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
14
14
  if (!resolvedCache.has(pkgName)) {
15
- resolvedCache.set(pkgName, isTypeScriptPackage(pkgName, args.resolveDir));
15
+ resolvedCache.set(pkgName, needsBundling(pkgName, args.resolveDir));
16
16
  }
17
17
  if (resolvedCache.get(pkgName)) return void 0;
18
18
  return { path: args.path, external: true };
@@ -20,11 +20,11 @@ function serverExternalsPlugin() {
20
20
  },
21
21
  };
22
22
  }
23
- function isTypeScriptPackage(pkgName, resolveDir) {
23
+ function needsBundling(pkgName, resolveDir) {
24
24
  try {
25
25
  const require2 = createRequire(resolveDir + '/');
26
26
  const resolved = require2.resolve(pkgName);
27
- return /\.tsx?$/.test(resolved);
27
+ return /\.([jt]sx|ts)$/.test(resolved);
28
28
  } catch {
29
29
  return false;
30
30
  }