create-avalon 0.1.13 → 0.1.15

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 (2) hide show
  1. package/dist/cli.js +284 -20
  2. package/package.json +39 -39
package/dist/cli.js CHANGED
@@ -1181,12 +1181,24 @@ async function collectProjectConfig(initialName) {
1181
1181
  Nt("Operation cancelled.");
1182
1182
  process.exit(1);
1183
1183
  }
1184
+ const deployResult = await Jt({
1185
+ message: "Where will you deploy?",
1186
+ options: [
1187
+ { value: "netlify", label: "Netlify", hint: "Generates netlify.toml, build.mjs, post-build.mjs" },
1188
+ { value: "none", label: "None / Other", hint: "Node server preset, no deploy config" }
1189
+ ]
1190
+ });
1191
+ if (Ct(deployResult)) {
1192
+ Nt("Operation cancelled.");
1193
+ process.exit(1);
1194
+ }
1184
1195
  return {
1185
1196
  projectName,
1186
1197
  integrations: integrationsResult,
1187
1198
  styling: stylingResult,
1188
1199
  plugins: pluginsResult,
1189
- middleware: middlewareResult
1200
+ middleware: middlewareResult,
1201
+ deploy: deployResult
1190
1202
  };
1191
1203
  }
1192
1204
 
@@ -1264,12 +1276,17 @@ function generatePackageJson(config) {
1264
1276
  private: true,
1265
1277
  scripts: {
1266
1278
  dev: "bunx --bun vite dev",
1267
- build: "bunx --bun vite build",
1268
- preview: "bunx --bun vite preview"
1279
+ build: config.deploy === "netlify" ? "bun build.mjs" : "bunx --bun vite build",
1280
+ "post-build": config.deploy === "netlify" ? "node post-build.mjs" : undefined,
1281
+ preview: "node .output/server/index.mjs"
1269
1282
  },
1270
1283
  dependencies,
1271
1284
  devDependencies
1272
1285
  };
1286
+ for (const key of Object.keys(pkg.scripts)) {
1287
+ if (pkg.scripts[key] === undefined)
1288
+ delete pkg.scripts[key];
1289
+ }
1273
1290
  return JSON.stringify(pkg, null, 2);
1274
1291
  }
1275
1292
 
