From 05ba15bcc4026c547741170c2ff88dff7374aaad Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 20 Dec 2023 13:00:27 +0530 Subject: [PATCH] refactor: small internal refactors and add documentation --- .github/stale.yml | 4 +- .gitignore | 1 + README.md | 390 +++++++++++++++++- package.json | 2 + src/assets_dev_server.ts | 22 +- src/bundler.ts | 73 +++- src/code_transformer/main.ts | 69 +++- src/code_transformer/rc_file_transformer.ts | 20 +- src/dev_server.ts | 41 +- src/helpers.ts | 6 +- src/test_runner.ts | 73 +++- src/types.ts | 150 +++++-- .../code_transformer.spec.ts.cjs | 45 +- tests/bundler.spec.ts | 11 +- tests/code_transformer.spec.ts | 186 ++++++++- tests/copy.spec.ts | 11 +- tests/helpers.spec.ts | 73 ++++ 17 files changed, 1039 insertions(+), 138 deletions(-) create mode 100644 tests/helpers.spec.ts diff --git a/.github/stale.yml b/.github/stale.yml index 7a6a571..f767674 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -6,10 +6,10 @@ daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - - "Type: Security" + - 'Type: Security' # Label to use when marking an issue as stale -staleLabel: "Status: Abandoned" +staleLabel: 'Status: Abandoned' # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > diff --git a/.gitignore b/.gitignore index 1223d04..104dae5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ yarn.lock shrinkwrap.yaml test/__app/ test/__app +tmp diff --git a/README.md b/README.md index 6acd931..881f9d2 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,398 @@ [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] ## Introduction -Assembler exports the API for starting the **AdonisJS development server**, **building project for production** and **running tests** in watch mode. Assembler must be used during development only. +AdonisJS Assembler is a development toolkit used by AdonisJS to perform tasks like **starting the dev server in watch mode**, **running tests in watch mode**, and **applying codemods** to modify source files. -## Official Documentation -The documentation is available on the official website +Assembler should always be installed as a development dependency. If your project needs Assembler APIs in production, you must reconsider your approach. + +## Goals +Assembler is built around the following goals. + +- Expose a coding interface and not a user interface. In other words, Assembler will never expose any CLI commands. +- Encapsulate tasks under a single API. Instead of providing ten different utilities to run a dev server, Assembler will expose one API to run the dev server. +- House all development APIs needed by AdonisJS. Therefore, the scope of the Assembler might increase over time. + +## Dev server +You can start the HTTP server of an AdonisJS application using the `node --loader=ts-node/esm bin/server.ts` file. However, this approach has some limitations and may not provide the best DX. + +### Using a file watcher +You might be tempted to use the Node.js built-in file watcher with the `--watch` flag. However, the Node.js file watcher does not integrate with TypeScript. As a result, you will be tweaking its configuration options to get an ideal experience. + +On the other hand, the Assembler file watcher takes the following approach. + +- Parses the `tsconfig.json` file to collect the list of files that are part of your TypeScript project. As a result, if you ever want to ignore any file, you do it directly within the `tsconfig.json` file, and the watcher will pick it up. +- It uses the `metaFiles` array defined inside the `adonisrc.ts` file to watch additional files that are not `.js` or `.ts`. It may be the Edge templates, markdown files, YAML files, etc. + +### Starting the asset bundler server +If you create a full-stack application, the chances of using Webpack or Vite are high. Instead of starting your assets bundler inside a separate process, you can also rely on Assembler to start a parallel process for the assets bundler. + +The [`node ace serve` command](https://github.com/adonisjs/core/blob/next/commands/serve.ts#L88) detects the assets bundler used by your AdonisJS project and passes it to Assembler. + +Therefore, if you run the `serve` command with a `vite.config.js` file, you will notice that the Assembler will start both Vite and the AdonisJS HTTP server. + +### Picking a random port +The PORT on which an AdonisJS application should run is configured inside the `.env` file of your AdonisJS application. However, you will often start multiple projects together and have to edit the `.env` file to ensure both projects run on different ports. + +With Assembler, you do not have to edit the `.env` files since Assembler will pick a random port of your application if the configured one is already in use. + +### Usage +You may import and use the `DevServer` as follows. + +```ts +import ts from 'typescript' +import { DevServer } from '@adonisjs/assembler' + +const appRoot = new URL('./', import.meta.url) + +const devServer = new DevServer(appRoot, { + /** + * Arguments to pass to the "bin/server.ts" file + */ + scriptArgs: [], + + /** + * Arguments to pass to the Node.js CLI + */ + nodeArgs: [], + + /** + * An array of metaFiles to watch and re-start the + * HTTP server only if the "reloadServer" flag is + * true. + */ + metaFiles: [ + { + pattern: 'resources/views/**/*.edge', + reloadServer: false, + } + ], + + /** + * The assets bundler process to start + */ + assets: { + enabled: true, + name: 'vite', + cmd: 'vite', + args: [] + } +}) + +devServer.onError((error) => { + process.exitCode = 1 +}) +devServer.onClose((exitCode) => { + process.exitCode = exitCode +}) + +await devServer.runAndWatch(ts) +``` + +You may start the dev server and assets bundler dev server using the `start` method. + +```ts +await devServer.start() +``` + +## Test runner +The `TestRunner` is used to execute the `bin/test.ts` file of your AdonisJS application. Like the `DevServer`, the `TestRunner` allows you to watch for file changes and re-run the tests. The following steps are taken to re-run tests in watch mode. + +> [!NOTE] +> Read [Using a file watcher](#using-a-file-watcher) section to understand which files are watched by the file watcher. + +- If the changed file is a test file, only tests for that file will be re-run. +- Otherwise, all tests will re-run with respect to the initial filters applied when running the `node ace test` command. + +### Usage + +You may import and use the `TestRunner` as follows. + +```ts +import ts from 'typescript' +import { TestRunner } from '@adonisjs/assembler' + +const appRoot = new URL('./', import.meta.url) + +const runner = new TestRunner(appRoot, { + /** + * Arguments to pass to the "bin/test.ts" file + */ + scriptArgs: [], + + /** + * Arguments to pass to the Node.js CLI + */ + nodeArgs: [], + + /** + * An array of suites and their glob patterns + */ + suites: [ + { + name: 'unit', + files: ['tests/unit/**/*.spec.ts'] + }, + { + name: 'functional', + files: ['tests/functional/**/*.spec.ts'] + } + ], + + /** + * Initial set of filters to apply. These filters + * will be re-applied when re-running tests in + * watch mode + */ + filters: { + suites: ['unit'], + tags: ['@slow'] + } +}) + +await runner.runAndWatch(ts) +``` + +You can run tests without the watcher using the `run` method. + +```ts +await runner.run() +``` + +## Bundler +The `Bundler` is used to create the production build of an AdonisJS application. The following steps are performed to generate the build. + +- Clean up the existing build directory. +- Compile frontend assets (if an assets bundler is configured). +- Create JavaScript build using `tsc` (The TypeScript's official compiler). +- Copy the `ace.js` file to the build folder. Since the ace file ends with the `.js` extension, it is not compiled by the TypeScript compiler. +- Copy `package.json` and the **lock-file of the package manager** you are using to the `build` folder. This operation only supports `bun | npm | yarn | pnpm`. For other bundlers, you will have to copy the lock file manually. +- The end. + +### Usage +You may import and use the `Bundler` as follows. + +```ts +import ts from 'typescript' +import { Bundler } from '@adonisjs/assembler' + +const appRoot = new URL('./', import.meta.url) + +const bundler = new Bundler(appRoot, ts, { + /** + * Metafiles to copy to the build folder + */ + metaFiles: [ + { + pattern: 'resources/views/**/*.edge', + reloadServer: false, + } + ], + + /** + * The assets bundler to use to bundle the frontend + * assets + */ + assets: { + enabled: true, + name: 'vite', + cmd: 'vite', + args: ['build'] + } +}) +``` + +## Codemods +Assembler also exports certain codemods to modify the source files of an AdonisJS project to configure packages. + +The codemods relies on the defaults of AdonisJS and will not work if a project does not follow the defaults. This is an intentional limit since we only have limited time to craft codemods that work with every possible setup. + +### Usage +You may import and use the `Codemods` as follows. + +```ts +import { CodeTransformer } from '@adonisjs/assembler/code_transformer' + +const appRoot = new URL('./', import.meta.url) + +const transformer = new CodeTransformer(appRoot) +``` + +### defineEnvValidations +Define validation rules for environment variables. The method accepts a key-value pair of variables. The `key` is the env variable name, and the `value` is the validation expression as a string. + +> [!IMPORTANT] +> This codemod expects the `start/env.ts` file to exist and must have the `export default await Env.create` method call. +> +> Also, the codemod does not overwrite the existing validation rule for a given environment variable. This is done to respect in-app modifications. + +```ts +const transformer = new CodeTransformer(appRoot) + +try { + await transformer.defineEnvValidations({ + leadingComment: 'App environment variables', + variables: { + PORT: 'Env.schema.number()', + HOST: 'Env.schema.string()', + } + }) +} catch (error) { + console.error('Unable to define env validations') + console.error(error) +} +``` + +Output + +```ts +import { Env } from '@adonisjs/core/env' + +export default await Env.create(new URL('../', import.meta.url), { + PORT: Env.schema.number(), + HOST: Env.schema.string(), +}) +``` + +### addMiddlewareToStack +Register AdonisJS middleware to one of the known middleware stacks. The method accepts the middleware stack and an array of middleware to register. + +The middleware stack could be one of `server | router | named`. + +> [!IMPORTANT] +> This codemod expects the `start/kernel.ts` file to exist and must have a function call for the middleware stack for which you are trying to register a middleware. + +```ts +const transformer = new CodeTransformer(appRoot) + +try { + await transformer.addMiddlewareToStack('router', [ + { + path: '@adonisjs/core/bodyparser_middleware' + } + ]) +} catch (error) { + console.error('Unable to register middleware') + console.error(error) +} +``` + +Output + +```ts +import router from '@adonisjs/core/services/router' + +router.use([ + () => import('@adonisjs/core/bodyparser_middleware') +]) +``` + +You may define named middleware as follows. + +```ts +const transformer = new CodeTransformer(appRoot) + +try { + await transformer.addMiddlewareToStack('named', [ + { + name: 'auth', + path: '@adonisjs/auth/auth_middleware' + } + ]) +} catch (error) { + console.error('Unable to register middleware') + console.error(error) +} +``` + +### updateRcFile +Register `providers`, `commands`, define `metaFiles` and `commandAliases` to the `adonisrc.ts` file. + +> [!IMPORTANT] +> This codemod expects the `adonisrc.ts` file to exist and must have an `export default defineConfig` function call. + +```ts +const transformer = new CodeTransformer(appRoot) + +try { + await transformer.updateRcFile((rcFile) => { + rcFile + .addProvider('@adonisjs/lucid/db_provider') + .addCommand('@adonisjs/lucid/commands'), + .setCommandAlias('migrate', 'migration:run') + }) +} catch (error) { + console.error('Unable to update adonisrc.ts file') + console.error(error) +} +``` + +Output + +```ts +import { defineConfig } from '@adonisjs/core/app' + +export default defineConfig({ + commands: [ + () => import('@adonisjs/lucid/commands') + ], + providers: [ + () => import('@adonisjs/lucid/db_provider') + ], + commandAliases: { + migrate: 'migration:run' + } +}) +``` + +### addJapaPlugin +Register a Japa plugin to the `tests/bootstrap.ts` file. + +> [!IMPORTANT] +> This codemod expects the `tests/bootstrap.ts` file to exist and must have the `export const plugins: Config['plugins']` export. + +```ts +const transformer = new CodeTransformer(appRoot) + +const imports = [ + { + isNamed: false, + module: '@adonisjs/core/services/app', + identifier: 'app' + }, + { + isNamed: true, + module: '@adonisjs/session/plugins/api_client', + identifier: 'sessionApiClient' + } +] +const pluginUsage = 'sessionApiClient(app)' + +try { + await transformer.addJapaPlugin(pluginUsage, imports) +} catch (error) { + console.error('Unable to register japa plugin') + console.error(error) +} +``` + +Output + +```ts +import app from '@adonisjs/core/services/app' +import { sessionApiClient } from '@adonisjs/session/plugins/api_client' + +export const plugins: Config['plugins'] = [ + sessionApiClient(app) +] +``` ## Contributing -One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. +One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believe in the framework's principles. We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. ## Code of Conduct -In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). +To ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). ## License AdonisJS Assembler is open-sourced software licensed under the [MIT license](LICENSE.md). diff --git a/package.json b/package.json index 5eaa9c6..bc149ab 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,8 @@ "exclude": [ "tests/**", "build/**", + "bin/**", + "tmp/**", "examples/**", "src/dev_server.ts", "src/test_runner.ts", diff --git a/src/assets_dev_server.ts b/src/assets_dev_server.ts index 3131882..5721c8d 100644 --- a/src/assets_dev_server.ts +++ b/src/assets_dev_server.ts @@ -54,16 +54,6 @@ export class AssetsDevServer { const dataString = data.toString() const lines = dataString.split('\n') - /** - * Logging VITE ready in message with proper - * spaces and newlines - */ - if (dataString.includes('ready in')) { - console.log('') - console.log(dataString.trim()) - return - } - /** * Put a wrapper around vite network address log */ @@ -80,6 +70,16 @@ export class AssetsDevServer { return } + /** + * Logging VITE ready in message with proper + * spaces and newlines + */ + if (dataString.includes('ready in')) { + console.log('') + console.log(dataString.trim()) + return + } + /** * Log rest of the lines */ @@ -117,7 +117,7 @@ export class AssetsDevServer { * any cleanup if it dies. */ start() { - if (!this.#options?.serve) { + if (!this.#options?.enabled) { return } diff --git a/src/bundler.ts b/src/bundler.ts index 3209367..a5d5367 100644 --- a/src/bundler.ts +++ b/src/bundler.ts @@ -18,7 +18,35 @@ import { detectPackageManager } from '@antfu/install-pkg' import type { BundlerOptions } from './types.js' import { run, parseConfig, copyFiles } from './helpers.js' -type SupportedPackageManager = 'npm' | 'yarn' | 'pnpm' +type SupportedPackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun' + +/** + * List of package managers we support in order to + * copy lockfiles + */ +const SUPPORT_PACKAGE_MANAGERS: { + [K in SupportedPackageManager]: { + lockFile: string + installCommand: string + } +} = { + npm: { + lockFile: 'package-lock.json', + installCommand: 'npm ci --omit="dev"', + }, + yarn: { + lockFile: 'yarn.lock', + installCommand: 'yarn install --production', + }, + pnpm: { + lockFile: 'pnpm-lock.yaml', + installCommand: 'pnpm i --prod', + }, + bun: { + lockFile: 'bun.lockb', + installCommand: 'bun install --production', + }, +} /** * Instance of CLIUI @@ -49,6 +77,10 @@ export class Bundler { this.#options = options } + /** + * Returns the relative unix path for an absolute + * file path + */ #getRelativeName(filePath: string) { return slash(relative(this.#cwdPath, filePath)) } @@ -61,11 +93,11 @@ export class Bundler { } /** - * Runs assets bundler command to build assets. + * Runs assets bundler command to build assets */ async #buildAssets(): Promise { const assetsBundler = this.#options.assets - if (!assetsBundler?.serve) { + if (!assetsBundler?.enabled) { return true } @@ -115,27 +147,20 @@ export class Bundler { * related to it. */ async #getPackageManager(client?: SupportedPackageManager) { - const pkgManagerInfo = { - npm: { - lockFile: 'package-lock.json', - installCommand: 'npm ci --omit="dev"', - }, - yarn: { - lockFile: 'yarn.lock', - installCommand: 'yarn install --production', - }, - pnpm: { - lockFile: 'pnpm-lock.yaml', - installCommand: 'pnpm i --prod', - }, + let pkgManager: string | null | undefined = client + + if (!pkgManager) { + pkgManager = await detectPackageManager(this.#cwdPath) + } + if (!pkgManager) { + pkgManager = 'npm' } - const pkgManager = client || (await detectPackageManager(this.#cwdPath)) || 'npm' - if (!['npm', 'yarn', 'pnpm'].includes(pkgManager)) { - throw new Error(`Unsupported package manager "${pkgManager}"`) + if (!Object.keys(SUPPORT_PACKAGE_MANAGERS).includes(pkgManager)) { + return null } - return pkgManagerInfo[pkgManager as SupportedPackageManager] + return SUPPORT_PACKAGE_MANAGERS[pkgManager as SupportedPackageManager] } /** @@ -207,7 +232,7 @@ export class Bundler { * Step 5: Copy meta files to the build directory */ const pkgManager = await this.#getPackageManager(client) - const pkgFiles = ['package.json', pkgManager.lockFile] + const pkgFiles = pkgManager ? ['package.json', pkgManager.lockFile] : ['package.json'] this.#logger.info('copying meta files to the output directory') await this.#copyMetaFiles(outDir, pkgFiles) @@ -221,7 +246,11 @@ export class Bundler { .useRenderer(this.#logger.getRenderer()) .heading('Run the following commands to start the server in production') .add(this.#colors.cyan(`cd ${this.#getRelativeName(outDir)}`)) - .add(this.#colors.cyan(pkgManager.installCommand)) + .add( + this.#colors.cyan( + pkgManager ? pkgManager.installCommand : 'Install production dependencies' + ) + ) .add(this.#colors.cyan('node bin/server.js')) .render() diff --git a/src/code_transformer/main.ts b/src/code_transformer/main.ts index d466b26..a0cc29a 100644 --- a/src/code_transformer/main.ts +++ b/src/code_transformer/main.ts @@ -21,7 +21,8 @@ import { } from 'ts-morph' import { RcFileTransformer } from './rc_file_transformer.js' -import type { AddMiddlewareEntry, EnvValidationDefinition } from '../types.js' +import type { MiddlewareNode, EnvValidationNode } from '../types.js' +import { exists } from 'node:fs' /** * This class is responsible for updating @@ -51,6 +52,8 @@ export class CodeTransformer { indentSize: 2, convertTabsToSpaces: true, trimTrailingWhitespace: true, + ensureNewLineAtEndOfFile: true, + indentStyle: 2, // @ts-expect-error SemicolonPreference doesn't seem to be re-exported from ts-morph semicolons: 'remove', } @@ -67,7 +70,7 @@ export class CodeTransformer { * Add a new middleware to the middleware array of the * given file */ - #addToMiddlewareArray(file: SourceFile, target: string, middlewareEntry: AddMiddlewareEntry) { + #addToMiddlewareArray(file: SourceFile, target: string, middlewareEntry: MiddlewareNode) { const callExpressions = file .getDescendantsOfKind(SyntaxKind.CallExpression) .filter((statement) => statement.getExpression().getText() === target) @@ -105,7 +108,7 @@ export class CodeTransformer { /** * Add a new middleware to the named middleware of the given file */ - #addToNamedMiddleware(file: SourceFile, middlewareEntry: AddMiddlewareEntry) { + #addToNamedMiddleware(file: SourceFile, middlewareEntry: MiddlewareNode) { if (!middlewareEntry.name) { throw new Error('Named middleware requires a name.') } @@ -158,7 +161,7 @@ export class CodeTransformer { * Add new env variable validation in the * `env.ts` file */ - async defineEnvValidations(definition: EnvValidationDefinition) { + async defineEnvValidations(definition: EnvValidationNode) { /** * Get the `start/env.ts` source file */ @@ -231,10 +234,7 @@ export class CodeTransformer { * and will not work if you significantly tweaked * your `start/kernel.ts` file. */ - async addMiddlewareToStack( - stack: 'server' | 'router' | 'named', - middleware: AddMiddlewareEntry[] - ) { + async addMiddlewareToStack(stack: 'server' | 'router' | 'named', middleware: MiddlewareNode[]) { /** * Get the `start/kernel.ts` source file */ @@ -270,7 +270,7 @@ export class CodeTransformer { */ async addJapaPlugin( pluginCall: string, - importDeclaration: { isNamed: boolean; module: string; identifier: string } + importDeclarations: { isNamed: boolean; module: string; identifier: string }[] ) { /** * Get the `tests/bootstrap.ts` source file @@ -281,11 +281,43 @@ export class CodeTransformer { /** * Add the import declaration */ - file.addImportDeclaration({ - ...(importDeclaration.isNamed - ? { namedImports: [importDeclaration.identifier] } - : { defaultImport: importDeclaration.identifier }), - moduleSpecifier: importDeclaration.module, + const existingImports = file.getImportDeclarations() + + importDeclarations.forEach((importDeclaration) => { + const existingImport = existingImports.find( + (existingImport) => existingImport.getModuleSpecifierValue() === importDeclaration.module + ) + + /** + * Add a new named import to existing import for the + * same module + */ + if (existingImport && importDeclaration.isNamed) { + if ( + !existingImport + .getNamedImports() + .find((namedImport) => namedImport.getName() === importDeclaration.identifier) + ) { + existingImport.addNamedImport(importDeclaration.identifier) + } + return + } + + /** + * Ignore default import when the same module is already imported. + * The chances are the existing default import and the importDeclaration + * identifiers are not the same. But we should not modify existing source + */ + if (existingImport) { + return + } + + file.addImportDeclaration({ + ...(importDeclaration.isNamed + ? { namedImports: [importDeclaration.identifier] } + : { defaultImport: importDeclaration.identifier }), + moduleSpecifier: importDeclaration.module, + }) }) /** @@ -295,7 +327,14 @@ export class CodeTransformer { .getVariableDeclaration('plugins') ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) - if (pluginsArray) pluginsArray.addElement(pluginCall) + /** + * Add plugin call to the plugins array + */ + if (pluginsArray) { + if (!pluginsArray.getElements().find((element) => element.getText() === pluginCall)) { + pluginsArray.addElement(pluginCall) + } + } file.formatText(this.#editorSettings) await file.save() diff --git a/src/code_transformer/rc_file_transformer.ts b/src/code_transformer/rc_file_transformer.ts index 2afeba0..a2a70fe 100644 --- a/src/code_transformer/rc_file_transformer.ts +++ b/src/code_transformer/rc_file_transformer.ts @@ -1,3 +1,12 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import { fileURLToPath } from 'node:url' import type { AppEnvironments } from '@adonisjs/application/types' import { @@ -7,6 +16,7 @@ import { SyntaxKind, CallExpression, PropertyAssignment, + FormatCodeSettings, ArrayLiteralExpression, } from 'ts-morph' @@ -21,10 +31,14 @@ export class RcFileTransformer { /** * Settings to use when persisting files */ - #editorSettings = { + #editorSettings: FormatCodeSettings = { indentSize: 2, convertTabsToSpaces: true, trimTrailingWhitespace: true, + ensureNewLineAtEndOfFile: true, + indentStyle: 2, + // @ts-expect-error SemicolonPreference doesn't seem to be re-exported from ts-morph + semicolons: 'remove', } constructor(cwd: URL, project: Project) { @@ -44,7 +58,9 @@ export class RcFileTransformer { * Check if environments array has a subset of available environments */ #isInSpecificEnvironment(environments?: AppEnvironments[]): boolean { - if (!environments) return false + if (!environments) { + return false + } return !!(['web', 'console', 'test', 'repl'] as const).find( (env) => !environments.includes(env) diff --git a/src/dev_server.ts b/src/dev_server.ts index 10856af..51ee93b 100644 --- a/src/dev_server.ts +++ b/src/dev_server.ts @@ -29,25 +29,59 @@ const ui = cliui() * * The Dev server performs the following actions * - * - Assigns a random PORT, when PORT inside .env file is in use + * - Assigns a random PORT, when PORT inside .env file is in use. * - Uses tsconfig.json file to collect a list of files to watch. - * - Uses metaFiles from .adonisrc.json file to collect a list of files to watch. + * - Uses metaFiles from adonisrc.ts file to collect a list of files to watch. * - Restart HTTP server on every file change. */ export class DevServer { #cwd: URL #logger = ui.logger #options: DevServerOptions + + /** + * Flag to know if the dev server is running in watch + * mode + */ #isWatching: boolean = false + + /** + * Script file to start the development server + */ #scriptFile: string = 'bin/server.js' + + /** + * Picomatch matcher function to know if a file path is a + * meta file with reloadServer option enabled + */ #isMetaFileWithReloadsEnabled: picomatch.Matcher + + /** + * Picomatch matcher function to know if a file path is a + * meta file with reloadServer option disabled + */ #isMetaFileWithReloadsDisabled: picomatch.Matcher + /** + * External listeners that are invoked when child process + * gets an error or closes + */ #onError?: (error: any) => any #onClose?: (exitCode: number) => any + /** + * Reference to the child process + */ #httpServer?: ExecaChildProcess + + /** + * Reference to the watcher + */ #watcher?: ReturnType + + /** + * Reference to the assets server + */ #assetsServer?: AssetsDevServer /** @@ -167,7 +201,8 @@ export class DevServer { } /** - * Restarts the HTTP server + * Restarts the HTTP server in the watch mode. Do not call this + * method when not in watch mode */ #restartHTTPServer(port: string) { if (this.#httpServer) { diff --git a/src/helpers.ts b/src/helpers.ts index 7432f38..943c578 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -29,7 +29,7 @@ import debug from './debug.js' const DEFAULT_NODE_ARGS = [ // Use ts-node/esm loader. The project must install it '--loader=ts-node/esm', - // Disable annonying warnings + // Disable annoying warnings '--no-warnings', // Enable expiremental meta resolve for cases where someone uses magic import string '--experimental-import-meta-resolve', @@ -133,8 +133,8 @@ export function isDotEnvFile(filePath: string) { * * - The "process.env.PORT" value is used if exists. * - The dot-env files are loaded using the "EnvLoader" and the PORT - * value is by iterating over all the loaded files. The iteration - * stops after first find. + * value is used by iterating over all the loaded files. The + * iteration stops after first find. */ export async function getPort(cwd: URL): Promise { /** diff --git a/src/test_runner.ts b/src/test_runner.ts index a799880..2a68536 100644 --- a/src/test_runner.ts +++ b/src/test_runner.ts @@ -23,24 +23,50 @@ import { getPort, isDotEnvFile, runNode, watch } from './helpers.js' const ui = cliui() /** - * Exposes the API to start the development. Optionally, the watch API can be - * used to watch for file changes and restart the development server. + * Exposes the API to run Japa tests and optionally watch for file + * changes to re-run the tests. * - * The Dev server performs the following actions + * The watch mode functions as follows. + * + * - If the changed file is a test file, then only tests for that file + * will be re-run. + * - Otherwise, all tests will re-run with respect to the initial + * filters applied when running the `node ace test` command. * - * - Assigns a random PORT, when PORT inside .env file is in use - * - Uses tsconfig.json file to collect a list of files to watch. - * - Uses metaFiles from .adonisrc.json file to collect a list of files to watch. - * - Restart HTTP server on every file change. */ export class TestRunner { #cwd: URL #logger = ui.logger #options: TestRunnerOptions + + /** + * The script file to run as a child process + */ #scriptFile: string = 'bin/test.js' + + /** + * Pico matcher function to check if the filepath is + * part of the `metaFiles` glob patterns + */ #isMetaFile: picomatch.Matcher + + /** + * Pico matcher function to check if the filepath is + * part of a test file. + */ #isTestFile: picomatch.Matcher + + /** + * Arguments to pass to the "bin/test.js" file. + */ #scriptArgs: string[] + + /** + * Set of initial filters applied when running the test + * command. In watch mode, we will append an additional + * filter to run tests only for the file that has been + * changed. + */ #initialFiltersArgs: string[] /** @@ -51,11 +77,26 @@ export class TestRunner { */ #isBusy: boolean = false + /** + * External listeners that are invoked when child process + * gets an error or closes + */ #onError?: (error: any) => any #onClose?: (exitCode: number) => any + /** + * Reference to the test script child process + */ #testScript?: ExecaChildProcess + + /** + * Reference to the watcher + */ #watcher?: ReturnType + + /** + * Reference to the assets server + */ #assetsServer?: AssetsDevServer /** @@ -68,14 +109,21 @@ export class TestRunner { constructor(cwd: URL, options: TestRunnerOptions) { this.#cwd = cwd this.#options = options + this.#isMetaFile = picomatch((this.#options.metaFiles || []).map(({ pattern }) => pattern)) + + /** + * Create a test file watch by collection all the globs + * used by all the suites. However, if a suite filter + * was used, then we only collect glob for the mentioned + * suites. + */ this.#isTestFile = picomatch( this.#options.suites .filter((suite) => { if (this.#options.filters.suites) { return this.#options.filters.suites.includes(suite.name) } - return true }) .map((suite) => suite.files) @@ -87,7 +135,7 @@ export class TestRunner { } /** - * Converts options to CLI args + * Convert test runner options to the CLI args */ #convertOptionsToArgs() { const args: string[] = [] @@ -170,6 +218,10 @@ export class TestRunner { ) { this.#isBusy = true + /** + * If inline filters are defined, then we ignore the + * initial filters + */ const scriptArgs = filters ? this.#convertFiltersToArgs(filters).concat(this.#scriptArgs) : this.#initialFiltersArgs.concat(this.#scriptArgs) @@ -200,7 +252,8 @@ export class TestRunner { } /** - * Restarts the HTTP server + * Re-run tests with additional inline filters. Should be + * executed in watch mode only. */ #rerunTests(port: string, filters?: TestRunnerOptions['filters']) { if (this.#testScript) { diff --git a/src/types.ts b/src/types.ts index 85a9e19..45129b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,10 +11,29 @@ * Options needed to run a script file */ export type RunOptions = { + /** + * Script to run + */ script: string + + /** + * Arguments to pass to the script + */ scriptArgs: string[] + + /** + * Arguments to pass to NodeJS CLI + */ nodeArgs: string[] + + /** + * Standard input ouput stream options + */ stdio?: 'pipe' | 'inherit' + + /** + * Environment variables to pass to the child process + */ env?: NodeJS.ProcessEnv } @@ -26,7 +45,7 @@ export type WatchOptions = { } /** - * Meta file config defined in ".adonisrc.json" file + * Meta file config defined in "adonisrc.ts" file */ export type MetaFile = { pattern: string @@ -34,11 +53,11 @@ export type MetaFile = { } /** - * Test suite defined in ".adonisrc.json" file + * Test suite defined in "adonisrc.ts" file */ export type Suite = { - files: string | string[] name: string + files: string | string[] } /** @@ -46,27 +65,54 @@ export type Suite = { */ export type AssetsBundlerOptions = | { - serve: false - args?: string[] + enabled: false driver?: string cmd?: string + args?: string[] } | { - serve: true - args: string[] + enabled: true driver: string cmd: string + args: string[] } /** - * Options accepted by the dev server + * Options accepted when starting the dev + * server */ export type DevServerOptions = { + /** + * Arguments to pass to the "bin/server.js" file + * executed a child process + */ scriptArgs: string[] + + /** + * Arguments to pass to Node.js CLI when executing + * the "bin/server.js" file + */ nodeArgs: string[] + + /** + * Clear screen after every file change + */ clearScreen?: boolean + + /** + * Environment variables to share with the "bin/server.js" + * file. + */ env?: NodeJS.ProcessEnv + + /** + * An array of metaFiles glob patterns to watch + */ metaFiles?: MetaFile[] + + /** + * Assets bundler options to start its dev server + */ assets?: AssetsBundlerOptions } @@ -75,40 +121,90 @@ export type DevServerOptions = { */ export type TestRunnerOptions = { /** - * Filter arguments are provided as a key-value - * pair, so that we can mutate them (if needed) + * Arguments to pass to the "bin/server.js" file + * executed a child process */ - filters: Partial<{ - tests: string[] - suites: string[] - groups: string[] - files: string[] - tags: string[] - }> - - reporters?: string[] - timeout?: number - retries?: number - failed?: boolean + scriptArgs: string[] /** - * All other tags are provided as a collection of - * arguments + * Arguments to pass to Node.js CLI when executing + * the "bin/server.js" file */ - scriptArgs: string[] nodeArgs: string[] + + /** + * Clear screen after every file change + */ clearScreen?: boolean + + /** + * Environment variables to share with the "bin/server.js" + * file. + */ env?: NodeJS.ProcessEnv + + /** + * An array of metaFiles glob patterns to watch + */ metaFiles?: MetaFile[] + + /** + * Assets bundler options to start its dev server + */ assets?: AssetsBundlerOptions + + /** + * An array of suites for which to run tests + */ suites: Suite[] + + /** + * Set the tests runner reporter via the CLI flag + */ + reporters?: string[] + + /** + * Set the tests global timeout via the CLI flag + */ + timeout?: number + + /** + * Define retries via the CLI flag + */ + retries?: number + + /** + * Run only failed tests + */ + failed?: boolean + + /** + * Filter arguments are provided as a key-value + * pair, so that we can mutate them (if needed) + */ + filters: Partial<{ + tests: string[] + suites: string[] + groups: string[] + files: string[] + tags: string[] + }> } /** * Options accepted by the project bundler */ export type BundlerOptions = { + /** + * An array of metaFiles glob patterns to copy the + * files to the build folder + */ metaFiles?: MetaFile[] + + /** + * Assets bundler options to create the production build + * for assets + */ assets?: AssetsBundlerOptions } @@ -116,7 +212,7 @@ export type BundlerOptions = { * Entry to add a middleware to a given middleware stack * via the CodeTransformer */ -export type AddMiddlewareEntry = { +export type MiddlewareNode = { /** * If you are adding a named middleware, then you must * define the name. @@ -146,7 +242,7 @@ export type AddMiddlewareEntry = { * Defines the structure of an environment variable validation * definition */ -export type EnvValidationDefinition = { +export type EnvValidationNode = { /** * Write a leading comment on top of your variables */ diff --git a/tests/__snapshots__/code_transformer.spec.ts.cjs b/tests/__snapshots__/code_transformer.spec.ts.cjs index 9267556..4bc3b2c 100644 --- a/tests/__snapshots__/code_transformer.spec.ts.cjs +++ b/tests/__snapshots__/code_transformer.spec.ts.cjs @@ -1,4 +1,5 @@ -exports[`Code transformer | addMiddlewareToStack > set correct position when defined 1`] = `"import router from '@adonisjs/core/services/router' +exports[`Code transformer | addMiddlewareToStack > set correct position when defined 1`] = + `"import router from '@adonisjs/core/services/router' import server from '@adonisjs/core/services/server' server.errorHandler(() => import('#exceptions/handler')) @@ -19,7 +20,8 @@ router.use([ export const middleware = router.named({}) "` -exports[`Code transformer | addMiddlewareToStack > add a route middleware 1`] = `"import router from '@adonisjs/core/services/router' +exports[`Code transformer | addMiddlewareToStack > add a route middleware 1`] = + `"import router from '@adonisjs/core/services/router' import server from '@adonisjs/core/services/server' server.errorHandler(() => import('#exceptions/handler')) @@ -39,7 +41,8 @@ router.use([ export const middleware = router.named({}) "` -exports[`Code transformer | addMiddlewareToStack > add route and server middleware 1`] = `"import router from '@adonisjs/core/services/router' +exports[`Code transformer | addMiddlewareToStack > add route and server middleware 1`] = + `"import router from '@adonisjs/core/services/router' import server from '@adonisjs/core/services/server' server.errorHandler(() => import('#exceptions/handler')) @@ -59,7 +62,8 @@ router.use([ export const middleware = router.named({}) "` -exports[`Code transformer | defineEnvValidations > add leading comment 1`] = `"import { Env } from '@adonisjs/core/env' +exports[`Code transformer | defineEnvValidations > add leading comment 1`] = + `"import { Env } from '@adonisjs/core/env' export default await Env.create(new URL('../', import.meta.url), { NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), @@ -75,7 +79,8 @@ export default await Env.create(new URL('../', import.meta.url), { }) "` -exports[`Code transformer | addProvider > add provider to rc file with specific environments 1`] = `"import { defineConfig } from '@adonisjs/core/app' +exports[`Code transformer | addProvider > add provider to rc file with specific environments 1`] = + `"import { defineConfig } from '@adonisjs/core/app' export default defineConfig({ typescript: true, @@ -109,7 +114,8 @@ export default defineConfig({ }) "` -exports[`Code transformer | addProvider > do no add environments when they are all specified 1`] = `"import { defineConfig } from '@adonisjs/core/app' +exports[`Code transformer | addProvider > do no add environments when they are all specified 1`] = + `"import { defineConfig } from '@adonisjs/core/app' export default defineConfig({ typescript: true, @@ -140,7 +146,8 @@ export default defineConfig({ }) "` -exports[`Code transformer | addMetaFile > add meta files to rc file with reload server 1`] = `"import { defineConfig } from '@adonisjs/core/app' +exports[`Code transformer | addMetaFile > add meta files to rc file with reload server 1`] = + `"import { defineConfig } from '@adonisjs/core/app' export default defineConfig({ typescript: true, @@ -174,7 +181,8 @@ export default defineConfig({ }) "` -exports[`Code transformer | setDirectory > set directory in rc file 1`] = `"import { defineConfig } from '@adonisjs/core/app' +exports[`Code transformer | setDirectory > set directory in rc file 1`] = + `"import { defineConfig } from '@adonisjs/core/app' export default defineConfig({ typescript: true, @@ -207,7 +215,8 @@ export default defineConfig({ }) "` -exports[`Code transformer | setCommandAlias > set command alias in rc file 1`] = `"import { defineConfig } from '@adonisjs/core/app' +exports[`Code transformer | setCommandAlias > set command alias in rc file 1`] = + `"import { defineConfig } from '@adonisjs/core/app' export default defineConfig({ typescript: true, @@ -240,7 +249,8 @@ export default defineConfig({ }) "` -exports[`Code transformer | addPreloadFile > add preload file with specific environments 1`] = `"import { defineConfig } from '@adonisjs/core/app' +exports[`Code transformer | addPreloadFile > add preload file with specific environments 1`] = + `"import { defineConfig } from '@adonisjs/core/app' export default defineConfig({ typescript: true, @@ -274,7 +284,9 @@ export default defineConfig({ }) "` -exports[`Code transformer | addMiddlewareToStack > override duplicates when adding named middelware 1`] = `"import router from '@adonisjs/core/services/router' +exports[ + `Code transformer | addMiddlewareToStack > override duplicates when adding named middelware 1` +] = `"import router from '@adonisjs/core/services/router' import server from '@adonisjs/core/services/server' server.errorHandler(() => import('#exceptions/handler')) @@ -294,7 +306,8 @@ export const middleware = router.named({ }) "` -exports[`Code transformer | addMiddlewareToStack > do not add duplicate named middleware 1`] = `"import router from '@adonisjs/core/services/router' +exports[`Code transformer | addMiddlewareToStack > do not add duplicate named middleware 1`] = + `"import router from '@adonisjs/core/services/router' import server from '@adonisjs/core/services/server' server.errorHandler(() => import('#exceptions/handler')) @@ -314,7 +327,8 @@ export const middleware = router.named({ }) "` -exports[`Code transformer | addCommand > add command to rc file 1`] = `"import { defineConfig } from '@adonisjs/core/app' +exports[`Code transformer | addCommand > add command to rc file 1`] = + `"import { defineConfig } from '@adonisjs/core/app' export default defineConfig({ typescript: true, @@ -346,7 +360,9 @@ export default defineConfig({ }) "` -exports[`Code transformer | addCommand > should add command even if commands property is missing 1`] = `"import { defineConfig } from '@adonisjs/core/app' +exports[ + `Code transformer | addCommand > should add command even if commands property is missing 1` +] = `"import { defineConfig } from '@adonisjs/core/app' export default defineConfig({ typescript: true, @@ -397,4 +413,3 @@ export const plugins: Config['plugins'] = [ fooPlugin() ] "` - diff --git a/tests/bundler.spec.ts b/tests/bundler.spec.ts index d9c21a5..2d0b040 100644 --- a/tests/bundler.spec.ts +++ b/tests/bundler.spec.ts @@ -1,6 +1,15 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import ts from 'typescript' import { test } from '@japa/runner' import { Bundler } from '../index.js' -import ts from 'typescript' test.group('Bundler', () => { test('should copy metafiles to the build directory', async ({ assert, fs }) => { diff --git a/tests/code_transformer.spec.ts b/tests/code_transformer.spec.ts index 317baa1..822f00d 100644 --- a/tests/code_transformer.spec.ts +++ b/tests/code_transformer.spec.ts @@ -1,8 +1,16 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import dedent from 'dedent' import { test } from '@japa/runner' import { readFile } from 'node:fs/promises' import type { FileSystem } from '@japa/file-system' - import { CodeTransformer } from '../src/code_transformer/main.js' async function setupFakeAdonisproject(fs: FileSystem) { @@ -162,19 +170,69 @@ test.group('Code transformer | defineEnvValidations', (group) => { const transformer = new CodeTransformer(fs.baseUrl) await transformer.defineEnvValidations({ + leadingComment: 'Redis configuration', variables: { - NODE_ENV: 'Env.schema.string.optional()', + REDIS_HOST: 'Env.schema.string.optional()', + REDIS_PORT: 'Env.schema.number()', }, }) - const file = await fs.contents('start/env.ts') - const occurrences = (file.match(/NODE_ENV/g) || []).length + await transformer.defineEnvValidations({ + leadingComment: 'Redis configuration', + variables: { + REDIS_HOST: 'Env.schema.string.optional()', + REDIS_PORT: 'Env.schema.number()', + }, + }) - assert.equal(occurrences, 1) - assert.fileContains( - 'start/env.ts', - `Env.schema.enum(['development', 'production', 'test'] as const)` - ) + assert.snapshot(await fs.contents('start/env.ts')).matchInline(` + "import { Env } from '@adonisjs/core/env' + + export default await Env.create(new URL('../', import.meta.url), { + NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), + PORT: Env.schema.number(), + + /* + |---------------------------------------------------------- + | Redis configuration + |---------------------------------------------------------- + */ + REDIS_HOST: Env.schema.string.optional(), + REDIS_PORT: Env.schema.number() + }) + " + `) + }) + + test('do not overwrite validation for existing variable', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.defineEnvValidations({ + variables: { + REDIS_HOST: 'Env.schema.string.optional()', + REDIS_PORT: 'Env.schema.number()', + }, + }) + + await transformer.defineEnvValidations({ + variables: { + REDIS_HOST: 'Env.schema.string()', + REDIS_PORT: 'Env.schema.number()', + }, + }) + + assert.snapshot(await fs.contents('start/env.ts')).matchInline(` + "import { Env } from '@adonisjs/core/env' + + export default await Env.create(new URL('../', import.meta.url), { + NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), + PORT: Env.schema.number(), + + REDIS_HOST: Env.schema.string.optional(), + REDIS_PORT: Env.schema.number() + }) + " + `) }) }) @@ -524,14 +582,32 @@ test.group('Code transformer | addJapaPlugin', (group) => { const transformer = new CodeTransformer(fs.baseUrl) - await transformer.addJapaPlugin('fooPlugin(app)', { - module: '@adonisjs/foo/plugin/japa', - identifier: 'fooPlugin', - isNamed: true, - }) + await transformer.addJapaPlugin('fooPlugin(app)', [ + { + module: '@adonisjs/foo/plugin/japa', + identifier: 'fooPlugin', + isNamed: true, + }, + { + module: '@adonisjs/core/services/app', + identifier: 'app', + isNamed: false, + }, + ]) const file = await fs.contents('tests/bootstrap.ts') - assert.snapshot(file).match() + assert.snapshot(file).matchInline(` + " + import app from '@adonisjs/core/services/app' + import { assert } from '@japa/assert' + import { fooPlugin } from '@adonisjs/foo/plugin/japa' + + export const plugins: Config['plugins'] = [ + assert(), + fooPlugin(app) + ] + " + `) }) test('add default import', async ({ assert, fs }) => { @@ -548,13 +624,81 @@ test.group('Code transformer | addJapaPlugin', (group) => { const transformer = new CodeTransformer(fs.baseUrl) - await transformer.addJapaPlugin('fooPlugin()', { - module: '@adonisjs/foo/plugin/japa', - identifier: 'fooPlugin', - isNamed: false, - }) + await transformer.addJapaPlugin('fooPlugin()', [ + { + module: '@adonisjs/foo/plugin/japa', + identifier: 'fooPlugin', + isNamed: false, + }, + ]) const file = await fs.contents('tests/bootstrap.ts') - assert.snapshot(file).match() + assert.snapshot(file).matchInline(` + " + import app from '@adonisjs/core/services/app' + import { assert } from '@japa/assert' + import fooPlugin from '@adonisjs/foo/plugin/japa' + + export const plugins: Config['plugins'] = [ + assert(), + fooPlugin() + ] + " + `) + }) + + test('ignore duplicate imports', async ({ assert, fs }) => { + await fs.create( + 'tests/bootstrap.ts', + ` + import app from '@adonisjs/core/services/app' + import { assert } from '@japa/assert' + + export const plugins: Config['plugins'] = [ + assert(), + ]` + ) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addJapaPlugin('fooPlugin(app)', [ + { + module: '@adonisjs/foo/plugin/japa', + identifier: 'fooPlugin', + isNamed: true, + }, + { + module: '@adonisjs/core/services/app', + identifier: 'app', + isNamed: false, + }, + ]) + + await transformer.addJapaPlugin('fooPlugin(app)', [ + { + module: '@adonisjs/foo/plugin/japa', + identifier: 'fooPlugin', + isNamed: true, + }, + { + module: '@adonisjs/core/services/app', + identifier: 'app', + isNamed: false, + }, + ]) + + const file = await fs.contents('tests/bootstrap.ts') + assert.snapshot(file).matchInline(` + " + import app from '@adonisjs/core/services/app' + import { assert } from '@japa/assert' + import { fooPlugin } from '@adonisjs/foo/plugin/japa' + + export const plugins: Config['plugins'] = [ + assert(), + fooPlugin(app) + ] + " + `) }) }) diff --git a/tests/copy.spec.ts b/tests/copy.spec.ts index 3339bcf..6f4ada2 100644 --- a/tests/copy.spec.ts +++ b/tests/copy.spec.ts @@ -1,6 +1,15 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { join } from 'node:path' import { test } from '@japa/runner' import { copyFiles } from '../src/helpers.js' -import { join } from 'node:path' test.group('Copy files', () => { test('expand glob patterns and copy files to the destination', async ({ assert, fs }) => { diff --git a/tests/helpers.spec.ts b/tests/helpers.spec.ts new file mode 100644 index 0000000..8105e0e --- /dev/null +++ b/tests/helpers.spec.ts @@ -0,0 +1,73 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import ts from 'typescript' +import { join } from 'node:path' +import { test } from '@japa/runner' +import { getPort, isDotEnvFile, parseConfig } from '../src/helpers.js' + +test.group('Helpers | Parse config', () => { + test('report error when config file is missing', async ({ assert, fs }) => { + const result = parseConfig(fs.baseUrl, ts) + assert.isUndefined(result) + }) + + test('report config errors to console', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', { + include: ['**/*'], + }) + + const result = parseConfig(fs.baseUrl, ts) + assert.isUndefined(result) + }) + + test('parse tsconfig file', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', { + include: ['**/*'], + }) + await fs.create('foo.ts', '') + + const result = parseConfig(fs.baseUrl, ts) + assert.deepEqual(result?.fileNames, [join(fs.basePath, 'foo.ts')]) + }) +}) + +test.group('Helpers | Is DotEnv file', () => { + test('check if file is a dot-env file', ({ assert }) => { + assert.isTrue(isDotEnvFile('.env')) + assert.isTrue(isDotEnvFile('.env.prod')) + assert.isTrue(isDotEnvFile('.env.local')) + assert.isFalse(isDotEnvFile('.env-file')) + }) +}) + +test.group('Helpers | getPort', () => { + test('use port set via process.env.PORT', async ({ fs, assert, cleanup }) => { + process.env.PORT = '4000' + cleanup(() => { + delete process.env.PORT + }) + assert.equal(await getPort(fs.baseUrl), 4000) + }) + + test('use port from the .env file', async ({ fs, assert }) => { + await fs.create('.env', 'PORT=3000') + assert.equal(await getPort(fs.baseUrl), 3000) + }) + + test('give preference to .env.local file', async ({ fs, assert }) => { + await fs.create('.env', 'PORT=3000') + await fs.create('.env.local', 'PORT=5000') + assert.equal(await getPort(fs.baseUrl), 5000) + }) + + test('use port 3333 when no environment variable or files exists', async ({ fs, assert }) => { + assert.equal(await getPort(fs.baseUrl), 3333) + }) +})