arcway 0.1.10 → 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/fetcher.js +1 -1
- package/client/router.js +101 -69
- package/package.json +3 -2
- package/server/bin/commands/migrate.js +4 -1
- package/server/bin/commands/seed.js +1 -2
- package/server/config/loader.js +2 -0
- package/server/config/modules/database.js +3 -3
- package/server/config/modules/events.js +3 -3
- package/server/config/modules/jobs.js +3 -3
- package/server/config/modules/seeds.js +15 -0
- package/server/db/index.js +9 -3
- package/server/events/drivers/memory.js +12 -24
- package/server/events/handler.js +4 -4
- package/server/jobs/runner.js +1 -1
- package/server/router/api-router.js +1 -1
- package/server/testing/index.js +2 -2
- package/client/router.jsx +0 -303
package/client/fetcher.js
CHANGED
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
|
|
10
|
+
createElement,
|
|
11
|
+
} from 'react';
|
|
14
12
|
import {
|
|
15
13
|
readClientManifest,
|
|
16
14
|
loadPage,
|
|
17
15
|
matchClientRoute,
|
|
18
16
|
loadLoadingComponents,
|
|
19
|
-
prefetchRoute
|
|
20
|
-
} from
|
|
21
|
-
import { parseQuery } from
|
|
22
|
-
|
|
23
|
-
const
|
|
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(
|
|
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 !==
|
|
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 !==
|
|
45
|
+
if (typeof window !== 'undefined') window.history.back();
|
|
44
46
|
},
|
|
45
47
|
forward: () => {
|
|
46
|
-
if (typeof window !==
|
|
48
|
+
if (typeof window !== 'undefined') window.history.forward();
|
|
47
49
|
},
|
|
48
50
|
refresh: () => {
|
|
49
|
-
if (typeof window !==
|
|
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 !==
|
|
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(
|
|
90
|
-
return () => window.removeEventListener(
|
|
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,
|
|
133
|
+
window.history.replaceState(null, '', to);
|
|
122
134
|
} else {
|
|
123
|
-
window.history.pushState(null,
|
|
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(
|
|
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
|
-
|
|
172
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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 !==
|
|
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 !==
|
|
251
|
+
if (prefetch !== 'viewport') return;
|
|
223
252
|
const el = linkRef.current;
|
|
224
|
-
if (!el || typeof IntersectionObserver ===
|
|
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:
|
|
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 ===
|
|
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,
|
|
260
|
-
window.dispatchEvent(new PopStateEvent(
|
|
293
|
+
window.history.pushState(null, '', href);
|
|
294
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
261
295
|
}
|
|
262
296
|
}
|
|
263
|
-
|
|
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.
|
|
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",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"lint": "eslint server/ client/ tests/",
|
|
38
38
|
"lint:fix": "eslint server/ client/ tests/ --fix",
|
|
39
39
|
"storybook": "storybook dev -p 6006",
|
|
40
|
-
"build-storybook": "storybook build"
|
|
40
|
+
"build-storybook": "storybook build",
|
|
41
|
+
"deploy:docs": "cd ~/Projects/home-server/baremetal/ansible && ansible-playbook deploy-arcway-docs.yml"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@aws-sdk/client-s3": "^3.987.0",
|
|
@@ -6,7 +6,10 @@ import { createDB } from '#server/db/index.js';
|
|
|
6
6
|
async function runMigrateMake(name) {
|
|
7
7
|
const rootDir = process.cwd();
|
|
8
8
|
try {
|
|
9
|
-
const
|
|
9
|
+
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
|
10
|
+
loadEnvFiles(rootDir, mode);
|
|
11
|
+
const config = await makeConfig(rootDir);
|
|
12
|
+
const migrationsDir = config.database.dir;
|
|
10
13
|
await fs.mkdir(migrationsDir, { recursive: true });
|
|
11
14
|
const now = new Date();
|
|
12
15
|
const timestamp = [
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { makeConfig } from '#server/config/loader.js';
|
|
3
2
|
import { loadEnvFiles } from '#server/env.js';
|
|
4
3
|
import { createDB } from '#server/db/index.js';
|
|
@@ -11,7 +10,7 @@ async function runSeed() {
|
|
|
11
10
|
const config = await makeConfig(rootDir);
|
|
12
11
|
const db = await createDB(config.database);
|
|
13
12
|
await db.runMigrations();
|
|
14
|
-
const seedsDir =
|
|
13
|
+
const seedsDir = config.seeds.dir;
|
|
15
14
|
const results = await runSeeds(db, seedsDir);
|
|
16
15
|
if (results.length === 0) {
|
|
17
16
|
console.log('No seed files found.');
|
package/server/config/loader.js
CHANGED
|
@@ -17,6 +17,7 @@ import resolvePages from './modules/pages.js';
|
|
|
17
17
|
import resolveBuild from './modules/build.js';
|
|
18
18
|
import resolveMcp from './modules/mcp.js';
|
|
19
19
|
import resolveWebsocket from './modules/websocket.js';
|
|
20
|
+
import resolveSeeds from './modules/seeds.js';
|
|
20
21
|
|
|
21
22
|
function deepMerge(target, source) {
|
|
22
23
|
const result = { ...target };
|
|
@@ -53,6 +54,7 @@ const modules = [
|
|
|
53
54
|
resolveBuild,
|
|
54
55
|
resolveMcp,
|
|
55
56
|
resolveWebsocket,
|
|
57
|
+
resolveSeeds,
|
|
56
58
|
];
|
|
57
59
|
|
|
58
60
|
async function makeConfig(rootDir, { overrides, mode } = {}) {
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
|
|
3
3
|
const DEFAULTS = {
|
|
4
4
|
sqliteFilename: '.build/db/arcway.db',
|
|
5
|
-
|
|
5
|
+
dir: 'migrations',
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
const CLIENT_MAP = {
|
|
@@ -15,8 +15,8 @@ function resolve(config, { rootDir } = {}) {
|
|
|
15
15
|
const db = { ...DEFAULTS, ...config.database };
|
|
16
16
|
// Resolve friendly client names to actual knex driver names
|
|
17
17
|
db.client = CLIENT_MAP[db.client] ?? db.client;
|
|
18
|
-
if (db.
|
|
19
|
-
db.
|
|
18
|
+
if (db.dir && !path.isAbsolute(db.dir)) {
|
|
19
|
+
db.dir = path.resolve(rootDir, db.dir);
|
|
20
20
|
}
|
|
21
21
|
const isSqlite = db.client === 'better-sqlite3' || db.client === 'sqlite3';
|
|
22
22
|
if (isSqlite && !db.connection) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
3
|
const DEFAULTS = {
|
|
4
|
-
|
|
4
|
+
dir: 'listeners',
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
function resolve(config, { rootDir } = {}) {
|
|
8
8
|
const events = { ...DEFAULTS, ...config.events };
|
|
9
|
-
if (events.
|
|
10
|
-
events.
|
|
9
|
+
if (events.dir && !path.isAbsolute(events.dir)) {
|
|
10
|
+
events.dir = path.resolve(rootDir, events.dir);
|
|
11
11
|
}
|
|
12
12
|
return { ...config, events };
|
|
13
13
|
}
|
|
@@ -6,13 +6,13 @@ const DEFAULTS = {
|
|
|
6
6
|
pollIntervalMs: 60000,
|
|
7
7
|
tableName: 'arcway_jobs',
|
|
8
8
|
leaseTable: 'arcway_job_leases',
|
|
9
|
-
|
|
9
|
+
dir: 'jobs',
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
function resolve(config, { rootDir } = {}) {
|
|
13
13
|
const jobs = { ...DEFAULTS, ...config.jobs };
|
|
14
|
-
if (jobs.
|
|
15
|
-
jobs.
|
|
14
|
+
if (jobs.dir && !path.isAbsolute(jobs.dir)) {
|
|
15
|
+
jobs.dir = path.resolve(rootDir, jobs.dir);
|
|
16
16
|
}
|
|
17
17
|
return { ...config, jobs };
|
|
18
18
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
dir: 'seeds',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function resolve(config, { rootDir } = {}) {
|
|
8
|
+
const seeds = { ...DEFAULTS, ...config.seeds };
|
|
9
|
+
if (seeds.dir && !path.isAbsolute(seeds.dir)) {
|
|
10
|
+
seeds.dir = path.resolve(rootDir, seeds.dir);
|
|
11
|
+
}
|
|
12
|
+
return { ...config, seeds };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default resolve;
|
package/server/db/index.js
CHANGED
|
@@ -45,6 +45,12 @@ async function createDB(config, { log } = {}) {
|
|
|
45
45
|
throw new Error(`Database connection failed: ${err}`);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
if (isSqlite) {
|
|
49
|
+
await db.raw('PRAGMA journal_mode = WAL');
|
|
50
|
+
await db.raw('PRAGMA busy_timeout = 5000');
|
|
51
|
+
await db.raw('PRAGMA synchronous = NORMAL');
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
const origDestroy = db.destroy.bind(db);
|
|
49
55
|
Object.defineProperty(db, 'destroy', {
|
|
50
56
|
value: async () => {
|
|
@@ -56,7 +62,7 @@ async function createDB(config, { log } = {}) {
|
|
|
56
62
|
});
|
|
57
63
|
|
|
58
64
|
db.runMigrations = async () => {
|
|
59
|
-
const migrationsDir = config.
|
|
65
|
+
const migrationsDir = config.dir;
|
|
60
66
|
if (!migrationsDir) return;
|
|
61
67
|
const [batch, migrations] = await db.migrate.latest({
|
|
62
68
|
migrationSource: new MigrationSource(migrationsDir),
|
|
@@ -69,8 +75,8 @@ async function createDB(config, { log } = {}) {
|
|
|
69
75
|
};
|
|
70
76
|
|
|
71
77
|
db.runRollback = async () => {
|
|
72
|
-
const migrationsDir = config.
|
|
73
|
-
if (!migrationsDir) throw new Error('No
|
|
78
|
+
const migrationsDir = config.dir;
|
|
79
|
+
if (!migrationsDir) throw new Error('No migrations dir configured');
|
|
74
80
|
const [batch, entries] = await db.migrate.rollback({
|
|
75
81
|
migrationSource: new MigrationSource(migrationsDir),
|
|
76
82
|
});
|
|
@@ -6,34 +6,22 @@ class MemoryTransport {
|
|
|
6
6
|
const regex = patternToRegex(pattern);
|
|
7
7
|
this.subscriptions.push({ pattern, regex, handler });
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
* Emit an event. All matching subscribers are called concurrently.
|
|
11
|
-
* Errors in individual listeners are caught and collected — one failing
|
|
12
|
-
* listener does not prevent others from running.
|
|
13
|
-
*/
|
|
9
|
+
/** Emit an event. Handlers run asynchronously — fire-and-forget. */
|
|
14
10
|
async emit(eventName, payload) {
|
|
15
11
|
const matching = this.subscriptions.filter((sub) => sub.regex.test(eventName));
|
|
16
|
-
if (matching.length === 0)
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
const errors = [];
|
|
20
|
-
const results = await Promise.allSettled(
|
|
12
|
+
if (matching.length === 0) return;
|
|
13
|
+
Promise.allSettled(
|
|
21
14
|
matching.map((sub) => sub.handler(eventName, payload)),
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
result.reason,
|
|
31
|
-
);
|
|
15
|
+
).then((results) => {
|
|
16
|
+
for (let i = 0; i < results.length; i++) {
|
|
17
|
+
if (results[i].status === 'rejected') {
|
|
18
|
+
console.error(
|
|
19
|
+
`Event listener error [${matching[i].pattern}] for event "${eventName}":`,
|
|
20
|
+
results[i].reason,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
32
23
|
}
|
|
33
|
-
}
|
|
34
|
-
if (errors.length > 0 && errors.length === matching.length) {
|
|
35
|
-
throw new Error(`All ${errors.length} listener(s) for event "${eventName}" failed`);
|
|
36
|
-
}
|
|
24
|
+
});
|
|
37
25
|
}
|
|
38
26
|
async disconnect() {}
|
|
39
27
|
|
package/server/events/handler.js
CHANGED
|
@@ -20,13 +20,13 @@ function validateHandler(item, filePath, index) {
|
|
|
20
20
|
|
|
21
21
|
class EventHandler {
|
|
22
22
|
_events;
|
|
23
|
-
|
|
23
|
+
_dir;
|
|
24
24
|
_log;
|
|
25
25
|
_appContext;
|
|
26
26
|
_listeners = [];
|
|
27
27
|
|
|
28
28
|
constructor(config, { events, log, appContext } = {}) {
|
|
29
|
-
this.
|
|
29
|
+
this._dir = config?.dir;
|
|
30
30
|
this._events = events;
|
|
31
31
|
this._log = log;
|
|
32
32
|
this._appContext = appContext ?? {
|
|
@@ -41,8 +41,8 @@ class EventHandler {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
async init() {
|
|
44
|
-
if (!this.
|
|
45
|
-
const entries = await discoverModules(this.
|
|
44
|
+
if (!this._dir) return;
|
|
45
|
+
const entries = await discoverModules(this._dir, {
|
|
46
46
|
recursive: true,
|
|
47
47
|
label: 'listener file',
|
|
48
48
|
});
|
package/server/jobs/runner.js
CHANGED
|
@@ -46,7 +46,7 @@ const validateRequest = validateRequestSchema;
|
|
|
46
46
|
async function serializeResponse(res, response, responseHeaders, statusCode) {
|
|
47
47
|
const customContentType = responseHeaders['Content-Type'] || responseHeaders['content-type'];
|
|
48
48
|
if (!customContentType || customContentType.includes('application/json')) {
|
|
49
|
-
const responseBody = response.error ? { error: response.error } :
|
|
49
|
+
const responseBody = response.error ? { error: response.error } : (response.data ?? null);
|
|
50
50
|
sendJson(res, statusCode, responseBody, responseHeaders);
|
|
51
51
|
return;
|
|
52
52
|
}
|
package/server/testing/index.js
CHANGED
|
@@ -128,9 +128,9 @@ async function createTestContext(domainName, options) {
|
|
|
128
128
|
if (options?.rootDir) {
|
|
129
129
|
const migrationsDir = path.join(options.rootDir, 'migrations');
|
|
130
130
|
await db.migrate.latest({ migrationSource: new MigrationSource(migrationsDir) });
|
|
131
|
-
} else if (options?.migrationsDir) {
|
|
131
|
+
} else if (options?.dir || options?.migrationsDir) {
|
|
132
132
|
await db.migrate.latest({
|
|
133
|
-
migrationSource: new MigrationSource(options.migrationsDir),
|
|
133
|
+
migrationSource: new MigrationSource(options.dir ?? options.migrationsDir),
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
136
|
const scopedDb = db;
|
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 };
|