lumix-js 0.1.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.
- package/README.md +156 -0
- package/bin/lumix.js +63 -0
- package/dist/bind.d.ts +13 -0
- package/dist/bind.js +103 -0
- package/dist/cli/build.d.ts +1 -0
- package/dist/cli/build.js +69 -0
- package/dist/cli/dev.d.ts +1 -0
- package/dist/cli/dev.js +69 -0
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +199 -0
- package/dist/cli/loader.d.ts +9 -0
- package/dist/cli/loader.js +34 -0
- package/dist/cli/preview.d.ts +1 -0
- package/dist/cli/preview.js +39 -0
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.js +4 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +21 -0
- package/dist/control.d.ts +17 -0
- package/dist/control.js +47 -0
- package/dist/dom.d.ts +8 -0
- package/dist/dom.js +136 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/lifecycle.d.ts +16 -0
- package/dist/lifecycle.js +45 -0
- package/dist/signals.d.ts +21 -0
- package/dist/signals.js +144 -0
- package/dist/store.d.ts +37 -0
- package/dist/store.js +147 -0
- package/package.json +47 -0
- package/templates/blank/lumix.config.mjs +10 -0
- package/templates/blank/main.js +5 -0
- package/templates/blank/package.json +15 -0
- package/templates/blank/src/App.lumix +28 -0
- package/templates/blank-ts/lumix-env.d.ts +4 -0
- package/templates/blank-ts/lumix.config.mts +10 -0
- package/templates/blank-ts/main.ts +5 -0
- package/templates/blank-ts/package.json +17 -0
- package/templates/blank-ts/src/App.lumix +28 -0
- package/templates/blank-ts/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
The `lumix-js` package provides a modular, fine-grained reactive runtime. It is the core engine that powers state management and DOM updates, and it also contains the primary LumixJS CLI.
|
|
2
|
+
|
|
3
|
+
## CLI Core Commands
|
|
4
|
+
|
|
5
|
+
The `lumix` binary is the entry point for managing LumixJS projects.
|
|
6
|
+
|
|
7
|
+
### `lumix init`
|
|
8
|
+
|
|
9
|
+
Creates a new project from a template.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
lumix init <project-name>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### `lumix dev`
|
|
16
|
+
|
|
17
|
+
Starts the development server.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
lumix dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### `lumix build`
|
|
24
|
+
|
|
25
|
+
Builds the project for production, generating compiled assets.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
lumix build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Core Concepts
|
|
32
|
+
|
|
33
|
+
The runtime is built on the principle of fine-grained reactivity, using Signals and Effects to ensure that only the parts of the DOM that actually change are updated.
|
|
34
|
+
|
|
35
|
+
### Signals
|
|
36
|
+
|
|
37
|
+
Signals are the primary primitive for reactive state. A signal holds a value and notifies its subscribers when that value changes.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { signal } from "lumix-js";
|
|
41
|
+
|
|
42
|
+
const count = signal(0);
|
|
43
|
+
|
|
44
|
+
// Read value
|
|
45
|
+
console.log(count());
|
|
46
|
+
|
|
47
|
+
// Update value
|
|
48
|
+
count(count() + 1);
|
|
49
|
+
|
|
50
|
+
// Bulk update with .value
|
|
51
|
+
count.value = 10;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Effects
|
|
55
|
+
|
|
56
|
+
Effects are functions that automatically re-run whenever the signals they depend on change.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { signal, effect } from "lumix-js";
|
|
60
|
+
|
|
61
|
+
const name = signal("Lumin");
|
|
62
|
+
|
|
63
|
+
effect(() => {
|
|
64
|
+
console.log(`Hello, ${name()}!`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
name("World"); // Logs: "Hello, World!"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Computed Signals
|
|
71
|
+
|
|
72
|
+
Computed signals derive their value from other signals. They are automatically updated whenever their dependencies change.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { signal, computed } from "lumix-js";
|
|
76
|
+
|
|
77
|
+
const first = signal("John");
|
|
78
|
+
const last = signal("Doe");
|
|
79
|
+
|
|
80
|
+
const full = computed(() => `${first()} ${last()}`);
|
|
81
|
+
|
|
82
|
+
console.log(full()); // "John Doe"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Lifecycle Hooks
|
|
86
|
+
|
|
87
|
+
LumixJS provides hooks for managing component lifecycles.
|
|
88
|
+
|
|
89
|
+
- `onMount(fn)`: Executes when the component is mounted to the DOM.
|
|
90
|
+
- `onDestroy(fn)`: Executes when the component is unmounted.
|
|
91
|
+
|
|
92
|
+
## Global State Management (Store)
|
|
93
|
+
|
|
94
|
+
For complex state that needs to be shared across components or persisted, LumixJS provides a robust Store system.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { store, persist } from "lumix-js";
|
|
98
|
+
|
|
99
|
+
// Create a reactive store
|
|
100
|
+
const auth = store({
|
|
101
|
+
user: null,
|
|
102
|
+
isLoggedIn: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Update multiple values in a batch
|
|
106
|
+
auth.update({ user: "LuminUser", isLoggedIn: true });
|
|
107
|
+
|
|
108
|
+
// Reactively listen to all changes
|
|
109
|
+
auth.subscribe((state) => {
|
|
110
|
+
console.log("Auth changed:", state);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Persist automatically to localStorage
|
|
114
|
+
persist(auth, { key: "lumin_auth" });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Two-Way Binding
|
|
118
|
+
|
|
119
|
+
The `bind` helper simplifies the synchronization between UI elements and signals.
|
|
120
|
+
|
|
121
|
+
```svelte
|
|
122
|
+
<script>
|
|
123
|
+
import { signal, bind } from "lumix-js";
|
|
124
|
+
|
|
125
|
+
const email = signal("");
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<div class="field">
|
|
129
|
+
<label>Email</label>
|
|
130
|
+
<input type="email" bind:value={email} />
|
|
131
|
+
</div>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The runtime includes specific helpers for different input types:
|
|
135
|
+
|
|
136
|
+
- `bindValue`: For text inputs, textareas, and selects.
|
|
137
|
+
- `bindChecked`: For checkboxes.
|
|
138
|
+
- `bindNumeric`: For number and range inputs.
|
|
139
|
+
- `bindGroup`: For radio button groups.
|
|
140
|
+
- `bindSelected`: For multiple selects.
|
|
141
|
+
|
|
142
|
+
## Universal Rendering
|
|
143
|
+
|
|
144
|
+
The runtime includes primitives for both Server-Side Rendering (SSR) and Client-Side Hydration.
|
|
145
|
+
|
|
146
|
+
- `renderToString(component)`: Produces a static HTML string.
|
|
147
|
+
- `hydrate(root, component)`: Attaches event listeners and reactivity to existing HTML.
|
|
148
|
+
|
|
149
|
+
## Advanced Control
|
|
150
|
+
|
|
151
|
+
- `batch(fn)`: Batches multiple signal updates to trigger effects only once.
|
|
152
|
+
- `untrack(fn)`: Executes a function without tracking dependencies.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
package/bin/lumix.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { dev } from "../dist/cli/dev.js";
|
|
5
|
+
import { build } from "../dist/cli/build.js";
|
|
6
|
+
import { preview } from "../dist/cli/preview.js";
|
|
7
|
+
import { init } from "../dist/cli/init.js";
|
|
8
|
+
|
|
9
|
+
const VERSION = "0.1.0";
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name("lumix")
|
|
15
|
+
.description("CLI for the LumixJS framework")
|
|
16
|
+
.version(VERSION);
|
|
17
|
+
|
|
18
|
+
program.command("dev").description("Start the development server").action(dev);
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command("build")
|
|
22
|
+
.description("Build the project for production")
|
|
23
|
+
.action(build);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command("preview")
|
|
27
|
+
.description("Preview the production build locally")
|
|
28
|
+
.action(preview);
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command("init")
|
|
32
|
+
.description("Scaffold a new LuminJS project")
|
|
33
|
+
.argument("[name]", "Project name")
|
|
34
|
+
.option("-t, --template <template>", "Project template (blank, blank-ts)")
|
|
35
|
+
.action((name, options) => init(name, options));
|
|
36
|
+
|
|
37
|
+
// Custom help output
|
|
38
|
+
program.configureHelp({
|
|
39
|
+
formatHelp(cmd, helper) {
|
|
40
|
+
const title = `\n ${pc.bold(pc.cyan("⚡ LumixJS"))} ${pc.dim(`v${VERSION}`)}\n`;
|
|
41
|
+
const desc = ` ${pc.dim(cmd.description())}\n`;
|
|
42
|
+
|
|
43
|
+
const cmds = cmd.commands.map((c) => {
|
|
44
|
+
const name = c.name().padEnd(12);
|
|
45
|
+
return ` ${pc.green(name)} ${pc.dim(c.description())}`;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const cmdSection = `\n ${pc.bold("Commands:")}\n${cmds.join("\n")}\n`;
|
|
49
|
+
|
|
50
|
+
const opts = cmd.options.map((o) => {
|
|
51
|
+
const flags = o.flags.padEnd(18);
|
|
52
|
+
return ` ${pc.yellow(flags)} ${pc.dim(o.description)}`;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const optSection = `\n ${pc.bold("Options:")}\n${opts.join("\n")}\n`;
|
|
56
|
+
|
|
57
|
+
const usage = `\n ${pc.bold("Usage:")} ${pc.dim("lumix")} ${pc.white("<command>")} ${pc.dim("[options]")}\n`;
|
|
58
|
+
|
|
59
|
+
return title + desc + usage + cmdSection + optSection + "\n";
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
program.parse();
|
package/dist/bind.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Signal } from "./signals.js";
|
|
2
|
+
/** Two-way binding for text inputs, textareas, and selects */
|
|
3
|
+
export declare function bindValue(el: HTMLElement, sig: Signal<string>): void;
|
|
4
|
+
/** Two-way binding for checkboxes */
|
|
5
|
+
export declare function bindChecked(el: HTMLElement, sig: Signal<boolean>): void;
|
|
6
|
+
/** Two-way binding for number/range inputs */
|
|
7
|
+
export declare function bindNumeric(el: HTMLElement, sig: Signal<number>): void;
|
|
8
|
+
/** Two-way binding for radio button groups */
|
|
9
|
+
export declare function bindGroup(el: HTMLElement, sig: Signal<string>): void;
|
|
10
|
+
/** Two-way binding for `<select multiple>` */
|
|
11
|
+
export declare function bindSelected(el: HTMLElement, sig: Signal<string[]>): void;
|
|
12
|
+
/** Automatically detect input type and apply the right bind */
|
|
13
|
+
export declare function bind(el: HTMLElement, property: string, sig: Signal<any>): void;
|
package/dist/bind.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { effect } from "./signals.js";
|
|
2
|
+
// ─── Core bind helper ──────────────────────────────────────
|
|
3
|
+
function setupBind(el, sig, domProp, event, transform) {
|
|
4
|
+
// Signal → DOM
|
|
5
|
+
effect(() => {
|
|
6
|
+
const v = sig();
|
|
7
|
+
el[domProp] = v;
|
|
8
|
+
});
|
|
9
|
+
// DOM → Signal
|
|
10
|
+
el.addEventListener(event, () => {
|
|
11
|
+
const raw = el[domProp];
|
|
12
|
+
sig(transform ? transform(raw) : raw);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
// ─── bindValue ─────────────────────────────────────────────
|
|
16
|
+
/** Two-way binding for text inputs, textareas, and selects */
|
|
17
|
+
export function bindValue(el, sig) {
|
|
18
|
+
const tag = el.tagName.toLowerCase();
|
|
19
|
+
const event = tag === "select" ? "change" : "input";
|
|
20
|
+
setupBind(el, sig, "value", event);
|
|
21
|
+
}
|
|
22
|
+
// ─── bindChecked ───────────────────────────────────────────
|
|
23
|
+
/** Two-way binding for checkboxes */
|
|
24
|
+
export function bindChecked(el, sig) {
|
|
25
|
+
setupBind(el, sig, "checked", "change");
|
|
26
|
+
}
|
|
27
|
+
// ─── bindNumeric ───────────────────────────────────────────
|
|
28
|
+
/** Two-way binding for number/range inputs */
|
|
29
|
+
export function bindNumeric(el, sig) {
|
|
30
|
+
// Signal → DOM
|
|
31
|
+
effect(() => {
|
|
32
|
+
el.value = String(sig());
|
|
33
|
+
});
|
|
34
|
+
// DOM → Signal (using valueAsNumber)
|
|
35
|
+
el.addEventListener("input", () => {
|
|
36
|
+
const n = el.valueAsNumber;
|
|
37
|
+
if (!Number.isNaN(n))
|
|
38
|
+
sig(n);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// ─── bindGroup ─────────────────────────────────────────────
|
|
42
|
+
/** Two-way binding for radio button groups */
|
|
43
|
+
export function bindGroup(el, sig) {
|
|
44
|
+
// Signal → DOM
|
|
45
|
+
effect(() => {
|
|
46
|
+
el.checked = el.value === sig();
|
|
47
|
+
});
|
|
48
|
+
// DOM → Signal
|
|
49
|
+
el.addEventListener("change", () => {
|
|
50
|
+
if (el.checked) {
|
|
51
|
+
sig(el.value);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// ─── bindSelected ──────────────────────────────────────────
|
|
56
|
+
/** Two-way binding for `<select multiple>` */
|
|
57
|
+
export function bindSelected(el, sig) {
|
|
58
|
+
const select = el;
|
|
59
|
+
// Signal → DOM
|
|
60
|
+
effect(() => {
|
|
61
|
+
const selected = sig();
|
|
62
|
+
for (const opt of Array.from(select.options)) {
|
|
63
|
+
opt.selected = selected.includes(opt.value);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// DOM → Signal
|
|
67
|
+
select.addEventListener("change", () => {
|
|
68
|
+
const values = [];
|
|
69
|
+
for (const opt of Array.from(select.selectedOptions)) {
|
|
70
|
+
values.push(opt.value);
|
|
71
|
+
}
|
|
72
|
+
sig(values);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// ─── Auto-detect bind ──────────────────────────────────────
|
|
76
|
+
/** Automatically detect input type and apply the right bind */
|
|
77
|
+
export function bind(el, property, sig) {
|
|
78
|
+
const tag = el.tagName.toLowerCase();
|
|
79
|
+
const type = el.type?.toLowerCase() || "";
|
|
80
|
+
switch (property) {
|
|
81
|
+
case "value":
|
|
82
|
+
if (tag === "input" && (type === "number" || type === "range")) {
|
|
83
|
+
bindNumeric(el, sig);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
bindValue(el, sig);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case "checked":
|
|
90
|
+
bindChecked(el, sig);
|
|
91
|
+
break;
|
|
92
|
+
case "group":
|
|
93
|
+
bindGroup(el, sig);
|
|
94
|
+
break;
|
|
95
|
+
case "selected":
|
|
96
|
+
bindSelected(el, sig);
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
// Generic: treat as a property bind
|
|
100
|
+
setupBind(el, sig, property, "input");
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function build(): Promise<void>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { build as viteBuild } from "vite";
|
|
2
|
+
import lumix from "../../../vite-plugin-lumix/dist/index.js";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { loadConfig } from "./loader.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs-extra";
|
|
7
|
+
import { __dirname } from "./utils.js";
|
|
8
|
+
export async function build() {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const config = await loadConfig(cwd);
|
|
11
|
+
console.log("");
|
|
12
|
+
console.log(` ${pc.bold(pc.cyan("⚡ LumixJS"))} ${pc.dim("v0.1.0")}`);
|
|
13
|
+
console.log(pc.dim(" Building for production...\n"));
|
|
14
|
+
const indexPath = path.join(cwd, "index.html");
|
|
15
|
+
const hasIndex = fs.existsSync(indexPath);
|
|
16
|
+
let tempIndex = false;
|
|
17
|
+
try {
|
|
18
|
+
if (!hasIndex) {
|
|
19
|
+
tempIndex = true;
|
|
20
|
+
// Auto-detect entry: main.ts > main.js
|
|
21
|
+
const entry = fs.existsSync(path.join(cwd, "main.ts"))
|
|
22
|
+
? "/main.ts"
|
|
23
|
+
: "/main.js";
|
|
24
|
+
const defaultHtml = `
|
|
25
|
+
<!DOCTYPE html>
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="UTF-8">
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<div id="${config?.rootId || "app"}"></div>
|
|
33
|
+
<script type="module" src="${entry}"></script>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
36
|
+
`.trim();
|
|
37
|
+
fs.writeFileSync(indexPath, defaultHtml);
|
|
38
|
+
}
|
|
39
|
+
await viteBuild({
|
|
40
|
+
root: cwd,
|
|
41
|
+
base: "/",
|
|
42
|
+
plugins: [lumix(config), ...(config.vite?.plugins || [])],
|
|
43
|
+
build: {
|
|
44
|
+
outDir: "dist",
|
|
45
|
+
emptyOutDir: true,
|
|
46
|
+
...config.vite?.build,
|
|
47
|
+
},
|
|
48
|
+
resolve: {
|
|
49
|
+
alias: {
|
|
50
|
+
"lumix-js": path.resolve(__dirname, "../index.js"),
|
|
51
|
+
...config.vite?.resolve?.alias,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
...config.vite,
|
|
55
|
+
});
|
|
56
|
+
console.log(pc.green(pc.bold("\n Build completed successfully!")));
|
|
57
|
+
console.log(pc.dim(` Output directory: ${pc.white("./dist")}\n`));
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
console.error(pc.red(pc.bold("\n Build failed!")));
|
|
61
|
+
console.error(pc.red(` ${e.message}\n`));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
if (tempIndex && fs.existsSync(indexPath)) {
|
|
66
|
+
fs.removeSync(indexPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function dev(): Promise<void>;
|
package/dist/cli/dev.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createServer } from "vite";
|
|
2
|
+
import lumix from "../../../vite-plugin-lumix/dist/index.js";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { loadConfig, findConfigPath } from "./loader.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { __dirname } from "./utils.js";
|
|
8
|
+
async function startServer(cwd) {
|
|
9
|
+
const config = await loadConfig(cwd);
|
|
10
|
+
const server = await createServer({
|
|
11
|
+
root: cwd,
|
|
12
|
+
base: "/",
|
|
13
|
+
mode: "development",
|
|
14
|
+
plugins: [lumix(config), ...(config.vite?.plugins || [])],
|
|
15
|
+
server: {
|
|
16
|
+
port: 3000,
|
|
17
|
+
...config.vite?.server,
|
|
18
|
+
},
|
|
19
|
+
resolve: {
|
|
20
|
+
alias: {
|
|
21
|
+
"lumix-js": path.resolve(__dirname, "../index.js"),
|
|
22
|
+
...config.vite?.resolve?.alias,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
...config.vite,
|
|
26
|
+
});
|
|
27
|
+
await server.listen();
|
|
28
|
+
return server;
|
|
29
|
+
}
|
|
30
|
+
export async function dev() {
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
console.log("");
|
|
33
|
+
console.log(` ${pc.bold(pc.cyan("⚡ LumixJS"))} ${pc.dim("v0.1.0")}`);
|
|
34
|
+
console.log(pc.dim(" Starting development server...\n"));
|
|
35
|
+
try {
|
|
36
|
+
let server = await startServer(cwd);
|
|
37
|
+
const port = server.config.server.port;
|
|
38
|
+
console.log(` ${pc.green("➜")} ${pc.bold("Local:")} ${pc.cyan(`http://localhost:${port}/`)}`);
|
|
39
|
+
console.log(` ${pc.green("➜")} ${pc.dim("Ready in")} ${pc.white(Math.round(performance.now()))}ms\n`);
|
|
40
|
+
// Watch config file for changes
|
|
41
|
+
const configPath = findConfigPath(cwd);
|
|
42
|
+
if (configPath) {
|
|
43
|
+
let debounce = null;
|
|
44
|
+
fs.watch(configPath, () => {
|
|
45
|
+
if (debounce)
|
|
46
|
+
return;
|
|
47
|
+
debounce = setTimeout(async () => {
|
|
48
|
+
debounce = null;
|
|
49
|
+
console.log(`\n ${pc.yellow("↻")} ${pc.dim("Config changed, restarting...")}`);
|
|
50
|
+
try {
|
|
51
|
+
await server.close();
|
|
52
|
+
server = await startServer(cwd);
|
|
53
|
+
const newPort = server.config.server.port;
|
|
54
|
+
console.log(` ${pc.green("➜")} ${pc.bold("Local:")} ${pc.cyan(`http://localhost:${newPort}/`)}`);
|
|
55
|
+
console.log(` ${pc.green("➜")} ${pc.dim("Restarted successfully")}\n`);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error(pc.red(` ✗ Restart failed: ${e.message}\n`));
|
|
59
|
+
}
|
|
60
|
+
}, 300);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
console.error(pc.red(pc.bold("\n Failed to start server:")));
|
|
66
|
+
console.error(pc.red(` ${e.message}\n`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { __dirname } from "./utils.js";
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
7
|
+
function hasCommand(cmd) {
|
|
8
|
+
const which = process.platform === "win32" ? "where" : "which";
|
|
9
|
+
const res = spawnSync(which, [cmd], { stdio: "ignore" });
|
|
10
|
+
return res.status === 0;
|
|
11
|
+
}
|
|
12
|
+
function detectPackageManager() {
|
|
13
|
+
if (hasCommand("bun"))
|
|
14
|
+
return "bun";
|
|
15
|
+
if (hasCommand("pnpm"))
|
|
16
|
+
return "pnpm";
|
|
17
|
+
return "npm";
|
|
18
|
+
}
|
|
19
|
+
function getInstalledPackageManagers() {
|
|
20
|
+
const out = [];
|
|
21
|
+
if (hasCommand("bun"))
|
|
22
|
+
out.push("bun");
|
|
23
|
+
if (hasCommand("pnpm"))
|
|
24
|
+
out.push("pnpm");
|
|
25
|
+
out.push("npm");
|
|
26
|
+
return Array.from(new Set(out));
|
|
27
|
+
}
|
|
28
|
+
function runDevCommand(pm) {
|
|
29
|
+
if (pm === "npm")
|
|
30
|
+
return "npm run dev";
|
|
31
|
+
return `${pm} run dev`;
|
|
32
|
+
}
|
|
33
|
+
function installCommand(pm) {
|
|
34
|
+
if (pm === "npm")
|
|
35
|
+
return "npm install";
|
|
36
|
+
return `${pm} install`;
|
|
37
|
+
}
|
|
38
|
+
function runInstall(pm, cwd) {
|
|
39
|
+
const cmd = pm;
|
|
40
|
+
const args = ["install"];
|
|
41
|
+
const res = spawnSync(cmd, args, { cwd, stdio: "inherit" });
|
|
42
|
+
if (res.error)
|
|
43
|
+
throw res.error;
|
|
44
|
+
if (res.signal === "SIGINT") {
|
|
45
|
+
const err = new Error("install interrupted");
|
|
46
|
+
err.code = "SIGINT";
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
if (typeof res.status === "number" && res.status !== 0) {
|
|
50
|
+
throw new Error(`${cmd} install failed with exit code ${res.status}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function isEnoentSpawnError(e) {
|
|
54
|
+
return Boolean(e && typeof e === "object" && "code" in e && e.code === "ENOENT");
|
|
55
|
+
}
|
|
56
|
+
function isSigintError(e) {
|
|
57
|
+
return Boolean(e && typeof e === "object" && "code" in e && e.code === "SIGINT");
|
|
58
|
+
}
|
|
59
|
+
export async function init(name, options) {
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(` ${pc.bold(pc.cyan("⚡ LumixJS"))} ${pc.dim("v0.1.0")}`);
|
|
62
|
+
console.log(pc.dim(" Scaffolding a new project...\n"));
|
|
63
|
+
const templates = [
|
|
64
|
+
{ title: "Blank", value: "blank" },
|
|
65
|
+
{ title: "Blank (TypeScript)", value: "blank-ts" },
|
|
66
|
+
{ title: "Sitemap (Coming soon...)", value: "sitemap", disabled: true },
|
|
67
|
+
];
|
|
68
|
+
const allowedTemplates = new Set(templates
|
|
69
|
+
.filter((t) => !t.disabled)
|
|
70
|
+
.map((t) => t.value));
|
|
71
|
+
let template = options?.template;
|
|
72
|
+
if (template && !allowedTemplates.has(template)) {
|
|
73
|
+
console.log(pc.red(`\n Error: Unknown template "${template}". Valid templates are: ${[...allowedTemplates].join(", ")}.\n`));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const questions = [];
|
|
77
|
+
if (!name) {
|
|
78
|
+
questions.push({
|
|
79
|
+
type: "text",
|
|
80
|
+
name: "projectName",
|
|
81
|
+
message: "Project name:",
|
|
82
|
+
initial: "my-lumin-app",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!template) {
|
|
86
|
+
questions.push({
|
|
87
|
+
type: "select",
|
|
88
|
+
name: "template",
|
|
89
|
+
message: "Pick a template:",
|
|
90
|
+
choices: templates,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const detectedPm = detectPackageManager();
|
|
94
|
+
const installedPms = getInstalledPackageManagers();
|
|
95
|
+
questions.push({
|
|
96
|
+
type: "toggle",
|
|
97
|
+
name: "install",
|
|
98
|
+
message: "Install dependencies?",
|
|
99
|
+
initial: true,
|
|
100
|
+
active: "yes",
|
|
101
|
+
inactive: "no",
|
|
102
|
+
});
|
|
103
|
+
questions.push({
|
|
104
|
+
type: (prev) => (prev ? "select" : null),
|
|
105
|
+
name: "packageManager",
|
|
106
|
+
message: "Package manager:",
|
|
107
|
+
initial: Math.max(0, installedPms.indexOf(detectedPm)),
|
|
108
|
+
choices: installedPms.map((pm) => ({ title: pm, value: pm })),
|
|
109
|
+
});
|
|
110
|
+
const response = questions.length > 0
|
|
111
|
+
? await prompts(questions, {
|
|
112
|
+
onCancel() {
|
|
113
|
+
console.log(pc.red("\n Aborted.\n"));
|
|
114
|
+
process.exit(0);
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
: {};
|
|
118
|
+
const projectName = name || response.projectName;
|
|
119
|
+
template = template || response.template;
|
|
120
|
+
const shouldInstall = response.install !== false;
|
|
121
|
+
const selected = (response.packageManager || detectedPm);
|
|
122
|
+
let packageManager = selected;
|
|
123
|
+
if (!projectName || !template) {
|
|
124
|
+
console.log(pc.red("\n Aborted.\n"));
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
128
|
+
if (fs.existsSync(targetDir)) {
|
|
129
|
+
console.log(pc.red(`\n Error: Directory "${projectName}" already exists.\n`));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const templateDir = path.resolve(__dirname, "../../templates", template);
|
|
133
|
+
console.log(pc.dim("\n Scaffolding project..."));
|
|
134
|
+
try {
|
|
135
|
+
await fs.ensureDir(targetDir);
|
|
136
|
+
await fs.copy(templateDir, targetDir);
|
|
137
|
+
// Update package.json name
|
|
138
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
139
|
+
if (fs.existsSync(pkgPath)) {
|
|
140
|
+
const pkg = await fs.readJson(pkgPath);
|
|
141
|
+
pkg.name = projectName;
|
|
142
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
143
|
+
}
|
|
144
|
+
console.log(pc.green(pc.bold("\n Success! Project created.")));
|
|
145
|
+
if (shouldInstall) {
|
|
146
|
+
console.log(pc.dim(`\n Installing dependencies (${packageManager})...`));
|
|
147
|
+
try {
|
|
148
|
+
runInstall(packageManager, targetDir);
|
|
149
|
+
console.log(pc.green(pc.bold("\n Dependencies installed.")));
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
if (isSigintError(e)) {
|
|
153
|
+
console.log(pc.red("\n Aborted.\n"));
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
if (isEnoentSpawnError(e)) {
|
|
157
|
+
const fallback = detectedPm;
|
|
158
|
+
if (fallback !== packageManager && hasCommand(fallback)) {
|
|
159
|
+
console.log(pc.yellow(`\n ${packageManager} not found. Retrying with ${fallback}...`));
|
|
160
|
+
try {
|
|
161
|
+
packageManager = fallback;
|
|
162
|
+
runInstall(packageManager, targetDir);
|
|
163
|
+
console.log(pc.green(pc.bold("\n Dependencies installed.")));
|
|
164
|
+
}
|
|
165
|
+
catch (e2) {
|
|
166
|
+
if (isSigintError(e2)) {
|
|
167
|
+
console.log(pc.red("\n Aborted.\n"));
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
console.log(pc.red(`\n Failed to install dependencies: ${e2?.message || e2}`));
|
|
171
|
+
console.log(pc.dim(" Project created at:"));
|
|
172
|
+
console.log(` ${targetDir}\n`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.log(pc.red(`\n Failed to install dependencies: ${e?.message || e}`));
|
|
177
|
+
console.log(pc.dim(" Project created at:"));
|
|
178
|
+
console.log(` ${targetDir}\n`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log(pc.red(`\n Failed to install dependencies: ${e?.message || e}`));
|
|
183
|
+
console.log(pc.dim(" Project created at:"));
|
|
184
|
+
console.log(` ${targetDir}\n`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.log(pc.dim("\n Next steps:"));
|
|
189
|
+
console.log(` cd ${projectName}`);
|
|
190
|
+
if (!shouldInstall) {
|
|
191
|
+
console.log(` ${installCommand(packageManager)}`);
|
|
192
|
+
}
|
|
193
|
+
console.log(` ${runDevCommand(packageManager)}\n`);
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
console.error(pc.red(`\n Failed to initialize project: ${e.message}\n`));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|