milkee 3.0.1-dev.0 → 3.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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "tabWidth": 2
3
+ }
package/README-ja.md CHANGED
@@ -1,7 +1,12 @@
1
- [English](./README.md) | **日本語**
2
-
3
1
  # Milkee
4
2
 
3
+ [![Vitest](https://github.com/otoneko1102/coffeescript-milkee/actions/workflows/test.yml/badge.svg)](https://github.com/otoneko1102/coffeescript-milkee/actions)
4
+ [![npm version](https://img.shields.io/npm/v/milkee.svg)](https://www.npmjs.com/package/milkee)
5
+ [![Downloads](https://img.shields.io/npm/dw/milkee.svg)](https://www.npmjs.com/package/milkee)
6
+ [![License](https://img.shields.io/npm/l/milkee.svg)](./LICENSE)
7
+
8
+ [English](./README.md) | **日本語**
9
+
5
10
  ![Milkee logo](./img/Milkee-logo.png)
6
11
 
7
12
  `coffee.config.cjs` を使ったシンプルな CoffeeScript ビルドツール
package/README.md CHANGED
@@ -1,7 +1,12 @@
1
- **English** | [日本語](./README-ja.md)
2
-
3
1
  # Milkee
4
2
 
3
+ [![Vitest](https://github.com/otoneko1102/coffeescript-milkee/actions/workflows/test.yml/badge.svg)](https://github.com/otoneko1102/coffeescript-milkee/actions)
4
+ [![npm version](https://img.shields.io/npm/v/milkee.svg)](https://www.npmjs.com/package/milkee)
5
+ [![Downloads](https://img.shields.io/npm/dw/milkee.svg)](https://www.npmjs.com/package/milkee)
6
+ [![License](https://img.shields.io/npm/l/milkee.svg)](./LICENSE)
7
+
8
+ **English** | [日本語](./README-ja.md)
9
+
5
10
  ![Milkee logo](./img/Milkee-logo.png)
6
11
 
7
12
  A simple CoffeeScript build tool with [coffee.config.cjs](./temp/coffee.config.cjs)
package/dist/lib/utils.js CHANGED
@@ -31,11 +31,11 @@ getCompiledFiles = function(targetPath) {
31
31
  filesList = filesList.concat(getCompiledFiles(itemPath));
32
32
  }
33
33
  } else if (stat.isFile()) {
34
- if (targetPath.endsWith('.js' || targetPath.endsWith('.js.map'))) {
34
+ if (targetPath.match(/\.js(?:\.map)?$/i)) {
35
35
  if (fs.existsSync(targetPath)) {
36
36
  consola.info(`Found file: \`${targetPath}\``);
37
+ filesList.push(targetPath);
37
38
  }
38
- filesList.push(targetPath);
39
39
  }
40
40
  }
41
41
  } catch (error1) {
@@ -11,8 +11,8 @@ consola = require('consola');
11
11
 
12
12
  executeCopy = function(config) {
13
13
  var copyNonCoffeeFiles, entryPath, error, outputPath, ref, stat;
14
- entryPath = path.join(CWD, config.entry);
15
- outputPath = path.join(CWD, config.output);
14
+ entryPath = path.isAbsolute(config.entry) ? config.entry : path.join(CWD, config.entry);
15
+ outputPath = path.isAbsolute(config.output) ? config.output : path.join(CWD, config.output);
16
16
  if (!fs.existsSync(entryPath)) {
17
17
  consola.warn(`Entry path does not exist: ${config.entry}`);
18
18
  return;
@@ -14,7 +14,7 @@ consola = require('consola');
14
14
  // Execute refresh processing
15
15
  executeRefresh = function(config, backupFiles) {
16
16
  var backupName, backupPath, dirName, error, fileName, hash, i, item, items, len, originalPath, stat, targetDir;
17
- targetDir = path.join(CWD, config.output);
17
+ targetDir = path.isAbsolute(config.output) ? config.output : path.join(CWD, config.output);
18
18
  if (fs.existsSync(targetDir)) {
19
19
  stat = fs.statSync(targetDir);
20
20
  hash = crypto.randomBytes(4).toString('hex');
package/docs/PLUGIN-ja.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Milkee プラグインの作成
2
2
 
3
+ [English](./PLUGIN.md) | **日本語**
4
+
3
5
  カスタムプラグインで Milkee の機能を拡張できます。
4
6
 
5
7
  ## クイックスタート
package/docs/PLUGIN.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Creating a Milkee Plugin
2
2
 
3
+ **English** | [日本語](./PLUGIN-ja.md)
4
+
3
5
  Extend Milkee's functionality with custom plugins.
4
6
 
5
7
  ## Quick Start
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "milkee",
3
- "version": "3.0.1-dev.0",
3
+ "version": "3.1.0",
4
4
  "description": "A simple CoffeeScript build tool with coffee.config.cjs",
5
5
  "main": "dist/main.js",
6
6
  "bin": {
7
7
  "milkee": "bin/milkee.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "echo \"No test specified\" && exit 0",
10
+ "test": "prettier --write ./test && vitest",
11
+ "test:ci": "vitest --run",
12
+ "coverage": "vitest --run --coverage",
11
13
  "build": "coffee --output dist/ --compile --bare src/",
14
+ "format": "node ./scripts/format.js",
12
15
  "deprecate:dev": "node ./scripts/deprecate-dev-versions.js"
13
16
  },
14
17
  "repository": {
@@ -31,7 +34,7 @@
31
34
  "bugs": {
32
35
  "url": "https://github.com/otoneko1102/coffeescript-milkee/issues"
33
36
  },
34
- "homepage": "https://github.com/otoneko1102/coffeescript-milkee#readme",
37
+ "homepage": "https://milkee.oto.im",
35
38
  "dependencies": {
36
39
  "@milkee/d": "^0.2.1",
37
40
  "consola": "^3.4.2",
@@ -41,7 +44,12 @@
41
44
  "devDependencies": {
42
45
  "@babel/core": "^7.28.5",
43
46
  "@types/coffeescript": "^2.5.7",
47
+ "@vitest/coverage-v8": "^4.0.17",
48
+ "coffee-fmt": "^0.12.0",
44
49
  "coffeescript": "^2.7.0",
45
- "dotenv": "^17.2.3"
50
+ "dotenv": "^17.2.3",
51
+ "execa": "^9.6.1",
52
+ "prettier": "^3.8.0",
53
+ "vitest": "^4.0.17"
46
54
  }
47
55
  }
@@ -0,0 +1,36 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const { execFile } = require("child_process");
5
+
6
+ describe("cli setup integration", () => {
7
+ it("runs `--setup` and creates coffee.config.cjs", async () => {
8
+ const dir = createTestDir("cli");
9
+
10
+ // run the setup command directly via CoffeeScript so we don't require the ESM-only `yargs` in `main`
11
+ const scriptPath = require("path").join(
12
+ process.cwd(),
13
+ "src",
14
+ "commands",
15
+ "setup.coffee",
16
+ );
17
+ const csRegister = require.resolve("coffeescript/register");
18
+ await new Promise((resolve, reject) => {
19
+ // -r <abs> -> preload CoffeeScript via absolute path so child process can find it
20
+ execFile(
21
+ "node",
22
+ ["-r", csRegister, "-e", `require(${JSON.stringify(scriptPath)})()`],
23
+ { cwd: dir },
24
+ (err) => {
25
+ if (err) return reject(err);
26
+ resolve();
27
+ },
28
+ );
29
+ });
30
+
31
+ const cfgPath = path.join(dir, "coffee.config.cjs");
32
+ expect(fs.existsSync(cfgPath)).toBe(true);
33
+
34
+ fs.rmSync(dir, { recursive: true, force: true });
35
+ });
36
+ });
package/test/setup.js ADDED
@@ -0,0 +1,113 @@
1
+ // Register CoffeeScript so tests can require .coffee files
2
+ require("coffeescript/register");
3
+
4
+ // Test sandbox guard: prevent accidental writes outside a temporary sandbox
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
8
+
9
+ const SANDBOX = fs.mkdtempSync(path.join(os.tmpdir(), "milkee-test-sandbox-"));
10
+ process.env.TEST_SANDBOX = SANDBOX;
11
+ globalThis.TEST_SANDBOX = SANDBOX;
12
+
13
+ function stripExtendedPrefix(s) {
14
+ if (typeof s !== "string") return s;
15
+ // Remove Windows extended path prefix like '\\?\\' or '\?\\'
16
+ let out = s.replace(/^\\+\?\\/, "");
17
+ // If we still have a leading backslash before a drive letter (e.g. '\C:\...'), remove it
18
+ out = out.replace(/^\\+([A-Za-z]:)/, "$1");
19
+ return out;
20
+ }
21
+
22
+ // Allowlist: sandbox + system temp + node_modules + any additional paths via TEST_WRITE_ALLOW
23
+ const allowed = [
24
+ path.resolve(stripExtendedPrefix(SANDBOX)),
25
+ path.resolve(stripExtendedPrefix(os.tmpdir())),
26
+ path.resolve(stripExtendedPrefix(process.cwd()), "node_modules"),
27
+ ];
28
+ if (process.env.TEST_WRITE_ALLOW) {
29
+ process.env.TEST_WRITE_ALLOW.split(",").forEach((p) => {
30
+ if (p) allowed.push(path.resolve(stripExtendedPrefix(p)));
31
+ });
32
+ }
33
+
34
+ function resolveSafe(p) {
35
+ if (!p) return p;
36
+ // accept Buffers as well
37
+ if (Buffer.isBuffer(p)) p = p.toString();
38
+ // strip Windows extended path prefix if present
39
+ if (typeof p === "string") {
40
+ p = stripExtendedPrefix(p);
41
+ }
42
+ if (!path.isAbsolute(p)) p = path.join(process.cwd(), String(p));
43
+ return path.resolve(p);
44
+ }
45
+
46
+ function isAllowed(p) {
47
+ const rp = resolveSafe(p);
48
+ return allowed.some(
49
+ (prefix) => rp === prefix || rp.startsWith(prefix + path.sep),
50
+ );
51
+ }
52
+
53
+ function makeGuard(orig, checkIndex = 0) {
54
+ return function (...args) {
55
+ const target = args[checkIndex];
56
+ if (!isAllowed(target)) {
57
+ throw new Error(
58
+ `Test attempted to modify outside sandbox: ${target} (sandbox=${SANDBOX})`,
59
+ );
60
+ }
61
+ return orig.apply(this, args);
62
+ };
63
+ }
64
+
65
+ // Patch common fs methods that write or modify the FS
66
+ const writeSyncMethods = [
67
+ "writeFileSync",
68
+ "appendFileSync",
69
+ "copyFileSync",
70
+ "mkdirSync",
71
+ "rmSync",
72
+ "rmdirSync",
73
+ "renameSync",
74
+ "unlinkSync",
75
+ ];
76
+ for (const m of writeSyncMethods) {
77
+ if (typeof fs[m] === "function") fs[m] = makeGuard(fs[m], 0);
78
+ }
79
+
80
+ // Async versions
81
+ if (typeof fs.writeFile === "function") {
82
+ const orig = fs.writeFile;
83
+ fs.writeFile = function (file, ...rest) {
84
+ if (!isAllowed(file)) {
85
+ const cb = rest.find((r) => typeof r === "function");
86
+ const err = new Error(
87
+ `Test attempted to modify outside sandbox: ${file} (sandbox=${SANDBOX})`,
88
+ );
89
+ if (cb) return process.nextTick(() => cb(err));
90
+ throw err;
91
+ }
92
+ return orig.apply(this, [file, ...rest]);
93
+ };
94
+ }
95
+ if (fs.promises && typeof fs.promises.writeFile === "function") {
96
+ const orig = fs.promises.writeFile;
97
+ fs.promises.writeFile = function (file, ...rest) {
98
+ if (!isAllowed(file))
99
+ throw new Error(
100
+ `Test attempted to modify outside sandbox: ${file} (sandbox=${SANDBOX})`,
101
+ );
102
+ return orig.apply(this, [file, ...rest]);
103
+ };
104
+ }
105
+
106
+ // Helper for tests to create safe temp dirs
107
+ globalThis.createTestDir = function (name = "") {
108
+ const dir = path.join(SANDBOX, name || String(Date.now()));
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ return dir;
111
+ };
112
+
113
+ console.info(`[test setup] test sandbox: ${SANDBOX}`);
@@ -0,0 +1,38 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const consola = require("consola");
4
+
5
+ // We re-require the checks module inside tests to allow us to mock dependencies
6
+ // that it captures at require-time.
7
+
8
+ describe("checks", () => {
9
+ afterEach(() => {
10
+ vi.restoreAllMocks();
11
+ vi.resetModules();
12
+ });
13
+
14
+ it("checkLatest returns true and shows box when not latest", async () => {
15
+ vi.mock("is-package-latest", () => ({
16
+ isPackageLatest: async () => ({
17
+ success: true,
18
+ isLatest: false,
19
+ currentVersion: "1.0.0",
20
+ latestVersion: "1.2.0",
21
+ }),
22
+ }));
23
+ const { checkLatest } = require("../../src/lib/checks.coffee");
24
+
25
+ vi.spyOn(consola, "box").mockImplementation(() => {});
26
+
27
+ const res = await checkLatest();
28
+ expect(res).toBe(true);
29
+ expect(consola.box).toHaveBeenCalled();
30
+ });
31
+
32
+ it("checkCoffee reads package.json and does not warn when coffeescript present", async () => {
33
+ const { checkCoffee } = require("../../src/lib/checks.coffee");
34
+ vi.spyOn(consola, "warn").mockImplementation(() => {});
35
+ await checkCoffee();
36
+ expect(consola.warn).not.toHaveBeenCalled();
37
+ });
38
+ });
@@ -0,0 +1,50 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+
5
+ const executeCopy = require("../../src/options/copy.coffee");
6
+
7
+ function createTree(base) {
8
+ fs.mkdirSync(base, { recursive: true });
9
+ fs.writeFileSync(path.join(base, "a.coffee"), "c");
10
+ fs.writeFileSync(path.join(base, "b.txt"), "b");
11
+ fs.mkdirSync(path.join(base, "sub"), { recursive: true });
12
+ fs.writeFileSync(path.join(base, "sub", "c.md"), "c");
13
+ }
14
+
15
+ describe("copy", () => {
16
+ it("copies non-coffee files recursively", () => {
17
+ const base = createTestDir("copy");
18
+ const entry = path.join(base, "entry");
19
+ const out = path.join(base, "out");
20
+ createTree(entry);
21
+ fs.mkdirSync(out, { recursive: true });
22
+
23
+ // preconditions
24
+ expect(fs.existsSync(entry)).toBe(true);
25
+ expect(fs.existsSync(path.join(entry, "b.txt"))).toBe(true);
26
+
27
+ // use absolute paths (sandboxed)
28
+ executeCopy({ entry, output: out });
29
+
30
+ expect(fs.existsSync(path.join(out, "b.txt"))).toBe(true);
31
+ expect(fs.existsSync(path.join(out, "sub", "c.md"))).toBe(true);
32
+ expect(fs.existsSync(path.join(out, "a.coffee"))).toBe(false);
33
+
34
+ fs.rmSync(base, { recursive: true, force: true });
35
+ });
36
+
37
+ it("skips when join option is enabled", () => {
38
+ const base = createTestDir("copy");
39
+ const entry = path.join(base, "entry");
40
+ const out = path.join(base, "out");
41
+ createTree(entry);
42
+ fs.mkdirSync(out, { recursive: true });
43
+
44
+ executeCopy({ entry, output: out, options: { join: true } });
45
+
46
+ expect(fs.existsSync(path.join(out, "b.txt"))).toBe(false);
47
+
48
+ fs.rmSync(base, { recursive: true, force: true });
49
+ });
50
+ });
@@ -0,0 +1,50 @@
1
+ const fs = require("fs");
2
+
3
+ const path = require("path");
4
+
5
+ const plugins = require("../../src/lib/plugins.coffee");
6
+
7
+ describe("plugins", () => {
8
+ it("runPlugins executes provided plugin functions", async () => {
9
+ let called = false;
10
+ const tempDir = createTestDir("plugins");
11
+ // create a dummy compiled file
12
+ const compiled = path.join(tempDir, "out.js");
13
+ fs.writeFileSync(compiled, "x");
14
+
15
+ const config = {
16
+ output: tempDir,
17
+ milkee: {
18
+ plugins: [
19
+ (res) => {
20
+ called = true;
21
+ return Promise.resolve();
22
+ },
23
+ ],
24
+ },
25
+ };
26
+ plugins.runPlugins(config, {});
27
+
28
+ // wait a tick for async IIFE to run
29
+ await new Promise((r) => setTimeout(r, 10));
30
+ expect(called).toBe(true);
31
+
32
+ fs.rmSync(tempDir, { recursive: true, force: true });
33
+ });
34
+
35
+ it("warns on invalid plugin entries", async () => {
36
+ const tempDir = fs.mkdtempSync(
37
+ path.join(require("os").tmpdir(), "milkee-plugins-"),
38
+ );
39
+ const consola = require("consola");
40
+ vi.spyOn(consola, "warn").mockImplementation(() => {});
41
+
42
+ const config = { output: tempDir, milkee: { plugins: ["not-fn"] } };
43
+ plugins.runPlugins(config, {});
44
+ await new Promise((r) => setTimeout(r, 10));
45
+
46
+ expect(consola.warn).toHaveBeenCalled();
47
+
48
+ fs.rmSync(tempDir, { recursive: true, force: true });
49
+ });
50
+ });
@@ -0,0 +1,63 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+
5
+ const {
6
+ executeRefresh,
7
+ restoreBackups,
8
+ clearBackups,
9
+ } = require("../../src/options/refresh.coffee");
10
+
11
+ describe("refresh", () => {
12
+ it("backs up directory files and restores them", () => {
13
+ const base = createTestDir("refresh");
14
+ const dir = path.join(base, "out");
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ const a = path.join(dir, "a.txt");
17
+ const b = path.join(dir, "b.txt");
18
+ fs.writeFileSync(a, "1");
19
+ fs.writeFileSync(b, "2");
20
+
21
+ const backupFiles = [];
22
+ // preconditions
23
+ expect(fs.existsSync(dir)).toBe(true);
24
+ expect(fs.existsSync(a)).toBe(true);
25
+
26
+ executeRefresh({ output: dir }, backupFiles);
27
+
28
+ expect(backupFiles.length).toBe(2);
29
+ for (const binfo of backupFiles) {
30
+ expect(fs.existsSync(binfo.backup)).toBe(true);
31
+ expect(fs.existsSync(binfo.original)).toBe(false);
32
+ }
33
+
34
+ // restore
35
+ restoreBackups(backupFiles);
36
+ for (const binfo of backupFiles) {
37
+ expect(fs.existsSync(binfo.original)).toBe(true);
38
+ expect(fs.existsSync(binfo.backup)).toBe(false);
39
+ }
40
+
41
+ // cleanup
42
+ fs.rmSync(base, { recursive: true, force: true });
43
+ });
44
+
45
+ it("clearBackups removes backup files", () => {
46
+ const base = createTestDir("refresh");
47
+ const dir = path.join(base, "out");
48
+ fs.mkdirSync(dir, { recursive: true });
49
+ const a = path.join(dir, "a.txt");
50
+ fs.writeFileSync(a, "1");
51
+
52
+ const backupFiles = [];
53
+ executeRefresh({ output: dir }, backupFiles);
54
+ expect(backupFiles.length).toBeGreaterThan(0);
55
+
56
+ clearBackups(backupFiles);
57
+ for (const binfo of backupFiles) {
58
+ expect(fs.existsSync(binfo.backup)).toBe(false);
59
+ }
60
+
61
+ fs.rmSync(base, { recursive: true, force: true });
62
+ });
63
+ });
@@ -0,0 +1,14 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ describe("sandbox Windows extended path handling", () => {
5
+ it("allows writes when path uses \\?\\ prefix (Windows only)", () => {
6
+ if (process.platform !== "win32") return;
7
+ const dir = createTestDir("win-prefix");
8
+ // simulate Windows extended path prefix
9
+ // use Windows extended path prefix with doubled backslashes
10
+ const prefixed = "\\\\?\\\\" + path.join(dir, "file.txt");
11
+ expect(() => fs.writeFileSync(prefixed, "ok")).not.toThrow();
12
+ expect(fs.existsSync(path.join(dir, "file.txt"))).toBe(true);
13
+ });
14
+ });
@@ -0,0 +1,70 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+
5
+ const setup = require("../../src/commands/setup.coffee");
6
+ const checks = require("../../src/lib/checks.coffee");
7
+ const { CONFIG_FILE } = require("../../src/lib/constants.coffee");
8
+
9
+ describe("setup", () => {
10
+ let cwd;
11
+ beforeEach(() => {
12
+ cwd = process.cwd();
13
+ vi.spyOn(checks, "checkCoffee").mockImplementation(() => {});
14
+ });
15
+
16
+ afterEach(() => {
17
+ process.chdir(cwd);
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ it("creates config file when not present", async () => {
22
+ const dir = fs.mkdtempSync(
23
+ path.join(require("os").tmpdir(), "milkee-setup-"),
24
+ );
25
+ process.chdir(dir);
26
+
27
+ // re-require to ensure constants.CWD is captured from the new cwd
28
+ delete require.cache[require.resolve("../../src/lib/constants.coffee")];
29
+ delete require.cache[require.resolve("../../src/commands/setup.coffee")];
30
+ const setupLocal = require("../../src/commands/setup.coffee");
31
+
32
+ // run setup
33
+ await setupLocal();
34
+
35
+ const cfgPath = path.join(dir, CONFIG_FILE);
36
+ expect(fs.existsSync(cfgPath)).toBe(true);
37
+
38
+ try {
39
+ fs.rmSync(dir, { recursive: true, force: true });
40
+ } catch (e) {
41
+ /* ignore */
42
+ }
43
+ });
44
+
45
+ it("does not overwrite when prompt says no", async () => {
46
+ const dir = fs.mkdtempSync(
47
+ path.join(require("os").tmpdir(), "milkee-setup-"),
48
+ );
49
+ process.chdir(dir);
50
+
51
+ const cfgPath = path.join(dir, CONFIG_FILE);
52
+ fs.writeFileSync(cfgPath, "original");
53
+
54
+ const consola = require("consola");
55
+ vi.spyOn(consola, "prompt").mockResolvedValue(false);
56
+
57
+ delete require.cache[require.resolve("../../src/lib/constants.coffee")];
58
+ delete require.cache[require.resolve("../../src/commands/setup.coffee")];
59
+ const setupLocal = require("../../src/commands/setup.coffee");
60
+
61
+ await setupLocal();
62
+
63
+ expect(fs.readFileSync(cfgPath, "utf-8")).toBe("original");
64
+
65
+ // try cleanup; ignore errors
66
+ try {
67
+ fs.rmSync(dir, { recursive: true, force: true });
68
+ } catch (e) {}
69
+ });
70
+ });
@@ -0,0 +1,56 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ function collectCoffeeFiles(dir) {
5
+ let out = [];
6
+ const items = fs.readdirSync(dir);
7
+ for (const item of items) {
8
+ const itemPath = path.join(dir, item);
9
+ const stat = fs.statSync(itemPath);
10
+ if (stat.isDirectory()) {
11
+ out = out.concat(collectCoffeeFiles(itemPath));
12
+ } else if (stat.isFile() && itemPath.endsWith(".coffee")) {
13
+ out.push(itemPath);
14
+ }
15
+ }
16
+ return out;
17
+ }
18
+
19
+ const SRC_DIR = path.join(__dirname, "../../src");
20
+ let files = [];
21
+ try {
22
+ files = collectCoffeeFiles(SRC_DIR);
23
+ } catch (e) {
24
+ // If src missing for some reason, test will fail explicitly below
25
+ }
26
+
27
+ // Exclude main.coffee and compile.coffee to avoid executing CLI/long-running flows on require
28
+ files = files.filter((f) => {
29
+ const excluded = [
30
+ path.join("src", "main.coffee"),
31
+ path.join("src", "commands", "compile.coffee"),
32
+ ];
33
+ return !excluded.some((e) => f.endsWith(e));
34
+ });
35
+
36
+ if (files.length === 0) {
37
+ it("has .coffee files under src", () => {
38
+ throw new Error("No .coffee files found under src");
39
+ });
40
+ } else {
41
+ describe("smoke: require all src .coffee files", () => {
42
+ for (const file of files) {
43
+ const rel = path.relative(process.cwd(), file);
44
+ it(`require ${rel}`, () => {
45
+ // clear cache to make require deterministic
46
+ try {
47
+ delete require.cache[require.resolve(file)];
48
+ } catch (e) {}
49
+ expect(() => {
50
+ const mod = require(file);
51
+ expect(mod).not.toBeUndefined();
52
+ }).not.toThrow();
53
+ });
54
+ }
55
+ });
56
+ }
@@ -0,0 +1,41 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+
5
+ const utils = require("../../src/lib/utils.coffee");
6
+
7
+ describe("utils", () => {
8
+ it("sleep resolves after given time", async () => {
9
+ vi.useFakeTimers();
10
+ const p = utils.sleep(100);
11
+ vi.advanceTimersByTime(100);
12
+ await vi.runAllTimersAsync();
13
+ await expect(p).resolves.toBeUndefined();
14
+ vi.useRealTimers();
15
+ });
16
+
17
+ it("getCompiledFiles finds .js and .js.map files recursively", () => {
18
+ // create directory inside test sandbox
19
+ const dir = createTestDir("utils");
20
+ const fileA = path.join(dir, "a.js");
21
+ const fileB = path.join(dir, "b.js.map");
22
+ const fileC = path.join(dir, "c.txt");
23
+ const subdir = path.join(dir, "sub");
24
+ fs.mkdirSync(subdir);
25
+ const fileD = path.join(subdir, "d.js");
26
+ fs.writeFileSync(fileA, "console.log(1)");
27
+ fs.writeFileSync(fileB, "map");
28
+ fs.writeFileSync(fileC, "no");
29
+ fs.writeFileSync(fileD, "console.log(2)");
30
+ const res = utils.getCompiledFiles(dir);
31
+ expect(res).toEqual(expect.arrayContaining([fileA, fileB, fileD]));
32
+ fs.rmSync(dir, { recursive: true, force: true });
33
+ });
34
+
35
+ it("returns empty if path does not exist", () => {
36
+ const res = utils.getCompiledFiles(
37
+ path.join(os.tmpdir(), "nonexistent-" + Date.now()),
38
+ );
39
+ expect(res).toEqual([]);
40
+ });
41
+ });
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ globals: true,
7
+ setupFiles: './test/setup.js',
8
+ },
9
+ coverage: {
10
+ provider: 'v8',
11
+ reporter: ['text', 'lcov'],
12
+ exclude: ['**/*.coffee']
13
+ }
14
+ })