milkee 3.0.0 → 3.0.1

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 ADDED
@@ -0,0 +1,160 @@
1
+ # Milkee
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
+
10
+ ![Milkee logo](./img/Milkee-logo.png)
11
+
12
+ `coffee.config.cjs` を使ったシンプルな CoffeeScript ビルドツール
13
+
14
+ ## はじめに
15
+
16
+ ### インストール
17
+
18
+ Milkee をインストールします:
19
+
20
+ ```bash
21
+ # グローバルインストール
22
+ npm i -g milkee
23
+
24
+ # ローカルインストール
25
+ npm i -D milkee
26
+ ```
27
+
28
+ CoffeeScript と `@babel/core` が必要です。
29
+
30
+ > [!TIP]
31
+ > `options.transpile` が `true` の場合は `@babel/core` が必要です。
32
+
33
+ ```bash
34
+ # グローバルインストール
35
+ npm i -g coffeescript @babel/core
36
+
37
+ # ローカルインストール
38
+ npm i -D coffeescript @babel/core
39
+ ```
40
+
41
+ ### セットアップ
42
+
43
+ `-s` (`--setup`) コマンドを実行すると、`coffee.config.cjs` が生成されます!
44
+
45
+ ```bash
46
+ # グローバル
47
+ milkee -s
48
+
49
+ # ローカル
50
+ npx milkee -s
51
+ ```
52
+
53
+ #### `coffee.config.cjs`
54
+
55
+ ```js
56
+ /** @type {import('@milkee/d').Config} */
57
+
58
+ module.exports = {
59
+ // コンパイルのエントリーポイント。
60
+ // 単一ファイルまたはディレクトリ(例: 'src/' や 'src/app.coffee')を指定できます。
61
+ entry: 'src',
62
+ // コンパイルされた JavaScript ファイルの出力先。
63
+ // `options.join` が true の場合は単一ファイルパス(例: 'dist/app.js')
64
+ // `options.join` が false の場合はディレクトリ(例: 'dist')を指定します。
65
+ output: 'dist',
66
+ // (任意) CoffeeScript コンパイラの追加オプション。
67
+ // 利用可能な全オプションは `coffee --help` を参照してください。
68
+ // Web: https://coffeescript.org/annotated-source/command.html
69
+ options: {
70
+ // 以下のオプションがサポートされています:
71
+ // bare: false,
72
+ // join: false,
73
+ // map: false,
74
+ // inlineMap: false,
75
+ // noHeader: false,
76
+ // transpile: false,
77
+ // literate: false,
78
+ // watch: false,
79
+ },
80
+ // (任意) Milkee ビルダーの追加オプション / プラグイン。
81
+ milkee: {
82
+ options: {
83
+ // コンパイル前に出力ディレクトリをリセットします。
84
+ // refresh: false,
85
+ // コンパイル前に「実行しますか?」と確認します。
86
+ // confirm: false,
87
+ // コンパイル後に entry から output へ非 CoffeeScript ファイルをコピーします。(`options.join` が false の場合のみ有効)
88
+ // copy: false,
89
+ },
90
+ plugins: []
91
+ },
92
+ };
93
+ ```
94
+
95
+ ##### `options` (CoffeeScript コンパイラオプション)
96
+
97
+ これらのオプションは `coffee` コンパイラに直接渡されます。
98
+
99
+ | オプション | 型 | デフォルト | 説明 |
100
+ | :--- | :--- | :--- | :--- |
101
+ | `bare` | `boolean` | `false` | トップレベルの関数ラッパーなしでコンパイルする |
102
+ | `join` | `boolean` | `false` | コンパイル前にソースを連結する |
103
+ | `map` | `boolean` | `false` | ソースマップを生成して `.js.map` として保存する |
104
+ | `inlineMap` | `boolean` | `false` | ソースマップを生成して出力内に直接含める |
105
+ | `noHeader` | `boolean` | `false` | "Generated by" ヘッダを抑制する |
106
+ | `transpile` | `boolean` | `false` | 生成された JavaScript を Babel に通す |
107
+ | `literate` | `boolean` | `false` | 標準入力をリテラート形式の CoffeeScript として扱う |
108
+ | `watch` | `boolean` | `false` | ファイル変更を監視してコマンドを再実行する |
109
+
110
+ [CoffeeScript - command.coffee](https://coffeescript.org/annotated-source/command.html)
111
+
112
+ ##### `milkee.options` (Milkee 固有オプション)
113
+
114
+ これらのオプションは Milkee の動作を制御します。
115
+
116
+ | オプション | 型 | デフォルト | 説明 |
117
+ | :--- | :--- | :--- | :--- |
118
+ | `refresh` | `boolean` | `false` | コンパイル前に出力ディレクトリをリセットする |
119
+ | `confirm` | `boolean` | `false` | コンパイル前に「実行しますか?」と確認する |
120
+ | `copy` | `boolean` | `false` | コンパイル後に entry から output へ非 CoffeeScript ファイルをコピーする(`options.join` が false の場合のみ) |
121
+
122
+ ##### `milkee.plugins` (Milkee 固有プラグイン)
123
+
124
+ Milkee の機能はプラグインで拡張できます。プラグインは、各コンパイル成功後に実行され、コンパイル済みファイルと設定にアクセスできる単純な関数です。
125
+
126
+ 例:
127
+
128
+ ```js
129
+ const myPlugin = require('./plugins/my-plugin.js');
130
+
131
+ module.exports = {
132
+ // ...
133
+ milkee: {
134
+ plugins: [
135
+ // この呼び出しは PluginExecutor を返します
136
+ myPlugin({ option: 'value' }),
137
+ // ...
138
+ ]
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### コンパイル
144
+
145
+ Milkee は自動で `coffee.config.cjs` を読み、`options` からコマンドを組み立ててコンパイルを開始します!
146
+
147
+ ```bash
148
+ # グローバル
149
+ milkee
150
+
151
+ # ローカル
152
+ npx milkee
153
+ ```
154
+
155
+ ## プラグインの作成
156
+
157
+ 独自の Milkee プラグインを作成したいですか?プラグインのドキュメントを確認してください:
158
+
159
+ - [English](./docs/PLUGIN.md)
160
+ - [日本語](./docs/PLUGIN-ja.md)
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Milkee
2
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
+
3
10
  ![Milkee logo](./img/Milkee-logo.png)
4
11
 
5
12
  A simple CoffeeScript build tool with [coffee.config.cjs](./temp/coffee.config.cjs)
@@ -89,16 +96,16 @@ module.exports = {
89
96
 
90
97
  These options are passed directly to the `coffee` compiler.
91
98
 
92
- | Option | Type | Default | Description |
93
- | :---------- | :-------- | :------ | :------------------------------------------------------ |
94
- | `bare` | `boolean` | `false` | compile without a top-level function wrapper |
95
- | `join` | `boolean` | `false` | concatenate the source CoffeeScript before compiling |
96
- | `map` | `boolean` | `false` | generate source map and save as `.js.map` files |
97
- | `inlineMap` | `boolean` | `false` | generate source map and include it directly in output |
98
- | `noHeader` | `boolean` | `false` | suppress the "Generated by" header |
99
- | `transpile` | `boolean` | `false` | pipe generated JavaScript through Babel |
100
- | `literate` | `boolean` | `false` | treat stdio as literate style coffeescript |
101
- | `watch` | `boolean` | `false` | watch scripts for changes and rerun commands |
99
+ | Option | Type | Default | Description |
100
+ | :--- | :--- | :--- | :--- |
101
+ | `bare` | `boolean` | `false` | compile without a top-level function wrapper |
102
+ | `join` | `boolean` | `false` | concatenate the source CoffeeScript before compiling |
103
+ | `map` | `boolean` | `false` | generate source map and save as `.js.map` files |
104
+ | `inlineMap` | `boolean` | `false` | generate source map and include it directly in output |
105
+ | `noHeader` | `boolean` | `false` | suppress the "Generated by" header |
106
+ | `transpile` | `boolean` | `false` | pipe generated JavaScript through Babel |
107
+ | `literate` | `boolean` | `false` | treat stdio as literate style coffeescript |
108
+ | `watch` | `boolean` | `false` | watch scripts for changes and rerun commands |
102
109
 
103
110
  [CoffeeScript - command.coffee](https://coffeescript.org/annotated-source/command.html)
104
111
 
@@ -106,11 +113,11 @@ These options are passed directly to the `coffee` compiler.
106
113
 
107
114
  These options control Milkee's behavior.
108
115
 
109
- | Option | Type | Default | Description |
110
- | :-------- | :-------- | :------ | :----------------------------------------------------- |
111
- | `refresh` | `boolean` | `false` | Before compiling, reset the output directory. |
112
- | `confirm` | `boolean` | `false` | Before compiling, prompt "Do you want to Continue?". |
113
- | `copy` | `boolean` | `false` | After compiling, copy non-coffee files from entry to output directory. (Only works when `options.join` is `false`) |
116
+ | Option | Type | Default | Description |
117
+ | :--- | :--- | :--- | :--- |
118
+ | `refresh` | `boolean` | `false` | Before compiling, reset the output directory. |
119
+ | `confirm` | `boolean` | `false` | Before compiling, prompt "Do you want to Continue?". |
120
+ | `copy` | `boolean` | `false` | After compiling, copy non-coffee files from entry to output directory. (Only works when `options.join` is `false`) |
114
121
 
115
122
  ##### `milkee.plugins` (Milkee Specific Plugins)
116
123
 
@@ -11,11 +11,11 @@ crypto = require('crypto');
11
11
 
12
12
  consola = require('consola');
13
13
 
14
- ({CWD, CONFIG_PATH, CONFIG_FILE} = require('../constants'));
14
+ ({CWD, CONFIG_PATH, CONFIG_FILE} = require('../lib/constants'));
15
15
 
16
- ({checkLatest, checkCoffee} = require('../checks'));
16
+ ({checkLatest, checkCoffee} = require('../lib/checks'));
17
17
 
18
- ({runPlugins} = require('../plugins'));
18
+ ({runPlugins} = require('../lib/plugins'));
19
19
 
20
20
  confirmContinue = require('../options/confirm');
21
21
 
@@ -10,7 +10,7 @@ path = require('path');
10
10
 
11
11
  consola = require('consola');
12
12
 
13
- ({CWD, CONFIG_FILE} = require('../constants'));
13
+ ({CWD, CONFIG_FILE} = require('../lib/constants'));
14
14
 
15
15
  confirmContinue = require('../options/confirm');
16
16
 
@@ -7,9 +7,9 @@ path = require('path');
7
7
 
8
8
  consola = require('consola');
9
9
 
10
- ({CONFIG_FILE, CONFIG_PATH} = require('../constants'));
10
+ ({CONFIG_FILE, CONFIG_PATH} = require('../lib/constants'));
11
11
 
12
- ({checkCoffee} = require('../checks'));
12
+ ({checkCoffee} = require('../lib/checks'));
13
13
 
14
14
  // async
15
15
  setup = async function() {
@@ -0,0 +1,58 @@
1
+ // Generated by CoffeeScript 2.7.0
2
+ var CWD, checkCoffee, checkLatest, consola, exec, fs, isPackageLatest, path, pkg;
3
+
4
+ fs = require('fs');
5
+
6
+ path = require('path');
7
+
8
+ ({exec} = require('child_process'));
9
+
10
+ consola = require('consola');
11
+
12
+ ({isPackageLatest} = require('is-package-latest'));
13
+
14
+ ({pkg, CWD} = require('./constants'));
15
+
16
+ // async
17
+ checkLatest = async function() {
18
+ var res;
19
+ try {
20
+ res = (await isPackageLatest(pkg));
21
+ if (res.success && !res.isLatest) {
22
+ consola.box({
23
+ title: "A new version is now available!",
24
+ message: `${res.currentVersion} --> \`${res.latestVersion}\`\n\n# global installation\n\`npm i -g milkee@latest\`\n# or local installation\n\`npm i -D milkee@latest\``
25
+ });
26
+ return true;
27
+ } else {
28
+ return false;
29
+ }
30
+ } catch (error1) {
31
+ return false;
32
+ }
33
+ };
34
+
35
+ checkCoffee = function() {
36
+ var PKG_PATH, error, pkgData, pkgFile, ref, ref1;
37
+ PKG_PATH = path.join(CWD, 'package.json');
38
+ if (fs.existsSync(PKG_PATH)) {
39
+ try {
40
+ pkgFile = fs.readFileSync(PKG_PATH, 'utf-8');
41
+ pkgData = JSON.parse(pkgFile);
42
+ if (((ref = pkgData.dependencies) != null ? ref.coffeescript : void 0) || ((ref1 = pkgData.devDependencies) != null ? ref1.coffeescript : void 0)) {
43
+ return;
44
+ }
45
+ } catch (error1) {
46
+ error = error1;
47
+ consola.warn(`Could not parse \`package.json\`: ${error.message}`);
48
+ }
49
+ }
50
+ return exec('coffee --version', function(error) {
51
+ if (error) {
52
+ consola.warn('CoffeeScript is not found in local dependencies (`dependencies`, `devDependencies`) or globally.');
53
+ return consola.info('Please install it via `npm install --save-dev coffeescript` to continue.');
54
+ }
55
+ });
56
+ };
57
+
58
+ module.exports = {checkLatest, checkCoffee};
@@ -0,0 +1,14 @@
1
+ // Generated by CoffeeScript 2.7.0
2
+ var CONFIG_FILE, CONFIG_PATH, CWD, path, pkg;
3
+
4
+ path = require('path');
5
+
6
+ pkg = require('../../package.json');
7
+
8
+ CWD = process.cwd();
9
+
10
+ CONFIG_FILE = 'coffee.config.cjs';
11
+
12
+ CONFIG_PATH = path.join(CWD, CONFIG_FILE);
13
+
14
+ module.exports = {pkg, CWD, CONFIG_FILE, CONFIG_PATH};
@@ -0,0 +1,59 @@
1
+ // Generated by CoffeeScript 2.7.0
2
+ var CWD, consola, executePlugins, fs, getCompiledFiles, path, runPlugins;
3
+
4
+ path = require('path');
5
+
6
+ fs = require('fs');
7
+
8
+ consola = require('consola');
9
+
10
+ ({CWD} = require('./constants'));
11
+
12
+ ({getCompiledFiles} = require('./utils'));
13
+
14
+ executePlugins = function(config, compilationResult) {
15
+ var plugins, ref;
16
+ plugins = ((ref = config.milkee) != null ? ref.plugins : void 0) || [];
17
+ if (!(plugins.length > 0)) {
18
+ return;
19
+ }
20
+ consola.start(`Running ${plugins.length} plugin(s)...`);
21
+ return (async function() { // async
22
+ var error, i, len, pluginFn;
23
+ try {
24
+ for (i = 0, len = plugins.length; i < len; i++) {
25
+ pluginFn = plugins[i];
26
+ if (typeof pluginFn === 'function') {
27
+ await Promise.resolve(pluginFn(compilationResult));
28
+ } else {
29
+ consola.warn(`Invalid plugin definition skipped (expected a function, got ${typeof pluginFn}).`);
30
+ }
31
+ }
32
+ return consola.success("Plugins executed successfully.");
33
+ } catch (error1) {
34
+ error = error1;
35
+ return consola.error("An error occurred during plugin execution:", error);
36
+ }
37
+ })();
38
+ };
39
+
40
+ runPlugins = function(config, options, stdout = '', stderr = '') {
41
+ var compilationResult, compiledFiles, mapPath, outputPath;
42
+ outputPath = path.join(CWD, config.output);
43
+ compiledFiles = getCompiledFiles(outputPath);
44
+ if (options.join && options.map && !options.inlineMap) {
45
+ mapPath = `${outputPath}.map`;
46
+ if (fs.existsSync(mapPath && !compiledFiles.includes(mapPath))) {
47
+ compiledFiles = compiledFiles.concat(getCompiledFiles(mapPath));
48
+ }
49
+ }
50
+ compilationResult = {
51
+ config: config,
52
+ compiledFiles: compiledFiles,
53
+ stdout: stdout,
54
+ stderr: stderr
55
+ };
56
+ return executePlugins(config, compilationResult);
57
+ };
58
+
59
+ module.exports = {runPlugins};
@@ -0,0 +1,48 @@
1
+ // Generated by CoffeeScript 2.7.0
2
+ var consola, fs, getCompiledFiles, path, sleep;
3
+
4
+ fs = require('fs');
5
+
6
+ path = require('path');
7
+
8
+ consola = require('consola');
9
+
10
+ sleep = function(time) {
11
+ return new Promise(function(resolve) {
12
+ return setTimeout(resolve, time);
13
+ });
14
+ };
15
+
16
+ getCompiledFiles = function(targetPath) {
17
+ var error, filesList, i, item, itemPath, items, len, stat;
18
+ filesList = [];
19
+ if (!fs.existsSync(targetPath)) {
20
+ consola.warn(`Path does not exist, skipping scan ${targetPath}`);
21
+ return [];
22
+ }
23
+ try {
24
+ stat = fs.statSync(targetPath);
25
+ if (stat.isDirectory()) {
26
+ consola.start(`Scanning directory: ${targetPath}`);
27
+ items = fs.readdirSync(targetPath);
28
+ for (i = 0, len = items.length; i < len; i++) {
29
+ item = items[i];
30
+ itemPath = path.join(targetPath, item);
31
+ filesList = filesList.concat(getCompiledFiles(itemPath));
32
+ }
33
+ } else if (stat.isFile()) {
34
+ if (targetPath.match(/\.js(?:\.map)?$/i)) {
35
+ if (fs.existsSync(targetPath)) {
36
+ consola.info(`Found file: \`${targetPath}\``);
37
+ filesList.push(targetPath);
38
+ }
39
+ }
40
+ }
41
+ } catch (error1) {
42
+ error = error1;
43
+ consola.warn(`Could not scan output path ${targetPath}: ${error.message}`);
44
+ }
45
+ return filesList;
46
+ };
47
+
48
+ module.exports = {sleep, getCompiledFiles};
package/dist/main.js CHANGED
@@ -5,7 +5,7 @@ yargs = require('yargs');
5
5
 
