aplosjs 0.15.0

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.
Files changed (56) hide show
  1. package/README.md +28 -0
  2. package/aplos.config.dist.js +30 -0
  3. package/bin/aplos +60 -0
  4. package/create-aplos/index.js +95 -0
  5. package/create-aplos/package.json +29 -0
  6. package/create-aplos/templates/minimal/README.md +38 -0
  7. package/create-aplos/templates/minimal/_gitignore +7 -0
  8. package/create-aplos/templates/minimal/aplos.config.js +13 -0
  9. package/create-aplos/templates/minimal/package.json +22 -0
  10. package/create-aplos/templates/minimal/public/favicon.svg +4 -0
  11. package/create-aplos/templates/minimal/src/pages/_app.tsx +6 -0
  12. package/create-aplos/templates/minimal/src/pages/about.tsx +24 -0
  13. package/create-aplos/templates/minimal/src/pages/index.tsx +40 -0
  14. package/create-aplos/templates/minimal/src/styles/global.css +53 -0
  15. package/create-aplos/templates/minimal/tsconfig.json +18 -0
  16. package/package.json +92 -0
  17. package/postcss.config.js +9 -0
  18. package/rspack.config.js +306 -0
  19. package/rspack.ssr.config.js +129 -0
  20. package/src/build/config.js +42 -0
  21. package/src/build/css-noop-loader.cjs +3 -0
  22. package/src/build/router.js +609 -0
  23. package/src/build/ssg.js +198 -0
  24. package/src/client/public/index.html +8 -0
  25. package/src/command/build.js +105 -0
  26. package/src/command/create.js +91 -0
  27. package/src/command/devServer.js +198 -0
  28. package/src/command/router.js +137 -0
  29. package/src/components/head.jsx +65 -0
  30. package/src/components/navigation.jsx +11 -0
  31. package/src/config.js +5 -0
  32. package/src/pages/_app.tsx +9 -0
  33. package/src/pages/blog/[slug].tsx +6 -0
  34. package/src/pages/crash.tsx +6 -0
  35. package/src/pages/index.tsx +10 -0
  36. package/src/pages/test.tsx +5 -0
  37. package/src/runtime/DefaultErrorPage.jsx +76 -0
  38. package/src/runtime/ErrorBoundary.jsx +40 -0
  39. package/src/runtime/MiddlewareGate.jsx +149 -0
  40. package/src/runtime/app-ssr.jsx +42 -0
  41. package/src/runtime/app.jsx +126 -0
  42. package/src/runtime/default-middleware.js +10 -0
  43. package/src/runtime/default-not-found.jsx +3 -0
  44. package/src/runtime/passthrough-layout.jsx +5 -0
  45. package/src/runtime/redirect.js +46 -0
  46. package/src/runtime/ssr-entry.jsx +104 -0
  47. package/templates/minimal/README.md +38 -0
  48. package/templates/minimal/_gitignore +7 -0
  49. package/templates/minimal/aplos.config.js +13 -0
  50. package/templates/minimal/package.json +22 -0
  51. package/templates/minimal/public/favicon.svg +4 -0
  52. package/templates/minimal/src/pages/_app.tsx +6 -0
  53. package/templates/minimal/src/pages/about.tsx +24 -0
  54. package/templates/minimal/src/pages/index.tsx +40 -0
  55. package/templates/minimal/src/styles/global.css +53 -0
  56. package/templates/minimal/tsconfig.json +18 -0
