@anaemia/cli 0.3.1 → 0.3.2

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.
@@ -3,10 +3,10 @@ import path from "path";
3
3
  import fs from "fs";
4
4
  import pc from "picocolors";
5
5
  import { generateSharedComponent, scaffoldFeature, scaffoldHook, scaffoldPage } from "../scaffold.js";
6
- import { execSync } from "node:child_process";
7
6
  import prompts from "prompts";
8
7
  import { transform } from "sucrase";
9
8
  import { fileURLToPath } from "node:url";
9
+ import { fetchTemplate } from "../utils/fetch-template.js";
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
12
12
  export function register(cli) {
@@ -103,25 +103,8 @@ export function register(cli) {
103
103
  });
104
104
  }
105
105
  else {
106
- logger.warn("local templates missing. fetching remote registry packages over the network...");
107
- const userAgent = process.env.npm_config_user_agent || "";
108
- let packageManager = "npm";
109
- if (userAgent.includes("pnpm"))
110
- packageManager = "pnpm";
111
- else if (userAgent.includes("yarn"))
112
- packageManager = "yarn";
113
- try {
114
- if (packageManager === "pnpm") {
115
- execSync(`pnpm dlx degit colourlabs/anaemia/templates/base-app "${targetPath}"`, { stdio: "ignore" });
116
- }
117
- else {
118
- execSync(`npx degit colourlabs/anaemia/templates/base-app "${targetPath}"`, { stdio: "ignore" });
119
- }
120
- }
121
- catch (err) {
122
- logger.error("could not source template workspace assets locally or from network registry nodes. " + err);
123
- process.exit(1);
124
- }
106
+ logger.info("fetching template from remote registry...");
107
+ await fetchTemplate(targetPath);
125
108
  }
126
109
  const removeGitKeepFiles = (dir) => {
127
110
  const files = fs.readdirSync(dir);
@@ -203,4 +186,3 @@ export function register(cli) {
203
186
  console.log(` ${pc.cyan("pnpm dev")} ${pc.dim("# launches hot reload server")}\n`);
204
187
  });
205
188
  }
206
- ;
@@ -7,4 +7,3 @@ export function register(cli) {
7
7
  process.exit(result.status ?? 1);
8
8
  });
9
9
  }
10
- ;
@@ -0,0 +1,30 @@
1
+ import * as tar from "tar";
2
+ import logger from "./logger.js";
3
+ const TAR_URL = "https://codeload.github.com/colourlabs/anaemia/tar.gz/main";
4
+ export async function fetchTemplate(targetPath) {
5
+ logger.info("downloading template...");
6
+ const res = await fetch(TAR_URL);
7
+ if (!res.ok)
8
+ throw new Error(`failed to download template: ${res.statusText}`);
9
+ await new Promise((resolve, reject) => {
10
+ const extract = tar.extract({
11
+ cwd: targetPath,
12
+ strip: 2,
13
+ filter: (p) => p.startsWith("anaemia-main/templates/base-app"),
14
+ });
15
+ extract.on("finish", resolve);
16
+ extract.on("error", reject);
17
+ const reader = res.body.getReader();
18
+ const pump = async () => {
19
+ while (true) {
20
+ const { done, value } = await reader.read();
21
+ if (done) {
22
+ extract.end();
23
+ break;
24
+ }
25
+ extract.write(value);
26
+ }
27
+ };
28
+ pump().catch(reject);
29
+ });
30
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@anaemia/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "anaemia": "./dist/index.js"
7
7
  },
8
8
  "dependencies": {
9
- "@anaemia/bundler": "^0.3.1",
10
- "@anaemia/core": "^0.3.1",
9
+ "@anaemia/bundler": "^0.3.2",
10
+ "@anaemia/core": "^0.3.2",
11
11
  "@rspack/core": "^2.0.5",
12
12
  "@rspack/dev-server": "2.0.1",
13
13
  "cac": "^7.0.0",
@@ -16,6 +16,7 @@
16
16
  "picocolors": "^1.1.1",
17
17
  "prompts": "^2.4.2",
18
18
  "sucrase": "^3.35.1",
19
+ "tar": "^7.5.15",
19
20
  "ws": "^8.21.0"
20
21
  },
