lucynui 0.1.1 → 0.1.3

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/cli/create.ts CHANGED
@@ -1,18 +1,23 @@
1
1
  import { closeSync, openSync } from "node:fs";
2
- import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
2
+ import { cp } from "node:fs/promises";
4
3
  import { basename, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
5
  import * as proc from "./proc";
6
6
  import * as ui from "./ui";
7
7
 
8
8
  /** Matches the line the framework logs once the server is listening. */
9
9
  const SERVER_READY = /dev server running on http:\/\/localhost:(\d+)/;
10
10
 
11
- /** The starter files, mirroring the template repo. */
11
+ /** Files copied from the bundled template into a new project. */
12
12
  const TEMPLATE_FILES = ["index.html", "style.css", "app.js", "app.ts", "lucyn.config.ts", "tsconfig.json", ".gitignore"];
13
13
 
14
14
  const bun = () => proc.findBun() ?? "bun";
15
15
 
16
+ /** Absolute path to the bundled template directory inside this package. */
17
+ function templateDir(): string {
18
+ return fileURLToPath(new URL("../template", import.meta.url));
19
+ }
20
+
16
21
  /* ------------------------------------------------------------------ commands */
17
22
 
18
23
  export async function dev(): Promise<never> {
@@ -37,13 +42,11 @@ export async function create(appName: string): Promise<never> {
37
42
 
38
43
  const bunPath = await proc.ensureBun();
39
44
  await proc.ensureSystemDeps();
40
- proc.requireGit();
41
45
 
42
- ui.step(1, "cloning the template, hang on");
43
- await proc.runStep(process.cwd(), ["git", "clone", "--depth", "1", proc.TEMPLATE_REPO, appName]);
44
- await rm(join(appName, ".git"), { recursive: true, force: true });
46
+ ui.step(1, "scaffolding the template, hang on");
47
+ await scaffoldInto(join(process.cwd(), appName));
45
48
  await pinFramework(appName);
46
- ui.done("cloning complete — fetching the deps and required builds");
49
+ ui.done("scaffolding complete — fetching the deps and required builds");
47
50
 
48
51
  ui.step(2, "installing modules including lucynui");
49
52
  await proc.runStep(appName, [bunPath, "install"]);
@@ -66,14 +69,13 @@ export async function create(appName: string): Promise<never> {
66
69
  export async function init(): Promise<never> {
67
70
  const bunPath = await proc.ensureBun();
68
71
  await proc.ensureSystemDeps();
69
- proc.requireGit();
70
72
 
71
73
  if (await isProject(process.cwd())) {
72
74
  ui.info("This is already a LucyN UI app — starting dev.");
73
75
  } else {
74
- ui.step(1, "cloning the template, hang on");
76
+ ui.step(1, "scaffolding the template, hang on");
75
77
  await scaffoldInto(process.cwd());
76
- ui.done("cloning complete — fetching the deps and required builds");
78
+ ui.done("scaffolding complete — fetching the deps and required builds");
77
79
 
78
80
  ui.step(2, "installing modules including lucynui");
79
81
  await proc.runStep(process.cwd(), [bunPath, "install"]);
@@ -101,38 +103,38 @@ export function isProject(dir: string) {
101
103
 
102
104
  /** Runs a build and throws away the binary — the point is proving it compiles. */
103
105
  export async function testBuild(dir: string) {
106
+ const { rm } = await import("node:fs/promises");
104
107
  const code = await proc.run(dir, [bun(), proc.frameworkPath("build.ts")]);
105
108
  await rm(join(dir, "dist"), { recursive: true, force: true });
106
109
 
107
110
  if (code !== 0) throw new Error("the test build failed");
108
111
  }
109
112
 
110
- /** Copies the starter files into an existing folder, never overwriting anything. */
113
+ /**
114
+ * Copies the bundled template files into `dir`, never overwriting existing
115
+ * files — except `package.json`: `bun add` creates a bare manifest (no scripts)
116
+ * before postinstall runs, so we always replace it with the full template version.
117
+ */
111
118
  export async function scaffoldInto(dir: string) {
112
- const source = await fetchTemplate();
113
-
114
- try {
115
- for (const file of TEMPLATE_FILES) {
116
- const target = join(dir, file);
117
- if (await Bun.file(target).exists()) continue;
118
- await cp(join(source, file), target);
119
- }
119
+ const { mkdir } = await import("node:fs/promises");
120
+ await mkdir(dir, { recursive: true });
120
121
 
121
- const manifest = join(dir, "package.json");
122
- if (!(await Bun.file(manifest).exists())) {
123
- await cp(join(source, "package.json"), manifest);
124
- }
122
+ const source = templateDir();
125
123
 
126
- await patchManifest(manifest);
127
- } finally {
128
- await rm(source, { recursive: true, force: true });
124
+ for (const file of TEMPLATE_FILES) {
125
+ const target = join(dir, file);
126
+ if (await Bun.file(target).exists()) continue;
127
+ await cp(join(source, file), target);
129
128
  }
129
+
130
+ // Always write the template package.json — the bare one bun creates before
131
+ // postinstall (no scripts, no name, no type) is useless and must be replaced.
132
+ await cp(join(source, "package.json"), join(dir, "package.json"));
130
133
  }
131
134
 
132
135
  /** Points the app at this CLI's own version, so template drift can never bite. */
133
136
  async function pinFramework(dir: string) {
134
- const manifestPath = join(dir, "package.json");
135
- const manifest = Bun.file(manifestPath);
137
+ const manifest = Bun.file(join(dir, "package.json"));
136
138
  if (!(await manifest.exists())) return;
137
139
 
138
140
  // Running from a checkout rather than an install? Point the app at this copy
@@ -144,43 +146,6 @@ async function pinFramework(dir: string) {
144
146
  const pkg = await manifest.json();
145
147
  pkg.dependencies = { ...(pkg.dependencies ?? {}), lucynui: spec };
146
148
  await Bun.write(manifest, JSON.stringify(pkg, null, 2) + "\n");
147
-
148
- await patchManifest(manifestPath);
149
- }
150
-
151
- /**
152
- * Ensures the scaffolded package.json has the scripts and trustedDependencies
153
- * that users expect — the template repo ships a minimal manifest intentionally,
154
- * so we inject these here rather than keeping them in sync in two repos.
155
- */
156
- async function patchManifest(manifestPath: string) {
157
- const file = Bun.file(manifestPath);
158
- if (!(await file.exists())) return;
159
-
160
- const pkg = await file.json();
161
-
162
- pkg.scripts = {
163
- dev: "lucyn dev",
164
- build: "lucyn build",
165
- ...(pkg.scripts ?? {}),
166
- };
167
-
168
- const trusted: string[] = pkg.trustedDependencies ?? [];
169
- if (!trusted.includes("lucynui")) trusted.push("lucynui");
170
- pkg.trustedDependencies = trusted;
171
-
172
- await Bun.write(file, JSON.stringify(pkg, null, 2) + "\n");
173
- }
174
-
175
- async function fetchTemplate() {
176
- const tmp = await mkdtemp(join(tmpdir(), "lucyn-template-"));
177
- await mkdir(tmp, { recursive: true });
178
- const checkout = join(tmp, "template");
179
-
180
- await proc.runStep(tmp, ["git", "clone", "--depth", "1", proc.TEMPLATE_REPO, checkout]);
181
- await rm(join(checkout, ".git"), { recursive: true, force: true });
182
-
183
- return checkout;
184
149
  }
185
150
 
186
151
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucynui",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Desktop apps from plain HTML, CSS and JS. No React, no Rust, no Electron.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,7 @@
13
13
  "bin/",
14
14
  "cli/",
15
15
  "src/",
16
+ "template/",
16
17
  "webview-bun/src/",
17
18
  "webview-bun/build/"
18
19
  ],
@@ -35,7 +36,8 @@
35
36
  "javascript"
36
37
  ],
37
38
  "dependencies": {
38
- "elysia": "^1.4.30"
39
+ "elysia": "^1.4.30",
40
+ "@sinclair/typebox": "^0.34.0"
39
41
  },
40
42
  "devDependencies": {
41
43
  "@types/bun": "latest"
@@ -0,0 +1,6 @@
1
+ const button = document.getElementById("ping");
2
+ const message = document.getElementById("message");
3
+
4
+ button?.addEventListener("click", () => {
5
+ if (message) message.textContent = `Hello from LucyN UI — ${new Date().toLocaleTimeString()}`;
6
+ });
@@ -0,0 +1,4 @@
1
+ import { createApp } from "lucynui";
2
+
3
+ // Entry point for `lucyn dev` and `lucyn build`.
4
+ createApp();
@@ -0,0 +1,20 @@
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>LucyN App</title>
7
+ <link rel="stylesheet" href="/style.css" />
8
+ </head>
9
+ <body>
10
+ <main class="app">
11
+ <p class="brand">LucyN UI</p>
12
+ <h1>Your desktop app starts here</h1>
13
+ <p id="message">
14
+ Edit <code>index.html</code>, <code>style.css</code> or <code>app.js</code> and the window reloads by itself.
15
+ </p>
16
+ <button id="ping">Say hello</button>
17
+ </main>
18
+ <script src="/app.js"></script>
19
+ </body>
20
+ </html>
@@ -0,0 +1,8 @@
1
+ // LucyN UI config — every field is optional.
2
+ export default {
3
+ title: "LucyN App",
4
+ width: 1024,
5
+ height: 768,
6
+ port: 3927,
7
+ dev: false,
8
+ };
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "lucyn-app",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "lucyn dev",
7
+ "build": "lucyn build"
8
+ },
9
+ "dependencies": {
10
+ "lucynui": "^0.1.0"
11
+ },
12
+ "trustedDependencies": ["lucynui"],
13
+ "devDependencies": {
14
+ "@types/bun": "latest"
15
+ }
16
+ }
@@ -0,0 +1,66 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ }
4
+
5
+ * {
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ body {
10
+ margin: 0;
11
+ min-height: 100vh;
12
+ display: grid;
13
+ place-items: center;
14
+ background: radial-gradient(circle at 50% 0%, #1b2133, #0d0f14 60%);
15
+ color: #e8eaf0;
16
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
17
+ }
18
+
19
+ .app {
20
+ max-width: 40rem;
21
+ padding: 3rem;
22
+ text-align: center;
23
+ }
24
+
25
+ .brand {
26
+ margin: 0 0 1rem;
27
+ font-size: 0.75rem;
28
+ letter-spacing: 0.22em;
29
+ text-transform: uppercase;
30
+ color: #e0b34d;
31
+ }
32
+
33
+ h1 {
34
+ margin: 0 0 1rem;
35
+ font-size: 2rem;
36
+ }
37
+
38
+ p {
39
+ line-height: 1.7;
40
+ color: #a3abbf;
41
+ }
42
+
43
+ code {
44
+ padding: 0.15rem 0.4rem;
45
+ border-radius: 5px;
46
+ background: #1d2231;
47
+ color: #e8eaf0;
48
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
49
+ font-size: 0.9em;
50
+ }
51
+
52
+ button {
53
+ margin-top: 1rem;
54
+ padding: 0.7rem 1.4rem;
55
+ border: 1px solid #2c3347;
56
+ border-radius: 10px;
57
+ background: #e0b34d;
58
+ color: #14181f;
59
+ font: inherit;
60
+ font-weight: 600;
61
+ cursor: pointer;
62
+ }
63
+
64
+ button:hover {
65
+ background: #f0c465;
66
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+ "types": ["bun"],
11
+
12
+ // Bundler mode
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "verbatimModuleSyntax": true,
16
+ "noEmit": true,
17
+
18
+ // Best practices
19
+ "strict": true,
20
+ "skipLibCheck": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "noImplicitOverride": true,
24
+
25
+ // Some stricter flags (disabled by default)
26
+ "noUnusedLocals": false,
27
+ "noUnusedParameters": false,
28
+ "noPropertyAccessFromIndexSignature": false
29
+ }
30
+ }