diff --git a/.vscode/settings.json b/.vscode/settings.json index 2792fce1..186ee292 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,11 +2,7 @@ "typescript.tsdk": "node_modules/typescript/lib", "mdPrettify.substitutions": [ { - "language": [ - "typescriptreact", - "typescript", - "astro" - ], + "language": ["typescriptreact", "typescript", "astro"], "substitutions": [ { "ugly": "\\bEffect\\b", @@ -217,4 +213,4 @@ "[yaml]": { "editor.defaultFormatter": "redhat.vscode-yaml" } -} \ No newline at end of file +} diff --git a/packages/@withstudiocms/buildkit/src/cmds/builder.js b/packages/@withstudiocms/buildkit/src/cmds/builder.js index 2292b28c..70cc4344 100644 --- a/packages/@withstudiocms/buildkit/src/cmds/builder.js +++ b/packages/@withstudiocms/buildkit/src/cmds/builder.js @@ -6,30 +6,30 @@ import { glob } from 'tinyglobby'; /** @type {import('esbuild').BuildOptions} */ const defaultConfig = { - minify: false, - format: 'esm', - platform: 'node', - target: 'node18', - sourcemap: false, - sourcesContent: false, - loader: { - '.astro': 'copy', - '.d.ts': 'copy', - '.json': 'copy', - '.gif': 'copy', - '.jpeg': 'copy', - '.jpg': 'copy', - '.png': 'copy', - '.tiff': 'copy', - '.webp': 'copy', - '.avif': 'copy', - '.svg': 'copy', - '.woff2': 'copy', - '.woff': 'copy', - '.ttf': 'copy', - '.eot': 'copy', - '.otf': 'copy', - }, + minify: false, + format: 'esm', + platform: 'node', + target: 'node18', + sourcemap: false, + sourcesContent: false, + loader: { + '.astro': 'copy', + '.d.ts': 'copy', + '.json': 'copy', + '.gif': 'copy', + '.jpeg': 'copy', + '.jpg': 'copy', + '.png': 'copy', + '.tiff': 'copy', + '.webp': 'copy', + '.avif': 'copy', + '.svg': 'copy', + '.woff2': 'copy', + '.woff': 'copy', + '.ttf': 'copy', + '.eot': 'copy', + '.otf': 'copy', + }, }; // DateTime format for logging @@ -37,8 +37,8 @@ const defaultConfig = { * @type {Intl.DateTimeFormat} */ const dt = new Intl.DateTimeFormat('en-us', { - hour: '2-digit', - minute: '2-digit', + hour: '2-digit', + minute: '2-digit', }); /** * Plugin to generate TypeScript declarations using the TypeScript compiler. @@ -47,31 +47,31 @@ const dt = new Intl.DateTimeFormat('en-us', { * @returns {import('esbuild').Plugin} The esbuild plugin for generating TypeScript declarations. */ const dtsGen = (buildTsConfig, outdir) => ({ - name: 'TypeScriptDeclarationsPlugin', - setup(build) { - build.onEnd((result) => { - if (result.errors.length > 0) return; - const date = dt.format(new Date()); - console.log(`${chalk.dim(`[${date}]`)} Generating TypeScript declarations...`); - try { - const res = execFileSync( - 'tsc', - ['--emitDeclarationOnly', '-p', buildTsConfig, '--outDir', `./${outdir}`], - { encoding: 'utf8' } - ); - if (res) console.log(res); - console.log(chalk.dim(`[${date}] `) + chalk.green('√ Generated TypeScript declarations')); - } catch (error) { - const msg = - (error && (error.message || String(error))) + - '\n\n' + - // stdout/stderr may be Buffer or string depending on exec options - (typeof error?.stdout === 'string' ? error.stdout : error?.stdout?.toString?.() ?? '') + - (typeof error?.stderr === 'string' ? error.stderr : error?.stderr?.toString?.() ?? ''); - console.error(chalk.dim(`[${date}] `) + chalk.red(msg)); - } - }); - }, + name: 'TypeScriptDeclarationsPlugin', + setup(build) { + build.onEnd((result) => { + if (result.errors.length > 0) return; + const date = dt.format(new Date()); + console.log(`${chalk.dim(`[${date}]`)} Generating TypeScript declarations...`); + try { + const res = execFileSync( + 'tsc', + ['--emitDeclarationOnly', '-p', buildTsConfig, '--outDir', `./${outdir}`], + { encoding: 'utf8' } + ); + if (res) console.log(res); + console.log(chalk.dim(`[${date}] `) + chalk.green('√ Generated TypeScript declarations')); + } catch (error) { + const msg = + (error && (error.message || String(error))) + + '\n\n' + + // stdout/stderr may be Buffer or string depending on exec options + (typeof error?.stdout === 'string' ? error.stdout : (error?.stdout?.toString?.() ?? '')) + + (typeof error?.stderr === 'string' ? error.stderr : (error?.stderr?.toString?.() ?? '')); + console.error(chalk.dim(`[${date}] `) + chalk.red(msg)); + } + }); + }, }); /** * Clean the output directory by removing all files except those specified in the skip array. @@ -81,9 +81,9 @@ const dtsGen = (buildTsConfig, outdir) => ({ * @throws {Error} If the glob operation fails or if there are issues removing files. */ async function clean(outdir, date, skip = []) { - const files = await glob([`${outdir}/**`, ...skip], { filesOnly: true }); - console.log(chalk.dim(`[${date}] `) + chalk.dim(`Cleaning ${files.length} files from ${outdir}`)); - await Promise.all(files.map((file) => fs.rm(file, { force: true }))); + const files = await glob([`${outdir}/**`, ...skip], { filesOnly: true }); + console.log(chalk.dim(`[${date}] `) + chalk.dim(`Cleaning ${files.length} files from ${outdir}`)); + await Promise.all(files.map((file) => fs.rm(file, { force: true }))); } /** * Read and parse the package.json file. @@ -92,16 +92,16 @@ async function clean(outdir, date, skip = []) { * @throws {Error} If the file cannot be read or is not valid JSON. */ async function readPackageJSON(path) { - try { - const content = await fs.readFile(path, { encoding: 'utf8' }); - try { - return JSON.parse(content); - } catch (parseError) { - throw new Error(`Invalid JSON in ${path}: ${parseError.message}`); - } - } catch (readError) { - throw new Error(`Failed to read ${path}: ${readError.message}`); - } + try { + const content = await fs.readFile(path, { encoding: 'utf8' }); + try { + return JSON.parse(content); + } catch (parseError) { + throw new Error(`Invalid JSON in ${path}: ${parseError.message}`); + } + } catch (readError) { + throw new Error(`Failed to read ${path}: ${readError.message}`); + } } /** @@ -110,116 +110,122 @@ async function readPackageJSON(path) { * @param {string[]} args - The arguments to pass to the command. */ export default async function builder(cmd, args) { - const config = Object.assign({}, defaultConfig); - const patterns = args - .filter((f) => !!f) // remove empty args - .map((f) => f.replace(/^'/, '').replace(/'$/, '')); // Needed for Windows: glob strings contain surrounding string chars??? remove these - - /** - * Collect all entry points based on the provided patterns. - * @type {string[]} - */ - const entryPoints = [].concat( - ...(await Promise.all( - patterns.map((pattern) => glob(pattern, { filesOnly: true, absolute: true })) - )) - ); - - if (entryPoints.length === 0) { - throw new Error(`No entry points found for pattern(s): ${patterns.join(', ')}`); - } - - const date = dt.format(new Date()); - - const noClean = args.includes('--no-clean-dist'); - const bundle = args.includes('--bundle'); - const forceCJS = args.includes('--force-cjs'); - const buildTsConfig = - args.find((arg) => arg.startsWith('--tsconfig='))?.split('=')[1] || 'tsconfig.json'; - const outdir = args.find((arg) => arg.startsWith('--outdir='))?.split('=')[1] || 'dist'; - - const { type = 'module', dependencies = {} } = await readPackageJSON('./package.json'); - - const format = type === 'module' && !forceCJS ? 'esm' : 'cjs'; - - switch (cmd) { - case 'dev': { - if (!noClean) { - console.log( - `${chalk.dim(`[${date}]`)} Cleaning ${outdir} directory... ${chalk.dim(`(${entryPoints.length} files found)`)}` - ); - await clean(outdir, date, [`!${outdir}/**/*.d.ts`]); - } - - /** - * Plugin to handle rebuilds during development. - * It logs the result of the build process and any warnings or errors. - * @type {import('esbuild').Plugin} - * @description This plugin is used to provide feedback during development builds. - */ - const rebuildPlugin = { - name: 'dev:rebuild', - setup(build) { - build.onEnd(async (result) => { - const date = dt.format(new Date()); - if (result?.errors.length) { - const formatted = await esbuild.formatMessages(result.errors, { kind: 'error', color: true }); - console.error(chalk.dim(`[${date}] `) + chalk.red(formatted.join('\n'))); - return; - } - if (result.warnings.length) { - const formattedWarns = await esbuild.formatMessages(result.warnings, { kind: 'warning', color: true }); - console.info( - chalk.dim(`[${date}] `) + - chalk.yellow(`! updated with warnings:\n${formattedWarns.join('\n')}`) - ); - } - console.info(chalk.dim(`[${date}] `) + chalk.green('√ updated')); - }); - }, - }; - - const builder = await esbuild.context({ - ...config, - entryPoints, - outdir, - format, - sourcemap: 'linked', - plugins: [rebuildPlugin], - }); - - console.log( - `${chalk.dim(`[${date}] `) + chalk.gray('Watching for changes...')} ${chalk.dim(`(${entryPoints.length} files found)`)}` - ); - await builder.watch(); - - process.on('beforeExit', () => { - builder.stop?.(); - }); - break; - } - case 'build': { - if (!noClean) { - console.log( - `${chalk.dim(`[${date}]`)} Cleaning ${outdir} directory... ${chalk.dim(`(${entryPoints.length} files found)`)}` - ); - await clean(outdir, date, [`!${outdir}/**/*.d.ts`]); - } - console.log( - `${chalk.dim(`[${date}]`)} Building...${bundle ? '(Bundling)' : ''} ${chalk.dim(`(${entryPoints.length} files found)`)}` - ); - await esbuild.build({ - ...config, - bundle, - external: bundle ? Object.keys(dependencies) : undefined, - entryPoints, - outdir, - outExtension: forceCJS ? { '.js': '.cjs' } : {}, - format, - plugins: [dtsGen(buildTsConfig, outdir)], - }); - console.log(chalk.dim(`[${date}] `) + chalk.green('√ Build Complete')); - break; - } - } -} \ No newline at end of file + const config = Object.assign({}, defaultConfig); + const patterns = args + .filter((f) => !!f) // remove empty args + .map((f) => f.replace(/^'/, '').replace(/'$/, '')); // Needed for Windows: glob strings contain surrounding string chars??? remove these + + /** + * Collect all entry points based on the provided patterns. + * @type {string[]} + */ + const entryPoints = [].concat( + ...(await Promise.all( + patterns.map((pattern) => glob(pattern, { filesOnly: true, absolute: true })) + )) + ); + + if (entryPoints.length === 0) { + throw new Error(`No entry points found for pattern(s): ${patterns.join(', ')}`); + } + + const date = dt.format(new Date()); + + const noClean = args.includes('--no-clean-dist'); + const bundle = args.includes('--bundle'); + const forceCJS = args.includes('--force-cjs'); + const buildTsConfig = + args.find((arg) => arg.startsWith('--tsconfig='))?.split('=')[1] || 'tsconfig.json'; + const outdir = args.find((arg) => arg.startsWith('--outdir='))?.split('=')[1] || 'dist'; + + const { type = 'module', dependencies = {} } = await readPackageJSON('./package.json'); + + const format = type === 'module' && !forceCJS ? 'esm' : 'cjs'; + + switch (cmd) { + case 'dev': { + if (!noClean) { + console.log( + `${chalk.dim(`[${date}]`)} Cleaning ${outdir} directory... ${chalk.dim(`(${entryPoints.length} files found)`)}` + ); + await clean(outdir, date, [`!${outdir}/**/*.d.ts`]); + } + + /** + * Plugin to handle rebuilds during development. + * It logs the result of the build process and any warnings or errors. + * @type {import('esbuild').Plugin} + * @description This plugin is used to provide feedback during development builds. + */ + const rebuildPlugin = { + name: 'dev:rebuild', + setup(build) { + build.onEnd(async (result) => { + const date = dt.format(new Date()); + if (result?.errors.length) { + const formatted = await esbuild.formatMessages(result.errors, { + kind: 'error', + color: true, + }); + console.error(chalk.dim(`[${date}] `) + chalk.red(formatted.join('\n'))); + return; + } + if (result.warnings.length) { + const formattedWarns = await esbuild.formatMessages(result.warnings, { + kind: 'warning', + color: true, + }); + console.info( + chalk.dim(`[${date}] `) + + chalk.yellow(`! updated with warnings:\n${formattedWarns.join('\n')}`) + ); + } + console.info(chalk.dim(`[${date}] `) + chalk.green('√ updated')); + }); + }, + }; + + const builder = await esbuild.context({ + ...config, + entryPoints, + outdir, + format, + sourcemap: 'linked', + plugins: [rebuildPlugin], + }); + + console.log( + `${chalk.dim(`[${date}] `) + chalk.gray('Watching for changes...')} ${chalk.dim(`(${entryPoints.length} files found)`)}` + ); + await builder.watch(); + + process.on('beforeExit', () => { + builder.stop?.(); + }); + break; + } + case 'build': { + if (!noClean) { + console.log( + `${chalk.dim(`[${date}]`)} Cleaning ${outdir} directory... ${chalk.dim(`(${entryPoints.length} files found)`)}` + ); + await clean(outdir, date, [`!${outdir}/**/*.d.ts`]); + } + console.log( + `${chalk.dim(`[${date}]`)} Building...${bundle ? '(Bundling)' : ''} ${chalk.dim(`(${entryPoints.length} files found)`)}` + ); + await esbuild.build({ + ...config, + bundle, + external: bundle ? Object.keys(dependencies) : undefined, + entryPoints, + outdir, + outExtension: forceCJS ? { '.js': '.cjs' } : {}, + format, + plugins: [dtsGen(buildTsConfig, outdir)], + }); + console.log(chalk.dim(`[${date}] `) + chalk.green('√ Build Complete')); + break; + } + } +} diff --git a/packages/@withstudiocms/buildkit/src/cmds/help.js b/packages/@withstudiocms/buildkit/src/cmds/help.js index 72fe3d35..d12dcc58 100644 --- a/packages/@withstudiocms/buildkit/src/cmds/help.js +++ b/packages/@withstudiocms/buildkit/src/cmds/help.js @@ -16,7 +16,7 @@ const defaultTimeout = isCI ? 1400000 : 600000; * Show the help message for the buildkit CLI. */ export default function showHelp() { - console.log(` + console.log(` ${chalk.green('StudioCMS Buildkit')} - Build tool for StudioCMS packages ${chalk.yellow('Usage:')} @@ -51,4 +51,4 @@ ${chalk.yellow('Examples:')} - buildkit test "test/**/*.test.js" --timeout 50000 - buildkit test "test/**/*.test.js" --match "studiocms" --only `); -} \ No newline at end of file +} diff --git a/packages/@withstudiocms/buildkit/src/cmds/test.js b/packages/@withstudiocms/buildkit/src/cmds/test.js index 4812657c..20e17fbf 100644 --- a/packages/@withstudiocms/buildkit/src/cmds/test.js +++ b/packages/@withstudiocms/buildkit/src/cmds/test.js @@ -24,8 +24,8 @@ const defaultTimeout = isCI ? 1400000 : 600000; * @type {Intl.DateTimeFormat} */ const dt = new Intl.DateTimeFormat('en-us', { - hour: '2-digit', - minute: '2-digit', + hour: '2-digit', + minute: '2-digit', }); /** @@ -33,108 +33,109 @@ const dt = new Intl.DateTimeFormat('en-us', { * @param {string[]} args - The command line arguments for the test command. */ export default async function test(args) { - const parsedArgs = parseArgs({ - args, - allowPositionals: true, - options: { - // aka --test-name-pattern: https://nodejs.org/api/test.html#filtering-tests-by-name - match: { type: 'string', alias: 'm' }, - // aka --test-only: https://nodejs.org/api/test.html#only-tests - only: { type: 'boolean', alias: 'o' }, - // aka --test-concurrency: https://nodejs.org/api/test.html#test-runner-execution-model - parallel: { type: 'boolean', alias: 'p' }, - // experimental: https://nodejs.org/api/test.html#watch-mode - watch: { type: 'boolean', alias: 'w' }, - // Test timeout in milliseconds (default: 30000ms) - timeout: { type: 'string', alias: 't' }, - // Test setup file - setup: { type: 'string', alias: 's' }, - // Test teardown file - teardown: { type: 'string' }, - }, - }); + const parsedArgs = parseArgs({ + args, + allowPositionals: true, + options: { + // aka --test-name-pattern: https://nodejs.org/api/test.html#filtering-tests-by-name + match: { type: 'string', alias: 'm' }, + // aka --test-only: https://nodejs.org/api/test.html#only-tests + only: { type: 'boolean', alias: 'o' }, + // aka --test-concurrency: https://nodejs.org/api/test.html#test-runner-execution-model + parallel: { type: 'boolean', alias: 'p' }, + // experimental: https://nodejs.org/api/test.html#watch-mode + watch: { type: 'boolean', alias: 'w' }, + // Test timeout in milliseconds (default: 30000ms) + timeout: { type: 'string', alias: 't' }, + // Test setup file + setup: { type: 'string', alias: 's' }, + // Test teardown file + teardown: { type: 'string' }, + }, + }); - // Find the package.json file in the current directory - // and read it to get the project name - const packageJSONPath = path.resolve('./package.json'); - let packageJSON; - try { - packageJSON = JSON.parse(await fs.readFile(packageJSONPath, { encoding: 'utf8' })); - } catch (error) { - throw new Error(`Failed to read package.json: ${error.message}`); - } + // Find the package.json file in the current directory + // and read it to get the project name + const packageJSONPath = path.resolve('./package.json'); + let packageJSON; + try { + packageJSON = JSON.parse(await fs.readFile(packageJSONPath, { encoding: 'utf8' })); + } catch (error) { + throw new Error(`Failed to read package.json: ${error.message}`); + } - console.log( - `${chalk.dim(`[${dt.format(new Date())}]`)} Running tests for ${chalk.bold(packageJSON.name)}...\n` - ); + console.log( + `${chalk.dim(`[${dt.format(new Date())}]`)} Running tests for ${chalk.bold(packageJSON.name)}...\n` + ); - const start = Date.now(); + const start = Date.now(); - const pattern = parsedArgs.positionals[0]; - if (!pattern) throw new Error('Missing test glob pattern'); + const pattern = parsedArgs.positionals[0]; + if (!pattern) throw new Error('Missing test glob pattern'); - const files = await glob(pattern, { - onlyFiles: true, - absolute: true, - ignore: ['**/node_modules/**'], - }); + const files = await glob(pattern, { + onlyFiles: true, + absolute: true, + ignore: ['**/node_modules/**'], + }); - if (files.length === 0) { - throw new Error(`No test files found matching pattern: ${pattern}`); - } + if (files.length === 0) { + throw new Error(`No test files found matching pattern: ${pattern}`); + } - // For some reason, the `only` option does not work and we need to explicitly set the CLI flag instead. - // Node.js requires opt-in to run .only tests :( - // https://nodejs.org/api/test.html#only-tests - if (parsedArgs.values.only) { - process.env.NODE_OPTIONS ??= ''; - process.env.NODE_OPTIONS += ' --test-only'; - } + // For some reason, the `only` option does not work and we need to explicitly set the CLI flag instead. + // Node.js requires opt-in to run .only tests :( + // https://nodejs.org/api/test.html#only-tests + if (parsedArgs.values.only) { + process.env.NODE_OPTIONS ??= ''; + process.env.NODE_OPTIONS += ' --test-only'; + } - if (!parsedArgs.values.parallel) { - // If not parallel, we create a temporary file that imports all the test files - // so that it all runs in a single process. - const tempTestFile = path.resolve('./node_modules/.withstudiocms/test.mjs'); - await fs.mkdir(path.dirname(tempTestFile), { recursive: true }); - await fs.writeFile( - tempTestFile, - files.map((f) => `import ${JSON.stringify(pathToFileURL(f).toString())};`).join('\n') - ); + if (!parsedArgs.values.parallel) { + // If not parallel, we create a temporary file that imports all the test files + // so that it all runs in a single process. + const tempTestFile = path.resolve('./node_modules/.withstudiocms/test.mjs'); + await fs.mkdir(path.dirname(tempTestFile), { recursive: true }); + await fs.writeFile( + tempTestFile, + files.map((f) => `import ${JSON.stringify(pathToFileURL(f).toString())};`).join('\n') + ); - files.length = 0; - files.push(tempTestFile); - } + files.length = 0; + files.push(tempTestFile); + } - const teardownModule = parsedArgs.values.teardown - ? await import(pathToFileURL(path.resolve(parsedArgs.values.teardown)).toString()) - : undefined; + const teardownModule = parsedArgs.values.teardown + ? await import(pathToFileURL(path.resolve(parsedArgs.values.teardown)).toString()) + : undefined; - // https://nodejs.org/api/test.html#runoptions - run({ - files, - testNamePatterns: parsedArgs.values.match, - concurrency: parsedArgs.values.parallel, - only: parsedArgs.values.only, - setup: parsedArgs.values.setup, - watch: parsedArgs.values.watch, - timeout: parsedArgs.values.timeout ? Number(parsedArgs.values.timeout) : defaultTimeout, // Node.js defaults to Infinity, so set better fallback - }) - .on('test:fail', () => { - // For some reason, a test fail using the JS API does not set an exit code of 1, - // so we set it here manually - process.exitCode = 1; - }) - .on('end', () => { - const testPassed = process.exitCode === 0 || process.exitCode === undefined; - teardownModule?.default(testPassed); - const end = Date.now(); - console.log( - `\n${chalk.dim(`[${dt.format(new Date())}]`)} Tests for ${chalk.bold(packageJSON.name)} completed in ${((end - start) / 1000).toFixed(2)} seconds. ${process.exitCode === 0 || process.exitCode === undefined - ? chalk.green('All tests passed!') - : chalk.red('Some tests failed!') - }` - ); - }) - .pipe(new spec()) - .pipe(process.stdout); -} \ No newline at end of file + // https://nodejs.org/api/test.html#runoptions + run({ + files, + testNamePatterns: parsedArgs.values.match, + concurrency: parsedArgs.values.parallel, + only: parsedArgs.values.only, + setup: parsedArgs.values.setup, + watch: parsedArgs.values.watch, + timeout: parsedArgs.values.timeout ? Number(parsedArgs.values.timeout) : defaultTimeout, // Node.js defaults to Infinity, so set better fallback + }) + .on('test:fail', () => { + // For some reason, a test fail using the JS API does not set an exit code of 1, + // so we set it here manually + process.exitCode = 1; + }) + .on('end', () => { + const testPassed = process.exitCode === 0 || process.exitCode === undefined; + teardownModule?.default(testPassed); + const end = Date.now(); + console.log( + `\n${chalk.dim(`[${dt.format(new Date())}]`)} Tests for ${chalk.bold(packageJSON.name)} completed in ${((end - start) / 1000).toFixed(2)} seconds. ${ + process.exitCode === 0 || process.exitCode === undefined + ? chalk.green('All tests passed!') + : chalk.red('Some tests failed!') + }` + ); + }) + .pipe(new spec()) + .pipe(process.stdout); +} diff --git a/packages/@withstudiocms/buildkit/src/index.js b/packages/@withstudiocms/buildkit/src/index.js index a2e37a91..de57e0f3 100755 --- a/packages/@withstudiocms/buildkit/src/index.js +++ b/packages/@withstudiocms/buildkit/src/index.js @@ -1,30 +1,30 @@ #!/usr/bin/env node -import builder from "./cmds/builder.js"; -import showHelp from "./cmds/help.js"; -import test from "./cmds/test.js"; +import builder from './cmds/builder.js'; +import showHelp from './cmds/help.js'; +import test from './cmds/test.js'; /** * Main function to handle command line arguments and execute the appropriate command. */ export default async function main() { - const [cmd, ...args] = process.argv.slice(2); - switch (cmd) { - case 'dev': - case 'build': - await builder(cmd, args); - break; - case 'test': - await test(args); - break; - default: { - showHelp(); - break; - } - } + const [cmd, ...args] = process.argv.slice(2); + switch (cmd) { + case 'dev': + case 'build': + await builder(cmd, args); + break; + case 'test': + await test(args); + break; + default: { + showHelp(); + break; + } + } } // THIS IS THE ENTRY POINT FOR THE CLI - DO NOT REMOVE main().catch((error) => { - console.error(error); - process.exit(1); + console.error(error); + process.exit(1); }); diff --git a/packages/@withstudiocms/buildkit/test/cli.test.js b/packages/@withstudiocms/buildkit/test/cli.test.js index 4633ae17..77c3fe06 100644 --- a/packages/@withstudiocms/buildkit/test/cli.test.js +++ b/packages/@withstudiocms/buildkit/test/cli.test.js @@ -15,7 +15,7 @@ beforeEach(() => { }, restore: () => { console.log = originalConsoleLog; - } + }, }; global.originalConsoleLog = console.log; console.log = consoleLogSpy.fn; @@ -27,7 +27,6 @@ afterEach(() => { }); describe('buildkit CLI', () => { - it('help command: should show help when no command is provided', async () => { process.argv = ['node', 'buildkit']; await run(); @@ -67,5 +66,4 @@ describe('buildkit CLI', () => { assert.ok(output.includes('StudioCMS Buildkit')); assert.ok(output.includes('Usage:')); }); - }); diff --git a/packages/@withstudiocms/config-utils/test/loader.test.js b/packages/@withstudiocms/config-utils/test/loader.test.js index 62b5cd76..32497a67 100644 --- a/packages/@withstudiocms/config-utils/test/loader.test.js +++ b/packages/@withstudiocms/config-utils/test/loader.test.js @@ -5,10 +5,10 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { after, before, describe, it } from 'node:test'; import { - bundleConfigFile, - importBundledFile, - loadAndBundleConfigFile, - loadConfigFile, + bundleConfigFile, + importBundledFile, + loadAndBundleConfigFile, + loadConfigFile, } from '../dist/loader.js'; // Create a temporary directory for testing @@ -18,44 +18,44 @@ const testDirUrl = new URL(`file://${testDir}/`); // Ensure unique directory if (existsSync(testDir)) { - throw new Error(`Test directory already exists: ${testDir}`); + throw new Error(`Test directory already exists: ${testDir}`); } before(async () => { - await mkdir(testDir, { recursive: true }); + await mkdir(testDir, { recursive: true }); }); after(async () => { - await rm(testDir, { recursive: true, force: true }); + await rm(testDir, { recursive: true, force: true }); }); describe('Config Loader Utils', () => { - describe('bundleConfigFile', () => { - it('should bundle a simple JavaScript config file', async () => { - // Create a simple config file - const configPath = join(testDir, 'simple.config.js'); - const configContent = ` + describe('bundleConfigFile', () => { + it('should bundle a simple JavaScript config file', async () => { + // Create a simple config file + const configPath = join(testDir, 'simple.config.js'); + const configContent = ` export default { name: 'test-config', value: 42 }; `; - await writeFile(configPath, configContent, 'utf8'); - - const result = await bundleConfigFile({ - fileUrl: new URL(`file://${configPath}`) - }); - - assert.ok(result.code); - assert.ok(typeof result.code === 'string'); - assert.ok(Array.isArray(result.dependencies)); - assert.ok(result.code.includes('test-config')); - }); - - it('should bundle a TypeScript config file', async () => { - // Create a simple TypeScript config file - const configPath = join(testDir, 'typescript.config.ts'); - const configContent = ` + await writeFile(configPath, configContent, 'utf8'); + + const result = await bundleConfigFile({ + fileUrl: new URL(`file://${configPath}`), + }); + + assert.ok(result.code); + assert.ok(typeof result.code === 'string'); + assert.ok(Array.isArray(result.dependencies)); + assert.ok(result.code.includes('test-config')); + }); + + it('should bundle a TypeScript config file', async () => { + // Create a simple TypeScript config file + const configPath = join(testDir, 'typescript.config.ts'); + const configContent = ` interface Config { name: string; value: number; @@ -68,21 +68,21 @@ describe('Config Loader Utils', () => { export default config; `; - await writeFile(configPath, configContent, 'utf8'); + await writeFile(configPath, configContent, 'utf8'); - const result = await bundleConfigFile({ - fileUrl: new URL(`file://${configPath}`) - }); + const result = await bundleConfigFile({ + fileUrl: new URL(`file://${configPath}`), + }); - assert.ok(result.code); - assert.ok(typeof result.code === 'string'); - assert.ok(result.code.includes('typescript-config')); - }); + assert.ok(result.code); + assert.ok(typeof result.code === 'string'); + assert.ok(result.code.includes('typescript-config')); + }); - it('should handle config with external imports', async () => { - // Create a config file that imports from node_modules - const configPath = join(testDir, 'external.config.js'); - const configContent = ` + it('should handle config with external imports', async () => { + // Create a config file that imports from node_modules + const configPath = join(testDir, 'external.config.js'); + const configContent = ` import { join } from 'node:path'; export default { @@ -90,150 +90,147 @@ describe('Config Loader Utils', () => { testPath: join('test', 'path') }; `; - await writeFile(configPath, configContent, 'utf8'); - - const result = await bundleConfigFile({ - fileUrl: new URL(`file://${configPath}`) - }); - - assert.ok(result.code); - assert.ok(result.dependencies.length > 0); - // Verify the bundled code includes the expected configuration - assert.ok(result.code.includes('external-config')); - }); - }); - - describe('importBundledFile', () => { - it('should import bundled code and return module', async () => { - const code = ` + await writeFile(configPath, configContent, 'utf8'); + + const result = await bundleConfigFile({ + fileUrl: new URL(`file://${configPath}`), + }); + + assert.ok(result.code); + assert.ok(result.dependencies.length > 0); + // Verify the bundled code includes the expected configuration + assert.ok(result.code.includes('external-config')); + }); + }); + + describe('importBundledFile', () => { + it('should import bundled code and return module', async () => { + const code = ` export default { imported: true, timestamp: Date.now() }; `; - const result = await importBundledFile({ - code, - root: testDirUrl, - label: 'test-import' - }); - - assert.ok(result.default); - assert.strictEqual(result.default.imported, true); - assert.ok(typeof result.default.timestamp === 'number'); - }); - - it('should successfully import and handle cleanup', async () => { - const code = `export default { cleanup: 'test' };`; - const result = await importBundledFile({ - code, - root: testDirUrl, - label: 'cleanup-test' - }); - // Verify the import worked correctly - assert.ok(result.default); - assert.strictEqual(result.default.cleanup, 'test'); - - // Note: Actual cleanup verification would require mocking fs operations - // or checking the temp directory, which is beyond the scope of this test - }); - }); - - describe('loadAndBundleConfigFile', () => { - it('should return empty result when no fileUrl is provided', async () => { - const result = await loadAndBundleConfigFile({ - root: testDirUrl, - fileUrl: undefined, - label: 'empty-test' - }); - - assert.strictEqual(result.mod, undefined); - assert.deepStrictEqual(result.dependencies, []); - }); - - it('should load and bundle a config file', async () => { - // Create a config file - const configPath = join(testDir, 'load-bundle.config.js'); - const configContent = ` + const result = await importBundledFile({ + code, + root: testDirUrl, + label: 'test-import', + }); + + assert.ok(result.default); + assert.strictEqual(result.default.imported, true); + assert.ok(typeof result.default.timestamp === 'number'); + }); + + it('should successfully import and handle cleanup', async () => { + const code = `export default { cleanup: 'test' };`; + const result = await importBundledFile({ + code, + root: testDirUrl, + label: 'cleanup-test', + }); + // Verify the import worked correctly + assert.ok(result.default); + assert.strictEqual(result.default.cleanup, 'test'); + + // Note: Actual cleanup verification would require mocking fs operations + // or checking the temp directory, which is beyond the scope of this test + }); + }); + + describe('loadAndBundleConfigFile', () => { + it('should return empty result when no fileUrl is provided', async () => { + const result = await loadAndBundleConfigFile({ + root: testDirUrl, + fileUrl: undefined, + label: 'empty-test', + }); + + assert.strictEqual(result.mod, undefined); + assert.deepStrictEqual(result.dependencies, []); + }); + + it('should load and bundle a config file', async () => { + // Create a config file + const configPath = join(testDir, 'load-bundle.config.js'); + const configContent = ` export default { loaded: true, bundled: true }; `; - await writeFile(configPath, configContent, 'utf8'); - - const result = await loadAndBundleConfigFile({ - root: testDirUrl, - fileUrl: new URL(`file://${configPath}`), - label: 'load-bundle-test' - }); - - assert.ok(result.mod); - assert.ok(result.mod.default); - assert.strictEqual(result.mod.default.loaded, true); - assert.strictEqual(result.mod.default.bundled, true); - assert.ok(Array.isArray(result.dependencies)); - }); - }); - - describe('loadConfigFile', () => { - it('should return undefined when no config files exist', async () => { - const result = await loadConfigFile( - testDirUrl, - ['nonexistent1.config.js', 'nonexistent2.config.ts'], - 'missing-test' - ); - - assert.strictEqual(result, undefined); - }); - - it('should load the first existing config file', async () => { - // Create multiple config files - const config1Path = join(testDir, 'first.config.js'); - const config2Path = join(testDir, 'second.config.js'); - - const config1Content = `export default { order: 'first' };`; - const config2Content = `export default { order: 'second' };`; - - await writeFile(config1Path, config1Content, 'utf8'); - await writeFile(config2Path, config2Content, 'utf8'); - - const result = await loadConfigFile( - testDirUrl, - ['nonexistent.config.js', 'first.config.js', 'second.config.js'], - 'order-test' - ); - - assert.ok(result); - assert.strictEqual(result.order, 'first'); - }); - - it('should throw error when config file has no default export', async () => { - // Create a config file without default export - const configPath = join(testDir, 'no-default.config.js'); - const configContent = ` + await writeFile(configPath, configContent, 'utf8'); + + const result = await loadAndBundleConfigFile({ + root: testDirUrl, + fileUrl: new URL(`file://${configPath}`), + label: 'load-bundle-test', + }); + + assert.ok(result.mod); + assert.ok(result.mod.default); + assert.strictEqual(result.mod.default.loaded, true); + assert.strictEqual(result.mod.default.bundled, true); + assert.ok(Array.isArray(result.dependencies)); + }); + }); + + describe('loadConfigFile', () => { + it('should return undefined when no config files exist', async () => { + const result = await loadConfigFile( + testDirUrl, + ['nonexistent1.config.js', 'nonexistent2.config.ts'], + 'missing-test' + ); + + assert.strictEqual(result, undefined); + }); + + it('should load the first existing config file', async () => { + // Create multiple config files + const config1Path = join(testDir, 'first.config.js'); + const config2Path = join(testDir, 'second.config.js'); + + const config1Content = `export default { order: 'first' };`; + const config2Content = `export default { order: 'second' };`; + + await writeFile(config1Path, config1Content, 'utf8'); + await writeFile(config2Path, config2Content, 'utf8'); + + const result = await loadConfigFile( + testDirUrl, + ['nonexistent.config.js', 'first.config.js', 'second.config.js'], + 'order-test' + ); + + assert.ok(result); + assert.strictEqual(result.order, 'first'); + }); + + it('should throw error when config file has no default export', async () => { + // Create a config file without default export + const configPath = join(testDir, 'no-default.config.js'); + const configContent = ` export const namedExport = { value: 'named' }; // No default export `; - await writeFile(configPath, configContent, 'utf8'); - - await assert.rejects( - async () => { - await loadConfigFile( - testDirUrl, - ['no-default.config.js'], - 'no-default-test' - ); - }, - { - message: 'Missing or invalid default export. Please export your config object as the default export.' - } - ); - }); - - it('should load TypeScript config file', async () => { - const configPath = join(testDir, 'typescript-load.config.ts'); - const configContent = ` + await writeFile(configPath, configContent, 'utf8'); + + await assert.rejects( + async () => { + await loadConfigFile(testDirUrl, ['no-default.config.js'], 'no-default-test'); + }, + { + message: + 'Missing or invalid default export. Please export your config object as the default export.', + } + ); + }); + + it('should load TypeScript config file', async () => { + const configPath = join(testDir, 'typescript-load.config.ts'); + const configContent = ` interface TestConfig { name: string; features: string[]; @@ -246,23 +243,19 @@ describe('Config Loader Utils', () => { export default config; `; - await writeFile(configPath, configContent, 'utf8'); - - const result = await loadConfigFile( - testDirUrl, - ['typescript-load.config.ts'], - 'ts-test' - ); - - assert.ok(result); - assert.strictEqual(result.name, 'typescript-test'); - assert.ok(Array.isArray(result.features)); - assert.ok(result.features.includes('typescript')); - }); - - it('should handle config file with complex exports', async () => { - const configPath = join(testDir, 'complex.config.js'); - const configContent = ` + await writeFile(configPath, configContent, 'utf8'); + + const result = await loadConfigFile(testDirUrl, ['typescript-load.config.ts'], 'ts-test'); + + assert.ok(result); + assert.strictEqual(result.name, 'typescript-test'); + assert.ok(Array.isArray(result.features)); + assert.ok(result.features.includes('typescript')); + }); + + it('should handle config file with complex exports', async () => { + const configPath = join(testDir, 'complex.config.js'); + const configContent = ` const baseConfig = { environment: 'test', database: { @@ -280,21 +273,17 @@ describe('Config Loader Utils', () => { timestamp: Date.now() }; `; - await writeFile(configPath, configContent, 'utf8'); - - const result = await loadConfigFile( - testDirUrl, - ['complex.config.js'], - 'complex-test' - ); - - assert.ok(result); - assert.strictEqual(result.environment, 'test'); - assert.strictEqual(result.database.host, 'localhost'); - assert.strictEqual(result.database.port, 5432); - assert.deepStrictEqual(result.features, ['feature1', 'feature2']); - assert.strictEqual(result.computed, 2); - assert.ok(typeof result.timestamp === 'number'); - }); - }); + await writeFile(configPath, configContent, 'utf8'); + + const result = await loadConfigFile(testDirUrl, ['complex.config.js'], 'complex-test'); + + assert.ok(result); + assert.strictEqual(result.environment, 'test'); + assert.strictEqual(result.database.host, 'localhost'); + assert.strictEqual(result.database.port, 5432); + assert.deepStrictEqual(result.features, ['feature1', 'feature2']); + assert.strictEqual(result.computed, 2); + assert.ok(typeof result.timestamp === 'number'); + }); + }); }); diff --git a/packages/@withstudiocms/config-utils/test/utils/tryCatch.test.js b/packages/@withstudiocms/config-utils/test/utils/tryCatch.test.js index 6be21328..0e76b0ce 100644 --- a/packages/@withstudiocms/config-utils/test/utils/tryCatch.test.js +++ b/packages/@withstudiocms/config-utils/test/utils/tryCatch.test.js @@ -3,311 +3,311 @@ import { describe, it } from 'node:test'; import { tryCatch } from '../../dist/utils/tryCatch.js'; describe('tryCatch', () => { - describe('with synchronous functions', () => { - it('should return result and null error for successful sync function', async () => { - const syncFn = () => 42; - - const [result, error] = await tryCatch(syncFn); + describe('with synchronous functions', () => { + it('should return result and null error for successful sync function', async () => { + const syncFn = () => 42; + + const [result, error] = await tryCatch(syncFn); - assert.strictEqual(result, 42); - assert.strictEqual(error, null); - }); + assert.strictEqual(result, 42); + assert.strictEqual(error, null); + }); - it('should return null result and error for throwing sync function', async () => { - const throwingFn = () => { - throw new Error('Sync error'); - }; + it('should return null result and error for throwing sync function', async () => { + const throwingFn = () => { + throw new Error('Sync error'); + }; - const [result, error] = await tryCatch(throwingFn); + const [result, error] = await tryCatch(throwingFn); - assert.strictEqual(result, null); - assert.ok(error instanceof Error); - assert.strictEqual(error.message, 'Sync error'); - }); + assert.strictEqual(result, null); + assert.ok(error instanceof Error); + assert.strictEqual(error.message, 'Sync error'); + }); - it('should handle sync function returning different data types', async () => { - // String - const [stringResult] = await tryCatch(() => 'hello'); - assert.strictEqual(stringResult, 'hello'); + it('should handle sync function returning different data types', async () => { + // String + const [stringResult] = await tryCatch(() => 'hello'); + assert.strictEqual(stringResult, 'hello'); - // Number - const [numberResult] = await tryCatch(() => 123); - assert.strictEqual(numberResult, 123); + // Number + const [numberResult] = await tryCatch(() => 123); + assert.strictEqual(numberResult, 123); - // Object - const obj = { foo: 'bar' }; - const [objectResult] = await tryCatch(() => obj); - assert.strictEqual(objectResult, obj); - - // Array - const arr = [1, 2, 3]; - const [arrayResult] = await tryCatch(() => arr); - assert.deepStrictEqual(arrayResult, arr); - - // Boolean - const [boolResult] = await tryCatch(() => true); - assert.strictEqual(boolResult, true); - - // Null - const [nullResult] = await tryCatch(() => null); - assert.strictEqual(nullResult, null); - - // Undefined - const [undefinedResult] = await tryCatch(() => undefined); - assert.strictEqual(undefinedResult, undefined); - }); - }); - - describe('with asynchronous functions', () => { - it('should return result and null error for successful async function', async () => { - const asyncFn = async () => { - await new Promise(resolve => setTimeout(resolve, 10)); - return 'async result'; - }; - - const [result, error] = await tryCatch(asyncFn); - - assert.strictEqual(result, 'async result'); - assert.strictEqual(error, null); - }); - - it('should return null result and error for rejecting async function', async () => { - const rejectingFn = async () => { - await new Promise(resolve => setTimeout(resolve, 10)); - throw new Error('Async error'); - }; - - const [result, error] = await tryCatch(rejectingFn); - - assert.strictEqual(result, null); - assert.ok(error instanceof Error); - assert.strictEqual(error.message, 'Async error'); - }); - - it('should handle function that returns a promise', async () => { - const promiseReturningFn = () => Promise.resolve('promise result'); - - const [result, error] = await tryCatch(promiseReturningFn); - - assert.strictEqual(result, 'promise result'); - assert.strictEqual(error, null); - }); - - it('should handle function that returns a rejecting promise', async () => { - const rejectingPromiseFn = () => Promise.reject(new Error('Promise rejection')); - - const [result, error] = await tryCatch(rejectingPromiseFn); - - assert.strictEqual(result, null); - assert.ok(error instanceof Error); - assert.strictEqual(error.message, 'Promise rejection'); - }); - }); - - describe('with direct promise values', () => { - it('should handle resolving promise directly', async () => { - const promise = Promise.resolve('direct promise'); - - const [result, error] = await tryCatch(promise); - - assert.strictEqual(result, 'direct promise'); - assert.strictEqual(error, null); - }); - - it('should handle rejecting promise directly', async () => { - const promise = Promise.reject(new Error('Direct promise rejection')); - - const [result, error] = await tryCatch(promise); - - assert.strictEqual(result, null); - assert.ok(error instanceof Error); - assert.strictEqual(error.message, 'Direct promise rejection'); - }); - - it('should handle promise that resolves with different data types', async () => { - // Object promise - const objPromise = Promise.resolve({ key: 'value' }); - const [objResult] = await tryCatch(objPromise); - assert.deepStrictEqual(objResult, { key: 'value' }); + // Object + const obj = { foo: 'bar' }; + const [objectResult] = await tryCatch(() => obj); + assert.strictEqual(objectResult, obj); + + // Array + const arr = [1, 2, 3]; + const [arrayResult] = await tryCatch(() => arr); + assert.deepStrictEqual(arrayResult, arr); + + // Boolean + const [boolResult] = await tryCatch(() => true); + assert.strictEqual(boolResult, true); + + // Null + const [nullResult] = await tryCatch(() => null); + assert.strictEqual(nullResult, null); + + // Undefined + const [undefinedResult] = await tryCatch(() => undefined); + assert.strictEqual(undefinedResult, undefined); + }); + }); + + describe('with asynchronous functions', () => { + it('should return result and null error for successful async function', async () => { + const asyncFn = async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return 'async result'; + }; + + const [result, error] = await tryCatch(asyncFn); + + assert.strictEqual(result, 'async result'); + assert.strictEqual(error, null); + }); + + it('should return null result and error for rejecting async function', async () => { + const rejectingFn = async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + throw new Error('Async error'); + }; + + const [result, error] = await tryCatch(rejectingFn); + + assert.strictEqual(result, null); + assert.ok(error instanceof Error); + assert.strictEqual(error.message, 'Async error'); + }); + + it('should handle function that returns a promise', async () => { + const promiseReturningFn = () => Promise.resolve('promise result'); + + const [result, error] = await tryCatch(promiseReturningFn); + + assert.strictEqual(result, 'promise result'); + assert.strictEqual(error, null); + }); + + it('should handle function that returns a rejecting promise', async () => { + const rejectingPromiseFn = () => Promise.reject(new Error('Promise rejection')); + + const [result, error] = await tryCatch(rejectingPromiseFn); + + assert.strictEqual(result, null); + assert.ok(error instanceof Error); + assert.strictEqual(error.message, 'Promise rejection'); + }); + }); + + describe('with direct promise values', () => { + it('should handle resolving promise directly', async () => { + const promise = Promise.resolve('direct promise'); + + const [result, error] = await tryCatch(promise); + + assert.strictEqual(result, 'direct promise'); + assert.strictEqual(error, null); + }); + + it('should handle rejecting promise directly', async () => { + const promise = Promise.reject(new Error('Direct promise rejection')); + + const [result, error] = await tryCatch(promise); + + assert.strictEqual(result, null); + assert.ok(error instanceof Error); + assert.strictEqual(error.message, 'Direct promise rejection'); + }); + + it('should handle promise that resolves with different data types', async () => { + // Object promise + const objPromise = Promise.resolve({ key: 'value' }); + const [objResult] = await tryCatch(objPromise); + assert.deepStrictEqual(objResult, { key: 'value' }); - // Array promise - const arrPromise = Promise.resolve([1, 2, 3]); - const [arrResult] = await tryCatch(arrPromise); - assert.deepStrictEqual(arrResult, [1, 2, 3]); + // Array promise + const arrPromise = Promise.resolve([1, 2, 3]); + const [arrResult] = await tryCatch(arrPromise); + assert.deepStrictEqual(arrResult, [1, 2, 3]); - // Null promise - const nullPromise = Promise.resolve(null); - const [nullResult] = await tryCatch(nullPromise); - assert.strictEqual(nullResult, null); - }); - }); + // Null promise + const nullPromise = Promise.resolve(null); + const [nullResult] = await tryCatch(nullPromise); + assert.strictEqual(nullResult, null); + }); + }); - describe('error handling and types', () => { - it('should handle custom error types', async () => { - class CustomError extends Error { - constructor(message, code) { - super(message); - this.name = 'CustomError'; - this.code = code; - } - } + describe('error handling and types', () => { + it('should handle custom error types', async () => { + class CustomError extends Error { + constructor(message, code) { + super(message); + this.name = 'CustomError'; + this.code = code; + } + } - const throwingFn = () => { - throw new CustomError('Custom error message', 'CUSTOM_CODE'); - }; + const throwingFn = () => { + throw new CustomError('Custom error message', 'CUSTOM_CODE'); + }; - const [result, error] = await tryCatch(throwingFn); + const [result, error] = await tryCatch(throwingFn); - assert.strictEqual(result, null); - assert.ok(error instanceof CustomError); - assert.strictEqual(error.message, 'Custom error message'); - assert.strictEqual(error.code, 'CUSTOM_CODE'); - }); + assert.strictEqual(result, null); + assert.ok(error instanceof CustomError); + assert.strictEqual(error.message, 'Custom error message'); + assert.strictEqual(error.code, 'CUSTOM_CODE'); + }); - it('should handle non-Error throws', async () => { - const throwingFn = () => { - throw 'string error'; - }; - - const [result, error] = await tryCatch(throwingFn); - - assert.strictEqual(result, null); - assert.strictEqual(error, 'string error'); - }); - - it('should handle thrown objects', async () => { - const errorObj = { message: 'Object error', code: 500 }; - const throwingFn = () => { - throw errorObj; - }; - - const [result, error] = await tryCatch(throwingFn); - - assert.strictEqual(result, null); - assert.strictEqual(error, errorObj); - }); - - it('should handle thrown numbers', async () => { - const throwingFn = () => { - throw 404; - }; - - const [result, error] = await tryCatch(throwingFn); - - assert.strictEqual(result, null); - assert.strictEqual(error, 404); - }); - }); - - describe('edge cases', () => { - it('should handle function that returns undefined', async () => { - const undefinedFn = () => undefined; - - const [result, error] = await tryCatch(undefinedFn); - - assert.strictEqual(result, undefined); - assert.strictEqual(error, null); - }); - - it('should handle function that returns null', async () => { - const nullFn = () => null; - - const [result, error] = await tryCatch(nullFn); - - assert.strictEqual(result, null); - assert.strictEqual(error, null); - }); - - it('should handle async function that returns undefined', async () => { - const asyncUndefinedFn = async () => { - await new Promise(resolve => setTimeout(resolve, 1)); - return undefined; - }; - - const [result, error] = await tryCatch(asyncUndefinedFn); - - assert.strictEqual(result, undefined); - assert.strictEqual(error, null); - }); - - it('should handle promise that resolves to undefined', async () => { - const promise = Promise.resolve(undefined); - - const [result, error] = await tryCatch(promise); - - assert.strictEqual(result, undefined); - assert.strictEqual(error, null); - }); - - it('should handle very long running operations', async () => { - const slowFn = async () => { - await new Promise(resolve => setTimeout(resolve, 50)); - return 'slow result'; - }; - - const [result, error] = await tryCatch(slowFn); - - assert.strictEqual(result, 'slow result'); - assert.strictEqual(error, null); - }); - }); - - describe('real-world usage examples', () => { - it('should handle file system operations', async () => { - // Simulate a file read operation - const readFile = async (path) => { - if (path === '/valid/path') { - return 'file contents'; - } - throw new Error(`ENOENT: no such file or directory, open '${path}'`); - }; - - // Successful read - const [content, readError] = await tryCatch(() => readFile('/valid/path')); - assert.strictEqual(content, 'file contents'); - assert.strictEqual(readError, null); - - // Failed read - const [failedContent, failedError] = await tryCatch(() => readFile('/invalid/path')); - assert.strictEqual(failedContent, null); - assert.ok(failedError instanceof Error); - assert.ok(failedError.message.includes('ENOENT')); - }); - - it('should handle JSON parsing', async () => { - const parseJSON = (str) => JSON.parse(str); - - // Valid JSON - const [validResult] = await tryCatch(() => parseJSON('{"key": "value"}')); - assert.deepStrictEqual(validResult, { key: 'value' }); - - // Invalid JSON - const [invalidResult, parseError] = await tryCatch(() => parseJSON('invalid json')); - assert.strictEqual(invalidResult, null); - assert.ok(parseError instanceof SyntaxError); - }); - - it('should handle network requests simulation', async () => { - const fetchData = async (url) => { - if (url.includes('success')) { - await new Promise(resolve => setTimeout(resolve, 10)); - return { data: 'success response' }; - } - if (url.includes('timeout')) { - await new Promise(resolve => setTimeout(resolve, 10)); - throw new Error('Request timeout'); - } - throw new Error('Network error'); - }; - - // Successful request - const [successData] = await tryCatch(() => fetchData('http://example.com/success')); - assert.deepStrictEqual(successData, { data: 'success response' }); - - // Failed request - const [failedData, networkError] = await tryCatch(() => fetchData('http://example.com/fail')); - assert.strictEqual(failedData, null); - assert.strictEqual(networkError.message, 'Network error'); - }); - }); -}); \ No newline at end of file + it('should handle non-Error throws', async () => { + const throwingFn = () => { + throw 'string error'; + }; + + const [result, error] = await tryCatch(throwingFn); + + assert.strictEqual(result, null); + assert.strictEqual(error, 'string error'); + }); + + it('should handle thrown objects', async () => { + const errorObj = { message: 'Object error', code: 500 }; + const throwingFn = () => { + throw errorObj; + }; + + const [result, error] = await tryCatch(throwingFn); + + assert.strictEqual(result, null); + assert.strictEqual(error, errorObj); + }); + + it('should handle thrown numbers', async () => { + const throwingFn = () => { + throw 404; + }; + + const [result, error] = await tryCatch(throwingFn); + + assert.strictEqual(result, null); + assert.strictEqual(error, 404); + }); + }); + + describe('edge cases', () => { + it('should handle function that returns undefined', async () => { + const undefinedFn = () => undefined; + + const [result, error] = await tryCatch(undefinedFn); + + assert.strictEqual(result, undefined); + assert.strictEqual(error, null); + }); + + it('should handle function that returns null', async () => { + const nullFn = () => null; + + const [result, error] = await tryCatch(nullFn); + + assert.strictEqual(result, null); + assert.strictEqual(error, null); + }); + + it('should handle async function that returns undefined', async () => { + const asyncUndefinedFn = async () => { + await new Promise((resolve) => setTimeout(resolve, 1)); + return undefined; + }; + + const [result, error] = await tryCatch(asyncUndefinedFn); + + assert.strictEqual(result, undefined); + assert.strictEqual(error, null); + }); + + it('should handle promise that resolves to undefined', async () => { + const promise = Promise.resolve(undefined); + + const [result, error] = await tryCatch(promise); + + assert.strictEqual(result, undefined); + assert.strictEqual(error, null); + }); + + it('should handle very long running operations', async () => { + const slowFn = async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return 'slow result'; + }; + + const [result, error] = await tryCatch(slowFn); + + assert.strictEqual(result, 'slow result'); + assert.strictEqual(error, null); + }); + }); + + describe('real-world usage examples', () => { + it('should handle file system operations', async () => { + // Simulate a file read operation + const readFile = async (path) => { + if (path === '/valid/path') { + return 'file contents'; + } + throw new Error(`ENOENT: no such file or directory, open '${path}'`); + }; + + // Successful read + const [content, readError] = await tryCatch(() => readFile('/valid/path')); + assert.strictEqual(content, 'file contents'); + assert.strictEqual(readError, null); + + // Failed read + const [failedContent, failedError] = await tryCatch(() => readFile('/invalid/path')); + assert.strictEqual(failedContent, null); + assert.ok(failedError instanceof Error); + assert.ok(failedError.message.includes('ENOENT')); + }); + + it('should handle JSON parsing', async () => { + const parseJSON = (str) => JSON.parse(str); + + // Valid JSON + const [validResult] = await tryCatch(() => parseJSON('{"key": "value"}')); + assert.deepStrictEqual(validResult, { key: 'value' }); + + // Invalid JSON + const [invalidResult, parseError] = await tryCatch(() => parseJSON('invalid json')); + assert.strictEqual(invalidResult, null); + assert.ok(parseError instanceof SyntaxError); + }); + + it('should handle network requests simulation', async () => { + const fetchData = async (url) => { + if (url.includes('success')) { + await new Promise((resolve) => setTimeout(resolve, 10)); + return { data: 'success response' }; + } + if (url.includes('timeout')) { + await new Promise((resolve) => setTimeout(resolve, 10)); + throw new Error('Request timeout'); + } + throw new Error('Network error'); + }; + + // Successful request + const [successData] = await tryCatch(() => fetchData('http://example.com/success')); + assert.deepStrictEqual(successData, { data: 'success response' }); + + // Failed request + const [failedData, networkError] = await tryCatch(() => fetchData('http://example.com/fail')); + assert.strictEqual(failedData, null); + assert.strictEqual(networkError.message, 'Network error'); + }); + }); +}); diff --git a/packages/@withstudiocms/config-utils/test/watcher.test.js b/packages/@withstudiocms/config-utils/test/watcher.test.js index 9bac37f6..7f978172 100644 --- a/packages/@withstudiocms/config-utils/test/watcher.test.js +++ b/packages/@withstudiocms/config-utils/test/watcher.test.js @@ -9,67 +9,67 @@ import { exists, findConfig } from '../dist/watcher.js'; const testDir = mkdtempSync(join(tmpdir(), 'watcher-test-')); after(() => { - rmSync(testDir, { recursive: true, force: true }); + rmSync(testDir, { recursive: true, force: true }); }); describe('Watcher Utils', () => { - describe('exists', () => { - it('returns false if path is undefined', () => { - assert.strictEqual(exists(undefined), false); - }); + describe('exists', () => { + it('returns false if path is undefined', () => { + assert.strictEqual(exists(undefined), false); + }); - it('returns true if file exists', () => { - const testFile = join(testDir, 'exists-test.txt'); - writeFileSync(testFile, 'test'); + it('returns true if file exists', () => { + const testFile = join(testDir, 'exists-test.txt'); + writeFileSync(testFile, 'test'); - assert.strictEqual(exists(testFile), true); + assert.strictEqual(exists(testFile), true); - unlinkSync(testFile); - }); + unlinkSync(testFile); + }); - it('returns false if file does not exist', () => { - const nonExistentFile = join(testDir, 'does-not-exist.txt'); - assert.strictEqual(exists(nonExistentFile), false); - }); - }); + it('returns false if file does not exist', () => { + const nonExistentFile = join(testDir, 'does-not-exist.txt'); + assert.strictEqual(exists(nonExistentFile), false); + }); + }); - describe('findConfig', () => { - const projectRootUrl = `${testDir}/`; + describe('findConfig', () => { + const projectRootUrl = `${testDir}/`; - it('returns undefined if configPaths is empty', () => { - assert.strictEqual(findConfig(projectRootUrl, []), undefined); - }); + it('returns undefined if configPaths is empty', () => { + assert.strictEqual(findConfig(projectRootUrl, []), undefined); + }); - it('returns the first configUrl that exists', () => { - const configPaths = ['a.js', 'b.js', 'c.js']; + it('returns the first configUrl that exists', () => { + const configPaths = ['a.js', 'b.js', 'c.js']; - // Create only b.js - const bPath = join(testDir, 'b.js'); - writeFileSync(bPath, 'module.exports = {};'); + // Create only b.js + const bPath = join(testDir, 'b.js'); + writeFileSync(bPath, 'module.exports = {};'); - assert.strictEqual(findConfig(projectRootUrl, configPaths), `${projectRootUrl}b.js`); + assert.strictEqual(findConfig(projectRootUrl, configPaths), `${projectRootUrl}b.js`); - unlinkSync(bPath); - }); + unlinkSync(bPath); + }); - it('returns undefined if none of the config files exist', () => { - const configPaths = ['nonexistent1.js', 'nonexistent2.js', 'nonexistent3.js']; - assert.strictEqual(findConfig(projectRootUrl, configPaths), undefined); - }); + it('returns undefined if none of the config files exist', () => { + const configPaths = ['nonexistent1.js', 'nonexistent2.js', 'nonexistent3.js']; + assert.strictEqual(findConfig(projectRootUrl, configPaths), undefined); + }); - it('returns the first configUrl if multiple exist', () => { - const configPaths = ['a.js', 'b.js', 'c.js']; + it('returns the first configUrl if multiple exist', () => { + const configPaths = ['a.js', 'b.js', 'c.js']; - // Create both a.js and b.js - const aPath = join(testDir, 'a.js'); - const bPath = join(testDir, 'b.js'); - writeFileSync(aPath, 'module.exports = {};'); - writeFileSync(bPath, 'module.exports = {};'); + // Create both a.js and b.js + const aPath = join(testDir, 'a.js'); + const bPath = join(testDir, 'b.js'); + writeFileSync(aPath, 'module.exports = {};'); + writeFileSync(bPath, 'module.exports = {};'); - assert.strictEqual(findConfig(projectRootUrl, configPaths), `${projectRootUrl}a.js`); + assert.strictEqual(findConfig(projectRootUrl, configPaths), `${projectRootUrl}a.js`); - unlinkSync(aPath); - unlinkSync(bPath); - }); - }); + unlinkSync(aPath); + unlinkSync(bPath); + }); + }); }); diff --git a/packages/@withstudiocms/config-utils/test/zod-utils.test.js b/packages/@withstudiocms/config-utils/test/zod-utils.test.js index 99d26f51..8bf32009 100644 --- a/packages/@withstudiocms/config-utils/test/zod-utils.test.js +++ b/packages/@withstudiocms/config-utils/test/zod-utils.test.js @@ -5,110 +5,115 @@ import { deepRemoveDefaults, parseAndMerge, parseConfig } from '../dist/zod-util // Helper: simple schema for most tests const simpleSchema = z.object({ - foo: z.string(), - bar: z.number().default(42), - baz: z.boolean().optional(), + foo: z.string(), + bar: z.number().default(42), + baz: z.boolean().optional(), }); describe('Zod Utils', () => { - describe('parseConfig', () => { - it('parseConfig parses valid config', () => { - const result = parseConfig(simpleSchema, { foo: 'hello', bar: 1 }); - assert.deepEqual(result, { foo: 'hello', bar: 1 }); - }); + describe('parseConfig', () => { + it('parseConfig parses valid config', () => { + const result = parseConfig(simpleSchema, { foo: 'hello', bar: 1 }); + assert.deepEqual(result, { foo: 'hello', bar: 1 }); + }); - it('parseConfig applies default values', () => { - const result = parseConfig(simpleSchema, { foo: 'hi' }); - assert.deepEqual(result, { foo: 'hi', bar: 42 }); - }); + it('parseConfig applies default values', () => { + const result = parseConfig(simpleSchema, { foo: 'hi' }); + assert.deepEqual(result, { foo: 'hi', bar: 42 }); + }); - it('parseConfig throws on invalid config', () => { - assert.throws( - () => parseConfig(simpleSchema, { bar: 1 }), - /Invalid Configuration Options/ - ); - }); - }); + it('parseConfig throws on invalid config', () => { + assert.throws(() => parseConfig(simpleSchema, { bar: 1 }), /Invalid Configuration Options/); + }); + }); - describe('deepRemoveDefaults', () => { - it('deepRemoveDefaults removes defaults and makes fields optional', () => { - const schema = z.object({ - a: z.string().default('x'), - b: z.number(), - c: z.object({ - d: z.boolean().default(true), - }), - }); - const noDefaults = deepRemoveDefaults(schema); - assert.deepEqual(noDefaults.safeParse({}).success, true); - assert.deepEqual(noDefaults.safeParse({ b: 2, c: { d: false } }).success, true); - }); + describe('deepRemoveDefaults', () => { + it('deepRemoveDefaults removes defaults and makes fields optional', () => { + const schema = z.object({ + a: z.string().default('x'), + b: z.number(), + c: z.object({ + d: z.boolean().default(true), + }), + }); + const noDefaults = deepRemoveDefaults(schema); + assert.deepEqual(noDefaults.safeParse({}).success, true); + assert.deepEqual(noDefaults.safeParse({ b: 2, c: { d: false } }).success, true); + }); - it('deepRemoveDefaults handles arrays, optionals, nullables, and tuples', () => { - const schema = z.object({ - arr: z.array(z.string().default('x')).default(['y']), - opt: z.string().optional(), - nul: z.number().nullable(), - tup: z.tuple([z.string().default('a'), z.number()]), - }); - const noDefaults = deepRemoveDefaults(schema); - assert.deepEqual(noDefaults.safeParse({}).success, true); - assert.deepEqual(noDefaults.safeParse({ arr: ['z'], tup: ['b', 2] }).success, true); - }); - }); + it('deepRemoveDefaults handles arrays, optionals, nullables, and tuples', () => { + const schema = z.object({ + arr: z.array(z.string().default('x')).default(['y']), + opt: z.string().optional(), + nul: z.number().nullable(), + tup: z.tuple([z.string().default('a'), z.number()]), + }); + const noDefaults = deepRemoveDefaults(schema); + assert.deepEqual(noDefaults.safeParse({}).success, true); + assert.deepEqual(noDefaults.safeParse({ arr: ['z'], tup: ['b', 2] }).success, true); + }); + }); - describe('parseAndMerge', () => { - it('parseAndMerge merges inlineConfig and configFile, configFile takes precedence', () => { - const schema = z.object({ - a: z.string().default('A'), - b: z.number().default(1), - c: z.object({ - d: z.boolean().default(false), - }).default({ d: false }), - }); - const inlineConfig = { a: 'inline', b: 2, c: { d: false } }; - const configFile = { a: 'file', c: { d: true } }; - const result = parseAndMerge(schema, inlineConfig, configFile); - assert.deepEqual(result, { a: 'file', b: 2, c: { d: true } }); - }); + describe('parseAndMerge', () => { + it('parseAndMerge merges inlineConfig and configFile, configFile takes precedence', () => { + const schema = z.object({ + a: z.string().default('A'), + b: z.number().default(1), + c: z + .object({ + d: z.boolean().default(false), + }) + .default({ d: false }), + }); + const inlineConfig = { a: 'inline', b: 2, c: { d: false } }; + const configFile = { a: 'file', c: { d: true } }; + const result = parseAndMerge(schema, inlineConfig, configFile); + assert.deepEqual(result, { a: 'file', b: 2, c: { d: true } }); + }); - it('parseAndMerge returns inlineConfig if configFile is undefined', () => { - const schema = z.object({ - a: z.string().default('A'), - b: z.number().default(1), - c: z.object({ - d: z.boolean().default(false), - }).default({ d: false }), - }); - const inlineConfig = { a: 'inline', b: 2, c: { d: true } }; - const result = parseAndMerge(schema, inlineConfig, undefined); - assert.deepEqual(result, inlineConfig); - }); + it('parseAndMerge returns inlineConfig if configFile is undefined', () => { + const schema = z.object({ + a: z.string().default('A'), + b: z.number().default(1), + c: z + .object({ + d: z.boolean().default(false), + }) + .default({ d: false }), + }); + const inlineConfig = { a: 'inline', b: 2, c: { d: true } }; + const result = parseAndMerge(schema, inlineConfig, undefined); + assert.deepEqual(result, inlineConfig); + }); - it('parseAndMerge returns schema defaults if both configs are undefined', () => { - const schema = z.object({ - a: z.string().default('A'), - b: z.number().default(1), - c: z.object({ - d: z.boolean().default(false), - }).default({ d: false }), - }); - const result = parseAndMerge(schema, undefined, undefined); - assert.deepEqual(result, { a: 'A', b: 1, c: { d: false } }); - }); + it('parseAndMerge returns schema defaults if both configs are undefined', () => { + const schema = z.object({ + a: z.string().default('A'), + b: z.number().default(1), + c: z + .object({ + d: z.boolean().default(false), + }) + .default({ d: false }), + }); + const result = parseAndMerge(schema, undefined, undefined); + assert.deepEqual(result, { a: 'A', b: 1, c: { d: false } }); + }); - it('parseAndMerge throws on invalid configFile', () => { - const schema = z.object({ - a: z.string().default('A'), - b: z.number().default(1), - c: z.object({ - d: z.boolean().default(false), - }).default({ d: false }), - }); - assert.throws( - () => parseAndMerge(schema, undefined, { b: 'not-a-number' }), - /Invalid Config Options/ - ); - }); - }); + it('parseAndMerge throws on invalid configFile', () => { + const schema = z.object({ + a: z.string().default('A'), + b: z.number().default(1), + c: z + .object({ + d: z.boolean().default(false), + }) + .default({ d: false }), + }); + assert.throws( + () => parseAndMerge(schema, undefined, { b: 'not-a-number' }), + /Invalid Config Options/ + ); + }); + }); }); diff --git a/packages/@withstudiocms/effect/test/astro/api-route.test.js b/packages/@withstudiocms/effect/test/astro/api-route.test.js index 7362c1f8..a998fc67 100644 --- a/packages/@withstudiocms/effect/test/astro/api-route.test.js +++ b/packages/@withstudiocms/effect/test/astro/api-route.test.js @@ -61,7 +61,9 @@ describe('Api-Route Tests', () => { it('should propagate errors thrown by the handler', async () => { const mockContext = { request: new Request('http://localhost') }; const error = new Error('fail'); - const handler = () => { return Effect.fail(error) }; + const handler = () => { + return Effect.fail(error); + }; const route = createEffectAPIRoute(handler); @@ -102,8 +104,11 @@ describe('Api-Route Tests', () => { }); it('should apply CORS headers if configured', async () => { - const mockContext = { request: new Request('https://example.com', { headers: { Origin: 'https://example.com' } }) }; - const handler = () => Effect.succeed(new Response(JSON.stringify({ message: 'ok' }), { status: 200 })); + const mockContext = { + request: new Request('https://example.com', { headers: { Origin: 'https://example.com' } }), + }; + const handler = () => + Effect.succeed(new Response(JSON.stringify({ message: 'ok' }), { status: 200 })); const route = withEffectAPI(handler, { cors: { @@ -117,7 +122,12 @@ describe('Api-Route Tests', () => { }); it('should handle preflight requests with CORS', async () => { - const mockContext = { request: new Request('https://example.com', { method: 'OPTIONS', headers: { Origin: 'https://example.com' } }) }; + const mockContext = { + request: new Request('https://example.com', { + method: 'OPTIONS', + headers: { Origin: 'https://example.com' }, + }), + }; const handler = () => Effect.succeed(new Response(null, { status: 204 })); const route = withEffectAPI(handler, { @@ -158,7 +168,10 @@ describe('Api-Route Tests', () => { const onSuccess = (response, ctx) => { assert.strictEqual(ctx, mockContext); - return new Response(response.body, { status: response.status, headers: { 'X-Custom-Header': 'value' } }); + return new Response(response.body, { + status: response.status, + headers: { 'X-Custom-Header': 'value' }, + }); }; const route = withEffectAPI(handler, { onSuccess }); @@ -191,8 +204,11 @@ describe('Api-Route Tests', () => { }); it('should handle multiple CORS origins', async () => { - const mockContext = { request: new Request('https://example.com', { headers: { Origin: 'https://example.com' } }) }; - const handler = () => Effect.succeed(new Response(JSON.stringify({ message: 'ok' }), { status: 200 })); + const mockContext = { + request: new Request('https://example.com', { headers: { Origin: 'https://example.com' } }), + }; + const handler = () => + Effect.succeed(new Response(JSON.stringify({ message: 'ok' }), { status: 200 })); const route = withEffectAPI(handler, { cors: { @@ -252,16 +268,9 @@ describe('Api-Route Tests', () => { return Effect.succeed(mockResponse); }; - const { GET, POST } = - new EffectAPIRouteBuilder() - .get(handler) - .post(handler) - .build(); + const { GET, POST } = new EffectAPIRouteBuilder().get(handler).post(handler).build(); - const [postResult, getResult] = await Promise.all([ - POST(mockContext), - GET(mockContext), - ]); + const [postResult, getResult] = await Promise.all([POST(mockContext), GET(mockContext)]); assert.strictEqual(handlerCalledWith, mockContext); assert.strictEqual(postResult, mockResponse); diff --git a/packages/@withstudiocms/effect/test/astro/context-utils.test.js b/packages/@withstudiocms/effect/test/astro/context-utils.test.js index 416fa1c9..f097f9d0 100644 --- a/packages/@withstudiocms/effect/test/astro/context-utils.test.js +++ b/packages/@withstudiocms/effect/test/astro/context-utils.test.js @@ -90,18 +90,13 @@ describe('API Context Utils', () => { }); it('parseAPIContextFormDataToObject: should parse and decode FormData with schema', async () => { - const effect = parseAPIContextFormDataToObject( - mockAPIContext, - dummySchema - ); + const effect = parseAPIContextFormDataToObject(mockAPIContext, dummySchema); const result = await Effect.runPromise(effect); assert.deepEqual(result, { foo: 'bar', file: mockFormData.get('file') }); }); it('parseAPIContextFormDataToObject: should parse FormData without schema', async () => { - const effect = parseAPIContextFormDataToObject( - mockAPIContext - ); + const effect = parseAPIContextFormDataToObject(mockAPIContext); const result = await Effect.runPromise(effect); assert.deepEqual(result, { foo: 'bar', file: mockFormData.get('file') }); }); @@ -135,5 +130,4 @@ describe('API Context Utils', () => { const result = await Effect.runPromise(effect); assert.equal(result, null); }); - }); diff --git a/packages/@withstudiocms/effect/test/astro/response-helpers.test.js b/packages/@withstudiocms/effect/test/astro/response-helpers.test.js index f3ea7517..0294d236 100644 --- a/packages/@withstudiocms/effect/test/astro/response-helpers.test.js +++ b/packages/@withstudiocms/effect/test/astro/response-helpers.test.js @@ -1,140 +1,150 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { AllResponse, createHtmlResponse, createJsonResponse, createRedirectResponse, createTextResponse, OptionsResponse } from '../../dist/astro/response-helpers.js'; +import { + AllResponse, + createHtmlResponse, + createJsonResponse, + createRedirectResponse, + createTextResponse, + OptionsResponse, +} from '../../dist/astro/response-helpers.js'; describe('response-helpers', () => { - describe('OptionsResponse', () => { - it('should return a 204 response with correct headers', () => { - const res = OptionsResponse({ - allowedMethods: ['GET', 'POST'], - allowedOrigins: ['https://example.com'], - headers: { 'X-Test': 'test' }, - }); - assert.strictEqual(res.status, 204); - assert.strictEqual(res.statusText, 'No Content'); - assert.strictEqual(res.headers.get('Allow'), 'OPTIONS, GET, POST'); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), 'https://example.com'); - assert.strictEqual(res.headers.get('X-Test'), 'test'); - assert.ok(res.headers.get('Date')); - }); + describe('OptionsResponse', () => { + it('should return a 204 response with correct headers', () => { + const res = OptionsResponse({ + allowedMethods: ['GET', 'POST'], + allowedOrigins: ['https://example.com'], + headers: { 'X-Test': 'test' }, + }); + assert.strictEqual(res.status, 204); + assert.strictEqual(res.statusText, 'No Content'); + assert.strictEqual(res.headers.get('Allow'), 'OPTIONS, GET, POST'); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), 'https://example.com'); + assert.strictEqual(res.headers.get('X-Test'), 'test'); + assert.ok(res.headers.get('Date')); + }); - it('should default allowedOrigins to *', () => { - const res = OptionsResponse({ - allowedMethods: ['GET'], - }); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - }); - }); + it('should default allowedOrigins to *', () => { + const res = OptionsResponse({ + allowedMethods: ['GET'], + }); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + }); + }); - describe('AllResponse', () => { - it('should return a 405 response with correct headers', () => { - const res = AllResponse({ - allowedOrigins: ['https://foo.com'], - headers: { 'X-Foo': 'bar' }, - }); - assert.strictEqual(res.status, 405); - assert.strictEqual(res.statusText, 'Method Not Allowed'); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), 'https://foo.com'); - assert.strictEqual(res.headers.get('X-Foo'), 'bar'); - assert.ok(res.headers.get('Date')); - }); + describe('AllResponse', () => { + it('should return a 405 response with correct headers', () => { + const res = AllResponse({ + allowedOrigins: ['https://foo.com'], + headers: { 'X-Foo': 'bar' }, + }); + assert.strictEqual(res.status, 405); + assert.strictEqual(res.statusText, 'Method Not Allowed'); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), 'https://foo.com'); + assert.strictEqual(res.headers.get('X-Foo'), 'bar'); + assert.ok(res.headers.get('Date')); + }); - it('should default allowedOrigins to *', () => { - const res = AllResponse({}); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - }); - }); + it('should default allowedOrigins to *', () => { + const res = AllResponse({}); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + }); + }); - describe('createJsonResponse', () => { - it('should return a 200 JSON response by default', async () => { - const data = { foo: 'bar' }; - const res = createJsonResponse(data); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.statusText, 'OK'); - assert.strictEqual(res.headers.get('Content-Type'), 'application/json'); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - assert.deepStrictEqual(JSON.parse(await res.text()), data); - }); + describe('createJsonResponse', () => { + it('should return a 200 JSON response by default', async () => { + const data = { foo: 'bar' }; + const res = createJsonResponse(data); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.statusText, 'OK'); + assert.strictEqual(res.headers.get('Content-Type'), 'application/json'); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + assert.deepStrictEqual(JSON.parse(await res.text()), data); + }); - it('should allow custom status and headers', async () => { - const res = createJsonResponse({ ok: true }, { - status: 201, - statusText: 'Created', - headers: { 'X-Custom': 'yes' }, - allowedOrigins: ['https://bar.com'], - }); - assert.strictEqual(res.status, 201); - assert.strictEqual(res.statusText, 'Created'); - assert.strictEqual(res.headers.get('X-Custom'), 'yes'); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), 'https://bar.com'); - }); - }); + it('should allow custom status and headers', async () => { + const res = createJsonResponse( + { ok: true }, + { + status: 201, + statusText: 'Created', + headers: { 'X-Custom': 'yes' }, + allowedOrigins: ['https://bar.com'], + } + ); + assert.strictEqual(res.status, 201); + assert.strictEqual(res.statusText, 'Created'); + assert.strictEqual(res.headers.get('X-Custom'), 'yes'); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), 'https://bar.com'); + }); + }); - describe('createTextResponse', () => { - it('should return a 200 text response by default', async () => { - const res = createTextResponse('hello world'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.statusText, 'OK'); - assert.strictEqual(res.headers.get('Content-Type'), 'text/plain'); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - assert.strictEqual(await res.text(), 'hello world'); - }); + describe('createTextResponse', () => { + it('should return a 200 text response by default', async () => { + const res = createTextResponse('hello world'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.statusText, 'OK'); + assert.strictEqual(res.headers.get('Content-Type'), 'text/plain'); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + assert.strictEqual(await res.text(), 'hello world'); + }); - it('should allow custom status and headers', async () => { - const res = createTextResponse('foo', { - status: 404, - statusText: 'Not Found', - headers: { 'X-Txt': 'bar' }, - allowedOrigins: ['https://baz.com'], - }); - assert.strictEqual(res.status, 404); - assert.strictEqual(res.statusText, 'Not Found'); - assert.strictEqual(res.headers.get('X-Txt'), 'bar'); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), 'https://baz.com'); - }); - }); + it('should allow custom status and headers', async () => { + const res = createTextResponse('foo', { + status: 404, + statusText: 'Not Found', + headers: { 'X-Txt': 'bar' }, + allowedOrigins: ['https://baz.com'], + }); + assert.strictEqual(res.status, 404); + assert.strictEqual(res.statusText, 'Not Found'); + assert.strictEqual(res.headers.get('X-Txt'), 'bar'); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), 'https://baz.com'); + }); + }); - describe('createHtmlResponse', () => { - it('should return a 200 HTML response by default', async () => { - const html = '