@withstudiocms/buildkit 0.1.0-beta.2 → 0.1.0-beta.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.
- package/CHANGELOG.md +45 -0
- package/package.json +10 -10
- package/src/cmds/builder.js +266 -0
- package/src/cmds/help.js +54 -0
- package/src/cmds/test.js +141 -0
- package/src/index.js +30 -0
- package/index.js +0 -212
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @withstudiocms/buildkit
|
|
2
|
+
|
|
3
|
+
## 0.1.0-beta.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#684](https://github.com/withstudiocms/studiocms/pull/684) [`15e6ee0`](https://github.com/withstudiocms/studiocms/commit/15e6ee0c50e37b22bcb24a0b67403e357e2502db) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - fix: add shebang to builder, help, test, and index scripts for executable compatibility
|
|
8
|
+
|
|
9
|
+
- [#711](https://github.com/withstudiocms/studiocms/pull/711) [`22d748f`](https://github.com/withstudiocms/studiocms/commit/22d748f445b53bc340aad9a99ac4ebac6b0e9d7c) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - Adds copy support for `.stub.js`
|
|
10
|
+
|
|
11
|
+
- [#680](https://github.com/withstudiocms/studiocms/pull/680) [`9c66603`](https://github.com/withstudiocms/studiocms/commit/9c6660397bc3a8c952713e7587df507b8c6d3d17) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - Refactors buildkit to be broken out into individual files to help with maintainability
|
|
12
|
+
|
|
13
|
+
## 0.1.0-beta.3
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#669](https://github.com/withstudiocms/studiocms/pull/669) [`d757989`](https://github.com/withstudiocms/studiocms/commit/d75798912aaadab3d874c48176f2f9902bfa8502) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - Implements node testing system for usage by StudioCMS and related packages
|
|
18
|
+
|
|
19
|
+
## 0.1.0-beta.2
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- [#643](https://github.com/withstudiocms/studiocms/pull/643) [`9cfba9a`](https://github.com/withstudiocms/studiocms/commit/9cfba9ad57f8fb1b2a10081fbe5f9dfc26bed57d) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - Add font file copy support to build-kit
|
|
24
|
+
|
|
25
|
+
- [#650](https://github.com/withstudiocms/studiocms/pull/650) [`3e7f7ca`](https://github.com/withstudiocms/studiocms/commit/3e7f7ca6ea2a304fe66eac95496542cc50169eb2) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - Update various deps and lint repo with updated biome version
|
|
26
|
+
|
|
27
|
+
- [#657](https://github.com/withstudiocms/studiocms/pull/657) [`a05bb16`](https://github.com/withstudiocms/studiocms/commit/a05bb16d3dd0d1a429558b4dce316ad7fb80b049) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - Update esbuild
|
|
28
|
+
|
|
29
|
+
- [#648](https://github.com/withstudiocms/studiocms/pull/648) [`e490385`](https://github.com/withstudiocms/studiocms/commit/e490385dbdad5392f23c46a832c8a555dbf48a9a) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - Update package.json to conform to new repository structure
|
|
30
|
+
|
|
31
|
+
## 0.1.0-beta.1
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- [#520](https://github.com/withstudiocms/studiocms/pull/520) [`940abc0`](https://github.com/withstudiocms/studiocms/commit/940abc014460b6c8cf4c5e9a0291e06a1f416f18) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - add support for additional image file types in asset handling and add README
|
|
36
|
+
|
|
37
|
+
- [#519](https://github.com/withstudiocms/studiocms/pull/519) [`1f11779`](https://github.com/withstudiocms/studiocms/commit/1f11779078c58cc1fd42f63af6f62d0ae315478a) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - add license
|
|
38
|
+
|
|
39
|
+
- [#512](https://github.com/withstudiocms/studiocms/pull/512) [`cd10407`](https://github.com/withstudiocms/studiocms/commit/cd1040779926a55db63ceb6ac1b9ddacb23330a8) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - Initial release, publish buildkit for usage with other withstudiocms repos
|
|
40
|
+
|
|
41
|
+
- [#541](https://github.com/withstudiocms/studiocms/pull/541) [`5dea0d4`](https://github.com/withstudiocms/studiocms/commit/5dea0d4419d358b22858ab7455bbbb96f5b01e95) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - fix buildkit help: Show correct command name
|
|
42
|
+
|
|
43
|
+
- [#540](https://github.com/withstudiocms/studiocms/pull/540) [`ae5c71a`](https://github.com/withstudiocms/studiocms/commit/ae5c71a49a777a2a7d4b322a3cb76978650d53de) Thanks [@dreyfus92](https://github.com/dreyfus92)! - Adds help command to the CLI buildkit.
|
|
44
|
+
|
|
45
|
+
- [#521](https://github.com/withstudiocms/studiocms/pull/521) [`dec39cc`](https://github.com/withstudiocms/studiocms/commit/dec39cc28fc557586f61472c1bf7953e8bd5c5a0) Thanks [@Adammatthiesen](https://github.com/Adammatthiesen)! - Simplify buildkit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withstudiocms/buildkit",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
4
|
"description": "Build kit based on esbuild for the withstudiocms github org",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "withstudiocms",
|
|
@@ -13,29 +13,29 @@
|
|
|
13
13
|
},
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"files": [
|
|
16
|
-
"
|
|
17
|
-
"
|
|
16
|
+
"src",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
18
20
|
],
|
|
19
21
|
"type": "module",
|
|
20
|
-
"main": "./index.js",
|
|
22
|
+
"main": "./src/index.js",
|
|
21
23
|
"publishConfig": {
|
|
22
24
|
"access": "public",
|
|
23
25
|
"provenance": true
|
|
24
26
|
},
|
|
25
27
|
"bin": {
|
|
26
|
-
"buildkit": "./index.js"
|
|
28
|
+
"buildkit": "./src/index.js"
|
|
27
29
|
},
|
|
28
30
|
"dependencies": {
|
|
29
31
|
"esbuild": "^0.25.8",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
+
"tinyglobby": "^0.2.14",
|
|
33
|
+
"chalk": "^5.6.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
|
-
"vitest": "^3.2.4",
|
|
35
36
|
"strip-ansi": "^7.1.0"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"test": "
|
|
39
|
-
"test:watch": "vitest"
|
|
39
|
+
"test": "buildkit test 'test/**/*.test.js'"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import esbuild from 'esbuild';
|
|
5
|
+
import { glob } from 'tinyglobby';
|
|
6
|
+
|
|
7
|
+
/** @type {import('esbuild').BuildOptions} */
|
|
8
|
+
const defaultConfig = {
|
|
9
|
+
minify: false,
|
|
10
|
+
format: 'esm',
|
|
11
|
+
platform: 'node',
|
|
12
|
+
target: 'node18',
|
|
13
|
+
sourcemap: false,
|
|
14
|
+
sourcesContent: false,
|
|
15
|
+
loader: {
|
|
16
|
+
'.astro': 'copy',
|
|
17
|
+
'.json': 'copy',
|
|
18
|
+
'.gif': 'copy',
|
|
19
|
+
'.jpeg': 'copy',
|
|
20
|
+
'.jpg': 'copy',
|
|
21
|
+
'.png': 'copy',
|
|
22
|
+
'.tiff': 'copy',
|
|
23
|
+
'.webp': 'copy',
|
|
24
|
+
'.avif': 'copy',
|
|
25
|
+
'.svg': 'copy',
|
|
26
|
+
'.woff2': 'copy',
|
|
27
|
+
'.woff': 'copy',
|
|
28
|
+
'.ttf': 'copy',
|
|
29
|
+
'.eot': 'copy',
|
|
30
|
+
'.otf': 'copy',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Plugin to copy *.stub.(js|mjs|cjs) without parsing/bundling.
|
|
36
|
+
* NOTE: Importing these will yield a file URL string at runtime, not executed JS.
|
|
37
|
+
* @type {import('esbuild').Plugin} The esbuild plugin for generating TypeScript declarations.
|
|
38
|
+
*/
|
|
39
|
+
const copyStubJsPlugin = {
|
|
40
|
+
name: 'copy-stub-js',
|
|
41
|
+
setup(build) {
|
|
42
|
+
// Match both entry points and imported modules
|
|
43
|
+
const filter = /\.stub\.(?:js|mjs|cjs)$/;
|
|
44
|
+
build.onLoad({ filter }, async (args) => {
|
|
45
|
+
const contents = await fs.readFile(args.path);
|
|
46
|
+
return { contents, loader: 'copy' };
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Plugin to copy *.d.ts without parsing/bundling.
|
|
53
|
+
* NOTE: Importing these will yield a file URL string at runtime, not executed JS.
|
|
54
|
+
* @type {import('esbuild').Plugin} The esbuild plugin for generating TypeScript declarations.
|
|
55
|
+
*/
|
|
56
|
+
const copyDTSPlugin = {
|
|
57
|
+
name: 'copy-dts',
|
|
58
|
+
setup(build) {
|
|
59
|
+
// Match both entry points and imported modules
|
|
60
|
+
const filter = /\.d\.ts$/;
|
|
61
|
+
build.onLoad({ filter }, async (args) => {
|
|
62
|
+
const contents = await fs.readFile(args.path);
|
|
63
|
+
return { contents, loader: 'copy' };
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const copyPlugins = [copyStubJsPlugin, copyDTSPlugin];
|
|
69
|
+
|
|
70
|
+
// DateTime format for logging
|
|
71
|
+
/**
|
|
72
|
+
* @type {Intl.DateTimeFormat}
|
|
73
|
+
*/
|
|
74
|
+
const dt = new Intl.DateTimeFormat('en-us', {
|
|
75
|
+
hour: '2-digit',
|
|
76
|
+
minute: '2-digit',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** * Plugin to generate TypeScript declarations using the TypeScript compiler.
|
|
80
|
+
* @param {string} buildTsConfig - The path to the TypeScript configuration file.
|
|
81
|
+
* @param {string} outdir - The output directory for the generated declarations.
|
|
82
|
+
* @returns {import('esbuild').Plugin} The esbuild plugin for generating TypeScript declarations.
|
|
83
|
+
*/
|
|
84
|
+
const dtsGen = (buildTsConfig, outdir) => ({
|
|
85
|
+
name: 'TypeScriptDeclarationsPlugin',
|
|
86
|
+
setup(build) {
|
|
87
|
+
build.onEnd((result) => {
|
|
88
|
+
if (result.errors.length > 0) return;
|
|
89
|
+
const date = dt.format(new Date());
|
|
90
|
+
console.log(`${chalk.dim(`[${date}]`)} Generating TypeScript declarations...`);
|
|
91
|
+
try {
|
|
92
|
+
const res = execFileSync(
|
|
93
|
+
'tsc',
|
|
94
|
+
['--emitDeclarationOnly', '-p', buildTsConfig, '--outDir', `./${outdir}`],
|
|
95
|
+
{ encoding: 'utf8' }
|
|
96
|
+
);
|
|
97
|
+
if (res) console.log(res);
|
|
98
|
+
console.log(chalk.dim(`[${date}] `) + chalk.green('√ Generated TypeScript declarations'));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const msg =
|
|
101
|
+
(error && (error.message || String(error))) +
|
|
102
|
+
'\n\n' +
|
|
103
|
+
// stdout/stderr may be Buffer or string depending on exec options
|
|
104
|
+
(typeof error?.stdout === 'string' ? error.stdout : (error?.stdout?.toString?.() ?? '')) +
|
|
105
|
+
(typeof error?.stderr === 'string' ? error.stderr : (error?.stderr?.toString?.() ?? ''));
|
|
106
|
+
console.error(chalk.dim(`[${date}] `) + chalk.red(msg));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/** * Clean the output directory by removing all files except those specified in the skip array.
|
|
113
|
+
* @param {string} outdir - The output directory to clean.
|
|
114
|
+
* @param {string} date - The date string for logging.
|
|
115
|
+
* @param {string[]} skip - An array of glob patterns to skip when cleaning.
|
|
116
|
+
* @throws {Error} If the glob operation fails or if there are issues removing files.
|
|
117
|
+
*/
|
|
118
|
+
async function clean(outdir, date, skip = []) {
|
|
119
|
+
const files = await glob([`${outdir}/**`, ...skip], { filesOnly: true });
|
|
120
|
+
console.log(chalk.dim(`[${date}] `) + chalk.dim(`Cleaning ${files.length} files from ${outdir}`));
|
|
121
|
+
await Promise.all(files.map((file) => fs.rm(file, { force: true })));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** * Read and parse the package.json file.
|
|
125
|
+
* @param {string} path - The path to the package.json file.
|
|
126
|
+
* @returns {Promise<Object>} The parsed JSON object.
|
|
127
|
+
* @throws {Error} If the file cannot be read or is not valid JSON.
|
|
128
|
+
*/
|
|
129
|
+
async function readPackageJSON(path) {
|
|
130
|
+
try {
|
|
131
|
+
const content = await fs.readFile(path, { encoding: 'utf8' });
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(content);
|
|
134
|
+
} catch (parseError) {
|
|
135
|
+
throw new Error(`Invalid JSON in ${path}: ${parseError.message}`);
|
|
136
|
+
}
|
|
137
|
+
} catch (readError) {
|
|
138
|
+
throw new Error(`Failed to read ${path}: ${readError.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run the dev or build command with the provided arguments.
|
|
144
|
+
* @param {string} cmd - The command to run ('dev' or 'build').
|
|
145
|
+
* @param {string[]} args - The arguments to pass to the command.
|
|
146
|
+
*/
|
|
147
|
+
export default async function builder(cmd, args) {
|
|
148
|
+
const config = Object.assign({}, defaultConfig);
|
|
149
|
+
const patterns = args
|
|
150
|
+
.filter((f) => !!f) // remove empty args
|
|
151
|
+
.map((f) => f.replace(/^'/, '').replace(/'$/, '')); // Needed for Windows: glob strings contain surrounding string chars??? remove these
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Collect all entry points based on the provided patterns.
|
|
155
|
+
* @type {string[]}
|
|
156
|
+
*/
|
|
157
|
+
const entryPoints = [].concat(
|
|
158
|
+
...(await Promise.all(
|
|
159
|
+
patterns.map((pattern) => glob(pattern, { filesOnly: true, absolute: true }))
|
|
160
|
+
))
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (entryPoints.length === 0) {
|
|
164
|
+
throw new Error(`No entry points found for pattern(s): ${patterns.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const date = dt.format(new Date());
|
|
168
|
+
|
|
169
|
+
const noClean = args.includes('--no-clean-dist');
|
|
170
|
+
const bundle = args.includes('--bundle');
|
|
171
|
+
const forceCJS = args.includes('--force-cjs');
|
|
172
|
+
const buildTsConfig =
|
|
173
|
+
args.find((arg) => arg.startsWith('--tsconfig='))?.split('=')[1] || 'tsconfig.json';
|
|
174
|
+
const outdir = args.find((arg) => arg.startsWith('--outdir='))?.split('=')[1] || 'dist';
|
|
175
|
+
|
|
176
|
+
const { type = 'module', dependencies = {} } = await readPackageJSON('./package.json');
|
|
177
|
+
|
|
178
|
+
const format = type === 'module' && !forceCJS ? 'esm' : 'cjs';
|
|
179
|
+
|
|
180
|
+
switch (cmd) {
|
|
181
|
+
case 'dev': {
|
|
182
|
+
if (!noClean) {
|
|
183
|
+
console.log(
|
|
184
|
+
`${chalk.dim(`[${date}]`)} Cleaning ${outdir} directory... ${chalk.dim(`(${entryPoints.length} files found)`)}`
|
|
185
|
+
);
|
|
186
|
+
await clean(outdir, date, [`!${outdir}/**/*.d.ts`]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Plugin to handle rebuilds during development.
|
|
191
|
+
* It logs the result of the build process and any warnings or errors.
|
|
192
|
+
* @type {import('esbuild').Plugin}
|
|
193
|
+
* @description This plugin is used to provide feedback during development builds.
|
|
194
|
+
*/
|
|
195
|
+
const rebuildPlugin = {
|
|
196
|
+
name: 'dev:rebuild',
|
|
197
|
+
setup(build) {
|
|
198
|
+
build.onEnd(async (result) => {
|
|
199
|
+
const date = dt.format(new Date());
|
|
200
|
+
if (result?.errors.length) {
|
|
201
|
+
const formatted = await esbuild.formatMessages(result.errors, {
|
|
202
|
+
kind: 'error',
|
|
203
|
+
color: true,
|
|
204
|
+
});
|
|
205
|
+
console.error(chalk.dim(`[${date}] `) + chalk.red(formatted.join('\n')));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (result.warnings.length) {
|
|
209
|
+
const formattedWarns = await esbuild.formatMessages(result.warnings, {
|
|
210
|
+
kind: 'warning',
|
|
211
|
+
color: true,
|
|
212
|
+
});
|
|
213
|
+
console.info(
|
|
214
|
+
chalk.dim(`[${date}] `) +
|
|
215
|
+
chalk.yellow(`! updated with warnings:\n${formattedWarns.join('\n')}`)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
console.info(chalk.dim(`[${date}] `) + chalk.green('√ updated'));
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const builder = await esbuild.context({
|
|
224
|
+
...config,
|
|
225
|
+
entryPoints,
|
|
226
|
+
outdir,
|
|
227
|
+
format,
|
|
228
|
+
sourcemap: 'linked',
|
|
229
|
+
plugins: [rebuildPlugin, ...copyPlugins],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
console.log(
|
|
233
|
+
`${chalk.dim(`[${date}] `) + chalk.gray('Watching for changes...')} ${chalk.dim(`(${entryPoints.length} files found)`)}`
|
|
234
|
+
);
|
|
235
|
+
await builder.watch();
|
|
236
|
+
|
|
237
|
+
process.on('beforeExit', () => {
|
|
238
|
+
builder.stop?.();
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case 'build': {
|
|
243
|
+
if (!noClean) {
|
|
244
|
+
console.log(
|
|
245
|
+
`${chalk.dim(`[${date}]`)} Cleaning ${outdir} directory... ${chalk.dim(`(${entryPoints.length} files found)`)}`
|
|
246
|
+
);
|
|
247
|
+
await clean(outdir, date, [`!${outdir}/**/*.d.ts`]);
|
|
248
|
+
}
|
|
249
|
+
console.log(
|
|
250
|
+
`${chalk.dim(`[${date}]`)} Building...${bundle ? '(Bundling)' : ''} ${chalk.dim(`(${entryPoints.length} files found)`)}`
|
|
251
|
+
);
|
|
252
|
+
await esbuild.build({
|
|
253
|
+
...config,
|
|
254
|
+
bundle,
|
|
255
|
+
external: bundle ? Object.keys(dependencies) : undefined,
|
|
256
|
+
entryPoints,
|
|
257
|
+
outdir,
|
|
258
|
+
outExtension: forceCJS ? { '.js': '.cjs' } : {},
|
|
259
|
+
format,
|
|
260
|
+
plugins: [dtsGen(buildTsConfig, outdir), ...copyPlugins],
|
|
261
|
+
});
|
|
262
|
+
console.log(chalk.dim(`[${date}] `) + chalk.green('√ Build Complete'));
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
package/src/cmds/help.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @type {boolean} Indicates if the script is running in a CI environment.
|
|
5
|
+
*/
|
|
6
|
+
const isCI = !!process.env.CI;
|
|
7
|
+
|
|
8
|
+
/** * Default timeout for tests in milliseconds.
|
|
9
|
+
* In CI, we set a longer timeout to accommodate potential delays.
|
|
10
|
+
* In local development, we use a shorter timeout for faster feedback.
|
|
11
|
+
* @type {number}
|
|
12
|
+
*/
|
|
13
|
+
const defaultTimeout = isCI ? 1400000 : 600000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Show the help message for the buildkit CLI.
|
|
17
|
+
*/
|
|
18
|
+
export default function showHelp() {
|
|
19
|
+
console.log(`
|
|
20
|
+
${chalk.green('StudioCMS Buildkit')} - Build tool for StudioCMS packages
|
|
21
|
+
|
|
22
|
+
${chalk.yellow('Usage:')}
|
|
23
|
+
buildkit <command> [...files] [...options]
|
|
24
|
+
|
|
25
|
+
${chalk.yellow('Commands:')}
|
|
26
|
+
dev Watch files and rebuild on changes
|
|
27
|
+
build Perform a one-time build
|
|
28
|
+
test Run tests with Node.js test runner
|
|
29
|
+
help Show this help message
|
|
30
|
+
|
|
31
|
+
${chalk.yellow('Dev and Build Options:')}
|
|
32
|
+
--no-clean-dist Skip cleaning the dist directory
|
|
33
|
+
--bundle Enable bundling mode
|
|
34
|
+
--force-cjs Force CommonJS output format
|
|
35
|
+
--tsconfig=<path> Specify TypeScript config file (default: tsconfig.json)
|
|
36
|
+
--outdir=<path> Specify output directory (default: dist)
|
|
37
|
+
|
|
38
|
+
${chalk.yellow('Test Options:')}
|
|
39
|
+
-m, --match <pattern> Filter tests by name pattern
|
|
40
|
+
-o, --only Run only tests marked with .only
|
|
41
|
+
-p, --parallel Run tests in parallel (default: true)
|
|
42
|
+
-w, --watch Watch for file changes and rerun tests
|
|
43
|
+
-t, --timeout <ms> Set test timeout in milliseconds (default: ${defaultTimeout})
|
|
44
|
+
-s, --setup <file> Specify setup file to run before tests
|
|
45
|
+
--teardown <file> Specify teardown file to run after tests
|
|
46
|
+
|
|
47
|
+
${chalk.yellow('Examples:')}
|
|
48
|
+
- buildkit dev "src/**/*.ts" --no-clean-dist
|
|
49
|
+
- buildkit build "src/**/*.ts"
|
|
50
|
+
- buildkit build "src/**/*.ts" --bundle --force-cjs
|
|
51
|
+
- buildkit test "test/**/*.test.js" --timeout 50000
|
|
52
|
+
- buildkit test "test/**/*.test.js" --match "studiocms" --only
|
|
53
|
+
`);
|
|
54
|
+
}
|
package/src/cmds/test.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { run } from 'node:test';
|
|
4
|
+
import { spec } from 'node:test/reporters';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { glob } from 'tinyglobby';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @type {boolean} Indicates if the script is running in a CI environment.
|
|
12
|
+
*/
|
|
13
|
+
const isCI = !!process.env.CI;
|
|
14
|
+
|
|
15
|
+
/** * Default timeout for tests in milliseconds.
|
|
16
|
+
* In CI, we set a longer timeout to accommodate potential delays.
|
|
17
|
+
* In local development, we use a shorter timeout for faster feedback.
|
|
18
|
+
* @type {number}
|
|
19
|
+
*/
|
|
20
|
+
const defaultTimeout = isCI ? 1400000 : 600000;
|
|
21
|
+
|
|
22
|
+
// DateTime format for logging
|
|
23
|
+
/**
|
|
24
|
+
* @type {Intl.DateTimeFormat}
|
|
25
|
+
*/
|
|
26
|
+
const dt = new Intl.DateTimeFormat('en-us', {
|
|
27
|
+
hour: '2-digit',
|
|
28
|
+
minute: '2-digit',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run tests using the Node.js test runner.
|
|
33
|
+
* @param {string[]} args - The command line arguments for the test command.
|
|
34
|
+
*/
|
|
35
|
+
export default async function test(args) {
|
|
36
|
+
const parsedArgs = parseArgs({
|
|
37
|
+
args,
|
|
38
|
+
allowPositionals: true,
|
|
39
|
+
options: {
|
|
40
|
+
// aka --test-name-pattern: https://nodejs.org/api/test.html#filtering-tests-by-name
|
|
41
|
+
match: { type: 'string', alias: 'm' },
|
|
42
|
+
// aka --test-only: https://nodejs.org/api/test.html#only-tests
|
|
43
|
+
only: { type: 'boolean', alias: 'o' },
|
|
44
|
+
// aka --test-concurrency: https://nodejs.org/api/test.html#test-runner-execution-model
|
|
45
|
+
parallel: { type: 'boolean', alias: 'p' },
|
|
46
|
+
// experimental: https://nodejs.org/api/test.html#watch-mode
|
|
47
|
+
watch: { type: 'boolean', alias: 'w' },
|
|
48
|
+
// Test timeout in milliseconds (default: 30000ms)
|
|
49
|
+
timeout: { type: 'string', alias: 't' },
|
|
50
|
+
// Test setup file
|
|
51
|
+
setup: { type: 'string', alias: 's' },
|
|
52
|
+
// Test teardown file
|
|
53
|
+
teardown: { type: 'string' },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Find the package.json file in the current directory
|
|
58
|
+
// and read it to get the project name
|
|
59
|
+
const packageJSONPath = path.resolve('./package.json');
|
|
60
|
+
let packageJSON;
|
|
61
|
+
try {
|
|
62
|
+
packageJSON = JSON.parse(await fs.readFile(packageJSONPath, { encoding: 'utf8' }));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`Failed to read package.json: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(
|
|
68
|
+
`${chalk.dim(`[${dt.format(new Date())}]`)} Running tests for ${chalk.bold(packageJSON.name)}...\n`
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const start = Date.now();
|
|
72
|
+
|
|
73
|
+
const pattern = parsedArgs.positionals[0];
|
|
74
|
+
if (!pattern) throw new Error('Missing test glob pattern');
|
|
75
|
+
|
|
76
|
+
const files = await glob(pattern, {
|
|
77
|
+
onlyFiles: true,
|
|
78
|
+
absolute: true,
|
|
79
|
+
ignore: ['**/node_modules/**'],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (files.length === 0) {
|
|
83
|
+
throw new Error(`No test files found matching pattern: ${pattern}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// For some reason, the `only` option does not work and we need to explicitly set the CLI flag instead.
|
|
87
|
+
// Node.js requires opt-in to run .only tests :(
|
|
88
|
+
// https://nodejs.org/api/test.html#only-tests
|
|
89
|
+
if (parsedArgs.values.only) {
|
|
90
|
+
process.env.NODE_OPTIONS ??= '';
|
|
91
|
+
process.env.NODE_OPTIONS += ' --test-only';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!parsedArgs.values.parallel) {
|
|
95
|
+
// If not parallel, we create a temporary file that imports all the test files
|
|
96
|
+
// so that it all runs in a single process.
|
|
97
|
+
const tempTestFile = path.resolve('./node_modules/.withstudiocms/test.mjs');
|
|
98
|
+
await fs.mkdir(path.dirname(tempTestFile), { recursive: true });
|
|
99
|
+
await fs.writeFile(
|
|
100
|
+
tempTestFile,
|
|
101
|
+
files.map((f) => `import ${JSON.stringify(pathToFileURL(f).toString())};`).join('\n')
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
files.length = 0;
|
|
105
|
+
files.push(tempTestFile);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const teardownModule = parsedArgs.values.teardown
|
|
109
|
+
? await import(pathToFileURL(path.resolve(parsedArgs.values.teardown)).toString())
|
|
110
|
+
: undefined;
|
|
111
|
+
|
|
112
|
+
// https://nodejs.org/api/test.html#runoptions
|
|
113
|
+
run({
|
|
114
|
+
files,
|
|
115
|
+
testNamePatterns: parsedArgs.values.match,
|
|
116
|
+
concurrency: parsedArgs.values.parallel,
|
|
117
|
+
only: parsedArgs.values.only,
|
|
118
|
+
setup: parsedArgs.values.setup,
|
|
119
|
+
watch: parsedArgs.values.watch,
|
|
120
|
+
timeout: parsedArgs.values.timeout ? Number(parsedArgs.values.timeout) : defaultTimeout, // Node.js defaults to Infinity, so set better fallback
|
|
121
|
+
})
|
|
122
|
+
.on('test:fail', () => {
|
|
123
|
+
// For some reason, a test fail using the JS API does not set an exit code of 1,
|
|
124
|
+
// so we set it here manually
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
})
|
|
127
|
+
.on('end', () => {
|
|
128
|
+
const testPassed = process.exitCode === 0 || process.exitCode === undefined;
|
|
129
|
+
teardownModule?.default(testPassed);
|
|
130
|
+
const end = Date.now();
|
|
131
|
+
console.log(
|
|
132
|
+
`\n${chalk.dim(`[${dt.format(new Date())}]`)} Tests for ${chalk.bold(packageJSON.name)} completed in ${((end - start) / 1000).toFixed(2)} seconds. ${
|
|
133
|
+
process.exitCode === 0 || process.exitCode === undefined
|
|
134
|
+
? chalk.green('All tests passed!')
|
|
135
|
+
: chalk.red('Some tests failed!')
|
|
136
|
+
}`
|
|
137
|
+
);
|
|
138
|
+
})
|
|
139
|
+
.pipe(new spec())
|
|
140
|
+
.pipe(process.stdout);
|
|
141
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import builder from './cmds/builder.js';
|
|
3
|
+
import showHelp from './cmds/help.js';
|
|
4
|
+
import test from './cmds/test.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Main function to handle command line arguments and execute the appropriate command.
|
|
8
|
+
*/
|
|
9
|
+
export default async function main() {
|
|
10
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
11
|
+
switch (cmd) {
|
|
12
|
+
case 'dev':
|
|
13
|
+
case 'build':
|
|
14
|
+
await builder(cmd, args);
|
|
15
|
+
break;
|
|
16
|
+
case 'test':
|
|
17
|
+
await test(args);
|
|
18
|
+
break;
|
|
19
|
+
default: {
|
|
20
|
+
showHelp();
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// THIS IS THE ENTRY POINT FOR THE CLI - DO NOT REMOVE
|
|
27
|
+
main().catch((error) => {
|
|
28
|
+
console.error(error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
package/index.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import esbuild from 'esbuild';
|
|
5
|
-
import glob from 'fast-glob';
|
|
6
|
-
import { dim, gray, green, red, yellow } from 'kleur/colors';
|
|
7
|
-
|
|
8
|
-
/** @type {import('esbuild').BuildOptions} */
|
|
9
|
-
const defaultConfig = {
|
|
10
|
-
minify: false,
|
|
11
|
-
format: 'esm',
|
|
12
|
-
platform: 'node',
|
|
13
|
-
target: 'node18',
|
|
14
|
-
sourcemap: false,
|
|
15
|
-
sourcesContent: false,
|
|
16
|
-
loader: {
|
|
17
|
-
'.astro': 'copy',
|
|
18
|
-
'.d.ts': 'copy',
|
|
19
|
-
'.json': 'copy',
|
|
20
|
-
'.gif': 'copy',
|
|
21
|
-
'.jpeg': 'copy',
|
|
22
|
-
'.jpg': 'copy',
|
|
23
|
-
'.png': 'copy',
|
|
24
|
-
'.tiff': 'copy',
|
|
25
|
-
'.webp': 'copy',
|
|
26
|
-
'.avif': 'copy',
|
|
27
|
-
'.svg': 'copy',
|
|
28
|
-
'.woff2': 'copy',
|
|
29
|
-
'.woff': 'copy',
|
|
30
|
-
'.ttf': 'copy',
|
|
31
|
-
'.eot': 'copy',
|
|
32
|
-
'.otf': 'copy',
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const dt = new Intl.DateTimeFormat('en-us', {
|
|
37
|
-
hour: '2-digit',
|
|
38
|
-
minute: '2-digit',
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const dtsGen = (buildTsConfig, outdir) => ({
|
|
42
|
-
name: 'TypeScriptDeclarationsPlugin',
|
|
43
|
-
setup(build) {
|
|
44
|
-
build.onEnd((result) => {
|
|
45
|
-
if (result.errors.length > 0) return;
|
|
46
|
-
const date = dt.format(new Date());
|
|
47
|
-
console.log(`${dim(`[${date}]`)} Generating TypeScript declarations...`);
|
|
48
|
-
try {
|
|
49
|
-
const res = execSync(`tsc --emitDeclarationOnly -p ${buildTsConfig} --outDir ./${outdir}`);
|
|
50
|
-
console.log(res.toString());
|
|
51
|
-
console.log(dim(`[${date}] `) + green('√ Generated TypeScript declarations'));
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.error(dim(`[${date}] `) + red(`${error}\n\n${error.stdout.toString()}`));
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
export default async function run() {
|
|
60
|
-
const [cmd, ...args] = process.argv.slice(2);
|
|
61
|
-
const config = Object.assign({}, defaultConfig);
|
|
62
|
-
const patterns = args
|
|
63
|
-
.filter((f) => !!f) // remove empty args
|
|
64
|
-
.map((f) => f.replace(/^'/, '').replace(/'$/, '')); // Needed for Windows: glob strings contain surrounding string chars??? remove these
|
|
65
|
-
const entryPoints = [].concat(
|
|
66
|
-
...(await Promise.all(
|
|
67
|
-
patterns.map((pattern) => glob(pattern, { filesOnly: true, absolute: true }))
|
|
68
|
-
))
|
|
69
|
-
);
|
|
70
|
-
const date = dt.format(new Date());
|
|
71
|
-
|
|
72
|
-
const noClean = args.includes('--no-clean-dist');
|
|
73
|
-
const bundle = args.includes('--bundle');
|
|
74
|
-
const forceCJS = args.includes('--force-cjs');
|
|
75
|
-
const buildTsConfig =
|
|
76
|
-
args.find((arg) => arg.startsWith('--tsconfig='))?.split('=')[1] || 'tsconfig.json';
|
|
77
|
-
const outdir = args.find((arg) => arg.startsWith('--outdir='))?.split('=')[1] || 'dist';
|
|
78
|
-
|
|
79
|
-
const { type = 'module', dependencies = {} } = await readPackageJSON('./package.json');
|
|
80
|
-
|
|
81
|
-
const format = type === 'module' && !forceCJS ? 'esm' : 'cjs';
|
|
82
|
-
|
|
83
|
-
switch (cmd) {
|
|
84
|
-
case 'dev': {
|
|
85
|
-
if (!noClean) {
|
|
86
|
-
console.log(
|
|
87
|
-
`${dim(`[${date}]`)} Cleaning ${outdir} directory... ${dim(`(${entryPoints.length} files found)`)}`
|
|
88
|
-
);
|
|
89
|
-
await clean(outdir, date, [`!${outdir}/**/*.d.ts`]);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const rebuildPlugin = {
|
|
93
|
-
name: 'dev:rebuild',
|
|
94
|
-
setup(build) {
|
|
95
|
-
build.onEnd(async (result) => {
|
|
96
|
-
const date = dt.format(new Date());
|
|
97
|
-
if (result?.errors.length) {
|
|
98
|
-
const errMsg = result.errors.join('\n');
|
|
99
|
-
console.error(dim(`[${date}] `) + red(errMsg));
|
|
100
|
-
} else {
|
|
101
|
-
if (result.warnings.length) {
|
|
102
|
-
console.info(
|
|
103
|
-
dim(`[${date}] `) +
|
|
104
|
-
yellow(`! updated with warnings:\n${result.warnings.join('\n')}`)
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
console.info(dim(`[${date}] `) + green('√ updated'));
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const builder = await esbuild.context({
|
|
114
|
-
...config,
|
|
115
|
-
entryPoints,
|
|
116
|
-
outdir,
|
|
117
|
-
format,
|
|
118
|
-
sourcemap: 'linked',
|
|
119
|
-
plugins: [rebuildPlugin],
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
console.log(
|
|
123
|
-
`${dim(`[${date}] `) + gray('Watching for changes...')} ${dim(`(${entryPoints.length} files found)`)}`
|
|
124
|
-
);
|
|
125
|
-
await builder.watch();
|
|
126
|
-
|
|
127
|
-
process.on('beforeExit', () => {
|
|
128
|
-
builder.stop?.();
|
|
129
|
-
});
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case 'build': {
|
|
133
|
-
if (!noClean) {
|
|
134
|
-
console.log(
|
|
135
|
-
`${dim(`[${date}]`)} Cleaning ${outdir} directory... ${dim(`(${entryPoints.length} files found)`)}`
|
|
136
|
-
);
|
|
137
|
-
await clean(outdir, date, [`!${outdir}/**/*.d.ts`]);
|
|
138
|
-
}
|
|
139
|
-
console.log(
|
|
140
|
-
`${dim(`[${date}]`)} Building...${bundle ? '(Bundling)' : ''} ${dim(`(${entryPoints.length} files found)`)}`
|
|
141
|
-
);
|
|
142
|
-
await esbuild.build({
|
|
143
|
-
...config,
|
|
144
|
-
bundle,
|
|
145
|
-
external: bundle ? Object.keys(dependencies) : undefined,
|
|
146
|
-
entryPoints,
|
|
147
|
-
outdir,
|
|
148
|
-
outExtension: forceCJS ? { '.js': '.cjs' } : {},
|
|
149
|
-
format,
|
|
150
|
-
plugins: [dtsGen(buildTsConfig, outdir)],
|
|
151
|
-
});
|
|
152
|
-
console.log(dim(`[${date}] `) + green('√ Build Complete'));
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
case 'help': {
|
|
156
|
-
showHelp();
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
default: {
|
|
160
|
-
showHelp();
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function showHelp() {
|
|
167
|
-
console.log(`
|
|
168
|
-
${green('StudioCMS Buildkit')} - Build tool for StudioCMS packages
|
|
169
|
-
|
|
170
|
-
${yellow('Usage:')}
|
|
171
|
-
buildkit <command> [...files] [...options]
|
|
172
|
-
|
|
173
|
-
${yellow('Commands:')}
|
|
174
|
-
dev Watch files and rebuild on changes
|
|
175
|
-
build Perform a one-time build
|
|
176
|
-
help Show this help message
|
|
177
|
-
|
|
178
|
-
${yellow('Options:')}
|
|
179
|
-
--no-clean-dist Skip cleaning the dist directory
|
|
180
|
-
--bundle Enable bundling mode
|
|
181
|
-
--force-cjs Force CommonJS output format
|
|
182
|
-
--tsconfig=<path> Specify TypeScript config file (default: tsconfig.json)
|
|
183
|
-
--outdir=<path> Specify output directory (default: dist)
|
|
184
|
-
|
|
185
|
-
${yellow('Examples:')}
|
|
186
|
-
buildkit build "src/**/*.ts"
|
|
187
|
-
buildkit dev "src/**/*.ts" --no-clean-dist
|
|
188
|
-
buildkit build "src/**/*.ts" --bundle --force-cjs
|
|
189
|
-
`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function clean(outdir, date, skip = []) {
|
|
193
|
-
const files = await glob([`${outdir}/**`, ...skip], { filesOnly: true });
|
|
194
|
-
console.log(dim(`[${date}] `) + dim(`Cleaning ${files.length} files from ${outdir}`));
|
|
195
|
-
await Promise.all(files.map((file) => fs.rm(file, { force: true })));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async function readPackageJSON(path) {
|
|
199
|
-
try {
|
|
200
|
-
const content = await fs.readFile(path, { encoding: 'utf8' });
|
|
201
|
-
try {
|
|
202
|
-
return JSON.parse(content);
|
|
203
|
-
} catch (parseError) {
|
|
204
|
-
throw new Error(`Invalid JSON in ${path}: ${parseError.message}`);
|
|
205
|
-
}
|
|
206
|
-
} catch (readError) {
|
|
207
|
-
throw new Error(`Failed to read ${path}: ${readError.message}`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// THIS IS THE ENTRY POINT FOR THE CLI - DO NOT REMOVE
|
|
212
|
-
run();
|