21
22
  "devDependencies": {
@@ -4,230 +4,215 @@ import path from "path";
4
4
  import fs from "fs";
5
5
  import pc from "picocolors";
6
6
  import { generateSharedComponent, scaffoldFeature, scaffoldHook, scaffoldPage } from "../scaffold.js";
7
- import { execSync } from "node:child_process";
8
7
  import prompts from "prompts";
9
8
  import { transform } from "sucrase";
10
9
  import { fileURLToPath } from "node:url";
10
+ import { fetchTemplate } from "../utils/fetch-template.js";
11
11
 
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
14
14
 
15
15
  export function register(cli: CAC) {
16
16
  cli
17
- .command("create [target]", "initialize an application or generate domain features (e.g., feature:name)")
18
- .alias("init")
19
- .action(async (target) => {
20
- const appRoot = process.cwd();
17
+ .command("create [target]", "initialize an application or generate domain features (e.g., feature:name)")
18
+ .alias("init")
19
+ .action(async (target) => {
20
+ const appRoot = process.cwd();
21
21
 
22
- if (target && target.includes(":")) {
23
- const [type, name] = target.split(":");
22
+ if (target && target.includes(":")) {
23
+ const [type, name] = target.split(":");
24
24
 
25
- if (!name) {
26
- logger.error(`missing name modifier. Use layout template like: ${pc.cyan(`create ${type}:your-name`)}`);
27
- process.exit(1);
28
- }
25
+ if (!name) {
26
+ logger.error(`missing name modifier. Use layout template like: ${pc.cyan(`create ${type}:your-name`)}`);
27
+ process.exit(1);
28
+ }
29
29
 
30
- if (!fs.existsSync(path.join(appRoot, "package.json"))) {
31
- logger.error("no package.json detected. code generation commands must run inside an Anaemia project root.");
32
- process.exit(1);
33
- }
30
+ if (!fs.existsSync(path.join(appRoot, "package.json"))) {
31
+ logger.error("no package.json detected. code generation commands must run inside an Anaemia project root.");
32
+ process.exit(1);
33
+ }
34
34
 
35
- const normalizedName = name.trim();
35
+ const normalizedName = name.trim();
36
36
 
37
- if (type === "feature") {
38
- scaffoldFeature(normalizedName, appRoot);
39
- return;
40
- }
37
+ if (type === "feature") {
38
+ scaffoldFeature(normalizedName, appRoot);
39
+ return;
40
+ }
41
41
 
42
- if (type === "component") {
43
- generateSharedComponent(appRoot, normalizedName, { logger, pc });
44
- return;
45
- }
42
+ if (type === "component") {
43
+ generateSharedComponent(appRoot, normalizedName, { logger, pc });
44
+ return;
45
+ }
46
+
47
+ if (type === "page") {
48
+ scaffoldPage(normalizedName, appRoot);
49
+ return;
50
+ }
51
+
52
+ if (type === "hook") {
53
+ scaffoldHook(normalizedName, appRoot);
54
+ return;
55
+ }
46
56
 
47
- if (type === "page") {
48
- scaffoldPage(normalizedName, appRoot);
49
- return;
57
+ logger.error(`unknown layout generator type "${type}". Supported variants: "feature:", "component:", "page:", "hook:"`);
58
+ process.exit(1);
50
59
  }
51
60
 
52
- if (type === "hook") {
53
- scaffoldHook(normalizedName, appRoot);
54
- return;
61
+ logger.compiler("launching Anaemia project initialization wizard...");
62
+
63
+ const response = await prompts([
64
+ {
65
+ type: target ? null : "text",
66
+ name: "projectName",
67
+ message: "Project name:",
68
+ initial: "anaemia-app",
69
+ },
70
+ {
71
+ type: "select",
72
+ name: "variant",
73
+ message: "Select a variant:",
74
+ choices: [
75
+ { title: pc.blue("TypeScript (Recommended)"), value: "ts" },
76
+ { title: pc.yellow("JavaScript"), value: "js" },
77
+ ],
78
+ initial: 0,
79
+ },
80
+ ]);
81
+
82
+ if (!response.variant && response.variant !== 0) {
83
+ logger.warn("project creation aborted.");
84
+ process.exit(0);
55
85
  }
56
86
 
57
- logger.error(`unknown layout generator type "${type}". Supported variants: "feature:", "component:", "page:", "hook:"`);
58
- process.exit(1);
59
- }
60
-
61
- logger.compiler("launching Anaemia project initialization wizard...");
62
-
63
- const response = await prompts([
64
- {
65
- type: target ? null : "text",
66
- name: "projectName",
67
- message: "Project name:",
68
- initial: "anaemia-app",
69
- },
70
- {
71
- type: "select",
72
- name: "variant",
73
- message: "Select a variant:",
74
- choices: [
75
- { title: pc.blue("TypeScript (Recommended)"), value: "ts" },
76
- { title: pc.yellow("JavaScript"), value: "js" },
77
- ],
78
- initial: 0,
79
- },
80
- ]);
81
-
82
- if (!response.variant && response.variant !== 0) {
83
- logger.warn("project creation aborted.");
84
- process.exit(0);
85
- }
86
-
87
- const targetDir = target || response.projectName;
88
- const targetPath = path.resolve(appRoot, targetDir);
89
-
90
- if (fs.existsSync(targetPath)) {
91
- const files = fs.readdirSync(targetPath);
92
- if (files.length > 0) {
93
- const { overwrite } = await prompts({
94
- type: "confirm",
95
- name: "overwrite",
96
- message: `target directory "${targetDir}" is not empty. remove existing files and continue?`,
97
- initial: false,
98
- });
87
+ const targetDir = target || response.projectName;
88
+ const targetPath = path.resolve(appRoot, targetDir);
89
+
90
+ if (fs.existsSync(targetPath)) {
91
+ const files = fs.readdirSync(targetPath);
92
+ if (files.length > 0) {
93
+ const { overwrite } = await prompts({
94
+ type: "confirm",
95
+ name: "overwrite",
96
+ message: `target directory "${targetDir}" is not empty. remove existing files and continue?`,
97
+ initial: false,
98
+ });
99
+
100
+ if (!overwrite) {
101
+ logger.error("aborted to protect existing project directory.");
102
+ process.exit(1);
103
+ }
99
104
 
100
- if (!overwrite) {
101
- logger.error("aborted to protect existing project directory.");
102
- process.exit(1);
105
+ logger.warn(`purging existing files inside ${targetDir}...`);
106
+ fs.rmSync(targetPath, { recursive: true, force: true });
107
+ fs.mkdirSync(targetPath, { recursive: true });
103
108
  }
104
-
105
- logger.warn(`purging existing files inside ${targetDir}...`);
106
- fs.rmSync(targetPath, { recursive: true, force: true });
109
+ } else {
107
110
  fs.mkdirSync(targetPath, { recursive: true });
108
111
  }
109
- } else {
110
- fs.mkdirSync(targetPath, { recursive: true });
111
- }
112
-
113
- let templatePath = path.resolve(__dirname, "../templates/template-base");
114
- if (!fs.existsSync(templatePath)) {
115
- templatePath = path.resolve(__dirname, "../templates/base-app");
116
- }
117
-
118
- if (fs.existsSync(templatePath)) {
119
- logger.info("unpacking localized scaffolding architecture layout structures...");
120
- fs.cpSync(templatePath, targetPath, {
121
- recursive: true,
122
- filter: (src) => !["node_modules", "dist", ".anaemia", ".rspack"].includes(path.basename(src)),
123
- });
124
- } else {
125
- logger.warn("local templates missing. fetching remote registry packages over the network...");
126
- const userAgent = process.env.npm_config_user_agent || "";
127
- let packageManager = "npm";
128
- if (userAgent.includes("pnpm")) packageManager = "pnpm";
129
- else if (userAgent.includes("yarn")) packageManager = "yarn";
130
-
131
- try {
132
- if (packageManager === "pnpm") {
133
- execSync(`pnpm dlx degit colourlabs/anaemia/templates/base-app "${targetPath}"`, { stdio: "ignore" });
134
- } else {
135
- execSync(`npx degit colourlabs/anaemia/templates/base-app "${targetPath}"`, { stdio: "ignore" });
136
- }
137
- } catch (err) {
138
- logger.error("could not source template workspace assets locally or from network registry nodes. " + err);
139
- process.exit(1);
140
- }
141
- }
142
-
143
- const removeGitKeepFiles = (dir: string) => {
144
- const files = fs.readdirSync(dir);
145
- for (const file of files) {
146
- const fullPath = path.join(dir, file);
147
- if (fs.statSync(fullPath).isDirectory()) {
148
- removeGitKeepFiles(fullPath);
149
- } else if (file === ".gitkeep") {
150
- fs.unlinkSync(fullPath);
151
- }
112
+
113
+ let templatePath = path.resolve(__dirname, "../templates/template-base");
114
+ if (!fs.existsSync(templatePath)) {
115
+ templatePath = path.resolve(__dirname, "../templates/base-app");
152
116
  }
153
- };
154
- removeGitKeepFiles(targetPath);
155
117
 
156
- if (response.variant === "js") {
157
- logger.info("converting workspace assets to vanilla JavaScript...");
118
+ if (fs.existsSync(templatePath)) {
119
+ logger.info("unpacking localized scaffolding architecture layout structures...");
120
+ fs.cpSync(templatePath, targetPath, {
121
+ recursive: true,
122
+ filter: (src) => !["node_modules", "dist", ".anaemia", ".rspack"].includes(path.basename(src)),
123
+ });
124
+ } else {
125
+ logger.info("fetching template from remote registry...");
126
+ await fetchTemplate(targetPath);
127
+ }
158
128
 
159
- const convertTypeScriptToJs = (dir: string) => {
129
+ const removeGitKeepFiles = (dir: string) => {
160
130
  const files = fs.readdirSync(dir);
161
-
162
131
  for (const file of files) {
163
132
  const fullPath = path.join(dir, file);
164
- const stat = fs.statSync(fullPath);
165
-
166
- if (stat.isDirectory()) {
167
- convertTypeScriptToJs(fullPath);
168
- } else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
169
- const isTsx = file.endsWith(".tsx");
170
- const code = fs.readFileSync(fullPath, "utf8");
171
-
172
- try {
173
- const compiled = transform(code, {
174
- transforms: isTsx ? ["typescript", "jsx"] : ["typescript"],
175
- jsxRuntime: "preserve",
176
- production: true,
177
- });
178
-
179
- const newExt = isTsx ? ".jsx" : ".js";
180
- const newPath = fullPath.replace(/\.tsx?$/, newExt);
181
-
182
- fs.writeFileSync(newPath, compiled.code, "utf8");
183
- fs.unlinkSync(fullPath);
184
- } catch {
185
- logger.warn(`failed to strip types from ${file}, skipping...`);
186
- }
133
+ if (fs.statSync(fullPath).isDirectory()) {
134
+ removeGitKeepFiles(fullPath);
135
+ } else if (file === ".gitkeep") {
136
+ fs.unlinkSync(fullPath);
187
137
  }
188
138
  }
189
139
  };
140
+ removeGitKeepFiles(targetPath);
141
+
142
+ if (response.variant === "js") {
143
+ logger.info("converting workspace assets to vanilla JavaScript...");
144
+
145
+ const convertTypeScriptToJs = (dir: string) => {
146
+ const files = fs.readdirSync(dir);
147
+
148
+ for (const file of files) {
149
+ const fullPath = path.join(dir, file);
150
+ const stat = fs.statSync(fullPath);
151
+
152
+ if (stat.isDirectory()) {
153
+ convertTypeScriptToJs(fullPath);
154
+ } else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
155
+ const isTsx = file.endsWith(".tsx");
156
+ const code = fs.readFileSync(fullPath, "utf8");
157
+
158
+ try {
159
+ const compiled = transform(code, {
160
+ transforms: isTsx ? ["typescript", "jsx"] : ["typescript"],
161
+ jsxRuntime: "preserve",
162
+ production: true,
163
+ });
164
+
165
+ const newExt = isTsx ? ".jsx" : ".js";
166
+ const newPath = fullPath.replace(/\.tsx?$/, newExt);
167
+
168
+ fs.writeFileSync(newPath, compiled.code, "utf8");
169
+ fs.unlinkSync(fullPath);
170
+ } catch {
171
+ logger.warn(`failed to strip types from ${file}, skipping...`);
172
+ }
173
+ }
174
+ }
175
+ };
190
176
 
191
- convertTypeScriptToJs(targetPath);
177
+ convertTypeScriptToJs(targetPath);
192
178
 
193
- const tsconfigPath = path.join(targetPath, "tsconfig.json");
194
- if (fs.existsSync(tsconfigPath)) {
195
- fs.unlinkSync(tsconfigPath);
179
+ const tsconfigPath = path.join(targetPath, "tsconfig.json");
180
+ if (fs.existsSync(tsconfigPath)) {
181
+ fs.unlinkSync(tsconfigPath);
182
+ }
196
183
  }
197
- }
198
-
199
- const pkgJsonPath = path.join(targetPath, "package.json");
200
- if (fs.existsSync(pkgJsonPath)) {
201
- try {
202
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
203
- pkg.name = path.basename(targetPath);
204
-
205
- if (response.variant === "js") {
206
- if (pkg.devDependencies) {
207
- delete pkg.devDependencies["typescript"];
208
- delete pkg.devDependencies["@types/node"];
209
- delete pkg.devDependencies["@typescript-eslint/eslint-plugin"];
210
- delete pkg.devDependencies["@typescript-eslint/parser"];
211
- }
212
- if (pkg.scripts && pkg.scripts.typecheck) {
213
- delete pkg.scripts.typecheck;
184
+
185
+ const pkgJsonPath = path.join(targetPath, "package.json");
186
+ if (fs.existsSync(pkgJsonPath)) {
187
+ try {
188
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
189
+ pkg.name = path.basename(targetPath);
190
+
191
+ if (response.variant === "js") {
192
+ if (pkg.devDependencies) {
193
+ delete pkg.devDependencies["typescript"];
194
+ delete pkg.devDependencies["@types/node"];
195
+ delete pkg.devDependencies["@typescript-eslint/eslint-plugin"];
196
+ delete pkg.devDependencies["@typescript-eslint/parser"];
197
+ }
198
+ if (pkg.scripts && pkg.scripts.typecheck) {
199
+ delete pkg.scripts.typecheck;
200
+ }
214
201
  }
215
- }
216
202
 
217
- fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2), "utf8");
218
- } catch (err) {
219
- logger.error("failed rewriting package.json manifest structures:", err);
203
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2), "utf8");
204
+ } catch (err) {
205
+ logger.error("failed rewriting package.json manifest structures:", err);
206
+ }
220
207
  }
221
- }
222
208
 
223
- logger.success(`\nšŸŽ‰ project successfully scaffolded into ${pc.magenta(targetDir)}!`);
224
- console.log(pc.dim("\nfollow these steps to begin execution:\n"));
209
+ logger.success(`\nšŸŽ‰ project successfully scaffolded into ${pc.magenta(targetDir)}!`);
210
+ console.log(pc.dim("\nfollow these steps to begin execution:\n"));
225
211
 
226
- if (targetDir !== ".") {
227
- console.log(` cd ${pc.cyan(targetDir)}`);
228
- }
229
- console.log(` ${pc.cyan("pnpm install")} ${pc.dim("# or npm i / yarn install")}`);
230
- console.log(` ${pc.cyan("pnpm dev")} ${pc.dim("# launches hot reload server")}\n`);
231
- });
232
-
233
- };
212
+ if (targetDir !== ".") {
213
+ console.log(` cd ${pc.cyan(targetDir)}`);
214
+ }
215
+ console.log(` ${pc.cyan("pnpm install")} ${pc.dim("# or npm i / yarn install")}`);
216
+ console.log(` ${pc.cyan("pnpm dev")} ${pc.dim("# launches hot reload server")}\n`);
217
+ });
218
+ }
@@ -58,19 +58,15 @@ export function register(cli: CAC) {
58
58
  }
59
59
 
60
60
  setTimeout(() => {
61
- serverProcess = spawn(
62
- "node",
63
- ["--enable-source-maps", path.resolve(appRoot, "./dist/server/index.js")],
64
- {
65
- stdio: "inherit",
66
- env: {
67
- ...process.env,
68
- NODE_ENV: "development",
69
- PORT: String(targetPort),
70
- RSPACK_DEV_PORT: String(targetPort + 1),
71
- },
72
- }
73
- );
61
+ serverProcess = spawn("node", ["--enable-source-maps", path.resolve(appRoot, "./dist/server/index.js")], {
62
+ stdio: "inherit",
63
+ env: {
64
+ ...process.env,
65
+ NODE_ENV: "development",
66
+ PORT: String(targetPort),
67
+ RSPACK_DEV_PORT: String(targetPort + 1),
68
+ },
69
+ });
74
70
  }, 200);
75
71
  };
76
72
 
@@ -7,4 +7,4 @@ export function register(cli: CAC) {
7
7
  const result = spawn.sync("tsc", ["--noEmit"], { stdio: "inherit", cwd: appRoot });
8
8
  if (result.status !== 0) process.exit(result.status ?? 1);
9
9
  });
10
- };
10
+ }
@@ -12,4 +12,4 @@ declare module "@rspack/dev-server" {
12
12
  }
