create-fluixi 0.1.0-alpha.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fluixi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/fluixi/assets/main/logos/128x128.png" alt="Fluixi" width="120" height="120" />
3
+ </p>
4
+
5
+ # create-fluixi
6
+
7
+ **Scaffold a new Fluixi app β€” `npm create fluixi <dir>`.**
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-22c55e.svg)](./LICENSE)
10
+ ![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)
11
+ ![npm](https://img.shields.io/npm/v/create-fluixi?logo=npm)
12
+
13
+ ---
14
+
15
+ ## ✨ Overview
16
+
17
+ Run via your package manager's `create` shortcut β€” no install needed:
18
+
19
+ ```bash
20
+ npm create fluixi my-app
21
+ # or
22
+ pnpm create fluixi my-app
23
+ # or
24
+ yarn create fluixi my-app
25
+ ```
26
+
27
+ It prompts for a project name and a **mode**, or take them on the command line:
28
+
29
+ ```bash
30
+ create-fluixi my-app --ssr # SSR app (@fluixi/start: file routing + SSR + hydration)
31
+ create-fluixi my-app --spa # SPA (vite + @fluixi/vite-plugin)
32
+ ```
33
+
34
+ Then:
35
+
36
+ ```bash
37
+ cd my-app
38
+ pnpm install
39
+ pnpm dev
40
+ ```
41
+
42
+ ## πŸ—‚οΈ Templates
43
+
44
+ | Mode | Stack | Entry |
45
+ | --- | --- | --- |
46
+ | **SSR** | [`@fluixi/start`](../start) + [`@fluixi/cli`](../cli) | `src/entry-{server,client}`, `src/routes/`, `fluixi.config.ts` |
47
+ | **SPA** | `vite` + [`@fluixi/vite-plugin`](../vite-plugin) | `src/main.tsx` β†’ `render(() => <App/>)` |
48
+
49
+ Both pin `@fluixi/*` to the `next` dist-tag and set `jsxImportSource: "@fluixi/jsx"`.
50
+
51
+ ## 🧱 Scaffolding pieces
52
+
53
+ To add components, routes, or middleware **inside** an existing app, use `fluixi generate`
54
+ from [`@fluixi/cli`](../cli):
55
+
56
+ ```bash
57
+ fluixi g component Button
58
+ fluixi g route about
59
+ ```
60
+
61
+ ## πŸ“„ License
62
+
63
+ [MIT](./LICENSE) Β© [Fluixi](https://fluixi.dev)
package/dist/index.js ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `npm create fluixi <dir>` β€” scaffold a new Fluixi app. Prompts for a name and a
4
+ * mode (SSR via @fluixi/start, or SPA via vite + @fluixi/vite-plugin), unless given on
5
+ * the command line: `create-fluixi my-app --spa` / `--ssr`. Zero runtime deps β€” the
6
+ * styled prompts (gradient wordmark, gutter, arrow-key select) are hand-rolled ANSI.
7
+ */
8
+ import { cpSync, renameSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
9
+ import { resolve, basename, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { createInterface } from 'node:readline/promises';
12
+ import { emitKeypressEvents } from 'node:readline';
13
+ import { stdin as input, stdout as output, argv, exit, env } from 'node:process';
14
+ const here = (p) => fileURLToPath(new URL(p, import.meta.url));
15
+ // ── tiny zero-dep ANSI styling ──────────────────────────────────────────────
16
+ const COLOR = env.NO_COLOR || env.FORCE_COLOR === '0'
17
+ ? false
18
+ : (!!output.isTTY || !!env.FORCE_COLOR) && env.TERM !== 'dumb';
19
+ const sgr = (o, x) => (s) => (COLOR ? `\x1b[${o}m${s}\x1b[${x}m` : `${s}`);
20
+ const c = {
21
+ bold: sgr(1, 22), dim: sgr(2, 22), green: sgr(32, 39), yellow: sgr(33, 39),
22
+ cyan: sgr(36, 39), gray: sgr(90, 39), red: sgr(31, 39),
23
+ };
24
+ const rgb = (r, g, b) => (s) => (COLOR ? `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m` : s);
25
+ const BRAND = [
26
+ [34, 211, 238], [56, 189, 248], [129, 140, 248], [167, 139, 250], [192, 132, 252],
27
+ ];
28
+ const gradient = (text) => {
29
+ if (!COLOR)
30
+ return text;
31
+ const ch = [...text];
32
+ return ch.map((x, i) => {
33
+ const [r, g, b] = BRAND[Math.min(BRAND.length - 1, Math.floor((i / Math.max(1, ch.length - 1)) * (BRAND.length - 1)))];
34
+ return rgb(r, g, b)(x);
35
+ }).join('');
36
+ };
37
+ const BAR = c.gray('β”‚');
38
+ // ── interactive prompt primitives (clack-style gutter) ──────────────────────
39
+ async function askText(label, fallback) {
40
+ output.write(`${c.green('β—‡')} ${c.bold(label)} ${c.gray(`(${fallback})`)}\n`);
41
+ const rl = createInterface({ input, output });
42
+ const answer = (await rl.question(`${BAR} `)).trim();
43
+ rl.close();
44
+ output.write(`${BAR}\n`);
45
+ return answer || fallback;
46
+ }
47
+ function askSelect(label, choices) {
48
+ return new Promise((done) => {
49
+ let idx = 0;
50
+ output.write(`${c.green('β—†')} ${c.bold(label)}\n`);
51
+ const draw = (first) => {
52
+ if (!first)
53
+ output.write(`\x1b[${choices.length}A`);
54
+ for (let i = 0; i < choices.length; i++) {
55
+ const on = i === idx;
56
+ const dot = on ? c.green('●') : c.gray('β—‹');
57
+ const name = on ? c.cyan(c.bold(choices[i].label)) : choices[i].label;
58
+ output.write(`\x1b[2K${BAR} ${dot} ${name} ${c.gray(choices[i].hint)}\n`);
59
+ }
60
+ };
61
+ draw(true);
62
+ emitKeypressEvents(input);
63
+ if (input.isTTY)
64
+ input.setRawMode(true);
65
+ const onKey = (_s, k) => {
66
+ if (k.name === 'up' || k.name === 'k') {
67
+ idx = (idx - 1 + choices.length) % choices.length;
68
+ draw(false);
69
+ }
70
+ else if (k.name === 'down' || k.name === 'j') {
71
+ idx = (idx + 1) % choices.length;
72
+ draw(false);
73
+ }
74
+ else if (k.name === 'return') {
75
+ finish();
76
+ done(choices[idx].value);
77
+ }
78
+ else if (k.name === 'c' && k.ctrl) {
79
+ finish();
80
+ output.write('\n');
81
+ exit(1);
82
+ }
83
+ };
84
+ const finish = () => {
85
+ input.off('keypress', onKey);
86
+ if (input.isTTY)
87
+ input.setRawMode(false);
88
+ // emitKeypressEvents() resumed stdin to read keys; pause it again or the
89
+ // live stdin handle keeps the event loop alive and the CLI never exits.
90
+ input.pause();
91
+ output.write(`${BAR}\n`);
92
+ };
93
+ input.on('keypress', onKey);
94
+ });
95
+ }
96
+ async function main() {
97
+ const args = argv.slice(2);
98
+ let dir = args.find((a) => !a.startsWith('-'));
99
+ let mode = args.includes('--spa') ? 'spa' : args.includes('--ssr') ? 'ssr' : undefined;
100
+ const interactive = input.isTTY && (!dir || !mode);
101
+ if (interactive) {
102
+ output.write(`\n${c.gray('β”Œ')} ${gradient('β—†')} ${c.bold(gradient('create-fluixi'))}\n${BAR}\n`);
103
+ if (!dir)
104
+ dir = await askText('Project name', 'fluixi-app');
105
+ if (!mode) {
106
+ mode = (await askSelect('App mode', [
107
+ { value: 'ssr', label: 'SSR', hint: 'server-rendered Β· @fluixi/start' },
108
+ { value: 'spa', label: 'SPA', hint: 'client-only Β· vite' },
109
+ ]));
110
+ }
111
+ }
112
+ dir = dir ?? 'fluixi-app';
113
+ mode = mode ?? 'ssr';
114
+ const target = resolve(dir);
115
+ const name = basename(target);
116
+ if (existsSync(target)) {
117
+ output.write(`${c.gray('β””')} ${c.red('βœ—')} "${c.bold(name)}" already exists β€” pick another name or remove it.\n\n`);
118
+ exit(1);
119
+ }
120
+ cpSync(here(mode === 'spa' ? '../template-spa' : '../template-ssr'), target, { recursive: true });
121
+ // npm strips a real .gitignore from published packages β†’ restore it from _gitignore.
122
+ const gi = join(target, '_gitignore');
123
+ if (existsSync(gi))
124
+ renameSync(gi, join(target, '.gitignore'));
125
+ const pkgPath = join(target, 'package.json');
126
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
127
+ pkg.name = name;
128
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
129
+ // Outro + next steps. The leading gutter only makes sense after the intro/prompts.
130
+ const tag = mode === 'ssr' ? c.cyan('SSR') : c.cyan('SPA');
131
+ if (interactive)
132
+ output.write(`${BAR}\n`);
133
+ else
134
+ output.write('\n');
135
+ output.write(`${c.green('β—‡')} ${c.green('Created')} ${c.bold(name)} ${c.gray('Β·')} ${tag}\n${c.gray('β””')}\n\n`);
136
+ output.write(` ${c.bold('Next')}\n`);
137
+ output.write(` ${c.gray('β€Ί')} cd ${dir}\n`);
138
+ output.write(` ${c.gray('β€Ί')} ${c.cyan('pnpm install')} ${c.gray('# or npm / yarn')}\n`);
139
+ output.write(` ${c.gray('β€Ί')} ${c.cyan('pnpm dev')}\n\n`);
140
+ }
141
+ main();
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "create-fluixi",
3
+ "version": "0.1.0-alpha.5",
4
+ "description": "Scaffold a new Fluixi app β€” `npm create fluixi <dir>`.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-fluixi": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "template-ssr",
12
+ "template-spa",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "registry": "https://registry.npmjs.org/",
17
+ "access": "public"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/fluixi/core.git",
22
+ "directory": "packages/create"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.lib.json"
26
+ }
27
+ }
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ *.log
4
+ .DS_Store
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Fluixi App</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "fluixi-app",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@fluixi/core": "next",
13
+ "@fluixi/jsx": "next",
14
+ "@fluixi/dom": "next",
15
+ "@fluixi/reactive": "next"
16
+ },
17
+ "devDependencies": {
18
+ "@fluixi/vite-plugin": "next",
19
+ "vite": "^5.0.0"
20
+ }
21
+ }
@@ -0,0 +1,13 @@
1
+ import { createSignal } from '@fluixi/core';
2
+
3
+ export function App() {
4
+ const [count, setCount] = createSignal(0);
5
+ return (
6
+ <div class="app">
7
+ <h1>Fluixi</h1>
8
+ <button onClick={() => setCount(count() - 1)}>-</button>
9
+ <span>{count()}</span>
10
+ <button onClick={() => setCount(count() + 1)}>+</button>
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,5 @@
1
+ import { render } from '@fluixi/core';
2
+ import { App } from './app';
3
+
4
+ const root = document.getElementById('app');
5
+ if (root) render(() => <App />, root);
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "preserve",
7
+ "jsxImportSource": "@fluixi/jsx",
8
+ "strict": false,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["vite/client"]
12
+ },
13
+ "include": ["src", "vite.config.ts"]
14
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vite';
2
+ import { fluixi } from '@fluixi/vite-plugin';
3
+
4
+ export default defineConfig({
5
+ plugins: [fluixi()],
6
+ });
@@ -0,0 +1,15 @@
1
+ # Fluixi app
2
+
3
+ Scaffolded with `npm create fluixi`. SSR + file-based routing + hydration via
4
+ [`@fluixi/start`](https://www.npmjs.com/package/@fluixi/start).
5
+
6
+ ```bash
7
+ pnpm install
8
+ pnpm dev # dev server (SSR + HMR)
9
+ pnpm build # production build
10
+ pnpm start # serve the production build
11
+ ```
12
+
13
+ - Routes live in `src/routes/` (file-based).
14
+ - `src/app.tsx` is the shell layout; `src/entry-{server,client}.{ts,tsx}` are the SSR/hydration entries.
15
+ - Config in `fluixi.config.ts`.
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ *.log
4
+ .DS_Store
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from '@fluixi/start/config';
2
+
3
+ export default defineConfig({
4
+ port: 3000,
5
+ });
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Fluixi Start Example</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/entry-client.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "fluixi-app",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "fluixi dev",
8
+ "build": "fluixi build",
9
+ "start": "fluixi start"
10
+ },
11
+ "dependencies": {
12
+ "@fluixi/cli": "next",
13
+ "@fluixi/start": "next",
14
+ "@fluixi/core": "next",
15
+ "@fluixi/jsx": "next",
16
+ "@fluixi/server": "next",
17
+ "@fluixi/reactive": "next",
18
+ "@fluixi/dom": "next"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.0.0",
22
+ "vite": "^5.0.0"
23
+ }
24
+ }
@@ -0,0 +1,36 @@
1
+ import { Router, Outlet, createMemoryHistory } from '@fluixi/start/router';
2
+ import { Suspense } from '@fluixi/core';
3
+ import { routes } from 'virtual:fluixi-routes';
4
+
5
+ // Shell layout around the routed page. File-routes (src/routes/*) are lazy, so the
6
+ // <Outlet/> sits under <Suspense> β€” async SSR resolves their content server-side.
7
+ function RootLayout() {
8
+ return (
9
+ <div class="app">
10
+ <nav>
11
+ <a href="/">home</a>
12
+ {' Β· '}
13
+ <a href="/about">about</a>
14
+ </nav>
15
+ <main>
16
+ <Suspense fallback={<p>loading…</p>}>
17
+ <Outlet />
18
+ </Suspense>
19
+ </main>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ // Exported so entry-client can hand it to startClient β€” it preloads the current route's
25
+ // lazy chunk before hydrating, so the server-rendered DOM is adopted in place (flash-free).
26
+ export const appRoutes = [{ path: '/', component: RootLayout, children: routes as any }];
27
+
28
+ export default function App(props?: { url?: string }) {
29
+ // On the server, match the request URL (memory history); on the client, default
30
+ // to browser history.
31
+ const history =
32
+ typeof window === 'undefined'
33
+ ? createMemoryHistory(props?.url ?? '/')
34
+ : undefined;
35
+ return <Router routes={appRoutes as any} history={history as any} />;
36
+ }
@@ -0,0 +1,10 @@
1
+ // Client entry β€” hydrates the server-rendered markup. The app HTML is injected into
2
+ // `#root` (the @fluixi/start mountId), so hydrate that container.
3
+ import { startClient } from '@fluixi/core/client';
4
+ import App, { appRoutes } from './app.js';
5
+
6
+ // Passing the route tree lets startClient preload the matched route's lazy chunk before
7
+ // hydrating β€” the lazy file-route then resolves synchronously and adopts the server DOM in
8
+ // place (no first-paint flash, no duplicate). It also enables auto-hydration of the
9
+ // server-rendered root.
10
+ startClient(App, { root: '#root', routes: appRoutes });
@@ -0,0 +1,17 @@
1
+ // SSR entry β€” @fluixi/start's dev/build/start load this. It calls `renderStream`
2
+ // (streaming: head flushed first, body streamed) when present, else `render` (string).
3
+ import App from './app.js';
4
+ import { renderRequestAsync, renderToBodyStream } from '@fluixi/server';
5
+
6
+ export function render(url: string): Promise<string> {
7
+ return renderRequestAsync(App, { url });
8
+ }
9
+
10
+ export function renderStream(url: string): ReadableStream<Uint8Array> {
11
+ return renderToBodyStream(App, { url });
12
+ }
13
+
14
+ // Server-function RPC dispatch β€” re-exported so the dispatcher shares the registry that
15
+ // this app's `"use server"` modules populate. @fluixi/start routes RPC calls here.
16
+ import 'virtual:fluixi-server-fns'; // registers every "use server" module (incl. lazy routes)
17
+ export { isServerFnRequest, handleServerFn } from '@fluixi/start/server-fn';
@@ -0,0 +1,7 @@
1
+ // Auto-generated by @fluixi/core fluixiRoutesPlugin β€” do not edit manually
2
+ declare module 'virtual:fluixi-routes' {
3
+ import type { RouteDefinition } from '@fluixi/core/router-next';
4
+ export const routes: RouteDefinition[];
5
+ }
6
+
7
+ declare module 'virtual:fluixi-server-fns';
@@ -0,0 +1,8 @@
1
+ export default function About() {
2
+ return (
3
+ <section class="about">
4
+ <h1>About</h1>
5
+ <p>A second file-route β€” proves URL-based SSR matching works.</p>
6
+ </section>
7
+ );
8
+ }
@@ -0,0 +1,25 @@
1
+ import { createResource } from '@fluixi/reactive/signal';
2
+ import { seo } from '@fluixi/start/head';
3
+
4
+ const tick = <T,>(v: T, ms = 30) => new Promise<T>((r) => setTimeout(() => r(v), ms));
5
+
6
+ // A data-gated route, like a real page: the resource is awaited during SSR so the
7
+ // content (not the spinner) is server-rendered, then hydrated.
8
+ export default function Home() {
9
+ const [data] = createResource(() => tick('Hello from Fluixi Start πŸ‘‹'));
10
+
11
+ // Per-route document head β€” rendered into <head> on the server (great for crawlers/links)
12
+ // and kept reactive on the client. The head engine is always on; seo() is optional, call it
13
+ // only where you want page metadata. Drop in og/twitter/canonical/jsonLd as your app grows.
14
+ seo({
15
+ title: 'Fluixi Start',
16
+ description: 'A server-rendered Fluixi app, scaffolded by the fluixi CLI.',
17
+ });
18
+
19
+ return (
20
+ <section class="home">
21
+ <h1>{data()}</h1>
22
+ <p>Server-rendered through the fluixi CLI, then hydrated.</p>
23
+ </section>
24
+ );
25
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "preserve",
7
+ "jsxImportSource": "@fluixi/jsx",
8
+ "strict": false,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["node", "vite/client"]
12
+ },
13
+ "include": ["src", "fluixi.config.ts"]
14
+ }