6
6
  ({hideBin} = require('yargs/helpers'));
7
7
 
8
- ({pkg} = require('./constants'));
8
+ ({pkg} = require('./lib/constants'));
9
9
 
10
10
  setup = require('./commands/setup');
11
11
 
@@ -7,12 +7,12 @@ path = require('path');
7
7
 
8
8
  consola = require('consola');
9
9
 
10
- ({CWD} = require('../constants'));
10
+ ({CWD} = require('../lib/constants'));
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;
@@ -9,12 +9,12 @@ crypto = require('crypto');
9
9
 
10
10
  consola = require('consola');
11
11
 
12
- ({CWD} = require('../constants'));
12
+ ({CWD} = require('../lib/constants'));
13
13
 
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
@@ -71,12 +71,12 @@ module.exports = main
71
71
 
72
72
  Milkee はコンパイル後にこのオブジェクトをプラグインに渡します:
73
73
 
74
- | プロパティ | 型 | 説明 |
75
- | :-------------- | :--------- | :--------------------------------------------- |
76
- | `config` | `object` | `coffee.config.cjs` の設定オブジェクト |
77
- | `compiledFiles` | `string[]` | コンパイルされた `.js` と `.js.map` のパス |
78
- | `stdout` | `string` | コンパイラの標準出力 |
79
- | `stderr` | `string` | コンパイラの標準エラー |
74
+ | プロパティ | 型 | 説明 |
75
+ | :--- | :--- | :--- |
76
+ | `config` | `object` | `coffee.config.cjs` の設定オブジェクト |
77
+ | `compiledFiles` | `string[]` | コンパイルされた `.js` と `.js.map` のパス |
78
+ | `stdout` | `string` | コンパイラの標準出力 |
79
+ | `stderr` | `string` | コンパイラの標準エラー |
80
80
 