13
13
 
14
14
  export default RspackDevServer;
15
- }
15
+ }
@@ -0,0 +1,37 @@
1
+ import * as tar from "tar";
2
+ import logger from "./logger.js";
3
+
4
+ const TAR_URL = "https://codeload.github.com/colourlabs/anaemia/tar.gz/main";
5
+
6
+ export async function fetchTemplate(targetPath: string): Promise<void> {
7
+ logger.info("downloading template...");
8
+
9
+ const res = await fetch(TAR_URL);
10
+ if (!res.ok) throw new Error(`failed to download template: ${res.statusText}`);
11
+
12
+ await new Promise<void>((resolve, reject) => {
13
+ const extract = tar.extract({
14
+ cwd: targetPath,
15
+ strip: 2,
16
+ filter: (p: string) => p.startsWith("anaemia-main/templates/base-app"),
17
+ });
18
+
19
+ extract.on("finish", resolve);
20
+ extract.on("error", reject);
21
+
22
+ const reader = res.body!.getReader();
23
+
24
+ const pump = async () => {
25
+ while (true) {
26
+ const { done, value } = await reader.read();
27
+ if (done) {
28
+ extract.end();
29
+ break;
30
+ }
31
+ extract.write(value);
32
+ }
33
+ };
34
+
35
+ pump().catch(reject);
36
+ });
37
+ }