@withstudiocms/buildkit 0.1.0-beta.1 → 0.1.0-beta.3

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.
Files changed (3) hide show
  1. package/LICENSE +1 -1
  2. package/index.js +218 -38
  3. package/package.json +5 -5
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 withStudioCMS
3
+ Copyright (c) 2025-present StudioCMS - withstudiocms (https://github.com/withstudiocms)
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/index.js CHANGED
@@ -1,10 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
3
  import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { run } from 'node:test';
6
+ import { spec } from 'node:test/reporters';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { parseArgs } from 'node:util';
4
9
  import esbuild from 'esbuild';
5
10
  import glob from 'fast-glob';
6
11
  import { dim, gray, green, red, yellow } from 'kleur/colors';
7
12
 
13
+ /**
14
+ * @type {boolean} Indicates if the script is running in a CI environment.
15
+ */
16
+ const isCI = !!process.env.CI;
17
+
18
+ /** * Default timeout for tests in milliseconds.
19
+ * In CI, we set a longer timeout to accommodate potential delays.
20
+ * In local development, we use a shorter timeout for faster feedback.
21
+ * @type {number}
22
+ */
23
+ const defaultTimeout = isCI ? 1400000 : 600000;
24
+
8
25
  /** @type {import('esbuild').BuildOptions} */
9
26
  const defaultConfig = {
10
27
  minify: false,
@@ -25,14 +42,28 @@ const defaultConfig = {
25
42
  '.webp': 'copy',
26
43
  '.avif': 'copy',
27
44
  '.svg': 'copy',
45
+ '.woff2': 'copy',
46
+ '.woff': 'copy',
47
+ '.ttf': 'copy',
48
+ '.eot': 'copy',
49
+ '.otf': 'copy',
28
50
  },
29
51
  };
30
52
 
53
+ // DateTime format for logging
54
+ /**
55
+ * @type {Intl.DateTimeFormat}
56
+ */
31
57
  const dt = new Intl.DateTimeFormat('en-us', {
32
58
  hour: '2-digit',
33
59
  minute: '2-digit',
34
60
  });
35
61
 
62
+ /** * Plugin to generate TypeScript declarations using the TypeScript compiler.
63
+ * @param {string} buildTsConfig - The path to the TypeScript configuration file.
64
+ * @param {string} outdir - The output directory for the generated declarations.
65
+ * @returns {import('esbuild').Plugin} The esbuild plugin for generating TypeScript declarations.
66
+ */
36
67
  const dtsGen = (buildTsConfig, outdir) => ({
37
68
  name: 'TypeScriptDeclarationsPlugin',
38
69
  setup(build) {
@@ -51,17 +82,61 @@ const dtsGen = (buildTsConfig, outdir) => ({
51
82
  },
52
83
  });
53
84
 
54
- export default async function run() {
55
- const [cmd, ...args] = process.argv.slice(2);
85
+ /** * Clean the output directory by removing all files except those specified in the skip array.
86
+ * @param {string} outdir - The output directory to clean.
87
+ * @param {string} date - The date string for logging.
88
+ * @param {string[]} skip - An array of glob patterns to skip when cleaning.
89
+ * @throws {Error} If the glob operation fails or if there are issues removing files.
90
+ */
91
+ async function clean(outdir, date, skip = []) {
92
+ const files = await glob([`${outdir}/**`, ...skip], { filesOnly: true });
93
+ console.log(dim(`[${date}] `) + dim(`Cleaning ${files.length} files from ${outdir}`));
94
+ await Promise.all(files.map((file) => fs.rm(file, { force: true })));
95
+ }
96
+
97
+ /** * Read and parse the package.json file.
98
+ * @param {string} path - The path to the package.json file.
99
+ * @returns {Promise<Object>} The parsed JSON object.
100
+ * @throws {Error} If the file cannot be read or is not valid JSON.
101
+ */
102
+ async function readPackageJSON(path) {
103
+ try {
104
+ const content = await fs.readFile(path, { encoding: 'utf8' });
105
+ try {
106
+ return JSON.parse(content);
107
+ } catch (parseError) {
108
+ throw new Error(`Invalid JSON in ${path}: ${parseError.message}`);
109
+ }
110
+ } catch (readError) {
111
+ throw new Error(`Failed to read ${path}: ${readError.message}`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Run the dev or build command with the provided arguments.
117
+ * @param {string} cmd - The command to run ('dev' or 'build').
118
+ * @param {string[]} args - The arguments to pass to the command.
119
+ */
120
+ async function devAndBuild(cmd, args) {
56
121
  const config = Object.assign({}, defaultConfig);
57
122
  const patterns = args
58
123
  .filter((f) => !!f) // remove empty args
59
124
  .map((f) => f.replace(/^'/, '').replace(/'$/, '')); // Needed for Windows: glob strings contain surrounding string chars??? remove these
125
+
126
+ /**
127
+ * Collect all entry points based on the provided patterns.
128
+ * @type {string[]}
129
+ */
60
130
  const entryPoints = [].concat(
61
131
  ...(await Promise.all(
62
132
  patterns.map((pattern) => glob(pattern, { filesOnly: true, absolute: true }))
63
133
  ))
64
134
  );
135
+
136
+ if (entryPoints.length === 0) {
137
+ throw new Error(`No entry points found for pattern(s): ${patterns.join(', ')}`);
138
+ }
139
+
65
140
  const date = dt.format(new Date());
66
141
 
67
142
  const noClean = args.includes('--no-clean-dist');
@@ -84,6 +159,12 @@ export default async function run() {
84
159
  await clean(outdir, date, [`!${outdir}/**/*.d.ts`]);
85
160
  }
86
161
 
162
+ /**
163
+ * Plugin to handle rebuilds during development.
164
+ * It logs the result of the build process and any warnings or errors.
165
+ * @type {import('esbuild').Plugin}
166
+ * @description This plugin is used to provide feedback during development builds.
167
+ */
87
168
  const rebuildPlugin = {
88
169
  name: 'dev:rebuild',
89
170
  setup(build) {
@@ -147,17 +228,100 @@ export default async function run() {
147
228
  console.log(dim(`[${date}] `) + green('√ Build Complete'));
148
229
  break;
149
230
  }
150
- case 'help': {
151
- showHelp();
152
- break;
153
- }
154
- default: {
155
- showHelp();
156
- break;
157
- }
158
231
  }
159
232
  }
160
233
 
234
+ /**
235
+ * Run tests using the Node.js test runner.
236
+ * @param {string[]} args - The command line arguments for the test command.
237
+ */
238
+ async function test(args) {
239
+ const parsedArgs = parseArgs({
240
+ args,
241
+ allowPositionals: true,
242
+ options: {
243
+ // aka --test-name-pattern: https://nodejs.org/api/test.html#filtering-tests-by-name
244
+ match: { type: 'string', alias: 'm' },
245
+ // aka --test-only: https://nodejs.org/api/test.html#only-tests
246
+ only: { type: 'boolean', alias: 'o' },
247
+ // aka --test-concurrency: https://nodejs.org/api/test.html#test-runner-execution-model
248
+ parallel: { type: 'boolean', alias: 'p' },
249
+ // experimental: https://nodejs.org/api/test.html#watch-mode
250
+ watch: { type: 'boolean', alias: 'w' },
251
+ // Test timeout in milliseconds (default: 30000ms)
252
+ timeout: { type: 'string', alias: 't' },
253
+ // Test setup file
254
+ setup: { type: 'string', alias: 's' },
255
+ // Test teardown file
256
+ teardown: { type: 'string' },
257
+ },
258
+ });
259
+
260
+ const pattern = parsedArgs.positionals[0];
261
+ if (!pattern) throw new Error('Missing test glob pattern');
262
+
263
+ const files = await glob(pattern, {
264
+ filesOnly: true,
265
+ absolute: true,
266
+ ignore: ['**/node_modules/**'],
267
+ });
268
+
269
+ if (files.length === 0) {
270
+ throw new Error(`No test files found matching pattern: ${pattern}`);
271
+ }
272
+
273
+ // For some reason, the `only` option does not work and we need to explicitly set the CLI flag instead.
274
+ // Node.js requires opt-in to run .only tests :(
275
+ // https://nodejs.org/api/test.html#only-tests
276
+ if (parsedArgs.values.only) {
277
+ process.env.NODE_OPTIONS ??= '';
278
+ process.env.NODE_OPTIONS += ' --test-only';
279
+ }
280
+
281
+ if (!parsedArgs.values.parallel) {
282
+ // If not parallel, we create a temporary file that imports all the test files
283
+ // so that it all runs in a single process.
284
+ const tempTestFile = path.resolve('./node_modules/.withstudiocms/test.mjs');
285
+ await fs.mkdir(path.dirname(tempTestFile), { recursive: true });
286
+ await fs.writeFile(
287
+ tempTestFile,
288
+ files.map((f) => `import ${JSON.stringify(pathToFileURL(f).toString())};`).join('\n')
289
+ );
290
+
291
+ files.length = 0;
292
+ files.push(tempTestFile);
293
+ }
294
+
295
+ const teardownModule = parsedArgs.values.teardown
296
+ ? await import(pathToFileURL(path.resolve(parsedArgs.values.teardown)).toString())
297
+ : undefined;
298
+
299
+ // https://nodejs.org/api/test.html#runoptions
300
+ run({
301
+ files,
302
+ testNamePatterns: parsedArgs.values.match,
303
+ concurrency: parsedArgs.values.parallel,
304
+ only: parsedArgs.values.only,
305
+ setup: parsedArgs.values.setup,
306
+ watch: parsedArgs.values.watch,
307
+ timeout: parsedArgs.values.timeout ? Number(parsedArgs.values.timeout) : defaultTimeout, // Node.js defaults to Infinity, so set better fallback
308
+ })
309
+ .on('test:fail', () => {
310
+ // For some reason, a test fail using the JS API does not set an exit code of 1,
311
+ // so we set it here manually
312
+ process.exitCode = 1;
313
+ })
314
+ .on('end', () => {
315
+ const testPassed = process.exitCode === 0 || process.exitCode === undefined;
316
+ teardownModule?.default(testPassed);
317
+ })
318
+ .pipe(new spec())
319
+ .pipe(process.stdout);
320
+ }
321
+
322
+ /**
323
+ * Show the help message for the buildkit CLI.
324
+ */
161
325
  function showHelp() {
162
326
  console.log(`
163
327
  ${green('StudioCMS Buildkit')} - Build tool for StudioCMS packages
@@ -166,42 +330,58 @@ ${yellow('Usage:')}
166
330
  buildkit <command> [...files] [...options]
167
331
 
168
332
  ${yellow('Commands:')}
169
- dev Watch files and rebuild on changes
170
- build Perform a one-time build
171
- help Show this help message
333
+ dev Watch files and rebuild on changes
334
+ build Perform a one-time build
335
+ test Run tests with Node.js test runner
336
+ help Show this help message
337
+
338
+ ${yellow('Dev and Build Options:')}
339
+ --no-clean-dist Skip cleaning the dist directory
340
+ --bundle Enable bundling mode
341
+ --force-cjs Force CommonJS output format
342
+ --tsconfig=<path> Specify TypeScript config file (default: tsconfig.json)
343
+ --outdir=<path> Specify output directory (default: dist)
172
344
 
173
- ${yellow('Options:')}
174
- --no-clean-dist Skip cleaning the dist directory
175
- --bundle Enable bundling mode
176
- --force-cjs Force CommonJS output format
177
- --tsconfig=<path> Specify TypeScript config file (default: tsconfig.json)
178
- --outdir=<path> Specify output directory (default: dist)
345
+ ${yellow('Test Options:')}
346
+ -m, --match <pattern> Filter tests by name pattern
347
+ -o, --only Run only tests marked with .only
348
+ -p, --parallel Run tests in parallel (default: true)
349
+ -w, --watch Watch for file changes and rerun tests
350
+ -t, --timeout <ms> Set test timeout in milliseconds (default: ${defaultTimeout})
351
+ -s, --setup <file> Specify setup file to run before tests
352
+ --teardown <file> Specify teardown file to run after tests
179
353
 
180
354
  ${yellow('Examples:')}
181
- buildkit build "src/**/*.ts"
182
- buildkit dev "src/**/*.ts" --no-clean-dist
183
- buildkit build "src/**/*.ts" --bundle --force-cjs
355
+ - buildkit dev "src/**/*.ts" --no-clean-dist
356
+ - buildkit build "src/**/*.ts"
357
+ - buildkit build "src/**/*.ts" --bundle --force-cjs
358
+ - buildkit test "test/**/*.test.js" --timeout 50000
359
+ - buildkit test "test/**/*.test.js" --match "studiocms" --only
184
360
  `);
185
361
  }
186
362
 
187
- async function clean(outdir, date, skip = []) {
188
- const files = await glob([`${outdir}/**`, ...skip], { filesOnly: true });
189
- console.log(dim(`[${date}] `) + dim(`Cleaning ${files.length} files from ${outdir}`));
190
- await Promise.all(files.map((file) => fs.rm(file, { force: true })));
191
- }
192
-
193
- async function readPackageJSON(path) {
194
- try {
195
- const content = await fs.readFile(path, { encoding: 'utf8' });
196
- try {
197
- return JSON.parse(content);
198
- } catch (parseError) {
199
- throw new Error(`Invalid JSON in ${path}: ${parseError.message}`);
363
+ /**
364
+ * Main function to handle command line arguments and execute the appropriate command.
365
+ */
366
+ export default async function main() {
367
+ const [cmd, ...args] = process.argv.slice(2);
368
+ switch (cmd) {
369
+ case 'dev':
370
+ case 'build':
371
+ await devAndBuild(cmd, args);
372
+ break;
373
+ case 'test':
374
+ await test(args);
375
+ break;
376
+ default: {
377
+ showHelp();
378
+ break;
200
379
  }
201
- } catch (readError) {
202
- throw new Error(`Failed to read ${path}: ${readError.message}`);
203
380
  }
204
381
  }
205
382
 
206
383
  // THIS IS THE ENTRY POINT FOR THE CLI - DO NOT REMOVE
207
- run();
384
+ main().catch((error) => {
385
+ console.error(error);
386
+ process.exit(1);
387
+ });
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@withstudiocms/buildkit",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Build kit based on esbuild for the withstudiocms github org",
5
5
  "author": {
6
- "name": "Adam Matthiesen | Jacob Jenkins | Paul Valladares",
6
+ "name": "withstudiocms",
7
7
  "url": "https://studiocms.dev"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/withstudiocms/studiocms.git",
12
- "directory": "packages/withstudiocms_buildkit"
12
+ "directory": "packages/@withstudiocms/buildkit"
13
13
  },
14
14
  "license": "MIT",
15
15
  "files": [
@@ -26,12 +26,12 @@
26
26
  "buildkit": "./index.js"
27
27
  },
28
28
  "dependencies": {
29
- "esbuild": "^0.25.0",
29
+ "esbuild": "^0.25.8",
30
30
  "fast-glob": "^3.3.3",
31
31
  "kleur": "^4.1.5"
32
32
  },
33
33
  "devDependencies": {
34
- "vitest": "^3.1.1",
34
+ "vitest": "^3.2.4",
35
35
  "strip-ansi": "^7.1.0"
36
36
  },
37
37
  "scripts": {