81
81
  ### coffee.config.cjs での使用
82
82
 
package/docs/PLUGIN.md CHANGED
@@ -71,12 +71,12 @@ module.exports = main
71
71
 
72
72
  Milkee passes this object to your plugin after compilation:
73
73
 
74
- | Property | Type | Description |
75
- | :-------------- | :--------- | :--------------------------------------------- |
76
- | `config` | `object` | Full config from `coffee.config.cjs` |
77
- | `compiledFiles` | `string[]` | Paths to compiled `.js` and `.js.map` files |
78
- | `stdout` | `string` | Compiler standard output |
79
- | `stderr` | `string` | Compiler standard error |
74
+ | Property | Type | Description |
75
+ | :--- | :--- | :--- |
76
+ | `config` | `object` | Full config from `coffee.config.cjs` |
77
+ | `compiledFiles` | `string[]` | Paths to compiled `.js` and `.js.map` files |
78
+ | `stdout` | `string` | Compiler standard output |
79
+ | `stderr` | `string` | Compiler standard error |
80
80
 
81
81
  ### Using in coffee.config.cjs
82
82
 
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "milkee",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
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",
11
- "build": "coffee --output dist/ --compile --bare src/"
10
+ "test": "prettier --write ./test && vitest",
11
+ "test:ci": "vitest --run",
12
+ "coverage": "vitest --run --coverage",
13
+ "build": "coffee --output dist/ --compile --bare src/",
14
+ "format": "node ./scripts/format.js",
15
+ "deprecate:dev": "node ./scripts/deprecate-dev-versions.js"
12
16
  },
13
17
  "repository": {
14
18
  "type": "git",
@@ -40,6 +44,12 @@
40
44
  "devDependencies": {
41
45
  "@babel/core": "^7.28.5",
42
46
  "@types/coffeescript": "^2.5.7",
43
- "coffeescript": "^2.7.0"
47
+ "@vitest/coverage-v8": "^4.0.17",
48
+ "coffee-fmt": "^0.12.0",
49
+ "coffeescript": "^2.7.0",
50
+ "dotenv": "^17.2.3",
51
+ "execa": "^9.6.1",
52
+ "prettier": "^3.8.0",
53
+ "vitest": "^4.0.17"
44
54
  }
45
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
+ })