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.
- package/.prettierrc.json +3 -0
- package/README-ja.md +7 -2
- package/README.md +7 -2
- package/dist/lib/utils.js +2 -2
- package/dist/options/copy.js +2 -2
- package/dist/options/refresh.js +1 -1
- package/docs/PLUGIN-ja.md +2 -0
- package/docs/PLUGIN.md +2 -0
- package/package.json +12 -4
- package/test/integration/setup.cli.test.js +36 -0
- package/test/setup.js +113 -0
- package/test/unit/checks.test.js +38 -0
- package/test/unit/copy.test.js +50 -0
- package/test/unit/plugins.test.js +50 -0
- package/test/unit/refresh.test.js +63 -0
- package/test/unit/sandbox-windows.test.js +14 -0
- package/test/unit/setup.test.js +70 -0
- package/test/unit/src.smoke.test.js +56 -0
- package/test/unit/utils.test.js +41 -0
- package/vitest.config.mjs +14 -0
package/.prettierrc.json
ADDED
package/README-ja.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
[English](./README.md) | **日本語**
|
|
2
|
-
|
|
3
1
|
# Milkee
|
|
4
2
|
|
|
3
|
+
[](https://github.com/otoneko1102/coffeescript-milkee/actions)
|
|
4
|
+
[](https://www.npmjs.com/package/milkee)
|
|
5
|
+
[](https://www.npmjs.com/package/milkee)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
[English](./README.md) | **日本語**
|
|
9
|
+
|
|
5
10
|

|
|
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
|
+
[](https://github.com/otoneko1102/coffeescript-milkee/actions)
|
|
4
|
+
[](https://www.npmjs.com/package/milkee)
|
|
5
|
+
[](https://www.npmjs.com/package/milkee)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
**English** | [日本語](./README-ja.md)
|
|
9
|
+
|
|
5
10
|

|
|
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.
|
|
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) {
|
package/dist/options/copy.js
CHANGED
|
@@ -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;
|
package/dist/options/refresh.js
CHANGED
|
@@ -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
package/docs/PLUGIN.md
CHANGED
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "milkee",
|
|
3
|
-
"version": "3.
|
|
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": "
|
|
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://
|
|
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
|
+
})
|