@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 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.2",
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
- "index.js",
17
- "cmd"
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
- "fast-glob": "^3.3.3",
31
- "kleur": "^4.1.5"
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": "vitest run",
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
+ }
@@ -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
+ }
@@ -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();