@@ -0,0 +1,137 @@
1
+ import { buildRouter } from "../build/router.js";
2
+ import get_config from "../build/config.js";
3
+ import Table from "cli-table3";
4
+ import fs from "fs";
5
+
6
+
7
+ // Function to check if URL matches a route pattern with requirements
8
+ const matchRoute = (url, routePath, requirements = {}) => {
9
+ // Convert React Router path to regex
10
+ let pattern = routePath.replace(/\//g, '\\/'); // Escape forward slashes
11
+
12
+ // Get parameter names first
13
+ const paramNames = [...routePath.matchAll(/:([^/]+)/g)].map(m => m[1]);
14
+
15
+ // Replace each parameter with its requirement pattern or default
16
+ paramNames.forEach(paramName => {
17
+ const requirement = requirements[paramName] || '[^/]+';
18
+ pattern = pattern.replace(`:${paramName}`, `(${requirement})`);
19
+ });
20
+
21
+ const regex = new RegExp(`^${pattern}$`);
22
+ const match = url.match(regex);
23
+
24
+ if (match) {
25
+ // Extract parameters
26
+ const params = {};
27
+ paramNames.forEach((name, index) => {
28
+ params[name] = match[index + 1];
29
+ });
30
+ return { match: true, params };
31
+ }
32
+
33
+ return { match: false };
34
+ };
35
+
36
+ export default async (options) => {
37
+ let projectDirectory = process.cwd();
38
+
39
+ await buildRouter(await get_config(projectDirectory));
40
+
41
+ const data = fs
42
+ .readFileSync(projectDirectory + "/.aplos/cache/router.js")
43
+ .toString();
44
+ const routes = JSON.parse(data);
45
+
46
+ // Handle router:match command
47
+ if (options && options.url) {
48
+ const url = options.url;
49
+ let matchedRoute = null;
50
+ let matchParams = {};
51
+
52
+ // Test each route
53
+ for (const route of routes) {
54
+ // Skip config routes without path
55
+ if (!route.path) continue;
56
+
57
+ const result = matchRoute(url, route.path, route.requirements || route.requirement || {});
58
+ if (result.match) {
59
+ matchedRoute = route;
60
+ matchParams = result.params;
61
+ break;
62
+ }
63
+ }
64
+
65
+ const table = new Table({
66
+ head: ["Property", "Value"],
67
+ });
68
+
69
+ if (matchedRoute) {
70
+ table.push(["Route Name", matchedRoute.component]);
71
+ table.push(["Path", matchedRoute.path]);
72
+ table.push(["File", matchedRoute.file ? `src/pages${matchedRoute.file}` : "-"]);
73
+ const regexPattern = matchedRoute.path.replace(/\//g, '\\/').replace(/:([^/]+)/g, '(?P<$1>[^/]++)');
74
+ table.push(["Path Regex", `{^${regexPattern}$}`]);
75
+ table.push(["Host", "ANY"]);
76
+ table.push(["Scheme", "ANY"]);
77
+ const reqs = matchedRoute.requirements || matchedRoute.requirement || {};
78
+ table.push(["Requirements", Object.keys(reqs).length > 0 ? JSON.stringify(reqs) : "NO CUSTOM"]);
79
+
80
+ if (Object.keys(matchParams).length > 0) {
81
+ table.push(["Parameters", JSON.stringify(matchParams)]);
82
+ }
83
+
84
+ console.log(`✅ URL "${url}" matches route:`);
85
+ console.log(table.toString());
86
+ } else {
87
+ console.log(`❌ URL "${url}" does not match any route.`);
88
+ console.log("\nAvailable routes:");
89
+ const availableTable = new Table({
90
+ head: ["Component", "Path"],
91
+ });
92
+ routes.forEach(route => {
93
+ if (route.path) {
94
+ availableTable.push([route.component, route.path]);
95
+ }
96
+ });
97
+ console.log(availableTable.toString());
98
+ }
99
+ return;
100
+ }
101
+
102
+ if (typeof options === "string") {
103
+ const table = new Table({
104
+ head: ["Property", "Value"],
105
+ });
106
+
107
+ const route = routes.find((route) => route.component === options);
108
+ if (route) {
109
+ table.push(["Route name", route.path]);
110
+
111
+ table.push(["Path", route.path]);
112
+
113
+ table.push(["File", route.file ? `src/pages${route.file}` : "-"]);
114
+
115
+ table.push(["Path Regex", ""]);
116
+
117
+ table.push(["Host", ""]);
118
+
119
+ table.push(["Scheme", ""]);
120
+
121
+ table.push(["Requirement", route.requirement]);
122
+
123
+ console.log(table.toString());
124
+ } else {
125
+ console.log("Component not found");
126
+ }
127
+ } else {
128
+ const table = new Table({
129
+ head: ["Component", "File", "Scheme", "Host", "Path"],
130
+ });
131
+
132
+ routes.map((route) => {
133
+ table.push([route.component, route.file ? `src/pages${route.file}` : "-", "Any", "Any", route.path]);
134
+ });
135
+ console.log(table.toString());
136
+ }
137
+ };
@@ -0,0 +1,65 @@
1
+ import { useEffect, Children } from "react";
2
+
3
+ const MANAGED_ATTR = "data-head";
4
+
5
+ export default function Head({ children }) {
6
+ useEffect(() => {
7
+ const managedElements = [];
8
+
9
+ Children.forEach(children, (child) => {
10
+ if (!child || !child.props) return;
11
+
12
+ const { type } = child;
13
+ const { children: textContent, ...props } = child.props;
14
+
15
+ if (type === "title") {
16
+ document.title = typeof textContent === "string" ? textContent : "";
17
+ return;
18
+ }
19
+
20
+ if (type === "html") {
21
+ Object.entries(props).forEach(([key, value]) => {
22
+ document.documentElement.setAttribute(key, value);
23
+ });
24
+ return;
25
+ }
26
+
27
+ if (type === "body") {
28
+ Object.entries(props).forEach(([key, value]) => {
29
+ document.body.setAttribute(key, value);
30
+ });
31
+ return;
32
+ }
33
+
34
+ const el = document.createElement(type);
35
+ el.setAttribute(MANAGED_ATTR, "true");
36
+
37
+ Object.entries(props).forEach(([key, value]) => {
38
+ if (key === "crossOrigin") {
39
+ el.setAttribute("crossorigin", value);
40
+ } else if (key === "hrefLang") {
41
+ el.setAttribute("hreflang", value);
42
+ } else {
43
+ el.setAttribute(key, value);
44
+ }
45
+ });
46
+
47
+ if (type === "script" && textContent) {
48
+ el.textContent = typeof textContent === "string" ? textContent : "";
49
+ }
50
+
51
+ document.head.appendChild(el);
52
+ managedElements.push(el);
53
+ });
54
+
55
+ return () => {
56
+ managedElements.forEach((el) => {
57
+ if (el.parentNode) {
58
+ el.parentNode.removeChild(el);
59
+ }
60
+ });
61
+ };
62
+ }, [children]);
63
+
64
+ return null;
65
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ Link,
3
+ NavLink,
4
+ Navigate,
5
+ Outlet,
6
+ useNavigate,
7
+ useLocation,
8
+ useParams,
9
+ useSearchParams,
10
+ useMatch,
11
+ } from "react-router-dom";
package/src/config.js ADDED
@@ -0,0 +1,5 @@
1
+ import Config from "@aplos_config";
2
+
3
+ export default function getConfig() {
4
+ return Config;
5
+ }
@@ -0,0 +1,9 @@
1
+ import {Outlet} from "react-router-dom";
2
+
3
+ export default function Toto() {
4
+ return (
5
+ <div>
6
+ <Outlet />
7
+ </div>
8
+ );
9
+ }
@@ -0,0 +1,6 @@
1
+ import { useParams } from 'react-router-dom';
2
+
3
+ export default function BlogPost() {
4
+ const { slug } = useParams();
5
+ return <h1>Blog Post: {slug}</h1>;
6
+ }
@@ -0,0 +1,6 @@
1
+ export default function CrashPage() {
2
+ // This will crash the component
3
+ throw new Error("This is a test error to demonstrate error boundary");
4
+
5
+ return <div>This won't render</div>;
6
+ }
@@ -0,0 +1,10 @@
1
+ import getConfig from 'aplos/config';
2
+
3
+ export default function Home() {
4
+ const config = getConfig();
5
+
6
+ console.log(config.publicRuntimeConfig)
7
+
8
+
9
+ return <div>Home</div>
10
+ }
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+
3
+ export default function Test() {
4
+ return <div style={{ color: "coucou" }}>Coucou</div>;
5
+ }
@@ -0,0 +1,76 @@
1
+ export default function DefaultErrorPage({ error }) {
2
+ const isDevelopment = process.env.NODE_ENV === 'development';
3
+
4
+ if (isDevelopment) {
5
+ return (
6
+ <div style={{
7
+ padding: '20px',
8
+ backgroundColor: '#fff5f5',
9
+ border: '1px solid #fed7d7',
10
+ borderRadius: '6px',
11
+ margin: '20px',
12
+ fontFamily: 'monospace'
13
+ }}>
14
+ <h1 style={{ color: '#c53030', fontSize: '24px', marginBottom: '16px' }}>
15
+ Something went wrong
16
+ </h1>
17
+ <h2 style={{ color: '#2d3748', fontSize: '18px', marginBottom: '12px' }}>
18
+ {error && error.toString()}
19
+ </h2>
20
+ <details style={{ whiteSpace: 'pre-wrap', fontSize: '14px', color: '#4a5568' }}>
21
+ <summary style={{ cursor: 'pointer', marginBottom: '8px' }}>
22
+ Click to see stack trace
23
+ </summary>
24
+ {error?.stack}
25
+ </details>
26
+ <button
27
+ onClick={() => window.location.reload()}
28
+ style={{
29
+ marginTop: '16px',
30
+ padding: '8px 16px',
31
+ backgroundColor: '#4299e1',
32
+ color: 'white',
33
+ border: 'none',
34
+ borderRadius: '4px',
35
+ cursor: 'pointer'
36
+ }}
37
+ >
38
+ Reload page
39
+ </button>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <div style={{
46
+ display: 'flex',
47
+ flexDirection: 'column',
48
+ alignItems: 'center',
49
+ justifyContent: 'center',
50
+ minHeight: '50vh',
51
+ textAlign: 'center',
52
+ padding: '20px'
53
+ }}>
54
+ <h2 style={{ fontSize: '24px', marginBottom: '8px', color: '#2d3748' }}>
55
+ Oops! Something went wrong
56
+ </h2>
57
+ <p style={{ fontSize: '16px', color: '#718096', marginBottom: '24px' }}>
58
+ We're sorry, but something unexpected happened. Please try refreshing the page.
59
+ </p>
60
+ <button
61
+ onClick={() => window.location.reload()}
62
+ style={{
63
+ padding: '12px 24px',
64
+ backgroundColor: '#4299e1',
65
+ color: 'white',
66
+ border: 'none',
67
+ borderRadius: '6px',
68
+ fontSize: '16px',
69
+ cursor: 'pointer'
70
+ }}
71
+ >
72
+ Refresh page
73
+ </button>
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+
3
+ export default function ErrorBoundary({ errorComponent, children }) {
4
+ const [hasError, setHasError] = React.useState(false);
5
+ const [error, setError] = React.useState(null);
6
+
7
+ React.useEffect(() => {
8
+ const handleError = (error, errorInfo) => {
9
+ setHasError(true);
10
+ setError(error);
11
+
12
+ if (process.env.NODE_ENV === 'development') {
13
+ console.error('Error caught by boundary:', error, errorInfo);
14
+ }
15
+ };
16
+
17
+ const handleGlobalError = (event) => {
18
+ handleError(event.error, { componentStack: event.error?.stack });
19
+ };
20
+
21
+ const handleUnhandledRejection = (event) => {
22
+ handleError(event.reason, { componentStack: event.reason?.stack });
23
+ };
24
+
25
+ window.addEventListener('error', handleGlobalError);
26
+ window.addEventListener('unhandledrejection', handleUnhandledRejection);
27
+
28
+ return () => {
29
+ window.removeEventListener('error', handleGlobalError);
30
+ window.removeEventListener('unhandledrejection', handleUnhandledRejection);
31
+ };
32
+ }, []);
33
+
34
+ if (hasError) {
35
+ const ErrorComponent = errorComponent;
36
+ return <ErrorComponent error={error} />;
37
+ }
38
+
39
+ return children;
40
+ }
@@ -0,0 +1,149 @@
1
+ import { useLayoutEffect, useRef, useState } from 'react';
2
+ import { useLocation, useNavigate } from 'react-router-dom';
3
+ import userMiddleware from '@aplos_middleware';
4
+ import defaultMiddleware from 'aplos/internal/default-middleware';
5
+ import { isRedirect } from './redirect.js';
6
+
7
+ // When the project defines no `src/middleware.*`, the generated cache
8
+ // re-exports the framework no-op, so `userMiddleware` is identically the
9
+ // default function (ESM re-export preserves reference identity). In that
10
+ // case there is no decision to make: skip the gate entirely so apps that
11
+ // don't use middleware pay zero cost — no `null` frame, no extra render,
12
+ // no layout effect on every navigation.
13
+ const HAS_MIDDLEWARE = userMiddleware !== defaultMiddleware;
14
+
15
+ // A middleware can redirect to a path it also intercepts. If the chain never
16
+ // reaches a route that renders (each target redirects again), navigation never
17
+ // settles: the app freezes on a blank `null` with navigate() looping. No
18
+ // amount of documentation prevents a user from writing that by accident, so
19
+ // the runtime breaks the loop itself. This bounds *consecutive* redirects
20
+ // with no route rendered in between — a legitimate auth→onboarding→dashboard
21
+ // hop is a handful; anything past this is a cycle.
22
+ const MAX_REDIRECT_CHAIN = 20;
23
+
24
+ /**
25
+ * Runs the project route middleware before the matched route renders.
26
+ *
27
+ * Placed inside `<BrowserRouter>` and wrapping `<Routes>`, it intercepts every
28
+ * navigation: it calls the user middleware with the current location, and if
29
+ * the middleware returns `redirect(...)` it navigates away *before* committing
30
+ * the guarded route to the DOM. This avoids the "flash of protected content"
31
+ * you get when guarding inside a `useEffect` in a layout/page.
32
+ *
33
+ * The decision runs synchronously in a layout effect (pre-paint). While a
34
+ * redirect is pending we render `null` so the guarded tree is never painted.
35
+ *
36
+ * A middleware may be sync or async. Async middleware cannot block the first
37
+ * paint (we can't await before rendering), so it renders `null` until the
38
+ * promise settles — async is intended for cases where a brief blank frame is
39
+ * acceptable (e.g. token refresh). Prefer sync middleware for auth guards.
40
+ */
41
+ export default function MiddlewareGate({ children }) {
42
+ // `HAS_MIDDLEWARE` is a module constant — it never changes for the life of
43
+ // the process — so branching on it here does not violate the Rules of
44
+ // Hooks: a given render path always calls the same hooks. Apps without a
45
+ // middleware render through with no gate machinery at all.
46
+ if (!HAS_MIDDLEWARE) {
47
+ return children;
48
+ }
49
+ return <ActiveMiddlewareGate>{children}</ActiveMiddlewareGate>;
50
+ }
51
+
52
+ function ActiveMiddlewareGate({ children }) {
53
+ const location = useLocation();
54
+ const navigate = useNavigate();
55
+
56
+ // Key the decision on the full location so back/forward and query changes
57
+ // re-run the middleware. `null` => undecided (don't paint yet).
58
+ const [decidedFor, setDecidedFor] = useState(null);
59
+ const pendingRef = useRef(null);
60
+ // Counts consecutive redirects with no route rendered in between. Reset to
61
+ // 0 whenever a navigation is allowed through (a route renders), so it only
62
+ // grows while the middleware is bouncing the user without ever settling.
63
+ const redirectChainRef = useRef(0);
64
+
65
+ const locationKey = location.pathname + location.search;
66
+
67
+ useLayoutEffect(() => {
68
+ let cancelled = false;
69
+
70
+ function letThrough() {
71
+ redirectChainRef.current = 0;
72
+ setDecidedFor(locationKey);
73
+ }
74
+
75
+ function applyResult(result) {
76
+ if (cancelled) return;
77
+ if (isRedirect(result)) {
78
+ if (redirectChainRef.current >= MAX_REDIRECT_CHAIN) {
79
+ // The middleware is in a redirect cycle (e.g. it redirects
80
+ // to a path it also intercepts, with no exit condition).
81
+ // Fail open like a thrown middleware: a guard bug must not
82
+ // freeze the app. The user sees the route instead of a
83
+ // hung blank page, and the error names the cause.
84
+ console.error(
85
+ `[aplos] route middleware redirected ${redirectChainRef.current} ` +
86
+ `times without settling — aborting the redirect chain to ` +
87
+ `avoid an infinite loop. Check that your redirect target ` +
88
+ `is not itself redirected (its condition must become ` +
89
+ `false after the redirect).`,
90
+ );
91
+ letThrough();
92
+ return;
93
+ }
94
+ redirectChainRef.current += 1;
95
+ navigate(result.to, { replace: result.replace });
96
+ // Leave `decidedFor` unset for this location: the navigate()
97
+ // will change the location and re-run this effect for the new
98
+ // target, which is where the decision that matters is made.
99
+ return;
100
+ }
101
+ letThrough();
102
+ }
103
+
104
+ let result;
105
+ try {
106
+ result = userMiddleware({
107
+ pathname: location.pathname,
108
+ search: location.search,
109
+ searchParams: new URLSearchParams(location.search),
110
+ hash: location.hash,
111
+ state: location.state,
112
+ });
113
+ } catch (error) {
114
+ // A throwing middleware must not wedge navigation. Surface it and
115
+ // fail open (let the route render) so the app stays usable.
116
+ console.error('[aplos] route middleware threw:', error);
117
+ letThrough();
118
+ return () => {
119
+ cancelled = true;
120
+ };
121
+ }
122
+
123
+ if (result && typeof result.then === 'function') {
124
+ const token = {};
125
+ pendingRef.current = token;
126
+ Promise.resolve(result)
127
+ .then((resolved) => {
128
+ if (pendingRef.current === token) applyResult(resolved);
129
+ })
130
+ .catch((error) => {
131
+ if (pendingRef.current !== token) return;
132
+ console.error('[aplos] async route middleware rejected:', error);
133
+ if (!cancelled) letThrough();
134
+ });
135
+ } else {
136
+ applyResult(result);
137
+ }
138
+
139
+ return () => {
140
+ cancelled = true;
141
+ };
142
+ }, [locationKey, location.pathname, location.search, location.hash, location.state, navigate]);
143
+
144
+ if (decidedFor !== locationKey) {
145
+ return null;
146
+ }
147
+
148
+ return children;
149
+ }
@@ -0,0 +1,42 @@
1
+ import React, { createElement } from 'react';
2
+ import { StaticRouter, Routes, Route } from 'react-router';
3
+
4
+ import { routeTree } from '@aplos_routes';
5
+ import { CustomError, NoMatch } from '@aplos_pages';
6
+ import { reactStrictMode } from '@aplos_head';
7
+
8
+ import ErrorBoundary from './ErrorBoundary.jsx';
9
+ import DefaultErrorPage from './DefaultErrorPage.jsx';
10
+
11
+ function renderRoutes(nodes) {
12
+ return nodes.map((node, i) => {
13
+ if (node.children) {
14
+ return (
15
+ <Route key={i} element={createElement(node.element)}>
16
+ {renderRoutes(node.children)}
17
+ </Route>
18
+ );
19
+ }
20
+ return <Route key={i} path={node.path} element={createElement(node.element)} />;
21
+ });
22
+ }
23
+
24
+ export default function AppSSR({ url }) {
25
+ const ErrorComponent = CustomError || DefaultErrorPage;
26
+ const tree = (
27
+ <ErrorBoundary errorComponent={ErrorComponent}>
28
+ <StaticRouter location={url}>
29
+ <Routes>
30
+ {renderRoutes(routeTree)}
31
+ <Route path="*" element={createElement(NoMatch)} />
32
+ </Routes>
33
+ </StaticRouter>
34
+ </ErrorBoundary>
35
+ );
36
+
37
+ if (reactStrictMode) {
38
+ const { StrictMode } = React;
39
+ return <StrictMode>{tree}</StrictMode>;
40
+ }
41
+ return tree;
42
+ }