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,198 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { pathToFileURL, fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const SSR_ENTRY = path.resolve(__dirname, '../runtime/ssr-entry.jsx');
10
+ const SSR_BUNDLE_NAME = 'ssr-bundle.cjs';
11
+
12
+ function outputHtmlPath(distDir, routePath) {
13
+ if (routePath === '/' || routePath === '') {
14
+ return path.join(distDir, 'index.html');
15
+ }
16
+ const trimmed = routePath.replace(/^\/+/, '').replace(/\/+$/, '');
17
+ return path.join(distDir, `${trimmed}.html`);
18
+ }
19
+
20
+ async function buildSSRBundle(projectDirectory, frameworkDirectory, mode) {
21
+ const ssrConfigPath = path.resolve(frameworkDirectory, 'rspack.ssr.config.js');
22
+ const rspackBin = path.join(projectDirectory, 'node_modules', '.bin', 'rspack');
23
+
24
+ return new Promise((resolve, reject) => {
25
+ const child = spawn(rspackBin, [
26
+ '--mode=' + mode,
27
+ '--config', ssrConfigPath,
28
+ '--entry', SSR_ENTRY,
29
+ ], {
30
+ env: { ...process.env, APLOS_SSR: '1' },
31
+ });
32
+
33
+ let stderr = '';
34
+ child.stdout.on('data', (d) => process.stdout.write(d));
35
+ child.stderr.on('data', (d) => {
36
+ stderr += d.toString();
37
+ process.stderr.write(d);
38
+ });
39
+ child.on('close', (code) => {
40
+ if (code !== 0) {
41
+ reject(new Error(`SSR bundle failed (exit ${code}): ${stderr}`));
42
+ } else {
43
+ resolve();
44
+ }
45
+ });
46
+ });
47
+ }
48
+
49
+ export default async function ssg({ mode, forceAll = false } = {}) {
50
+ const projectDirectory = process.cwd();
51
+ const frameworkDirectory = path.resolve(__dirname, '../..');
52
+ const distDir = path.join(projectDirectory, 'public', 'dist');
53
+ const cacheDir = path.join(projectDirectory, '.aplos', 'cache');
54
+
55
+ const indexHtmlPath = path.join(distDir, 'index.html');
56
+ if (!fs.existsSync(indexHtmlPath)) {
57
+ throw new Error(`SSG: ${indexHtmlPath} not found — run the client build first.`);
58
+ }
59
+
60
+ // Skip the expensive SSR bundle build if no page opted in and --static wasn't passed.
61
+ if (!forceAll) {
62
+ const routesPath = path.join(cacheDir, 'routes.js');
63
+ if (!fs.existsSync(routesPath)) {
64
+ return;
65
+ }
66
+ const source = fs.readFileSync(routesPath, 'utf-8');
67
+ if (!/static:\s*true/.test(source)) {
68
+ return;
69
+ }
70
+ }
71
+
72
+ console.log('\n Building SSR bundle...');
73
+ await buildSSRBundle(projectDirectory, frameworkDirectory, mode || 'production');
74
+
75
+ const ssrBundlePath = path.join(cacheDir, SSR_BUNDLE_NAME);
76
+ if (!fs.existsSync(ssrBundlePath)) {
77
+ throw new Error(`SSG: SSR bundle not produced at ${ssrBundlePath}.`);
78
+ }
79
+
80
+ const ssrMod = await import(pathToFileURL(ssrBundlePath).href);
81
+ const render = ssrMod.render || ssrMod.default?.render;
82
+ const getStaticRoutes = ssrMod.getStaticRoutes || ssrMod.default?.getStaticRoutes;
83
+ const getRouteMeta = ssrMod.getRouteMeta || ssrMod.default?.getRouteMeta;
84
+ if (typeof render !== 'function' || typeof getStaticRoutes !== 'function') {
85
+ throw new Error('SSG: SSR bundle must export render(url) and getStaticRoutes().');
86
+ }
87
+
88
+ const staticRoutes = getStaticRoutes({ forceAll });
89
+ if (staticRoutes.length === 0) {
90
+ return;
91
+ }
92
+
93
+ const template = fs.readFileSync(indexHtmlPath, 'utf-8');
94
+ const rootMarker = /<div id="root">\s*<\/div>/;
95
+ if (!rootMarker.test(template)) {
96
+ throw new Error('SSG: could not find <div id="root"></div> in index.html template.');
97
+ }
98
+
99
+ console.log(` Pre-rendering ${staticRoutes.length} route(s)...`);
100
+ let rendered = 0;
101
+ for (const route of staticRoutes) {
102
+ try {
103
+ const html = render(route);
104
+ const meta = typeof getRouteMeta === 'function' ? getRouteMeta(route) : null;
105
+ let page = template.replace(rootMarker, `<div id="root">${html}</div>`);
106
+ if (meta) {
107
+ page = injectMetaTags(page, meta);
108
+ }
109
+ const outPath = outputHtmlPath(distDir, route);
110
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
111
+ fs.writeFileSync(outPath, page, 'utf-8');
112
+ console.log(` ✓ ${route} → ${path.relative(projectDirectory, outPath)}`);
113
+ rendered++;
114
+ } catch (err) {
115
+ console.error(` ✗ ${route}: ${err.message}`);
116
+ }
117
+ }
118
+ console.log(` Pre-rendered ${rendered}/${staticRoutes.length} route(s).`);
119
+ }
120
+
121
+ function escapeHtml(value) {
122
+ return String(value)
123
+ .replace(/&/g, '&amp;')
124
+ .replace(/</g, '&lt;')
125
+ .replace(/>/g, '&gt;')
126
+ .replace(/"/g, '&quot;');
127
+ }
128
+
129
+ function metaTag(attrs) {
130
+ const parts = Object.entries(attrs)
131
+ .filter(([, v]) => v !== undefined && v !== null && v !== false)
132
+ .map(([k, v]) => `${k}="${escapeHtml(v)}"`);
133
+ return `<meta ${parts.join(' ')}>`;
134
+ }
135
+
136
+ function buildMetaHtml(meta) {
137
+ const tags = [];
138
+ if (meta.title) {
139
+ tags.push(`<title>${escapeHtml(meta.title)}</title>`);
140
+ }
141
+ if (meta.description) {
142
+ tags.push(metaTag({ name: 'description', content: meta.description }));
143
+ }
144
+ if (meta.canonical) {
145
+ tags.push(`<link rel="canonical" href="${escapeHtml(meta.canonical)}">`);
146
+ }
147
+ if (Array.isArray(meta.keywords) && meta.keywords.length > 0) {
148
+ tags.push(metaTag({ name: 'keywords', content: meta.keywords.join(', ') }));
149
+ }
150
+ if (meta.og && typeof meta.og === 'object') {
151
+ for (const [key, value] of Object.entries(meta.og)) {
152
+ tags.push(metaTag({ property: `og:${key}`, content: value }));
153
+ }
154
+ }
155
+ if (meta.twitter && typeof meta.twitter === 'object') {
156
+ for (const [key, value] of Object.entries(meta.twitter)) {
157
+ tags.push(metaTag({ name: `twitter:${key}`, content: value }));
158
+ }
159
+ }
160
+ if (Array.isArray(meta.meta)) {
161
+ for (const entry of meta.meta) {
162
+ if (entry && typeof entry === 'object') {
163
+ tags.push(metaTag(entry));
164
+ }
165
+ }
166
+ }
167
+ if (Array.isArray(meta.link)) {
168
+ for (const entry of meta.link) {
169
+ if (entry && typeof entry === 'object') {
170
+ const parts = Object.entries(entry)
171
+ .filter(([, v]) => v !== undefined && v !== null && v !== false)
172
+ .map(([k, v]) => `${k}="${escapeHtml(v)}"`);
173
+ tags.push(`<link ${parts.join(' ')}>`);
174
+ }
175
+ }
176
+ }
177
+ return tags.join('\n ');
178
+ }
179
+
180
+ export function injectMetaTags(html, meta) {
181
+ const block = buildMetaHtml(meta);
182
+ if (!block) {
183
+ return html;
184
+ }
185
+ const titleMatch = block.match(/<title>[\s\S]*?<\/title>/);
186
+ if (titleMatch) {
187
+ if (/<title>[\s\S]*?<\/title>/.test(html)) {
188
+ html = html.replace(/<title>[\s\S]*?<\/title>/, titleMatch[0]);
189
+ } else {
190
+ html = html.replace('</head>', ` ${titleMatch[0]}\n </head>`);
191
+ }
192
+ }
193
+ const remaining = block.replace(/<title>[\s\S]*?<\/title>\s*/, '');
194
+ if (remaining) {
195
+ html = html.replace('</head>', ` ${remaining}\n </head>`);
196
+ }
197
+ return html;
198
+ }
@@ -0,0 +1,8 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ </head>
5
+ <body>
6
+ <div id="root"></div>
7
+ </body>
8
+ </html>
@@ -0,0 +1,105 @@
1
+ import {spawn} from "child_process";
2
+ import {buildRouter} from "../build/router.js";
3
+ import get_config from '../build/config.js';
4
+ import ssg from '../build/ssg.js';
5
+ import { fileURLToPath } from 'url';
6
+ import path from 'path';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ import fs from 'fs';
12
+
13
+ const showBundleAnalysis = (projectDirectory) => {
14
+ const distPath = path.join(projectDirectory, 'public', 'dist');
15
+
16
+ if (!fs.existsSync(distPath)) {
17
+ console.log('❌ Dist folder not found');
18
+ return;
19
+ }
20
+
21
+ const files = fs.readdirSync(distPath)
22
+ .filter(file => file.endsWith('.js') && !file.endsWith('.map'))
23
+ .map(file => {
24
+ const filePath = path.join(distPath, file);
25
+ const stats = fs.statSync(filePath);
26
+ return {
27
+ name: file,
28
+ size: stats.size,
29
+ sizeKB: Math.round(stats.size / 1024),
30
+ };
31
+ })
32
+ .sort((a, b) => b.size - a.size);
33
+
34
+ const totalSize = files.reduce((sum, file) => sum + file.size, 0);
35
+ const totalKB = Math.round(totalSize / 1024);
36
+
37
+ console.log('\n📊 Bundle Analysis:');
38
+ console.log('┌─────────────────────────────────┬─────────┐');
39
+ console.log('│ File │ Size │');
40
+ console.log('├─────────────────────────────────┼─────────┤');
41
+
42
+ files.forEach(file => {
43
+ const nameField = file.name.padEnd(31);
44
+ const sizeField = `${file.sizeKB}KB`.padStart(7);
45
+ console.log(`│ ${nameField} │ ${sizeField} │`);
46
+ });
47
+
48
+ console.log('├─────────────────────────────────┼─────────┤');
49
+ const totalField = `${totalKB}KB`.padStart(7);
50
+ console.log(`│ TOTAL │ ${totalField} │`);
51
+ console.log('└─────────────────────────────────┴─────────┘');
52
+
53
+ // Performance tips
54
+ if (totalKB > 500) {
55
+ console.log('⚠️ Bundle size is large. Consider code splitting or removing unused dependencies.');
56
+ } else if (totalKB < 200) {
57
+ console.log('✅ Great bundle size! Your app will load fast.');
58
+ } else {
59
+ console.log('👍 Good bundle size for most applications.');
60
+ }
61
+ };
62
+
63
+ export default async (options) => {
64
+ let projectDirectory = process.cwd();
65
+ let runtime_dir = __dirname + "/..";
66
+ let node_modules = projectDirectory + "/node_modules";
67
+
68
+ const buildStart = performance.now();
69
+ await buildRouter(await get_config(projectDirectory));
70
+
71
+ const rspack = spawn(node_modules + "/.bin/rspack", [
72
+ "--mode=" + options.mode,
73
+ "--config", runtime_dir + "/../rspack.config.js",
74
+ "--entry", runtime_dir + "/runtime/app.jsx"
75
+ ]);
76
+
77
+ rspack.stdout.on('data', (data) => {
78
+ console.log(data.toString());
79
+ });
80
+
81
+ rspack.stderr.on('data', (data) => {
82
+ console.log(`stderr: ${data.toString()}`);
83
+ });
84
+
85
+ rspack.on('close', async (code) => {
86
+ if (code !== 0) {
87
+ console.log(`error: Process exited with code ${code}`);
88
+ return;
89
+ }
90
+
91
+ const totalTime = Math.round(performance.now() - buildStart);
92
+ console.log(`\n Built in ${totalTime}ms`);
93
+
94
+ if (options.mode === 'production' || process.env.NODE_ENV === 'production') {
95
+ showBundleAnalysis(projectDirectory);
96
+ }
97
+
98
+ try {
99
+ await ssg({ mode: options.mode, forceAll: options.static });
100
+ } catch (err) {
101
+ console.error(`SSG failed: ${err.message}`);
102
+ process.exitCode = 1;
103
+ }
104
+ });
105
+ };
@@ -0,0 +1,91 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const TEMPLATE_DIR = path.resolve(__dirname, "../../templates/minimal");
9
+ const VALID_NAME = /^[a-z0-9][a-z0-9._-]*$/i;
10
+
11
+ export default async function create(projectName) {
12
+ if (!projectName) {
13
+ console.error("Usage: aplos create <project-name>");
14
+ process.exit(1);
15
+ }
16
+
17
+ if (!VALID_NAME.test(projectName) || projectName.startsWith(".")) {
18
+ console.error(
19
+ `Invalid project name: "${projectName}". Use letters, digits, dashes, dots or underscores.`
20
+ );
21
+ process.exit(1);
22
+ }
23
+
24
+ const targetDir = path.resolve(process.cwd(), projectName);
25
+
26
+ if (fs.existsSync(targetDir)) {
27
+ const entries = fs.readdirSync(targetDir);
28
+ if (entries.length > 0) {
29
+ console.error(
30
+ `Target directory "${projectName}" already exists and is not empty.`
31
+ );
32
+ process.exit(1);
33
+ }
34
+ } else {
35
+ fs.mkdirSync(targetDir, { recursive: true });
36
+ }
37
+
38
+ console.log(`Creating Aplos project in ${targetDir}...`);
39
+
40
+ copyDirectory(TEMPLATE_DIR, targetDir, projectName);
41
+ renameGitignore(targetDir);
42
+
43
+ console.log("");
44
+ console.log("Done. Next steps:");
45
+ console.log("");
46
+ console.log(` cd ${projectName}`);
47
+ console.log(" bun install");
48
+ console.log(" bun dev");
49
+ console.log("");
50
+ console.log("Happy hacking!");
51
+ }
52
+
53
+ function copyDirectory(src, dest, projectName) {
54
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
55
+ const srcPath = path.join(src, entry.name);
56
+ const destPath = path.join(dest, entry.name);
57
+
58
+ if (entry.isDirectory()) {
59
+ fs.mkdirSync(destPath, { recursive: true });
60
+ copyDirectory(srcPath, destPath, projectName);
61
+ } else if (entry.isFile()) {
62
+ copyFile(srcPath, destPath, projectName);
63
+ }
64
+ }
65
+ }
66
+
67
+ function copyFile(src, dest, projectName) {
68
+ if (isBinary(src)) {
69
+ fs.copyFileSync(src, dest);
70
+ return;
71
+ }
72
+
73
+ const content = fs.readFileSync(src, "utf8");
74
+ const replaced = content.replace(/\{\{NAME\}\}/g, projectName);
75
+ fs.writeFileSync(dest, replaced);
76
+ }
77
+
78
+ function isBinary(filePath) {
79
+ const ext = path.extname(filePath).toLowerCase();
80
+ return [".png", ".jpg", ".jpeg", ".gif", ".ico", ".webp", ".woff", ".woff2"].includes(
81
+ ext
82
+ );
83
+ }
84
+
85
+ function renameGitignore(targetDir) {
86
+ const stub = path.join(targetDir, "_gitignore");
87
+ const real = path.join(targetDir, ".gitignore");
88
+ if (fs.existsSync(stub) && !fs.existsSync(real)) {
89
+ fs.renameSync(stub, real);
90
+ }
91
+ }
@@ -0,0 +1,198 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { rspack } from "@rspack/core";
4
+ import { RspackDevServer } from "@rspack/dev-server";
5
+ import {buildRouter} from "../build/router.js";
6
+ import get_config from "../build/config.js";
7
+ import { fileURLToPath } from 'url';
8
+ import { createRequire } from 'module';
9
+ import net from 'net';
10
+ import os from 'os';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const require = createRequire(import.meta.url);
15
+
16
+ const getNetworkUrl = (port) => {
17
+ const interfaces = os.networkInterfaces();
18
+ for (const name of Object.keys(interfaces)) {
19
+ for (const iface of interfaces[name]) {
20
+ if (iface.family === 'IPv4' && !iface.internal) {
21
+ return `http://${iface.address}:${port}`;
22
+ }
23
+ }
24
+ }
25
+ return null;
26
+ };
27
+
28
+ const detectFeatures = (projectDirectory) => {
29
+ const features = [];
30
+ const hasTsConfig = fs.existsSync(path.join(projectDirectory, 'tsconfig.json'));
31
+ const hasPostcss = fs.existsSync(path.join(projectDirectory, 'postcss.config.js'))
32
+ || fs.existsSync(path.join(projectDirectory, 'postcss.config.cjs'));
33
+
34
+ if (hasTsConfig) features.push('TypeScript');
35
+ if (hasPostcss) features.push('PostCSS');
36
+ features.push('React Compiler');
37
+ features.push('HMR');
38
+
39
+ return features;
40
+ };
41
+
42
+ const printStartupMessage = (port, projectDirectory, readyTime) => {
43
+ const networkUrl = getNetworkUrl(port);
44
+ const features = detectFeatures(projectDirectory);
45
+
46
+ console.log();
47
+ console.log(' \x1b[1m\x1b[36mAPLOS\x1b[0m \x1b[2mv0.0.1\x1b[0m \x1b[32mready in ' + readyTime + 'ms\x1b[0m');
48
+ console.log();
49
+ console.log(` \x1b[1mLocal:\x1b[0m http://localhost:${port}/`);
50
+ if (networkUrl) {
51
+ console.log(` \x1b[1mNetwork:\x1b[0m ${networkUrl}/`);
52
+ }
53
+ console.log(` \x1b[2m${features.join(' | ')}\x1b[0m`);
54
+ console.log();
55
+ };
56
+
57
+ // Function to check if port is available
58
+ const isPortAvailable = (port) => {
59
+ return new Promise((resolve) => {
60
+ const server = net.createServer();
61
+
62
+ server.listen(port, () => {
63
+ server.once('close', () => resolve(true));
64
+ server.close();
65
+ });
66
+
67
+ server.on('error', () => resolve(false));
68
+ });
69
+ };
70
+
71
+ // Function to find next available port
72
+ const findAvailablePort = async (startPort) => {
73
+ let port = startPort;
74
+ while (port < startPort + 100) { // Try up to 100 ports
75
+ if (await isPortAvailable(port)) {
76
+ return port;
77
+ }
78
+ port++;
79
+ }
80
+ throw new Error(`No available port found starting from ${startPort}`);
81
+ };
82
+
83
+ export default async () => {
84
+ const config = await get_config(process.cwd());
85
+ const cacheDirectory = process.cwd() + "/.aplos/cache";
86
+
87
+ // Ensure cache directory exists first
88
+ try {
89
+ if (!fs.existsSync(cacheDirectory)) {
90
+ fs.mkdirSync(cacheDirectory, { recursive: true });
91
+ }
92
+ } catch (err) {
93
+ console.error('Error creating cache directory:', err);
94
+ }
95
+
96
+ fs.writeFileSync(
97
+ cacheDirectory + "/config.js",
98
+ "module.exports = " + JSON.stringify(config),
99
+ );
100
+ let projectDirectory = process.cwd();
101
+
102
+ // Initial build
103
+ const startTime = Date.now();
104
+ console.log('\x1b[36m⚡ Building routes...\x1b[0m');
105
+ const buildStart = performance.now();
106
+ await buildRouter(config);
107
+ console.log(`\x1b[32m✓ Ready in ${Date.now() - startTime}ms\x1b[0m`);
108
+
109
+ let runtime_dir = __dirname + "/..";
110
+
111
+ const { default: rspackConfig } = await import(runtime_dir + "/../rspack.config.js");
112
+ rspackConfig.mode = "development";
113
+ rspackConfig.entry = [runtime_dir + "/runtime/app.jsx"];
114
+
115
+ const compiler = rspack(rspackConfig);
116
+
117
+ // Regenerate the router cache only when a page file is added or removed.
118
+ const pagesDir = path.join(projectDirectory, "src", "pages");
119
+ const touchesPages = (files) => {
120
+ if (!files) return false;
121
+ for (const f of files) {
122
+ if (f && f.startsWith(pagesDir)) return true;
123
+ }
124
+ return false;
125
+ };
126
+
127
+ let routerReady = Promise.resolve();
128
+ compiler.hooks.watchRun.tapPromise("aplos-router", async (c) => {
129
+ const added = c.modifiedFiles;
130
+ const removed = c.removedFiles;
131
+ if (!added && !removed) return;
132
+ if (!touchesPages(added) && !touchesPages(removed)) return;
133
+
134
+ const startTime = Date.now();
135
+ console.log(`\x1b[36m⚡ Rebuilding routes...\x1b[0m`);
136
+ routerReady = buildRouter(config);
137
+ await routerReady;
138
+ console.log(`\x1b[32m✓ Built in ${Date.now() - startTime}ms\x1b[0m`);
139
+ });
140
+
141
+ let isFirstCompilation = true;
142
+ compiler.hooks.done.tap('aplos-startup', () => {
143
+ if (isFirstCompilation) {
144
+ const readyTime = Math.round(performance.now() - buildStart);
145
+ printStartupMessage(finalPort, projectDirectory, readyTime);
146
+ isFirstCompilation = false;
147
+ }
148
+ });
149
+
150
+ // Determine port logic based on environment variable
151
+ let finalPort = config.server.port;
152
+ const isPortExplicitlySet = process.env.APLOS_SERVER_PORT;
153
+
154
+ // If no explicit port set, find available port
155
+ if (!isPortExplicitlySet) {
156
+ try {
157
+ finalPort = await findAvailablePort(config.server.port);
158
+ if (finalPort !== config.server.port) {
159
+ console.log(`Port ${config.server.port} is busy, using port ${finalPort}`);
160
+ }
161
+ } catch (error) {
162
+ console.error(`Could not find available port: ${error.message}`);
163
+ process.exit(1);
164
+ }
165
+ }
166
+
167
+ const devServerOptions = {
168
+ hot: true,
169
+ open: false,
170
+ port: finalPort,
171
+ historyApiFallback: true,
172
+ client: {
173
+ overlay: {
174
+ errors: true,
175
+ warnings: false,
176
+ },
177
+ logging: 'error',
178
+ },
179
+ static: [
180
+ { directory: path.join(projectDirectory, "public") },
181
+ { directory: path.join(__dirname + "/../client/", "public") },
182
+ ],
183
+ };
184
+ const server = new RspackDevServer(devServerOptions, compiler);
185
+
186
+ const runServer = async () => {
187
+ await server.start();
188
+ };
189
+
190
+ runServer().catch((error) => {
191
+ if (isPortExplicitlySet && error.code === 'EADDRINUSE') {
192
+ console.error(`Port ${finalPort} is already in use. Since APLOS_SERVER_PORT is set, refusing to use alternative port.`);
193
+ } else {
194
+ console.error(error);
195
+ }
196
+ process.exit(1);
197
+ });
198
+ };