@@ -1352,7 +1369,7 @@ function generateViteConfig(config) {
1352
1369
  ` layoutsDir: 'app/shared/layouts',`,
1353
1370
  ` image: true,`,
1354
1371
  ` nitro: {`,
1355
- ` preset: 'node_server',`,
1372
+ ` preset: process.env.NITRO_PRESET || 'node_server',`,
1356
1373
  ` streaming: true,`,
1357
1374
  ` },`,
1358
1375
  ` });`,
@@ -1699,6 +1716,245 @@ function getFaviconBuffer() {
1699
1716
  return Buffer.from(FAVICON_BASE64, "base64");
1700
1717
  }
1701
1718
 
1719
+ // src/templates/deploy.ts
1720
+ function generateNetlifyToml(config) {
1721
+ return `[build]
1722
+ base = "."
1723
+ command = "bun install && bun build.mjs"
1724
+ publish = "dist"
1725
+
1726
+ [build.environment]
1727
+ NODE_VERSION = "22"
1728
+ BUN_VERSION = "latest"
1729
+ NITRO_PRESET = "netlify"
1730
+
1731
+ [functions]
1732
+ directory = "netlify/functions"
1733
+ `;
1734
+ }
1735
+ function generateBuildMjs() {
1736
+ return `/**
1737
+ * Netlify build wrapper.
1738
+ *
1739
+ * Vite/Nitro leaves open handles after the build completes, preventing
1740
+ * the Node process from exiting. This wrapper detects when the build
1741
+ * output is ready, kills the entire process group, then runs post-build.
1742
+ */
1743
+
1744
+ import { spawn, execSync } from 'node:child_process';
1745
+ import { existsSync, rmSync } from 'node:fs';
1746
+ import { join } from 'node:path';
1747
+
1748
+ const CWD = process.cwd();
1749
+ const NITRO_JSON = join(CWD, '.netlify', 'functions-internal', 'nitro.json');
1750
+ const SERVER_MJS = join(CWD, '.netlify', 'functions-internal', 'server', 'server.mjs');
1751
+ const OUTPUT_SSR = join(CWD, '.output', 'server', '_ssr', 'ssr.mjs');
1752
+
1753
+ console.log('[build] Starting vite build...');
1754
+
1755
+ for (const dir of ['.netlify', '.output', 'netlify']) {
1756
+ const full = join(CWD, dir);
1757
+ if (existsSync(full)) {
1758
+ rmSync(full, { recursive: true, force: true });
1759
+ console.log(\`[build] Cleaned stale \${dir}/\`);
1760
+ }
1761
+ }
1762
+
1763
+ const child = spawn('bunx', ['--bun', 'vite', 'build'], {
1764
+ cwd: CWD,
1765
+ stdio: 'inherit',
1766
+ detached: true,
1767
+ });
1768
+
1769
+ const childPid = child.pid;
1770
+ let done = false;
1771
+
1772
+ function killTree() {
1773
+ try { process.kill(-childPid, 'SIGKILL'); } catch {}
1774
+ try { child.kill('SIGKILL'); } catch {}
1775
+ }
1776
+
1777
+ function finish() {
1778
+ if (done) return;
1779
+ done = true;
1780
+ clearInterval(poll);
1781
+ clearTimeout(absoluteTimeout);
1782
+ killTree();
1783
+
1784
+ setTimeout(() => {
1785
+ console.log('[build] Running post-build...');
1786
+ try {
1787
+ execSync('node post-build.mjs', { cwd: CWD, stdio: 'inherit', timeout: 60_000 });
1788
+ } catch (err) {
1789
+ console.error('[build] post-build warning:', err.message);
1790
+ }
1791
+
1792
+ const V1_SERVER = join(CWD, '.netlify', 'v1', 'functions', 'server', 'server.mjs');
1793
+ if (existsSync(V1_SERVER)) console.log('[build] ✅ Server function found (v1 API)');
1794
+ else if (existsSync(SERVER_MJS)) console.log('[build] ✅ Server function found (legacy)');
1795
+ else if (existsSync(OUTPUT_SSR)) console.log('[build] ✅ SSR bundle found');
1796
+ else console.error('[build] ❌ No server output found');
1797
+
1798
+ console.log('[build] ✅ Complete');
1799
+ process.exit(0);
1800
+ }, 500);
1801
+ }
1802
+
1803
+ child.on('exit', (code) => {
1804
+ console.log(\`[build] vite build exited with code \${code}\`);
1805
+ finish();
1806
+ });
1807
+
1808
+ child.on('error', (err) => {
1809
+ console.error('[build] spawn error:', err);
1810
+ process.exit(1);
1811
+ });
1812
+
1813
+ const poll = setInterval(() => {
1814
+ const netlifyReady = existsSync(NITRO_JSON) && existsSync(SERVER_MJS);
1815
+ const nodeServerReady = existsSync(OUTPUT_SSR);
1816
+ if (netlifyReady || nodeServerReady) {
1817
+ console.log(\`[build] Output detected (\${netlifyReady ? 'netlify' : 'node-server'}), waiting 3s for final writes...\`);
1818
+ clearInterval(poll);
1819
+ setTimeout(finish, 3_000);
1820
+ }
1821
+ }, 1_000);
1822
+
1823
+ const absoluteTimeout = setTimeout(() => {
1824
+ console.error('[build] Timeout — killing build');
1825
+ finish();
1826
+ }, 240_000);
1827
+ `;
1828
+ }
1829
+
1830
+ // src/templates/post-build.ts
1831
+ function generatePostBuildMjs() {
1832
+ const lines = [
1833
+ `/**`,
1834
+ ` * Post-build script for Netlify deployment.`,
1835
+ ` *`,
1836
+ ` * - Removes stale index.html that would shadow SSR routes`,
1837
+ ` * - Generates _redirects for island JS path mapping`,
1838
+ ` * - Copies island JS to clean paths for local preview`,
1839
+ ` * - Copies server function to all Netlify function paths`,
1840
+ ` * - Ensures SSR catch-all redirect exists`,
1841
+ ` */`,
1842
+ ``,
1843
+ `import {`,
1844
+ ` existsSync, unlinkSync, readFileSync, writeFileSync,`,
1845
+ ` readdirSync, copyFileSync, mkdirSync, cpSync,`,
1846
+ `} from 'node:fs';`,
1847
+ `import { join, dirname, relative } from 'node:path';`,
1848
+ ``,
1849
+ `const CWD = process.cwd();`,
1850
+ `const DIST_DIR = join(CWD, 'dist');`,
1851
+ `const ASSETS_DIR = join(CWD, 'dist', 'assets');`,
1852
+ ``,
1853
+ `function collectFiles(dir, predicate, result = []) {`,
1854
+ ` if (!existsSync(dir)) return result;`,
1855
+ ` for (const entry of readdirSync(dir, { withFileTypes: true })) {`,
1856
+ ` const full = join(dir, entry.name);`,
1857
+ ` if (entry.isDirectory()) collectFiles(full, predicate, result);`,
1858
+ ` else if (predicate(entry.name)) result.push(full);`,
1859
+ ` }`,
1860
+ ` return result;`,
1861
+ `}`,
1862
+ ``,
1863
+ `function toServePath(absPath) {`,
1864
+ ` return '/' + relative(DIST_DIR, absPath).replaceAll('\\\\', '/');`,
1865
+ `}`,
1866
+ ``,
1867
+ `// ── Cleanup stale index.html ─────────────────────────────────────`,
1868
+ `for (const htmlPath of ['dist/index.html', '.netlify/functions-internal/server/public/index.html']) {`,
1869
+ ` const full = join(CWD, htmlPath);`,
1870
+ ` if (existsSync(full)) { unlinkSync(full); console.log('[cleanup] Removed ' + htmlPath); }`,
1871
+ `}`,
1872
+ ``,
1873
+ `// ── Generate island redirects + local copies ─────────────────────`,
1874
+ `function generateIslandRedirects() {`,
1875
+ ` const islandsDir = join(ASSETS_DIR, 'islands');`,
1876
+ ` if (!existsSync(islandsDir)) return;`,
1877
+ ` const islandFiles = collectFiles(islandsDir, n => n.endsWith('.js') && !n.endsWith('.js.map'));`,
1878
+ ` if (islandFiles.length === 0) return;`,
1879
+ ` const redirectLines = [];`,
1880
+ ` for (const absPath of islandFiles) {`,
1881
+ ` const servePath = toServePath(absPath);`,
1882
+ ` const cleanPath = servePath.replace('/assets/', '/').replace(/-[A-Za-z0-9_-]{6,12}\\.js$/, '.js');`,
1883
+ ` redirectLines.push(cleanPath + ' ' + servePath + ' 200');`,
1884
+ ` const cleanAbsPath = join(DIST_DIR, cleanPath.slice(1));`,
1885
+ ` mkdirSync(dirname(cleanAbsPath), { recursive: true });`,
1886
+ ` copyFileSync(absPath, cleanAbsPath);`,
1887
+ ` }`,
1888
+ ` const redirectsPath = join(DIST_DIR, '_redirects');`,
1889
+ ` let existing = existsSync(redirectsPath) ? readFileSync(redirectsPath, 'utf-8') : '';`,
1890
+ ` existing = existing.replaceAll(/# Island JS path rewrites[^\\n]*\\n(?:\\/islands\\/[^\\n]*\\n)*/g, '').trim();`,
1891
+ ` const header = '# Island JS path rewrites (generated by post-build.mjs)\\n';`,
1892
+ ` const content = existing`,
1893
+ ` ? existing + '\\n\\n' + header + redirectLines.join('\\n') + '\\n'`,
1894
+ ` : header + redirectLines.join('\\n') + '\\n';`,
1895
+ ` writeFileSync(redirectsPath, content);`,
1896
+ ` console.log('[redirects] Wrote ' + redirectLines.length + ' island redirects');`,
1897
+ `}`,
1898
+ ``,
1899
+ `// ── Copy framework adapters ───────────────────────────────────────`,
1900
+ `function copyAdapters() {`,
1901
+ ` const sources = [`,
1902
+ ` join(CWD, '.output', 'public', '_adapters'),`,
1903
+ ` join(CWD, 'dist', '_adapters'),`,
1904
+ ` ];`,
1905
+ ` for (const srcDir of sources) {`,
1906
+ ` if (!existsSync(srcDir)) continue;`,
1907
+ ` const files = readdirSync(srcDir).filter(f => f.endsWith('.js'));`,
1908
+ ` if (files.length === 0) continue;`,
1909
+ ` const destDir = join(DIST_DIR, '_adapters');`,
1910
+ ` mkdirSync(destDir, { recursive: true });`,
1911
+ ` for (const file of files) {`,
1912
+ ` const src = join(srcDir, file);`,
1913
+ ` const dest = join(destDir, file);`,
1914
+ ` if (src !== dest) copyFileSync(src, dest);`,
1915
+ ` }`,
1916
+ ` console.log('[adapters] Copied ' + files.length + ' framework adapters');`,
1917
+ ` return;`,
1918
+ ` }`,
1919
+ `}`,
1920
+ ``,
1921
+ `// ── Copy function to all Netlify paths ────────────────────────────`,
1922
+ `function copyToNetlifyPaths() {`,
1923
+ ` const legacyDir = join(CWD, '.netlify', 'functions-internal', 'server');`,
1924
+ ` if (!existsSync(legacyDir)) return;`,
1925
+ ` const targets = [`,
1926
+ ` join(CWD, '.netlify', 'v1', 'functions', 'server'),`,
1927
+ ` join(CWD, 'netlify', 'functions', 'server'),`,
1928
+ ` ];`,
1929
+ ` for (const target of targets) {`,
1930
+ ` cpSync(legacyDir, target, { recursive: true, force: true });`,
1931
+ ` }`,
1932
+ ` console.log('[netlify-fn] Copied server function to all Netlify paths');`,
1933
+ `}`,
1934
+ ``,
1935
+ `// ── Ensure SSR catch-all redirect ─────────────────────────────────`,
1936
+ `function ensureNetlifyRedirects() {`,
1937
+ ` const redirectsPath = join(DIST_DIR, '_redirects');`,
1938
+ ` let content = existsSync(redirectsPath) ? readFileSync(redirectsPath, 'utf-8') : '';`,
1939
+ ` if (content.includes('/.netlify/functions/server')) return;`,
1940
+ ` const catchAll = '\\n# SSR catch-all (Nitro server function)\\n/* /.netlify/functions/server 200\\n';`,
1941
+ ` content = content.trimEnd() + '\\n' + catchAll;`,
1942
+ ` writeFileSync(redirectsPath, content);`,
1943
+ ` console.log('[redirects] Added SSR catch-all to _redirects');`,
1944
+ `}`,
1945
+ ``,
1946
+ `// ── Run ──────────────────────────────────────────────────────────`,
1947
+ `generateIslandRedirects();`,
1948
+ `copyAdapters();`,
1949
+ `copyToNetlifyPaths();`,
1950
+ `ensureNetlifyRedirects();`,
1951
+ `console.log('[post-build] Complete');`,
1952
+ ``
1953
+ ];
1954
+ return lines.join(`
1955
+ `);
1956
+ }
1957
+
1702
1958
  // src/scaffold.ts
1703
1959
  function generateHonoServerEntry() {
1704
1960
  return `import { Hono } from 'hono';
@@ -1763,30 +2019,32 @@ async function scaffoldProject(config, targetDir) {
1763
2019
  await writeFile(join(targetDir, "server/renderer.ts"), [
1764
2020
  `import { createNitroRenderer } from '@useavalon/avalon/nitro/renderer';`,
1765
2021
  `import avalonConfig from 'virtual:avalon/config';`,
2022
+ `import { loadPage } from 'virtual:avalon/page-loader';`,
1766
2023
  ``,
1767
2024
  `export default createNitroRenderer({`,
1768
2025
  ` avalonConfig,`,
1769
2026
  ` isDev: avalonConfig.isDev,`,
2027
+ ` resolvePageRoute: async (pathname) => {`,
2028
+ ` const mod = loadPage(pathname);`,
2029
+ ` if (!mod) return null;`,
2030
+ ` return { filePath: \`[virtual:\${pathname}]\`, pattern: pathname, params: {} };`,
2031
+ ` },`,
2032
+ ` loadPageModule: async (filePath) => {`,
2033
+ ` const match = filePath.match(/^\\[virtual:(.+)\\]$/);`,
2034
+ ` const pathname = match ? match[1] : filePath;`,
2035
+ ` const mod = loadPage(pathname);`,
2036
+ ` if (mod) return mod;`,
2037
+ ` return { default: () => null, metadata: { title: 'Avalon' } };`,
2038
+ ` },`,
1770
2039
  `});`,
1771
2040
  ``
1772
2041
  ].join(`
1773
2042
  `));
1774
- await writeFile(join(targetDir, "index.html"), [
1775
- `<!DOCTYPE html>`,
1776
- `<html lang="en">`,
1777
- ` <head>`,
1778
- ` <meta charset="utf-8" />`,
1779
- ` <meta name="viewport" content="width=device-width, initial-scale=1" />`,
1780
- ` <title>Avalon</title>`,
1781
- ` </head>`,
1782
- ` <body>`,
1783
- ` <!--ssr-outlet-->`,
1784
- ` <script type="module" src="/src/client/main.js"></script>`,
1785
- ` </body>`,
1786
- `</html>`,
1787
- ``
1788
- ].join(`
1789
- `));
2043
+ if (config.deploy === "netlify") {
2044
+ await writeFile(join(targetDir, "netlify.toml"), generateNetlifyToml(config));
2045
+ await writeFile(join(targetDir, "build.mjs"), generateBuildMjs());
2046
+ await writeFile(join(targetDir, "post-build.mjs"), generatePostBuildMjs());
2047
+ }
1790
2048
  }
1791
2049
 
1792
2050
  // src/summary.ts
@@ -1795,10 +2053,15 @@ var STYLING_LABELS = {
1795
2053
  tailwind: "Tailwind CSS",
1796
2054
  shadcn: "shadcn"
1797
2055
  };
2056
+ var DEPLOY_LABELS = {
2057
+ netlify: "Netlify",
2058
+ none: "None"
2059
+ };
1798
2060
  function formatSummary(config, scaffoldedInPlace = false) {
1799
2061
  const integrations = config.integrations.length > 0 ? config.integrations.join(", ") : "none";
1800
2062
  const styling = STYLING_LABELS[config.styling] ?? config.styling;
1801
2063
  const plugins = config.plugins.length > 0 ? config.plugins.join(", ") : "none";
2064
+ const deploy = DEPLOY_LABELS[config.deploy] ?? config.deploy;
1802
2065
  const nextSteps = scaffoldedInPlace ? [" bun install", " bun run dev"] : [` cd ${config.projectName}`, " bun install", " bun run dev"];
1803
2066
  return [
1804
2067
  "",
@@ -1807,6 +2070,7 @@ function formatSummary(config, scaffoldedInPlace = false) {
1807
2070
  ` Styling: ${styling}`,
1808
2071
  ` Plugins: ${plugins}`,
1809
2072
  ` Middleware: ${config.middleware}`,
2073
+ ` Deploy: ${deploy}`,
1810
2074
  "",
1811
2075
  " Next steps:",
1812
2076
  ...nextSteps,
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "create-avalon",
3
- "version": "0.1.13",
4
- "description": "Scaffold a new Avalon project with multi-framework islands architecture",
5
- "license": "MIT",
6
- "type": "module",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/useAvalon/Avalon.git",
10
- "directory": "packages/create-avalon"
11
- },
12
- "homepage": "https://useavalon.dev",
13
- "keywords": [
14
- "avalon",
15
- "create",
16
- "scaffold",
17
- "cli",
18
- "islands",
19
- "vite"
20
- ],
21
- "bin": {
22
- "create-avalon": "dist/cli.js"
23
- },
24
- "files": [
25
- "dist",
26
- "README.md"
27
- ],
28
- "scripts": {
29
- "build": "bun build src/cli.ts --outdir dist --target node --format esm",
30
- "prepublishOnly": "bun run build"
31
- },
32
- "dependencies": {
33
- "@clack/prompts": "^1.1.0"
34
- },
35
- "devDependencies": {
36
- "fast-check": "^4.6.0",
37
- "vitest": "^4.1.0"
38
- }
39
- }
1
+ {
2
+ "name": "create-avalon",
3
+ "version": "0.1.15",
4
+ "description": "Scaffold a new Avalon project with multi-framework islands architecture",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/useAvalon/Avalon.git",
10
+ "directory": "packages/create-avalon"
11
+ },
12
+ "homepage": "https://useavalon.dev",
13
+ "keywords": [
14
+ "avalon",
15
+ "create",
16
+ "scaffold",
17
+ "cli",
18
+ "islands",
19
+ "vite"
20
+ ],
21
+ "bin": {
22
+ "create-avalon": "dist/cli.js"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "build": "bun build src/cli.ts --outdir dist --target node --format esm",
30
+ "prepublishOnly": "bun run build"
31
+ },
32
+ "dependencies": {
33
+ "@clack/prompts": "^1.1.0"
34
+ },
35
+ "devDependencies": {
36
+ "fast-check": "^4.6.0",
37
+ "vitest": "^4.1.0"
38
+ }
39
+ }