@anaemia/cli 0.3.2 → 0.3.4

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.
@@ -1,12 +1,12 @@
1
1
  import logger from "../utils/logger.js";
2
- import path from "path";
3
- import fs from "fs";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
4
  import pc from "picocolors";
5
5
  import { generateSharedComponent, scaffoldFeature, scaffoldHook, scaffoldPage } from "../scaffold.js";
6
6
  import prompts from "prompts";
7
- import { transform } from "sucrase";
8
7
  import { fileURLToPath } from "node:url";
9
8
  import { fetchTemplate } from "../utils/fetch-template.js";
9
+ import { convertTypeScriptToJs } from "../utils/ts-to-js.js";
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
12
12
  export function register(cli) {
@@ -91,12 +91,9 @@ export function register(cli) {
91
91
  else {
92
92
  fs.mkdirSync(targetPath, { recursive: true });
93
93
  }
94
- let templatePath = path.resolve(__dirname, "../templates/template-base");
95
- if (!fs.existsSync(templatePath)) {
96
- templatePath = path.resolve(__dirname, "../templates/base-app");
97
- }
94
+ const templatePath = path.resolve(__dirname, "../templates/base-app");
98
95
  if (fs.existsSync(templatePath)) {
99
- logger.info("unpacking localized scaffolding architecture layout structures...");
96
+ logger.info("unpacking local template...");
100
97
  fs.cpSync(templatePath, targetPath, {
101
98
  recursive: true,
102
99
  filter: (src) => !["node_modules", "dist", ".anaemia", ".rspack"].includes(path.basename(src)),
@@ -121,39 +118,7 @@ export function register(cli) {
121
118
  removeGitKeepFiles(targetPath);
122
119
  if (response.variant === "js") {
123
120
  logger.info("converting workspace assets to vanilla JavaScript...");
124
- const convertTypeScriptToJs = (dir) => {
125
- const files = fs.readdirSync(dir);
126
- for (const file of files) {
127
- const fullPath = path.join(dir, file);
128
- const stat = fs.statSync(fullPath);
129
- if (stat.isDirectory()) {
130
- convertTypeScriptToJs(fullPath);
131
- }
132
- else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
133
- const isTsx = file.endsWith(".tsx");
134
- const code = fs.readFileSync(fullPath, "utf8");
135
- try {
136
- const compiled = transform(code, {
137
- transforms: isTsx ? ["typescript", "jsx"] : ["typescript"],
138
- jsxRuntime: "preserve",
139
- production: true,
140
- });
141
- const newExt = isTsx ? ".jsx" : ".js";
142
- const newPath = fullPath.replace(/\.tsx?$/, newExt);
143
- fs.writeFileSync(newPath, compiled.code, "utf8");
144
- fs.unlinkSync(fullPath);
145
- }
146
- catch {
147
- logger.warn(`failed to strip types from ${file}, skipping...`);
148
- }
149
- }
150
- }
151
- };
152
121
  convertTypeScriptToJs(targetPath);
153
- const tsconfigPath = path.join(targetPath, "tsconfig.json");
154
- if (fs.existsSync(tsconfigPath)) {
155
- fs.unlinkSync(tsconfigPath);
156
- }
157
122
  }
158
123
  const pkgJsonPath = path.join(targetPath, "package.json");
159
124
  if (fs.existsSync(pkgJsonPath)) {
@@ -8,6 +8,7 @@ import { WebSocketServer } from "ws";
8
8
  import { WebSocket as NodeWS } from "ws";
9
9
  import { loadUserConfig } from "../utils/config.js";
10
10
  import logger from "../utils/logger.js";
11
+ import { flattenWsMessage } from "../utils/flatten-ws-message.js";
11
12
  export function register(cli) {
12
13
  cli.command("dev", "launch local development environment").action(async () => {
13
14
  process.env.NODE_ENV = "development";
@@ -22,11 +23,7 @@ export function register(cli) {
22
23
  wss.on("connection", (clientWs) => {
23
24
  const rspackSocket = new NodeWS(`ws://localhost:${targetPort + 1}/ws`);
24
25
  rspackSocket.on("message", (data) => {
25
- const flattened = Array.isArray(data) ? Buffer.concat(data) : data;
26
- if (Buffer.isBuffer(flattened))
27
- clientWs.send(flattened.toString("utf-8"));
28
- else
29
- clientWs.send(String(flattened));
26
+ clientWs.send(flattenWsMessage(data));
30
27
  });
31
28
  rspackSocket.on("error", (err) => {
32
29
  console.warn("[anaemia hmr] rspack socket error:", err.message);
@@ -9,7 +9,7 @@ export async function fetchTemplate(targetPath) {
9
9
  await new Promise((resolve, reject) => {
10
10
  const extract = tar.extract({
11
11
  cwd: targetPath,
12
- strip: 2,
12
+ strip: 3,
13
13
  filter: (p) => p.startsWith("anaemia-main/templates/base-app"),
14
14
  });
15
15
  extract.on("finish", resolve);
@@ -0,0 +1,9 @@
1
+ export function flattenWsMessage(data) {
2
+ if (Array.isArray(data))
3
+ return Buffer.concat(data).toString("utf-8");
4
+ if (Buffer.isBuffer(data))
5
+ return data.toString("utf-8");
6
+ if (data instanceof ArrayBuffer)
7
+ return Buffer.from(data).toString("utf-8");
8
+ return String(data);
9
+ }
@@ -0,0 +1,39 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { transform } from "sucrase";
4
+ import logger from "./logger.js";
5
+ export function convertTypeScriptToJs(dir) {
6
+ const files = fs.readdirSync(dir);
7
+ for (const file of files) {
8
+ const fullPath = path.join(dir, file);
9
+ const stat = fs.statSync(fullPath);
10
+ if (stat.isDirectory()) {
11
+ convertTypeScriptToJs(fullPath);
12
+ continue;
13
+ }
14
+ if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
15
+ continue;
16
+ const isTsx = file.endsWith(".tsx");
17
+ const code = fs.readFileSync(fullPath, "utf8");
18
+ try {
19
+ const compiled = transform(code, {
20
+ transforms: isTsx ? ["typescript", "jsx"] : ["typescript"],
21
+ jsxRuntime: "preserve",
22
+ production: true,
23
+ });
24
+ const cleaned = compiled.code
25
+ .replace(/\n{3,}/g, "\n\n")
26
+ .trimStart();
27
+ const newPath = fullPath.replace(/\.tsx?$/, isTsx ? ".jsx" : ".js");
28
+ fs.writeFileSync(newPath, cleaned, "utf8");
29
+ fs.unlinkSync(fullPath);
30
+ }
31
+ catch {
32
+ logger.warn(`failed to strip types from ${file}, skipping...`);
33
+ }
34
+ }
35
+ const tsconfigPath = path.join(dir, "tsconfig.json");
36
+ if (fs.existsSync(tsconfigPath)) {
37
+ fs.unlinkSync(tsconfigPath);
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@anaemia/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "anaemia": "./dist/index.js"
7
7
  },
8
8
  "dependencies": {
9
- "@anaemia/bundler": "^0.3.2",
10
- "@anaemia/core": "^0.3.2",
9
+ "@anaemia/bundler": "^0.3.4",
10
+ "@anaemia/core": "^0.3.4",
11
11
  "@rspack/core": "^2.0.5",
12
12
  "@rspack/dev-server": "2.0.1",
13
13
  "cac": "^7.0.0",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "scripts": {
29
29
  "build": "tsc",
30
- "dev": "tsc --watch"
30
+ "dev": "tsc --watch",
31
+ "test": "pnpm run build && node --test test/*.test.mjs test/commands/*.test.mjs"
31
32
  }
32
33
  }
@@ -1,13 +1,13 @@
1
1
  import type { CAC } from "cac";
2
2
  import logger from "../utils/logger.js";
3
- import path from "path";
4
- import fs from "fs";
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
5
  import pc from "picocolors";
6
6
  import { generateSharedComponent, scaffoldFeature, scaffoldHook, scaffoldPage } from "../scaffold.js";
7
7
  import prompts from "prompts";
8
- import { transform } from "sucrase";
9
8
  import { fileURLToPath } from "node:url";
10
9
  import { fetchTemplate } from "../utils/fetch-template.js";
10
+ import { convertTypeScriptToJs } from "../utils/ts-to-js.js";
11
11
 
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
@@ -110,13 +110,10 @@ export function register(cli: CAC) {
110
110
  fs.mkdirSync(targetPath, { recursive: true });
111
111
  }
112
112
 
113
- let templatePath = path.resolve(__dirname, "../templates/template-base");
114
- if (!fs.existsSync(templatePath)) {
115
- templatePath = path.resolve(__dirname, "../templates/base-app");
116
- }
113
+ const templatePath = path.resolve(__dirname, "../templates/base-app");
117
114
 
118
115
  if (fs.existsSync(templatePath)) {
119
- logger.info("unpacking localized scaffolding architecture layout structures...");
116
+ logger.info("unpacking local template...");
120
117
  fs.cpSync(templatePath, targetPath, {
121
118
  recursive: true,
122
119
  filter: (src) => !["node_modules", "dist", ".anaemia", ".rspack"].includes(path.basename(src)),
@@ -137,49 +134,12 @@ export function register(cli: CAC) {
137
134
  }
138
135
  }
139
136
  };
137
+
140
138
  removeGitKeepFiles(targetPath);
141
139
 
142
140
  if (response.variant === "js") {
143
141
  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
- };
176
-
177
142
  convertTypeScriptToJs(targetPath);
178
-
179
- const tsconfigPath = path.join(targetPath, "tsconfig.json");
180
- if (fs.existsSync(tsconfigPath)) {
181
- fs.unlinkSync(tsconfigPath);
182
- }
183
143
  }
184
144
 
185
145
  const pkgJsonPath = path.join(targetPath, "package.json");
@@ -10,6 +10,7 @@ import { WebSocket as NodeWS } from "ws";
10
10
  import { loadUserConfig } from "../utils/config.js";
11
11
  import logger from "../utils/logger.js";
12
12
  import { ChildProcess } from "node:child_process";
13
+ import { flattenWsMessage } from "../utils/flatten-ws-message.js";
13
14
 
14
15
  export function register(cli: CAC) {
15
16
  cli.command("dev", "launch local development environment").action(async () => {
@@ -31,9 +32,7 @@ export function register(cli: CAC) {
31
32
  const rspackSocket = new NodeWS(`ws://localhost:${targetPort + 1}/ws`);
32
33
 
33
34
  rspackSocket.on("message", (data) => {
34
- const flattened = Array.isArray(data) ? Buffer.concat(data) : data;
35
- if (Buffer.isBuffer(flattened)) clientWs.send(flattened.toString("utf-8"));
36
- else clientWs.send(String(flattened));
35
+ clientWs.send(flattenWsMessage(data));
37
36
  });
38
37
 
39
38
  rspackSocket.on("error", (err) => {
@@ -12,7 +12,7 @@ export async function fetchTemplate(targetPath: string): Promise<void> {
12
12
  await new Promise<void>((resolve, reject) => {
13
13
  const extract = tar.extract({
14
14
  cwd: targetPath,
15
- strip: 2,
15
+ strip: 3,
16
16
  filter: (p: string) => p.startsWith("anaemia-main/templates/base-app"),
17
17
  });
18
18
 
@@ -0,0 +1,6 @@
1
+ export function flattenWsMessage(data: Buffer | Buffer[] | ArrayBuffer | string): string {
2
+ if (Array.isArray(data)) return Buffer.concat(data).toString("utf-8");
3
+ if (Buffer.isBuffer(data)) return data.toString("utf-8");
4
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf-8");
5
+ return String(data);
6
+ }
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { transform } from "sucrase";
4
+ import logger from "./logger.js";
5
+
6
+ export function convertTypeScriptToJs(dir: string): void {
7
+ const files = fs.readdirSync(dir);
8
+
9
+ for (const file of files) {
10
+ const fullPath = path.join(dir, file);
11
+ const stat = fs.statSync(fullPath);
12
+
13
+ if (stat.isDirectory()) {
14
+ convertTypeScriptToJs(fullPath);
15
+ continue;
16
+ }
17
+
18
+ if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
19
+
20
+ const isTsx = file.endsWith(".tsx");
21
+ const code = fs.readFileSync(fullPath, "utf8");
22
+
23
+ try {
24
+ const compiled = transform(code, {
25
+ transforms: isTsx ? ["typescript", "jsx"] : ["typescript"],
26
+ jsxRuntime: "preserve",
27
+ production: true,
28
+ });
29
+
30
+ const cleaned = compiled.code
31
+ .replace(/\n{3,}/g, "\n\n")
32
+ .trimStart();
33
+
34
+ const newPath = fullPath.replace(/\.tsx?$/, isTsx ? ".jsx" : ".js");
35
+ fs.writeFileSync(newPath, cleaned, "utf8");
36
+ fs.unlinkSync(fullPath);
37
+ } catch {
38
+ logger.warn(`failed to strip types from ${file}, skipping...`);
39
+ }
40
+ }
41
+
42
+ const tsconfigPath = path.join(dir, "tsconfig.json");
43
+ if (fs.existsSync(tsconfigPath)) {
44
+ fs.unlinkSync(tsconfigPath);
45
+ }
46
+ }
@@ -0,0 +1,73 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+
7
+ function createTmpDir() {
8
+ return fs.mkdtempSync(path.join(os.tmpdir(), "anaemia-create-test-"));
9
+ }
10
+
11
+ test("convertTypeScriptToJs strips types from .ts files", async () => {
12
+ const dir = createTmpDir();
13
+ try {
14
+ fs.writeFileSync(path.join(dir, "index.ts"), `const x: string = "hello";\nexport default x;\n`);
15
+
16
+ const { convertTypeScriptToJs } = await import("../../dist/utils/ts-to-js.js");
17
+ convertTypeScriptToJs(dir);
18
+
19
+ assert.ok(!fs.existsSync(path.join(dir, "index.ts")), "ts file should be removed");
20
+ assert.ok(fs.existsSync(path.join(dir, "index.js")), "js file should exist");
21
+ const content = fs.readFileSync(path.join(dir, "index.js"), "utf-8");
22
+ assert.ok(!content.includes(": string"), "type annotation should be stripped");
23
+ } finally {
24
+ fs.rmSync(dir, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ test("convertTypeScriptToJs strips types from .tsx files", async () => {
29
+ const dir = createTmpDir();
30
+ try {
31
+ fs.writeFileSync(path.join(dir, "Component.tsx"), `export default function Comp(props: { name: string }) {\n return <div>{props.name}</div>;\n}\n`);
32
+
33
+ const { convertTypeScriptToJs } = await import("../../dist/utils/ts-to-js.js");
34
+ convertTypeScriptToJs(dir);
35
+
36
+ assert.ok(!fs.existsSync(path.join(dir, "Component.tsx")));
37
+ assert.ok(fs.existsSync(path.join(dir, "Component.jsx")));
38
+ const content = fs.readFileSync(path.join(dir, "Component.jsx"), "utf-8");
39
+ assert.ok(!content.includes(": string"));
40
+ } finally {
41
+ fs.rmSync(dir, { recursive: true, force: true });
42
+ }
43
+ });
44
+
45
+ test("convertTypeScriptToJs cleans up leading blank lines", async () => {
46
+ const dir = createTmpDir();
47
+ try {
48
+ fs.writeFileSync(path.join(dir, "root.tsx"), `import { JSX } from "solid-js";\n\nexport default function Root(props: { children: JSX.Element }) {\n return <>{props.children}</>;\n}\n`);
49
+
50
+ const { convertTypeScriptToJs } = await import("../../dist/utils/ts-to-js.js");
51
+ convertTypeScriptToJs(dir);
52
+
53
+ const content = fs.readFileSync(path.join(dir, "root.jsx"), "utf-8");
54
+ assert.ok(!content.startsWith("\n"), "file should not start with blank lines");
55
+ } finally {
56
+ fs.rmSync(dir, { recursive: true, force: true });
57
+ }
58
+ });
59
+
60
+ test("convertTypeScriptToJs removes tsconfig.json", async () => {
61
+ const dir = createTmpDir();
62
+ try {
63
+ fs.writeFileSync(path.join(dir, "tsconfig.json"), JSON.stringify({ compilerOptions: {} }));
64
+ fs.writeFileSync(path.join(dir, "index.ts"), `export const x = 1;\n`);
65
+
66
+ const { convertTypeScriptToJs } = await import("../../dist/utils/ts-to-js.js");
67
+ convertTypeScriptToJs(dir);
68
+
69
+ assert.ok(!fs.existsSync(path.join(dir, "tsconfig.json")));
70
+ } finally {
71
+ fs.rmSync(dir, { recursive: true, force: true });
72
+ }
73
+ });
@@ -0,0 +1,17 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { flattenWsMessage } from "../../dist/utils/flatten-ws-message.js";
4
+
5
+ test("flattenWsMessage handles a plain string", () => {
6
+ assert.equal(flattenWsMessage("hello"), "hello");
7
+ });
8
+
9
+ test("flattenWsMessage handles a single Buffer", () => {
10
+ const buf = Buffer.from("hello");
11
+ assert.equal(flattenWsMessage(buf), "hello");
12
+ });
13
+
14
+ test("flattenWsMessage concatenates a Buffer array", () => {
15
+ const bufs = [Buffer.from("hel"), Buffer.from("lo")];
16
+ assert.equal(flattenWsMessage(bufs), "hello");
17
+ });
@@ -0,0 +1,23 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { fetchTemplate } from "../dist/utils/fetch-template.js";
8
+
9
+ test("fetchTemplate extracts template contents directly into target directory", async () => {
10
+ const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), "anaemia-template-"));
11
+
12
+ try {
13
+ await fetchTemplate(targetDir);
14
+
15
+ const files = fs.readdirSync(targetDir);
16
+ assert.ok(files.includes("package.json"), "missing package.json");
17
+ assert.ok(files.includes("src"), "missing src directory");
18
+ assert.ok(files.includes("anaemia.config.ts"), "missing anaemia.config.ts");
19
+ assert.ok(!files.includes("base-app"), "base-app subdirectory should not exist");
20
+ } finally {
21
+ fs.rmSync(targetDir, { recursive: true, force: true });
22
+ }
23
+ });
@@ -1,15 +0,0 @@
1
- // fuck you typescript
2
-
3
- declare module "@rspack/dev-server" {
4
- import type { Compiler, MultiCompiler, DevServer } from "@rspack/core";
5
-
6
- export type DevServerConfiguration = DevServer;
7
-
8
- export class RspackDevServer {
9
- constructor(config: DevServer, compiler: Compiler | MultiCompiler);
10
- start(): Promise<void>;
11
- stop(): Promise<void>;
12
- }
13
-
14
- export default RspackDevServer;
15
- }