diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md deleted file mode 100644 index fc852af..0000000 --- a/.github/COMMIT_CONVENTION.md +++ /dev/null @@ -1,70 +0,0 @@ -## Git Commit Message Convention - -> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). - -Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. - -``` js -/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ -``` - -## Commit Message Format -A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: - -> The **scope** is optional - -``` -feat(router): add support for prefix - -Prefix makes it easier to append a path to a group of routes -``` - -1. `feat` is type. -2. `router` is scope and is optional -3. `add support for prefix` is the subject -4. The **body** is followed by a blank line. -5. The optional **footer** can be added after the body, followed by a blank line. - -## Types -Only one type can be used at a time and only following types are allowed. - -- feat -- fix -- docs -- style -- refactor -- perf -- test -- workflow -- ci -- chore -- types -- build - -If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. - -### Revert -If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. - -## Scope -The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. - -## Subject -The subject contains succinct description of the change: - -- use the imperative, present tense: "change" not "changed" nor "changes". -- don't capitalize first letter -- no dot (.) at the end - -## Body - -Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". -The body should include the motivation for the change and contrast this with previous behavior. - -## Footer - -The footer should contain any information about **Breaking Changes** and is also the place to -reference GitHub issues that this commit **Closes**. - -**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. - diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index f0c5446..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributing - -AdonisJS is a community driven project. You are free to contribute in any of the following ways. - -- [Coding style](coding-style) -- [Fix bugs by creating PR's](fix-bugs-by-creating-prs) -- [Share an RFC for new features or big changes](share-an-rfc-for-new-features-or-big-changes) -- [Report security issues](report-security-issues) -- [Be a part of the community](be-a-part-of-community) - -## Coding style - -Majority of AdonisJS core packages are written in Typescript. Having a brief knowledge of Typescript is required to contribute to the core. - -## Fix bugs by creating PR's - -We appreciate every time you report a bug in the framework or related libraries. However, taking time to submit a PR can help us in fixing bugs quickly and ensure a healthy and stable eco-system. - -Go through the following points, before creating a new PR. - -1. Create an issue discussing the bug or short-coming in the framework. -2. Once approved, go ahead and fork the REPO. -3. Make sure to start from the `develop`, since this is the upto date branch. -4. Make sure to keep commits small and relevant. -5. We follow [conventional-commits](https://github.com/conventional-changelog/conventional-changelog) to structure our commit messages. Instead of running `git commit`, you must run `npm commit`, which will show you prompts to create a valid commit message. -6. Once done with all the changes, create a PR against the `develop` branch. - -## Share an RFC for new features or big changes - -Sharing PR's for small changes works great. However, when contributing big features to the framework, it is required to go through the RFC process. - -### What is an RFC? - -RFC stands for **Request for Commits**, a standard process followed by many other frameworks including [Ember](https://github.com/emberjs/rfcs), [yarn](https://github.com/yarnpkg/rfcs) and [rust](https://github.com/rust-lang/rfcs). - -In brief, RFC process allows you to talk about the changes with everyone in the community and get a view of the core team before dedicating your time to work on the feature. - -The RFC proposals are created as Pull Request on [adonisjs/rfcs](https://github.com/adonisjs/rfcs) repo. Make sure to read the README to learn about the process in depth. - -## Report security issues - -All of the security issues, must be reported via [email](mailto:virk@adonisjs.com) and not using any of the public channels. - -## Be a part of community - -We welcome you to participate in [GitHub Discussion](https://github.com/adonisjs/core/discussions) and the AdonisJS [Discord Server](https://discord.gg/vDcEjq6). You are free to ask your questions and share your work or contributions made to AdonisJS eco-system. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e65000c..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Report identified bugs ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -- Lots of raised issues are directly not bugs but instead are design decisions taken by us. -- Make use of our [GH discussions](https://github.com/adonisjs/core/discussions), or [discord server](https://discord.me/adonisjs), if you are not sure that you are reporting a bug. -- Ensure the issue isn't already reported. -- Ensure you are reporting the bug in the correct repo. - -*Delete the above section and the instructions in the sections below before submitting* - -## Package version - - -## Node.js and npm version - - -## Sample Code (to reproduce the issue) - - -## BONUS (a sample repo to reproduce the issue) - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index abd44a5..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Feature request -about: Propose changes for adding a new feature ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -## Consider an RFC - -Please create an [RFC](https://github.com/adonisjs/rfcs) instead, if - -- Feature introduces a breaking change -- Demands lots of time and changes in the current code base. - -*Delete the above section and the instructions in the sections below before submitting* - -## Why this feature is required (specific use-cases will be appreciated)? - - -## Have you tried any other work arounds? - - -## Are you willing to work on it with little guidance? - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 844839b..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## Proposed changes - -Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. - -## Types of changes - -What types of changes does your code introduce? - -_Put an `x` in the boxes that apply_ - -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - -## Checklist - -_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ - -- [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/assembler/blob/master/.github/CONTRIBUTING.md) doc -- [ ] Lint and unit tests pass locally with my changes -- [ ] I have added tests that prove my fix is effective or that my feature works. -- [ ] I have added necessary documentation (if appropriate) - -## Further comments - -If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..ba001c6 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,170 @@ +[ + { + "name": "Priority: Critical", + "color": "ea0056", + "description": "The issue needs urgent attention", + "aliases": [] + }, + { + "name": "Priority: High", + "color": "5666ed", + "description": "Look into this issue before picking up any new work", + "aliases": [] + }, + { + "name": "Priority: Medium", + "color": "f4ff61", + "description": "Try to fix the issue for the next patch/minor release", + "aliases": [] + }, + { + "name": "Priority: Low", + "color": "87dfd6", + "description": "Something worth considering, but not a top priority for the team", + "aliases": [] + }, + { + "name": "Semver: Alpha", + "color": "008480", + "description": "Will make it's way to the next alpha version of the package", + "aliases": [] + }, + { + "name": "Semver: Major", + "color": "ea0056", + "description": "Has breaking changes", + "aliases": [] + }, + { + "name": "Semver: Minor", + "color": "fbe555", + "description": "Mainly new features and improvements", + "aliases": [] + }, + { + "name": "Semver: Next", + "color": "5666ed", + "description": "Will make it's way to the bleeding edge version of the package", + "aliases": [] + }, + { + "name": "Semver: Patch", + "color": "87dfd6", + "description": "A bug fix", + "aliases": [] + }, + { + "name": "Status: Abandoned", + "color": "ffffff", + "description": "Dropped and not into consideration", + "aliases": ["wontfix"] + }, + { + "name": "Status: Accepted", + "color": "e5fbf2", + "description": "The proposal or the feature has been accepted for the future versions", + "aliases": [] + }, + { + "name": "Status: Blocked", + "color": "ea0056", + "description": "The work on the issue or the PR is blocked. Check comments for reasoning", + "aliases": [] + }, + { + "name": "Status: Completed", + "color": "008672", + "description": "The work has been completed, but not released yet", + "aliases": [] + }, + { + "name": "Status: In Progress", + "color": "73dbc4", + "description": "Still banging the keyboard", + "aliases": ["in progress"] + }, + { + "name": "Status: On Hold", + "color": "f4ff61", + "description": "The work was started earlier, but is on hold now. Check comments for reasoning", + "aliases": ["On Hold"] + }, + { + "name": "Status: Review Needed", + "color": "fbe555", + "description": "Review from the core team is required before moving forward", + "aliases": [] + }, + { + "name": "Status: Awaiting More Information", + "color": "89f8ce", + "description": "Waiting on the issue reporter or PR author to provide more information", + "aliases": [] + }, + { + "name": "Status: Need Contributors", + "color": "7057ff", + "description": "Looking for contributors to help us move forward with this issue or PR", + "aliases": [] + }, + { + "name": "Type: Bug", + "color": "ea0056", + "description": "The issue has indentified a bug", + "aliases": ["bug"] + }, + { + "name": "Type: Security", + "color": "ea0056", + "description": "Spotted security vulnerability and is a top priority for the core team", + "aliases": [] + }, + { + "name": "Type: Duplicate", + "color": "00837e", + "description": "Already answered or fixed previously", + "aliases": ["duplicate"] + }, + { + "name": "Type: Enhancement", + "color": "89f8ce", + "description": "Improving an existing feature", + "aliases": ["enhancement"] + }, + { + "name": "Type: Feature Request", + "color": "483add", + "description": "Request to add a new feature to the package", + "aliases": [] + }, + { + "name": "Type: Invalid", + "color": "dbdbdb", + "description": "Doesn't really belong here. Maybe use discussion threads?", + "aliases": ["invalid"] + }, + { + "name": "Type: Question", + "color": "eceafc", + "description": "Needs clarification", + "aliases": ["help wanted", "question"] + }, + { + "name": "Type: Documentation Change", + "color": "7057ff", + "description": "Documentation needs some improvements", + "aliases": ["documentation"] + }, + { + "name": "Type: Dependencies Update", + "color": "00837e", + "description": "Bump dependencies", + "aliases": ["dependencies"] + }, + { + "name": "Good First Issue", + "color": "008480", + "description": "Want to contribute? Just filter by this label", + "aliases": ["good first issue"] + } +] 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/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..c27fb04 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,14 @@ +name: checks +on: + - push + - pull_request + +jobs: + test: + uses: adonisjs/.github/.github/workflows/test.yml@main + + lint: + uses: adonisjs/.github/.github/workflows/lint.yml@main + + typecheck: + uses: adonisjs/.github/.github/workflows/typecheck.yml@main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 788df91..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: test -on: - - push - - pull_request -jobs: - linux: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: - - 14.15.4 - - 17.x - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install - run: npm install - - name: Run tests - run: npm test - windows: - runs-on: windows-latest - strategy: - matrix: - node-version: - - 14.15.4 - - 17.x - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install - run: npm install - - name: Run tests - run: npm test 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/.husky/commit-msg b/.husky/commit-msg index 4654c12..4002db7 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,3 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit diff --git a/.prettierignore b/.prettierignore index e843a17..8b58fb4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,4 @@ config.json package.json *.html *.txt +tests/__snapshots__ \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index 1c19428..381426b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License -Copyright 2022 Harminder Virk, contributors +Copyright (c) 2023 Harminder Virk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index f2c1227..92fc27b 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,443 @@ -
- -
+# @adonisjs/assembler
-
-

Core Commands for building AdonisJS projects

-

- Assembler contains a set of core commands to build and serve the AdonisJS typescript project, along with scaffolding make commands. -

-
+[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] -
+## Introduction +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. + +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. -[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] [![synk-image]][synk-url] +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. -
-

- - Website - - | - - Guides - - | - - Contributing - -

-
+```ts +import { CodeTransformer } from '@adonisjs/assembler/code_transformer' -
- Built with ❤︎ by Harminder Virk -
+const appRoot = new URL('./', import.meta.url) -[gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/assembler/test?style=for-the-badge -[gh-workflow-url]: https://github.com/adonisjs/assembler/actions/workflows/test.yml "Github action" +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) +] +``` + +### addPolicies +Register AdonisJS bouncer policies to the list of `policies` object exported from the `app/policies/main.ts` file. + +> [!IMPORTANT] +> This codemod expects the `app/policies/main.ts` file to exist and must export a `policies` object from it. + +```ts +const transformer = new CodeTransformer(appRoot) + +try { + await transformer.addPolicies([ + { + name: 'PostPolicy', + path: '#policies/post_policy' + } + ]) +} catch (error) { + console.error('Unable to register policy') + console.error(error) +} +``` + +Output + +```ts +export const policies = { + UserPolicy: () => import('#policies/post_policy') +} +``` + +## Contributing +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 +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). + +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/assembler/checks.yml?style=for-the-badge +[gh-workflow-url]: https://github.com/adonisjs/assembler/actions/workflows/checks.yml "Github action" [npm-image]: https://img.shields.io/npm/v/@adonisjs/assembler/latest.svg?style=for-the-badge&logo=npm [npm-url]: https://npmjs.org/package/@adonisjs/assembler/v/latest "npm" -[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/assembler?label=Synk%20Vulnerabilities&style=for-the-badge -[synk-url]: https://snyk.io/test/github/adonisjs/assembler?targetFile=package.json "synk" +[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript + +[license-url]: LICENSE.md +[license-image]: https://img.shields.io/github/license/adonisjs/ace?style=for-the-badge diff --git a/bin/index.ts b/bin/index.ts deleted file mode 100644 index 778afc5..0000000 --- a/bin/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { ManifestGenerator } from '@adonisjs/ace' -import { fsReadAll } from '@poppinss/utils/build/helpers' - -/** - * Get the file path to every assembler commands - */ -const commandsPaths = fsReadAll( - join(__dirname, '../commands'), - (file) => !file.includes('Base') && file.endsWith('.js') -) - .map((file) => `./commands/${file}`) - .map((file) => file.replace(/\\/g, '/')) - -/** - * Generates ace-manifest file - */ -new ManifestGenerator(join(__dirname, '..'), commandsPaths).generate() diff --git a/bin/japaTypes.ts b/bin/japaTypes.ts deleted file mode 100644 index d42cac6..0000000 --- a/bin/japaTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Assert } from '@japa/assert' - -declare module '@japa/runner' { - interface TestContext { - assert: Assert - } -} diff --git a/bin/test.ts b/bin/test.ts index 5aba7ce..2b1d61a 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,7 +1,11 @@ import { assert } from '@japa/assert' -import { specReporter } from '@japa/spec-reporter' -import { runFailedTests } from '@japa/run-failed-tests' -import { processCliArgs, configure, run } from '@japa/runner' +import { fileURLToPath } from 'node:url' + +import { snapshot } from '@japa/snapshot' +import { fileSystem } from '@japa/file-system' +import { processCLIArgs, configure, run } from '@japa/runner' + +const TEST_TMP_DIR_PATH = fileURLToPath(new URL('../tmp', import.meta.url)) /* |-------------------------------------------------------------------------- @@ -16,14 +20,11 @@ import { processCliArgs, configure, run } from '@japa/runner' | | Please consult japa.dev/runner-config for the config docs. */ +processCLIArgs(process.argv.slice(2)) configure({ - ...processCliArgs(process.argv.slice(2)), - ...{ - files: ['test/**/*.spec.ts'], - plugins: [assert(), runFailedTests()], - reporters: [specReporter()], - importer: (filePath: string) => import(filePath), - }, + files: ['tests/**/*.spec.ts'], + plugins: [assert(), fileSystem({ basePath: TEST_TMP_DIR_PATH }), snapshot()], + timeout: 5 * 1000, }) /* diff --git a/commands/Build.ts b/commands/Build.ts deleted file mode 100644 index 56f9105..0000000 --- a/commands/Build.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import hasYarn from 'has-yarn' -import { BaseCommand, flags } from '@adonisjs/core/build/standalone' -import { TSCONFIG_FILE_NAME } from '../config/paths' - -/** - * Compile typescript project Javascript - */ -export default class Build extends BaseCommand { - public static commandName = 'build' - public static description = - 'Compile project from Typescript to Javascript. Also compiles the frontend assets if using webpack encore' - - /** - * Build for production - */ - @flags.boolean({ description: 'Build for production', alias: 'prod' }) - public production: boolean - - /** - * Bundle frontend assets. Defaults to true - */ - @flags.boolean({ - description: - 'Build frontend assets when webpack encore is installed. Use --no-assets to disable', - }) - public assets: boolean = true - - /** - * Ignore ts errors and complete the build process. Defaults to false - */ - @flags.boolean({ - description: 'Ignore typescript errors and complete the build process', - }) - public ignoreTsErrors: boolean - - /** - * Path to the TypeScript project configuration file. Defaults to "tsconfig.json" - */ - @flags.string({ - description: 'Path to the TypeScript project configuration file', - }) - public tsconfig: string = TSCONFIG_FILE_NAME - - /** - * Arguments to pass to the `encore` binary - */ - @flags.array({ description: 'CLI options to pass to the encore command line' }) - public encoreArgs: string[] = [] - - /** - * Select the client for deciding the lock file to copy to the - * build folder - */ - @flags.string({ - description: 'Select the package manager to decide which lock file to copy to the build folder', - }) - public client: string - - /** - * Invoked automatically by ace - */ - public async run() { - const { Compiler } = await import('../src/Compiler') - - /** - * Deciding the client to use for installing dependencies - */ - this.client = this.client || hasYarn(this.application.appRoot) ? 'yarn' : 'npm' - if (this.client !== 'npm' && this.client !== 'yarn') { - this.logger.warning('--client must be set to "npm" or "yarn"') - this.exitCode = 1 - return - } - - /** - * Stop on error when "ignoreTsErrors" is not set - */ - const stopOnError = !this.ignoreTsErrors - - try { - const compiler = new Compiler( - this.application.appRoot, - this.encoreArgs, - this.assets, - this.logger, - this.tsconfig - ) - - const compiled = this.production - ? await compiler.compileForProduction(stopOnError, this.client) - : await compiler.compile(stopOnError) - - /** - * Set exitCode based upon the compiled status - */ - if (!compiled) { - this.exitCode = 1 - } - } catch (error) { - this.logger.fatal(error) - this.exitCode = 1 - } - } -} diff --git a/commands/Invoke.ts b/commands/Invoke.ts deleted file mode 100644 index ea6c194..0000000 --- a/commands/Invoke.ts +++ /dev/null @@ -1,268 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { tasks, files, logger, utils } from '@adonisjs/sink' -import { BaseCommand, args } from '@adonisjs/core/build/standalone' - -import { Manifest } from '../src/Manifest' - -/** - * Configure a package - */ -export default class Configure extends BaseCommand { - public static commandName = 'configure' - public static description = 'Configure one or more AdonisJS packages' - public static aliases = ['invoke'] - - private appType = process.env['ADONIS_CREATE_APP_BOILERPLATE'] || 'web' - - /** - * Use yarn when building for production to install dependencies - */ - @args.spread({ - description: 'Name of the package(s) you want to configure', - }) - public packages: string[] - - /** - * Returns package manager for installing dependencies - */ - private getPackageManager() { - if (process.env['ADONIS_CREATE_APP_CLIENT']) { - return process.env['ADONIS_CREATE_APP_CLIENT'] as 'yarn' | 'npm' | 'pnpm' - } - return utils.getPackageManager(this.application.appRoot) - } - - /** - * Configure encore - */ - private async configureEncore() { - /** - * Create the webpack config file - */ - const webpackConfigFile = new files.MustacheFile( - this.application.appRoot, - 'webpack.config.js', - join(__dirname, '..', 'templates/webpack.config.txt') - ) - if (!webpackConfigFile.exists()) { - webpackConfigFile.apply({}).commit() - logger.action('create').succeeded('webpack.config.js') - } - - /** - * Create app.js entrypoint - */ - const entryPointFile = new files.NewLineFile(this.application.appRoot, 'resources/js/app.js') - if (!entryPointFile.exists()) { - entryPointFile.add('// app entrypoint').commit() - logger.action('create').succeeded('resources/js/app.js') - } - - /** - * Install Encore - */ - const pkgFile = new files.PackageJsonFile(this.application.appRoot) - pkgFile.install('@symfony/webpack-encore@4.1.1') - pkgFile.install('webpack@^5.72') - pkgFile.install('webpack-cli@^4.9.1') - pkgFile.install('@babel/core@^7.17.0') - pkgFile.install('@babel/preset-env@^7.16.0') - pkgFile.useClient(this.getPackageManager()) - - const spinner = logger.await(logger.colors.gray('configure @symfony/webpack-encore')) - - try { - const response = await pkgFile.commitAsync() - if (response && response.status === 1) { - spinner.stop() - logger.fatal({ message: 'Unable to configure encore', stack: response.stderr.toString() }) - } else { - spinner.stop() - logger.success('Configured encore successfully') - } - } catch (error) { - spinner.stop() - logger.fatal(error) - } - } - - /** - * Configure tests - */ - private async configureTests() { - /** - * Create "test.ts" file - */ - const testsEntryPointFile = new files.MustacheFile( - this.application.appRoot, - 'test.ts', - join(__dirname, '..', 'templates/test-entrypoint.txt') - ) - if (!testsEntryPointFile.exists()) { - testsEntryPointFile.apply({}).commit() - logger.action('create').succeeded('test.ts') - } - - /** - * Create "tests/bootstrap.ts" file - */ - const testsBootstrapFile = new files.MustacheFile( - this.application.appRoot, - 'tests/bootstrap.ts', - join(__dirname, '..', 'templates/tests/bootstrap.txt') - ) - if (!testsBootstrapFile.exists()) { - testsBootstrapFile.apply({}).commit() - logger.action('create').succeeded('tests/bootstrap.ts') - } - - /** - * Create "tests/functional/hello_world.spec.ts" file - */ - const helloWorldTestFile = new files.MustacheFile( - this.application.appRoot, - 'tests/functional/hello_world.spec.ts', - join(__dirname, '..', `templates/tests/functional/hello_world_${this.appType}.spec.txt`) - ) - if (!helloWorldTestFile.exists()) { - helloWorldTestFile.apply({}).commit() - logger.action('create').succeeded('tests/functional/hello_world.spec.ts') - } - - /** - * Create "contracts/tests.ts" file - */ - const testsContractsFile = new files.MustacheFile( - this.application.appRoot, - 'contracts/tests.ts', - join(__dirname, '..', 'templates/tests-contract.txt') - ) - if (!testsContractsFile.exists()) { - testsContractsFile.apply({}).commit() - logger.action('create').succeeded('contracts/tests.ts') - } - - /** - * Update AdonisRc file with test suites - */ - const rcFile = new files.AdonisRcFile(this.application.appRoot) - rcFile.set('tests', { - suites: [ - { - name: 'functional', - files: ['tests/functional/**/*.spec(.ts|.js)'], - timeout: 60 * 1000, - }, - ], - }) - rcFile.addTestProvider('@japa/preset-adonis/TestsProvider') - - rcFile.commit() - logger.action('update').succeeded('.adonisrc.json') - - /** - * Create ".env.test" file - */ - const testEnvFile = new files.NewLineFile(this.application.appRoot, '.env.test') - if (!testEnvFile.exists()) { - testEnvFile.add('NODE_ENV=test') - - /** - * Set additional .env variables for "web" boilerplate - */ - if (this.appType === 'web') { - testEnvFile.add(['ASSETS_DRIVER=fake', 'SESSION_DRIVER=memory']) - } - - testEnvFile.commit() - logger.action('create').succeeded('.env.test') - } - - /** - * Update "tsconfig.json" - */ - const tsConfig = new files.JsonFile(this.application.appRoot, 'tsconfig.json') - const existingTypes = tsConfig.get('compilerOptions.types') || [] - - if (!existingTypes.includes('@japa/preset-adonis/build/adonis-typings')) { - existingTypes.push('@japa/preset-adonis/build/adonis-typings') - } - tsConfig.set('compilerOptions.types', existingTypes) - - tsConfig.commit() - logger.action('update').succeeded('tsconfig.json') - - /** - * Set additional .env variables for "web" boilerplate - */ - if (this.appType === 'web') { - testEnvFile.add(['ASSETS_DRIVER=fake', 'SESSION_DRIVER=memory']) - } - - testEnvFile.commit() - logger.action('create').succeeded('.env.test') - - /** - * Install required dependencies - */ - const pkgFile = new files.PackageJsonFile(this.application.appRoot) - pkgFile.install('@japa/runner@2.5.1') - pkgFile.install('@japa/preset-adonis') - pkgFile.useClient(this.getPackageManager()) - - const spinner = logger.await(logger.colors.gray('installing @japa/runner, @japa/preset-adonis')) - - try { - const response = await pkgFile.commitAsync() - if (response && response.status === 1) { - spinner.stop() - logger.fatal({ - message: 'Unable to configure tests runner', - stack: response.stderr.toString(), - }) - } else { - spinner.stop() - logger.success('Configured tests runner successfully') - } - } catch (error) { - spinner.stop() - logger.fatal(error) - } - } - - /** - * Configure a give package - */ - private async configurePackage(name: string) { - if (name === 'encore') { - await this.configureEncore() - return - } - - if (name === 'tests') { - await this.configureTests() - return - } - - await new tasks.Instructions(name, this.application.appRoot, this.application, true).execute() - await new Manifest(this.application.appRoot, this.logger).generate() - } - - /** - * Invoked automatically by ace - */ - public async run() { - for (let name of this.packages) { - await this.configurePackage(name) - } - } -} diff --git a/commands/Make/Base.ts b/commands/Make/Base.ts deleted file mode 100644 index 5e704b8..0000000 --- a/commands/Make/Base.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { pathExists } from 'fs-extra' -import { BaseCommand } from '@adonisjs/core/build/standalone' - -/** - * Base class to generate framework entities - */ -export abstract class BaseGenerator extends BaseCommand { - protected abstract resourceName: string - protected abstract createExact: boolean - protected abstract getStub(): string - protected abstract getDestinationPath(): string - - protected suffix?: string - protected extname: string = '.ts' - protected form?: 'singular' | 'plural' - protected pattern?: 'camelcase' | 'snakecase' | 'pascalcase' - protected formIgnoreList?: string[] - protected templateData(): any { - return {} - } - - /** - * Returns path for a given namespace by replacing the base namespace - * with the defined directories map inside the `.adonisrc.json` - * file - */ - protected getPathForNamespace(namespaceFor: string): string | null { - return this.application.resolveNamespaceDirectory(namespaceFor) - } - - /** - * Returns contents of the rcFile - */ - protected async hasRcFile(cwd: string) { - const filePath = join(cwd, '.adonisrc.json') - return pathExists(filePath) - } - - /** - * Handle command - */ - public async generate() { - const hasRcFile = await this.hasRcFile(this.application.appRoot) - - /** - * Ensure `.adonisrc.json` file exists - */ - if (!hasRcFile) { - this.logger.error('Make sure your project root has ".adonisrc.json" file') - return - } - - const transformations = this.createExact - ? { - extname: this.extname, - } - : { - form: this.form, - suffix: this.suffix, - formIgnoreList: this.formIgnoreList, - pattern: this.pattern, - extname: this.extname, - } - - const file = this.generator - .addFile(this.resourceName, transformations) - .stub(this.getStub()) - .useMustache() - .destinationDir(this.getDestinationPath()) - .appRoot(this.application.appRoot) - .apply(this.templateData()) - - await this.generator.run() - return file - } -} diff --git a/commands/Make/Command.ts b/commands/Make/Command.ts deleted file mode 100644 index 0cfacdb..0000000 --- a/commands/Make/Command.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { args, flags } from '@adonisjs/core/build/standalone' -import { string } from '@poppinss/utils/build/helpers' - -import { BaseGenerator } from './Base' - -/** - * Command to make a new command - */ -export default class MakeCommand extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected pattern = 'pascalcase' as const - protected resourceName: string - protected createExact: boolean - - /** - * Command meta data - */ - public static commandName = 'make:command' - public static description = 'Make a new ace command' - - @args.string({ description: 'Name of the command class' }) - public name: string - - @flags.boolean({ - description: 'Create the command with the exact name as provided', - alias: 'e', - }) - public exact: boolean - - /** - * Returns the template stub based upon the `--resource` - * flag value - */ - protected getStub(): string { - return join(__dirname, '..', '..', 'templates', 'command.txt') - } - - /** - * Path to the commands directory - */ - protected getDestinationPath(): string { - return this.application.rcFile.directories.commands || 'commands' - } - - /** - * Passed down to the template. - */ - protected templateData() { - return { - toCommandName: () => { - return function (filename: string, render: any) { - return string.snakeCase(render(filename)).replace(/_/, ':') - } - }, - } - } - - public async run(): Promise { - this.resourceName = this.name - this.createExact = this.exact - await super.generate() - } -} diff --git a/commands/Make/Controller.ts b/commands/Make/Controller.ts deleted file mode 100644 index 813680d..0000000 --- a/commands/Make/Controller.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { args, flags } from '@adonisjs/core/build/standalone' -import { BaseGenerator } from './Base' - -/** - * Command to make a new HTTP Controller - */ -export default class MakeController extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected suffix = 'Controller' - protected form = 'plural' as const - protected pattern = 'pascalcase' as const - protected resourceName: string - protected createExact: boolean - - /** - * Do not pluralize following controller names - */ - protected formIgnoreList = [ - 'Home', - 'Auth', - 'Login', - 'Authentication', - 'Adonis', - 'Dashboard', - 'Signup', - 'Api', - ] - - /** - * Command meta data - */ - public static commandName = 'make:controller' - public static description = 'Make a new HTTP controller' - - @args.string({ description: 'Name of the controller class' }) - public name: string - - @flags.boolean({ description: 'Add resourceful methods to the controller class', alias: 'r' }) - public resource: boolean - - @flags.boolean({ - description: 'Create the controller with the exact name as provided', - alias: 'e', - }) - public exact: boolean - - /** - * Returns the template stub based upon the `--resource` - * flag value - */ - protected getStub(): string { - return join( - __dirname, - '..', - '..', - 'templates', - this.resource ? 'resource-controller.txt' : 'controller.txt' - ) - } - - /** - * Pull path from the `httpControllers` directory declaration from - * the `.adonisrc.json` file or fallback to `app/Controllers/Http` - */ - protected getDestinationPath(): string { - return this.getPathForNamespace('httpControllers') || 'app/Controllers/Http' - } - - public async run() { - this.resourceName = this.name - this.createExact = this.exact - await super.generate() - } -} diff --git a/commands/Make/Exception.ts b/commands/Make/Exception.ts deleted file mode 100644 index 0421af7..0000000 --- a/commands/Make/Exception.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { args, flags } from '@adonisjs/core/build/standalone' -import { BaseGenerator } from './Base' - -/** - * Command to make a new event exceptions class - */ -export default class MakeException extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected form = 'singular' as const - protected pattern = 'pascalcase' as const - protected resourceName: string - protected suffix = 'Exception' - protected createExact: boolean - - /** - * Command meta data - */ - public static commandName = 'make:exception' - public static description = 'Make a new custom exception class' - - @args.string({ description: 'Name of the exception class' }) - public name: string - - @flags.boolean({ description: 'Add the handle method to self handle the exception' }) - public selfHandle: boolean - - @flags.boolean({ - description: 'Create the exception class with the exact name as provided', - alias: 'e', - }) - public exact: boolean - - /** - * Returns the template stub - */ - protected getStub(): string { - return join( - __dirname, - '..', - '..', - 'templates', - this.selfHandle ? 'self-handle-exception.txt' : 'exception.txt' - ) - } - - /** - * Pull path from the `exceptions` namespace declaration from - * the `.adonisrc.json` file or fallback to `app/Exceptions` - */ - protected getDestinationPath(): string { - return this.getPathForNamespace('exceptions') || 'app/Exceptions' - } - - public async run() { - this.resourceName = this.name - this.createExact = this.exact - await super.generate() - } -} diff --git a/commands/Make/Listener.ts b/commands/Make/Listener.ts deleted file mode 100644 index f6920b0..0000000 --- a/commands/Make/Listener.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { args, flags } from '@adonisjs/core/build/standalone' -import { BaseGenerator } from './Base' - -/** - * Command to make a new event listener class - */ -export default class MakeListener extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected form = 'singular' as const - protected pattern = 'pascalcase' as const - protected resourceName: string - protected createExact: boolean - - /** - * Command meta data - */ - public static commandName = 'make:listener' - public static description = 'Make a new event listener class' - - @args.string({ description: 'Name of the event listener class' }) - public name: string - - @flags.boolean({ - description: 'Create the listener with the exact name as provided', - alias: 'e', - }) - public exact: boolean - - /** - * Returns the template stub - */ - protected getStub(): string { - return join(__dirname, '..', '..', 'templates', 'event-listener.txt') - } - - /** - * Pull path from the `listeners` directory declaration from - * the `.adonisrc.json` file or fallback to `app/Listeners` - */ - protected getDestinationPath(): string { - return this.getPathForNamespace('eventListeners') || 'app/Listeners' - } - - public async run() { - this.resourceName = this.name - this.createExact = this.exact - await super.generate() - } -} diff --git a/commands/Make/Middleware.ts b/commands/Make/Middleware.ts deleted file mode 100644 index af65627..0000000 --- a/commands/Make/Middleware.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { args, flags } from '@adonisjs/core/build/standalone' -import { BaseGenerator } from './Base' - -/** - * Command to make a new middleware - */ -export default class MakeMiddleware extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected suffix = '' - protected form = 'singular' as const - protected pattern = 'pascalcase' as const - protected resourceName: string - protected createExact: boolean - - /** - * Command meta data - */ - public static commandName = 'make:middleware' - public static description = 'Make a new middleware' - - @args.string({ description: 'Name of the middleware class' }) - public name: string - - @flags.boolean({ - description: 'Create the middleware with the exact name as provided', - alias: 'e', - }) - public exact: boolean - - /** - * Returns the template stub path - */ - protected getStub(): string { - return join(__dirname, '..', '..', 'templates', 'middleware.txt') - } - - /** - * Middleware are always created inside `app/Middleware` directory. - * We can look into configuring it later. - */ - protected getDestinationPath(): string { - return this.getPathForNamespace('middleware') || 'app/Middleware' - } - - public async run() { - this.resourceName = this.name - this.createExact = this.exact - const middlewareNamespace = this.application.rcFile.namespaces.middleware || 'App/Middleware' - - const file = await super.generate() - if (!file) { - return - } - - const fileJSON = file.toJSON() - - if (fileJSON.state === 'persisted') { - this.ui - .instructions() - .heading('Register middleware') - .add(`Open ${this.colors.cyan('start/kernel.ts')} file`) - .add(`Register the following function as a global or a named middleware`) - .add( - this.colors - .cyan() - .underline(`() => import('${middlewareNamespace}/${fileJSON.filename}')`) - ) - .render() - } - } -} diff --git a/commands/Make/PreloadFile.ts b/commands/Make/PreloadFile.ts deleted file mode 100644 index dd8237a..0000000 --- a/commands/Make/PreloadFile.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import slash from 'slash' -import { join, extname } from 'path' -import { args, flags } from '@adonisjs/core/build/standalone' - -import { BaseGenerator } from './Base' -import type { AppEnvironments } from '@ioc:Adonis/Core/Application' - -const ALLOWED_ENVIRONMENTS: AppEnvironments[] = ['console', 'web', 'repl', 'test'] - -/** - * Command to make a new preloaded file - */ -export default class MakePreloadFile extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected resourceName: string - protected createExact = true - - /** - * Command name - */ - public static commandName = 'make:prldfile' - - /** - * Command description - */ - public static description = 'Make a new preload file' - - @args.string({ description: 'Name of the file' }) - public name: string - - @flags.array({ - description: `Define the preload file environment. Accepted values "${ALLOWED_ENVIRONMENTS}"`, - }) - public environment: AppEnvironments[] - - /** - * Check if the mentioned environments are valid - */ - private isValidEnviroment(environment: string[]): environment is AppEnvironments[] { - return !environment.find((one) => !ALLOWED_ENVIRONMENTS.includes(one as any)) - } - - /** - * Returns the template stub path - */ - protected getStub(): string { - return join(__dirname, '..', '..', 'templates', 'preload-file.txt') - } - - /** - * Path to the start directory - */ - protected getDestinationPath(): string { - return this.application.rcFile.directories.start || 'start' - } - - /** - * Run command - */ - public async run() { - /** - * Ensure the environments are valid when provided via flag - */ - if (this.environment && this.environment.length && !this.isValidEnviroment(this.environment)) { - this.logger.error( - `Invalid environment(s) "${this.environment}". Only "${ALLOWED_ENVIRONMENTS}" are allowed` - ) - return - } - - let environments: string[] = this.environment - - /** - * Prompt user to select one or more environments - */ - if (!environments) { - environments = await this.prompt.multiple( - 'Select the environment(s) in which you want to load this file', - [ - { - name: 'all', - message: 'Load file in all environments', - }, - { - name: 'console', - message: 'Environment for ace commands', - }, - { - name: 'repl', - message: 'Environment for the REPL session', - }, - { - name: 'web', - message: 'Environment for HTTP requests', - }, - { - name: 'test', - message: 'Environment for the test process', - }, - ] - ) - } - - /** - * Generate resource file - */ - this.resourceName = this.name - const file = await super.generate() - - if (!file) { - return - } - - /** - * Update preload file - */ - const { files } = await import('@adonisjs/sink') - const relativePath = file.toJSON().relativepath - const rcFile = new files.AdonisRcFile(this.application.appRoot) - - if (!environments || !environments.length || environments.includes('all')) { - rcFile.setPreload(`./${slash(relativePath).replace(extname(relativePath), '')}`) - } else { - rcFile.setPreload( - `./${slash(relativePath).replace(extname(relativePath), '')}`, - environments as AppEnvironments[] - ) - } - - rcFile.commit() - } -} diff --git a/commands/Make/Provider.ts b/commands/Make/Provider.ts deleted file mode 100644 index 3e4f897..0000000 --- a/commands/Make/Provider.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import slash from 'slash' -import { join, extname } from 'path' -import { args, flags } from '@adonisjs/core/build/standalone' - -import { BaseGenerator } from './Base' - -/** - * Command to make a new provider - */ -export default class MakeProvider extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected suffix = 'Provider' - protected form = 'singular' as const - protected pattern = 'pascalcase' as const - protected resourceName: string - protected createExact: boolean - - /** - * Command meta data - */ - public static commandName = 'make:provider' - public static description = 'Make a new provider class' - - @args.string({ description: 'Name of the provider class' }) - public name: string - - @flags.boolean({ description: 'Register provider under the ace providers array' }) - public ace: boolean - - @flags.boolean({ - description: 'Create the provider with the exact name as provided', - alias: 'e', - }) - public exact: boolean - - /** - * Returns the template stub path - */ - protected getStub(): string { - return join(__dirname, '..', '..', 'templates', 'provider.txt') - } - - /** - * Path to the providers directory - */ - protected getDestinationPath(): string { - return this.application.rcFile.directories.providers || 'providers' - } - - public async run() { - this.resourceName = this.name - this.createExact = this.exact - const file = await super.generate() - - if (!file) { - return - } - - const { files } = await import('@adonisjs/sink') - const relativePath = file.toJSON().relativepath - const rcFile = new files.AdonisRcFile(this.application.appRoot) - - if (this.ace) { - rcFile.addAceProvider(`./${slash(relativePath).replace(extname(relativePath), '')}`) - } else { - rcFile.addProvider(`./${slash(relativePath).replace(extname(relativePath), '')}`) - } - - rcFile.commit() - } -} diff --git a/commands/Make/Suite.ts b/commands/Make/Suite.ts deleted file mode 100644 index 6859718..0000000 --- a/commands/Make/Suite.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { BaseCommand, args, flags } from '@adonisjs/core/build/standalone' -import { files, logger } from '@adonisjs/sink' -import globParent from 'glob-parent' -import { join } from 'path' - -/** - * Create a new test suite - */ -export default class CreateSuite extends BaseCommand { - public static commandName = 'make:suite' - public static description = 'Create a new test suite' - - /** - * Name of the test suite to be created - */ - @args.string({ description: 'Name of the test suite' }) - public suite: string - - /** - * Glob pattern for the test suite, or only location to the test suite - */ - @args.string({ description: 'Path to the test suite directory', required: false }) - public location: string = '' - - /** - * Should add a sample test file - */ - @flags.boolean({ description: 'Add a sample test file' }) - public withExampleTest: boolean = true - - /** - * Get the destination path for the sample test file - */ - private getExampleTestDestinationPath() { - return globParent(this.location) + '/test.spec.ts' - } - - /** - * Generate suite glob pattern based on `location` argument - */ - private generateSuiteGlobPattern() { - if (!this.location) { - this.location = `tests/${this.suite}` - } - - if (!['*', '.js', '.ts'].find((keyword) => this.location.includes(keyword))) { - this.location = `${this.location}/**/*.spec(.ts|.js)` - } - } - - /** - * Check if the suite name is already defined in RcFile - */ - private checkIfSuiteExists(rcFile: files.AdonisRcFile) { - const existingSuites = rcFile.get('tests.suites') || [] - const existingSuitesNames = existingSuites.map((suite) => suite.name) - - return existingSuitesNames.includes(this.suite) - } - - /** - * Add the new test suite to the AdonisRC File and save it - */ - private async addSuiteToRcFile() { - const rcFile = new files.AdonisRcFile(this.application.appRoot) - const existingSuites = rcFile.get('tests.suites') || [] - - if (this.checkIfSuiteExists(rcFile)) { - return logger.action('update').skipped(`Suite ${this.suite} already exists`) - } - - rcFile.set('tests.suites', [ - ...existingSuites, - { - name: this.suite, - files: [this.location], - timeout: 60 * 1000, - }, - ]) - - rcFile.commit() - logger.action('update').succeeded('.adonisrc.json') - } - - /** - * Add a sample test file to the new suite folder - */ - private createSampleTestFile() { - const path = this.getExampleTestDestinationPath() - const testFile = new files.MustacheFile( - this.application.appRoot, - path, - join(__dirname, '../..', 'templates/test.txt') - ) - - if (!testFile.exists()) { - testFile.apply({}).commit() - logger.action('create').succeeded(path) - } - } - - public async run() { - this.generateSuiteGlobPattern() - - await this.addSuiteToRcFile() - - if (this.withExampleTest) { - this.createSampleTestFile() - } - } -} diff --git a/commands/Make/Test.ts b/commands/Make/Test.ts deleted file mode 100644 index a08506e..0000000 --- a/commands/Make/Test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import globParent from 'glob-parent' -import { string } from '@poppinss/utils/build/helpers' -import { args, flags } from '@adonisjs/core/build/standalone' -import { BaseGenerator } from './Base' - -/** - * Command to make a new test - */ -export default class MakeTest extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected extname = '.spec.ts' - protected form = 'singular' as const - protected pattern = 'snakecase' as const - protected resourceName: string - protected createExact: boolean - - /** - * Command meta data - */ - public static commandName = 'make:test' - public static description = 'Make a new test' - - @args.string({ description: 'Name of the test suite' }) - public suite: string - - @args.string({ description: 'Name of the test file' }) - public name: string - - @flags.boolean({ - description: 'Create the test file with the exact name as provided', - alias: 'e', - }) - public exact: boolean - - /** - * Returns the template stub path - */ - protected getStub(): string { - return join(__dirname, '..', '..', 'templates', 'test.txt') - } - - /** - * The file is created inside the parent directory of the first - * glob pattern - */ - protected getDestinationPath(): string { - const testSuites = this.application.rcFile.tests.suites - const mentionedSuite = testSuites.find(({ name }) => this.suite === name)! - const suiteGlob = Array.isArray(mentionedSuite.files) - ? mentionedSuite.files[0] - : mentionedSuite.files - - return globParent(suiteGlob) - } - - protected templateData() { - return { - name: string.sentenceCase(this.name), - } - } - - public async run() { - const testSuites = this.application.rcFile.tests.suites - const mentionedSuite = testSuites.find(({ name }) => this.suite === name)! - if (!mentionedSuite) { - this.logger.error( - `Invalid suite "${this.suite}". Make sure the suite is registered inside the .adonisrc.json file` - ) - return - } - - this.resourceName = this.name - this.createExact = this.exact - await super.generate() - } -} diff --git a/commands/Make/Validator.ts b/commands/Make/Validator.ts deleted file mode 100644 index 333d9be..0000000 --- a/commands/Make/Validator.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { args, flags } from '@adonisjs/core/build/standalone' -import { BaseGenerator } from './Base' - -/** - * Command to make a new validator - */ -export default class MakeValidator extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected suffix = 'Validator' - protected form = 'singular' as const - protected pattern = 'pascalcase' as const - protected resourceName: string - protected createExact: boolean - - /** - * Command meta data - */ - public static commandName = 'make:validator' - public static description = 'Make a new validator' - - @args.string({ description: 'Name of the validator class' }) - public name: string - - @flags.boolean({ - description: 'Create the validator with the exact name as provided', - alias: 'e', - }) - public exact: boolean - - /** - * Returns the template stub path - */ - protected getStub(): string { - return join(__dirname, '..', '..', 'templates', 'validator.txt') - } - - /** - * Pull path for the `validators` directory declaration from - * the `.adonisrc.json` file or fallback to `app/Validators` - */ - protected getDestinationPath(): string { - return this.getPathForNamespace('validators') || 'app/Validators' - } - - public async run() { - this.resourceName = this.name - this.createExact = this.exact - await super.generate() - } -} diff --git a/commands/Make/View.ts b/commands/Make/View.ts deleted file mode 100644 index ae101b1..0000000 --- a/commands/Make/View.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { args, flags } from '@adonisjs/core/build/standalone' -import { BaseGenerator } from './Base' - -/** - * Command to make a new view - */ -export default class MakeView extends BaseGenerator { - /** - * Required by BaseGenerator - */ - protected suffix = '' - protected extname = '.edge' - protected pattern = 'snakecase' as const - protected resourceName: string - protected createExact: boolean - - /** - * Command meta data - */ - public static commandName = 'make:view' - public static description = 'Make a new view template' - - @args.string({ description: 'Name of the view' }) - public name: string - - @flags.boolean({ - description: 'Create the template file with the exact name as provided', - alias: 'e', - }) - public exact: boolean - - /** - * Returns the template stub path - */ - protected getStub(): string { - return join(__dirname, '..', '..', 'templates', 'view.txt') - } - - /** - * Path to the providers directory - */ - protected getDestinationPath(): string { - return this.application.rcFile.directories.views || 'resources/views' - } - - public async run() { - this.resourceName = this.name - this.createExact = this.exact - await super.generate() - } -} diff --git a/commands/Serve.ts b/commands/Serve.ts deleted file mode 100644 index 1cf608b..0000000 --- a/commands/Serve.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { BaseCommand, flags } from '@adonisjs/core/build/standalone' - -/** - * Compile typescript project to Javascript and start - * the HTTP server - */ -export default class Serve extends BaseCommand { - public static commandName = 'serve' - public static description = - 'Start the AdonisJS HTTP server, along with the file watcher. Also starts the webpack dev server when webpack encore is installed' - - public static settings = { - stayAlive: true, - } - - /** - * Bundle frontend assets. Defaults to true - */ - @flags.boolean({ - description: 'Start webpack dev server when encore is installed. Use "--no-assets" to disable', - }) - public assets: boolean = true - - /** - * Allows watching for file changes - */ - @flags.boolean({ - description: 'Watch for file changes and re-start the HTTP server on change', - alias: 'w', - }) - public watch: boolean - - /** - * Detect changes by polling files - */ - @flags.boolean({ - description: 'Detect file changes by polling files instead of listening to filesystem events', - alias: 'p', - }) - public poll: boolean - - /** - * Arguments to pass to the `node` binary - */ - @flags.array({ description: 'CLI options to pass to the node command line' }) - public nodeArgs: string[] = [] - - /** - * Arguments to pass to the `encore` binary - */ - @flags.array({ description: 'CLI options to pass to the encore command line' }) - public encoreArgs: string[] = [] - - public async run() { - const { DevServer } = await import('../src/DevServer') - - try { - if (this.watch) { - await new DevServer( - this.application.appRoot, - this.nodeArgs, - this.encoreArgs, - this.assets, - this.logger - ).watch(this.poll) - } else { - await new DevServer( - this.application.appRoot, - this.nodeArgs, - this.encoreArgs, - this.assets, - this.logger - ).start() - } - } catch (error) { - this.logger.fatal(error) - } - } -} diff --git a/commands/Test.ts b/commands/Test.ts deleted file mode 100644 index ca79310..0000000 --- a/commands/Test.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { BaseCommand, flags, args } from '@adonisjs/core/build/standalone' -import { JapaFlags } from '../src/Contracts' - -/** - * Run tests - */ -export default class Test extends BaseCommand { - public static commandName = 'test' - public static description = 'Run AdonisJS tests' - public static settings = { - stayAlive: true, - } - - @args.spread({ description: 'Run tests for only the specified suites', required: false }) - public suites: string[] - - /** - * Allows watching for file changes - */ - @flags.array({ - description: 'Run tests for the mentioned files only', - }) - public files: string[] - - /** - * Allows watching for file changes - */ - @flags.boolean({ - description: 'Watch for file changes and re-run tests on file change', - alias: 'w', - }) - public watch: boolean - - /** - * Detect changes by polling files - */ - @flags.boolean({ - description: 'Detect file changes by polling files instead of listening to filesystem events', - alias: 'p', - }) - public poll: boolean - - /** - * Arguments to pass to the `node` binary - */ - @flags.array({ description: 'CLI options to pass to the node command line' }) - public nodeArgs: string[] = [] - - /** - * Filter by tags - */ - @flags.array({ description: 'Filter tests by tags' }) - public tags: string[] - - /** - * Filter by tags - */ - @flags.array({ description: 'Filter tests by ignoring tags' }) - public ignoreTags: string[] - - /** - * Filter by test title - */ - @flags.array({ description: 'Filter tests by title' }) - public tests: string[] - - /** - * Filter by group title - */ - @flags.array({ description: 'Filter tests by group title' }) - public groups: string[] - - /** - * Customize tests timeout - */ - @flags.number({ description: 'Customize tests timeout' }) - public timeout: number - - /** - * Force exit the tests runner - */ - @flags.boolean({ description: 'Force exit the tests runner process' }) - public forceExit: boolean - - /** - * Convert command flags to test filters - */ - private getTestFilters() { - const filters: JapaFlags = {} - if (this.forceExit) { - filters['--force-exit'] = true - } - - if (this.files) { - filters['--files'] = this.files - } - - if (this.timeout !== undefined) { - filters['--timeout'] = this.timeout - } - - if (this.tags) { - filters['--tags'] = this.tags - } - - if (this.suites) { - filters._ = this.suites - } - - if (this.ignoreTags) { - filters['--ignore-tags'] = this.ignoreTags - } - - if (this.tests) { - filters['--tests'] = this.tests - } - - if (this.groups) { - filters['--groups'] = this.groups - } - - return filters - } - - public async run() { - const { TestsServer } = await import('../src/Test') - - try { - if (this.watch) { - await new TestsServer( - this.application.appRoot, - this.getTestFilters(), - this.nodeArgs, - this.logger - ).watch() - } else { - await new TestsServer( - this.application.appRoot, - this.getTestFilters(), - this.nodeArgs, - this.logger - ).run() - } - } catch (error) { - this.exitCode = 1 - this.logger.fatal(error) - } - } -} diff --git a/commands/TypeCheck.ts b/commands/TypeCheck.ts deleted file mode 100644 index a790099..0000000 --- a/commands/TypeCheck.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { BaseCommand, flags } from '@adonisjs/core/build/standalone' -import { TSCONFIG_FILE_NAME } from '../config/paths' - -/** - * TypeCheck project without writing the compiled output to the disk - */ -export default class TypeCheck extends BaseCommand { - public static commandName = 'type-check' - public static description = - 'Type check TypeScript source without writing the compiled output on disk' - - /** - * Path to the TypeScript project configuration file. Defaults to "tsconfig.json" - */ - @flags.string({ - description: 'Path to the TypeScript project configuration file', - }) - public tsconfig: string = TSCONFIG_FILE_NAME - - /** - * Invoked automatically by ace - */ - public async run() { - const { Compiler } = await import('../src/Compiler') - - try { - const compiler = new Compiler(this.application.appRoot, [], false, this.logger, this.tsconfig) - const success = await compiler.typeCheck() - - /** - * Set exitCode based upon the typecheck status - */ - if (!success) { - this.exitCode = 1 - } - } catch (error) { - this.logger.fatal(error) - this.exitCode = 1 - } - } -} diff --git a/config/paths.ts b/config/paths.ts deleted file mode 100644 index e599df4..0000000 --- a/config/paths.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -export const ACE_FILE_NAME = 'ace' -export const DEFAULT_BUILD_DIR = 'build' -export const RCFILE_NAME = '.adonisrc.json' -export const ENV_FILES = ['.env', '.env.testing'] -export const SERVER_ENTRY_FILE = 'server.ts' -export const TESTS_ENTRY_FILE = 'test.ts' -export const TSCONFIG_FILE_NAME = 'tsconfig.json' diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..47b5f4d --- /dev/null +++ b/index.ts @@ -0,0 +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. + */ + +export { Bundler } from './src/bundler.js' +export { DevServer } from './src/dev_server.js' +export { TestRunner } from './src/test_runner.js' diff --git a/package.json b/package.json index 4497d92..5270410 100644 --- a/package.json +++ b/package.json @@ -1,166 +1,146 @@ { "name": "@adonisjs/assembler", - "version": "5.9.6", - "description": "Core commands to compiler and build AdonisJs project", - "main": "build/ace-manifest.json", + "description": "Provides utilities to run AdonisJS development server and build project for production", + "version": "7.0.0-1", "engines": { - "node": ">=14.0.0" + "node": ">=18.16.0" }, + "main": "build/index.js", + "type": "module", "files": [ - "build/commands", - "build/config", - "build/templates", - "build/src", - "build/register.js", - "build/register.d.ts", - "build/ace-manifest.json" + "build", + "!build/bin", + "!build/tests" ], + "exports": { + ".": "./build/index.js", + "./code_transformer": "./build/src/code_transformer/main.js", + "./types": "./build/src/types.js" + }, "scripts": { - "mrm": "mrm --preset=@adonisjs/mrm-preset", "pretest": "npm run lint", - "test": "cross-env FORCE_COLOR=true node -r @adonisjs/require-ts/build/register ./bin/test.ts", + "test": "c8 npm run quick:test", "lint": "eslint . --ext=.ts", "clean": "del-cli build", - "compile": "npm run lint && npm run clean && tsc", - "build": "npm run compile && node build/bin/index.js && copyfiles \"templates/**\" build", - "commit": "git-cz", - "release": "np --message=\"chore(release): %s\"", + "typecheck": "tsc --noEmit", + "precompile": "npm run lint && npm run clean", + "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", + "build": "npm run compile", + "release": "np", "version": "npm run build", - "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/assembler", + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/assembler", "format": "prettier --write .", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "quick:test": "cross-env NODE_DEBUG=adonisjs:assembler node --enable-source-maps --loader=ts-node/esm bin/test.ts" + }, + "devDependencies": { + "@adonisjs/application": "^8.0.0", + "@adonisjs/eslint-config": "^1.2.1", + "@adonisjs/prettier-config": "^1.2.1", + "@adonisjs/tsconfig": "^1.2.1", + "@commitlint/cli": "^18.4.4", + "@commitlint/config-conventional": "^18.4.4", + "@japa/assert": "^2.1.0", + "@japa/file-system": "^2.1.0", + "@japa/runner": "^3.1.1", + "@japa/snapshot": "^2.0.4", + "@swc/core": "^1.3.102", + "@types/node": "^20.10.7", + "@types/picomatch": "^2.3.3", + "@types/pretty-hrtime": "^1.0.3", + "c8": "^9.0.0", + "cross-env": "^7.0.3", + "dedent": "^1.5.1", + "del-cli": "^5.0.0", + "eslint": "^8.56.0", + "github-label-sync": "^2.3.1", + "husky": "^8.0.3", + "np": "^9.2.0", + "p-event": "^6.0.0", + "prettier": "^3.1.1", + "ts-node": "^10.9.2", + "tsup": "^8.0.1", + "typescript": "^5.3.3" }, + "dependencies": { + "@adonisjs/env": "^5.0.0", + "@antfu/install-pkg": "^0.3.1", + "@poppinss/chokidar-ts": "^4.1.3", + "@poppinss/cliui": "^6.3.0", + "cpy": "^11.0.0", + "execa": "^8.0.1", + "fast-glob": "^3.3.2", + "get-port": "^7.0.0", + "junk": "^4.0.1", + "picomatch": "^3.0.1", + "pretty-hrtime": "^1.0.3", + "slash": "^5.1.0", + "ts-morph": "^21.0.1" + }, + "peerDependencies": { + "typescript": "^4.0.0 || ^5.0.0" + }, + "author": "virk,adonisjs", + "license": "MIT", + "homepage": "https://github.com/adonisjs/assembler#readme", "repository": { "type": "git", "url": "git+ssh://git@github.com/adonisjs/assembler.git" }, + "bugs": { + "url": "https://github.com/adonisjs/assembler/issues" + }, "keywords": [ "adonisjs", - "boot", "build", "ts" ], - "author": "virk,adonisjs", - "license": "MIT", - "bugs": { - "url": "https://github.com/adonisjs/assembler/issues" - }, - "homepage": "https://github.com/adonisjs/assembler#readme", - "devDependencies": { - "@adonisjs/ace": "^11.3.1", - "@adonisjs/core": "^5.8.9", - "@adonisjs/mrm-preset": "^5.0.3", - "@japa/assert": "^1.3.6", - "@japa/run-failed-tests": "^1.1.0", - "@japa/runner": "^2.2.2", - "@japa/spec-reporter": "^1.3.2", - "@poppinss/dev-utils": "^2.0.3", - "@types/node": "^18.15.1", - "commitizen": "^4.2.5", - "copyfiles": "^2.4.1", - "cross-env": "^7.0.3", - "cz-conventional-changelog": "^3.3.0", - "del-cli": "^5.0.0", - "eslint": "^8.27.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-adonis": "^2.1.1", - "eslint-plugin-prettier": "^4.2.1", - "github-label-sync": "^2.2.0", - "husky": "^8.0.2", - "mrm": "^4.1.13", - "np": "^8.0.2", - "prettier": "^2.7.1", - "typescript": "^4.9.3" + "eslintConfig": { + "extends": "@adonisjs/eslint-config/package" }, - "nyc": { - "exclude": [ - "test" - ], - "extension": [ - ".ts" + "prettier": "@adonisjs/prettier-config", + "commitlint": { + "extends": [ + "@commitlint/config-conventional" ] }, - "husky": { - "hooks": { - "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" - } - }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } + "publishConfig": { + "access": "public", + "tag": "next" }, "np": { - "contents": ".", + "message": "chore(release): %s", + "tag": "next", + "branch": "main", "anyBranch": false }, - "dependencies": { - "@adonisjs/application": "^5.2.5", - "@adonisjs/env": "^3.0.9", - "@adonisjs/ioc-transformer": "^2.3.4", - "@adonisjs/require-ts": "^2.0.13", - "@adonisjs/sink": "^5.4.2", - "@poppinss/chokidar-ts": "^3.3.5", - "@poppinss/cliui": "^3.0.5", - "@poppinss/utils": "^5.0.0", - "cpy": "^8.1.2", - "emittery": "^0.13.1", - "execa": "^5.1.1", - "fs-extra": "^10.1.0", - "get-port": "^5.1.1", - "glob-parent": "^6.0.2", - "has-yarn": "^2.1.0", - "picomatch": "^2.3.1", - "slash": "^3.0.0" - }, - "peerDependencies": { - "@adonisjs/core": "^5.1.0" - }, - "publishConfig": { - "access": "public", - "tag": "latest" - }, - "mrmConfig": { - "core": true, - "license": "MIT", - "services": [ - "github-actions" + "c8": { + "reporter": [ + "text", + "html" ], - "minNodeVersion": "14.15.4", - "probotApps": [ - "stale", - "lock" - ], - "runGhActionsOnWindows": true + "exclude": [ + "tests/**", + "build/**", + "bin/**", + "tmp/**", + "examples/**", + "src/dev_server.ts", + "src/test_runner.ts", + "src/assets_dev_server.ts" + ] }, - "eslintConfig": { - "extends": [ - "plugin:adonis/typescriptPackage", - "prettier" - ], - "plugins": [ - "prettier" + "tsup": { + "entry": [ + "./index.ts", + "./src/types.ts", + "./src/code_transformer/main.ts" ], - "rules": { - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ] - } - }, - "eslintIgnore": [ - "build" - ], - "prettier": { - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "useTabs": false, - "quoteProps": "consistent", - "bracketSpacing": true, - "arrowParens": "always", - "printWidth": 100 + "outDir": "./build", + "clean": true, + "format": "esm", + "dts": false, + "sourcemap": true, + "target": "esnext" } } diff --git a/src/AssetsBundler/index.ts b/src/AssetsBundler/index.ts deleted file mode 100644 index 3390083..0000000 --- a/src/AssetsBundler/index.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import execa from 'execa' -import getPort from 'get-port' -import Emittery from 'emittery' -import { logger as uiLogger } from '@poppinss/cliui' -import { resolveDir } from '@poppinss/utils/build/helpers' - -export type DevServerResponse = - | { - state: 'not-installed' | 'no-assets' - } - | { - state: 'running' - port: string - host: string - } - -/** - * Assets bundler uses webpack encore to build frontend dependencies - */ -export class AssetsBundler extends Emittery { - /** - * Binary to execute - */ - private binaryName = 'encore' - - private encoreArgs: string[] = [] - - /** - * Options passed to spawn a child process - */ - private execaOptions = { - preferLocal: true, - buffer: false, - stdio: 'pipe' as const, - localDir: this.projectRoot, - cwd: this.projectRoot, - windowsHide: false, - env: { - FORCE_COLOR: 'true', - ...this.env, - }, - } - - constructor( - private projectRoot: string, - encoreArgs: string[] = [], - private buildAssets: boolean = true, - private logger: typeof uiLogger, - private env: { [key: string]: string } = {} - ) { - super() - this.encoreArgs = encoreArgs.reduce((result, arg) => { - result = result.concat(arg.split(' ')) - return result - }, [] as string[]) - } - - /** - * Find if encore is installed - */ - private isEncoreInstalled() { - try { - resolveDir(this.projectRoot, '@symfony/webpack-encore') - return true - } catch { - return false - } - } - - /** - * Notify user that we are about use encore - */ - private notifyAboutEncore() { - this.logger.info(`detected { ${this.logger.colors.dim().yellow('@symfony/webpack-encore')} }`) - this.logger.info( - `building frontend assets. Use { ${this.logger.colors - .dim() - .yellow('--no-assets')} } to disable` - ) - } - - /** - * Logs the line to stdout - */ - private log(line: Buffer | string) { - line = line.toString().trim() - if (!line.length) { - return - } - console.log(`[ ${this.logger.colors.cyan('encore')} ] ${line}`) - } - - /** - * Logs the line to stderr - */ - private logError(line: Buffer | string) { - line = line.toString().trim() - if (!line.length) { - return - } - console.error(`[ ${this.logger.colors.cyan('encore')} ] ${line}`) - } - - /** - * Returns the custom port defined using the `--port` flag in encore - * flags - */ - private findCustomPort(): undefined | string { - let portIndex = this.encoreArgs.findIndex((arg) => arg === '--port') - if (portIndex > -1) { - return this.encoreArgs[portIndex + 1] - } - - portIndex = this.encoreArgs.findIndex((arg) => arg.includes('--port')) - if (portIndex > -1) { - const tokens = this.encoreArgs[portIndex].split('=') - return tokens[1] && tokens[1].trim() - } - } - - /** - * Returns the custom host defined using the `--host` flag in encore - * flags - */ - private findCustomHost(): undefined | string { - let hostIndex = this.encoreArgs.findIndex((arg) => arg === '--host') - if (hostIndex > -1) { - return this.encoreArgs[hostIndex + 1] - } - - hostIndex = this.encoreArgs.findIndex((arg) => arg.includes('--host')) - if (hostIndex > -1) { - const tokens = this.encoreArgs[hostIndex].split('=') - return tokens[1] && tokens[1].trim() - } - } - - /** - * Execute command - */ - private exec(args: string[]): Promise { - return new Promise((resolve, reject) => { - const childProcess = execa(this.binaryName, args, this.execaOptions) - - childProcess.stdout?.on('data', (line: Buffer) => this.log(line)) - childProcess.stderr?.on('data', (line: Buffer) => this.logError(line)) - childProcess.on('error', (error) => reject(error)) - childProcess.on('close', (code) => { - if (code && code !== 0) { - reject(`Process exited with code ${code}`) - } else { - resolve() - } - }) - }) - } - - /** - * Build assets using encore - */ - public async build(): Promise<{ hasErrors: boolean }> { - if (!this.buildAssets) { - return { hasErrors: false } - } - - if (!this.isEncoreInstalled()) { - return { hasErrors: false } - } - - this.notifyAboutEncore() - - try { - await this.exec(['dev'].concat(this.encoreArgs)) - return { hasErrors: false } - } catch (error) { - return { hasErrors: true } - } - } - - /** - * Build assets for production - */ - public async buildForProduction(): Promise<{ hasErrors: boolean }> { - if (!this.buildAssets) { - return { hasErrors: false } - } - - if (!this.isEncoreInstalled()) { - return { hasErrors: false } - } - - this.notifyAboutEncore() - - try { - await this.exec(['production'].concat(this.encoreArgs)) - return { hasErrors: false } - } catch (error) { - return { hasErrors: true } - } - } - - /** - * Start the webpack dev server - */ - public async startDevServer(): Promise { - if (!this.isEncoreInstalled()) { - return { state: 'not-installed' } - } - - if (!this.buildAssets) { - return { state: 'no-assets' } - } - - const customHost = this.findCustomHost() || 'localhost' - - /** - * Define a random port when the "--port" flag is not passed. - * - * Encore anyways doesn't allow defining port inside the webpack.config.js - * file for generating the manifest and entrypoints file. - * - * @see - * https://github.com/symfony/webpack-encore/issues/941#issuecomment-787568811 - */ - let customPort = this.findCustomPort() - if (!customPort) { - const randomPort = await getPort({ port: 8080, host: 'localhost' }) - customPort = String(randomPort) - this.encoreArgs.push('--port', customPort) - } - - const childProcess = execa( - this.binaryName, - ['dev-server'].concat(this.encoreArgs), - this.execaOptions - ) - - childProcess.stdout?.on('data', (line: Buffer) => this.log(line)) - childProcess.stderr?.on('data', (line: Buffer) => this.logError(line)) - childProcess.on('close', (code, signal) => this.emit('close', { code, signal })) - childProcess.on('exit', (code, signal) => this.emit('exit', { code, signal })) - - return { state: 'running', port: customPort, host: customHost } - } -} diff --git a/src/Compiler/index.ts b/src/Compiler/index.ts deleted file mode 100644 index b8a97d0..0000000 --- a/src/Compiler/index.ts +++ /dev/null @@ -1,367 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import slash from 'slash' -import copyfiles from 'cpy' -import tsStatic from 'typescript' -import { join, relative } from 'path' -import { remove, outputJSON } from 'fs-extra' -import { iocTransformer } from '@adonisjs/ioc-transformer' -import { logger as uiLogger, instructions } from '@poppinss/cliui' - -import { Ts } from '../Ts' -import { RcFile } from '../RcFile' -import { Manifest } from '../Manifest' -import { RCFILE_NAME } from '../../config/paths' -import { AssetsBundler } from '../AssetsBundler' - -/** - * Exposes the API to build the AdonisJs project for development or - * production. The production build has it's own set of node_modules - */ -export class Compiler { - /** - * Reference to typescript compiler - */ - private ts: Ts - - /** - * Reference to rc File - */ - private rcFile = new RcFile(this.appRoot) - - constructor( - public appRoot: string, - private encoreArgs: string[], - private buildAssets: boolean, - private logger: typeof uiLogger = uiLogger, - tsconfig?: string - ) { - this.ts = new Ts(this.appRoot, this.logger, tsconfig) - this.ts.tsCompiler.use(() => { - return iocTransformer(this.ts.tsCompiler.ts, this.rcFile.application.rcFile) - }, 'after') - } - - /** - * Returns relative unix path from the project root. Used for - * display only - */ - private getRelativeUnixPath(absPath: string): string { - return slash(relative(this.appRoot, absPath)) - } - - /** - * Cleans up the build directory - */ - private async cleanupBuildDirectory(outDir: string) { - this.getRelativeUnixPath(outDir) - this.logger.info( - `cleaning up ${this.logger.colors - .dim() - .yellow(`"./${this.getRelativeUnixPath(outDir)}"`)} directory` - ) - await remove(outDir) - } - - /** - * Copies .adonisrc.json file to the destination - */ - private async copyAdonisRcFile(outDir: string) { - this.logger.info( - `copy { ${this.logger.colors - .dim() - .yellow(`${RCFILE_NAME} => ${this.getRelativeUnixPath(outDir)}`)} }` - ) - - await outputJSON( - join(outDir, RCFILE_NAME), - Object.assign({}, this.rcFile.getDiskContents(), { - typescript: false, - lastCompiledAt: new Date().toISOString(), - }), - { spaces: 2 } - ) - } - - /** - * Copy all meta files to the build directory - */ - private async copyMetaFiles(outDir: string, extraFiles?: string[]) { - const metaFiles = this.rcFile.getMetaFilesGlob().concat(extraFiles || []) - this.logger.info( - `copy { ${this.logger.colors - .dim() - .yellow(`${metaFiles.join(',')} => ${this.getRelativeUnixPath(outDir)}`)} }` - ) - await this.copyFiles(metaFiles, outDir) - } - - /** - * Copy files to destination directory - */ - private async copyFiles(files: string[], outDir: string) { - try { - await copyfiles(files, outDir, { cwd: this.appRoot, parents: true }) - } catch (error) { - if (!error.message.includes("the file doesn't exist")) { - throw error - } - } - } - - /** - * Build typescript source files - */ - private buildTypescriptSource(config: tsStatic.ParsedCommandLine): { - skipped: boolean - hasErrors: boolean - } { - this.logger.info('compiling typescript source files') - - const builder = this.ts.tsCompiler.builder(config) - const { skipped, diagnostics } = builder.build() - - if (skipped) { - this.logger.warning('typescript emit skipped') - } - - if (diagnostics.length) { - this.logger.error('typescript compiler errors') - this.ts.renderDiagnostics(diagnostics, builder.host) - } - - return { - skipped, - hasErrors: diagnostics.length > 0, - } - } - - /** - * Log the message that ts build and failed - */ - private logTsBuildFailed() { - this.logger.logError('') - this.logger.logError( - this.logger.colors.bgRed( - `Cannot complete the build process as there are typescript errors. Use "--ignore-ts-errors" flag to ignore Typescript errors` - ) - ) - } - - /** - * Typecheck the project without emit - */ - public async typeCheck(): Promise { - const config = this.ts.parseConfig() - if (!config) { - return false - } - - this.logger.info('type checking typescript source files') - - config.options.noEmit = true - const builder = this.ts.tsCompiler.builder(config) - const { diagnostics } = builder.build() - - if (diagnostics.length) { - this.logger.error('typescript compiler errors') - this.ts.renderDiagnostics(diagnostics, builder.host) - return false - } - - this.logger.success('built successfully') - return true - } - - /** - * Compile project. See [[Compiler.compileForProduction]] for - * production build - */ - public async compile(stopOnError: boolean = true, extraFiles?: string[]): Promise { - const config = this.ts.parseConfig() - if (!config) { - return false - } - - /** - * Bundle frontend assets when encore is installed - */ - const encore = await new AssetsBundler( - this.appRoot, - this.encoreArgs, - this.buildAssets, - this.logger - ).build() - - /** - * Skipped, coz of frontend errors - */ - if (encore.hasErrors) { - return false - } - - /** - * Always cleanup the out directory - */ - await this.cleanupBuildDirectory(config.options.outDir!) - - /** - * Build typescript source - */ - const ts = this.buildTypescriptSource(config) - - /** - * Do not continue when output was skipped - */ - if (ts.skipped) { - return false - } - - /** - * Do not continue when has errors and "stopOnError" is true - */ - if (stopOnError && ts.hasErrors) { - this.logTsBuildFailed() - await this.cleanupBuildDirectory(config.options.outDir!) - return false - } - - /** - * Begin by copying meta files - */ - await this.copyMetaFiles(config.options.outDir!, extraFiles) - - /** - * Copy `.adonisrc.json` file - */ - await this.copyAdonisRcFile(config.options.outDir!) - - /** - * Manifest instance to generate ace manifest file - */ - const manifest = new Manifest(config.options.outDir!, this.logger) - const created = await manifest.generate() - - /** - * Do not continue when unable to generate the manifest file as commands - * won't be available - */ - if (!created) { - await this.cleanupBuildDirectory(config.options.outDir!) - return false - } - - this.logger.success('built successfully') - return true - } - - /** - * Compile project. See [[Compiler.compile]] for development build - */ - public async compileForProduction( - stopOnError: boolean = true, - client: 'npm' | 'yarn' - ): Promise { - const config = this.ts.parseConfig() - if (!config) { - return false - } - - /** - * Bundle frontend assets when encore is installed - */ - const encore = await new AssetsBundler( - this.appRoot, - this.encoreArgs, - this.buildAssets, - this.logger - ).buildForProduction() - - /** - * Skipped, coz of frontend errors - */ - if (encore.hasErrors) { - return false - } - - const pkgFiles = - client === 'npm' ? ['package.json', 'package-lock.json'] : ['package.json', 'yarn.lock'] - - /** - * Always cleanup the out directory - */ - await this.cleanupBuildDirectory(config.options.outDir!) - - /** - * Build typescript source - */ - const { skipped, hasErrors } = this.buildTypescriptSource(config) - - /** - * Do not continue when output was skipped - */ - if (skipped) { - return false - } - - /** - * Do not continue when has errors and "stopOnError" is true and cleanup - * the build directory - */ - if (stopOnError && hasErrors) { - this.logTsBuildFailed() - await this.cleanupBuildDirectory(config.options.outDir!) - return false - } - - /** - * Begin by copying meta files - */ - await this.copyMetaFiles(config.options.outDir!, pkgFiles) - - /** - * Copy `.adonisrc.json` file - */ - await this.copyAdonisRcFile(config.options.outDir!) - - /** - * Generate commands manifest - */ - const manifest = new Manifest(config.options.outDir!, this.logger) - const created = await manifest.generate() - - /** - * Do not continue when unable to generate the manifest file as commands - * won't be available - */ - if (!created) { - await this.cleanupBuildDirectory(config.options.outDir!) - return false - } - - /** - * Print usage instructions - */ - const installCommand = client === 'npm' ? 'npm ci --production' : 'yarn install --production' - const relativeBuildPath = this.getRelativeUnixPath(config.options.outDir!) - - this.logger.success('built successfully') - this.logger.log('') - - instructions() - .heading('Run the following commands to start the server in production') - .add(this.logger.colors.cyan(`cd ${relativeBuildPath}`)) - .add(this.logger.colors.cyan(installCommand)) - .add(this.logger.colors.cyan('node server.js')) - .render() - - return true - } -} diff --git a/src/Contracts/index.ts b/src/Contracts/index.ts deleted file mode 100644 index a7aee0e..0000000 --- a/src/Contracts/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -export type JapaFlags = Partial<{ - '_': string[] - '--tags': string[] - '--ignore-tags': string[] - '--files': string[] - '--timeout': number - '--force-exit': boolean -}> diff --git a/src/DevServer/index.ts b/src/DevServer/index.ts deleted file mode 100644 index 60686d0..0000000 --- a/src/DevServer/index.ts +++ /dev/null @@ -1,388 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import getPort from 'get-port' -import { getWatcherHelpers } from '@adonisjs/require-ts' -import { logger as uiLogger, sticker } from '@poppinss/cliui' - -import { Ts } from '../Ts' -import { RcFile } from '../RcFile' -import { Manifest } from '../Manifest' -import { EnvParser } from '../EnvParser' -import { HttpServer } from '../HttpServer' - -import { ENV_FILES, SERVER_ENTRY_FILE } from '../../config/paths' -import { AssetsBundler, DevServerResponse } from '../AssetsBundler' - -/** - * Exposes the API to watch project for compilition changes. - */ -export class DevServer { - private httpServer: HttpServer - - /** - * HTTP server port - */ - private serverPort?: number - - /** - * HTTP server host - */ - private serverHost?: string - - /** - * Encore dev server host - */ - private encoreDevServerResponse: DevServerResponse - - /** - * A boolean to know if we are watching for filesystem - */ - private watchingFileSystem: boolean = false - - /** - * Watcher state - */ - private watcherState: 'pending' | 'error' | 'ready' = 'pending' - - /** - * Reference to the typescript compiler - */ - private ts = new Ts(this.appRoot, this.logger) - - /** - * Reference to the RCFile - */ - private rcFile = new RcFile(this.appRoot) - - /** - * Manifest instance to generate ace manifest file - */ - private manifest = new Manifest(this.appRoot, this.logger) - - /** - * Require-ts watch helpers - */ - private watchHelpers = getWatcherHelpers(this.appRoot) - - constructor( - private appRoot: string, - private nodeArgs: string[] = [], - private encoreArgs: string[], - private buildAssets: boolean, - private logger: typeof uiLogger = uiLogger - ) {} - - /** - * Kill current process - */ - private kill() { - this.logger.info('shutting down') - process.exit() - } - - /** - * Create the http server - */ - private async createHttpServer() { - if (this.httpServer) { - return - } - - const envParser = new EnvParser() - await envParser.parse(this.appRoot) - - const envOptions = envParser.asEnvObject(['PORT', 'TZ', 'HOST']) - const HOST = process.env.HOST || envOptions.HOST || '0.0.0.0' - let PORT = process.env.PORT || envOptions.PORT || '3333' - - /** - * Obtains a random port by giving preference to the one defined inside - * the `.env` file. This eases the process of running the application - * without manually changing ports inside the `.env` file when - * original port is in use. - */ - if (!isNaN(Number(PORT))) { - PORT = String( - await getPort({ - port: [Number(PORT)], - host: HOST, - }) - ) - } - - this.httpServer = new HttpServer(SERVER_ENTRY_FILE, this.appRoot, this.nodeArgs, this.logger, { - PORT, - HOST, - TZ: envOptions.TZ, - }) - } - - /** - * Renders box to notify about the server state - */ - private renderServerIsReady() { - if (!this.serverHost || !this.serverPort) { - return - } - - if (this.watchingFileSystem && this.watcherState === 'pending') { - return - } - - const stickerInstance = sticker() - - stickerInstance - .add( - `Server address: ${this.logger.colors.cyan( - `http://${this.serverHost === '0.0.0.0' ? '127.0.0.1' : this.serverHost}:${ - this.serverPort - }` - )}` - ) - .add( - `Watching filesystem for changes: ${this.logger.colors.cyan( - this.watchingFileSystem ? 'YES' : 'NO' - )}` - ) - - /** - * Running the encore dev server - */ - if (this.encoreDevServerResponse.state === 'running') { - stickerInstance.add( - `Encore server address: ${this.logger.colors.cyan( - `http://${this.encoreDevServerResponse.host}:${this.encoreDevServerResponse.port}` - )}` - ) - } - - stickerInstance.render() - } - - /** - * Start the dev server. Use [[watch]] to also watch for file - * changes - */ - public async start() { - /** - * Log getting ready - */ - this.logger.info('building project...') - - /** - * Start the HTTP server right away - */ - await this.createHttpServer() - this.httpServer.start() - - /** - * Notify that the http server has died - */ - this.httpServer.on('exit', ({ code }) => { - this.logger.warning(`Underlying HTTP server died with "${code} code"`) - }) - - /** - * Notify that the http server is running - */ - this.httpServer.on('ready', ({ port, host }) => { - this.serverPort = port - this.serverHost = host - this.renderServerIsReady() - }) - - const encore = new AssetsBundler(this.appRoot, this.encoreArgs, this.buildAssets, this.logger) - encore.on('exit', ({ code }) => { - this.logger.warning(`Underlying encore dev server died with "${code} code"`) - }) - - this.encoreDevServerResponse = await encore.startDevServer() - } - - /** - * Build and watch for file changes - */ - public async watch(poll = false) { - this.watchingFileSystem = true - - /** - * Clear require-ts cache - */ - this.watchHelpers.clear() - - /** - * Start HTTP server - */ - await this.start() - - /** - * Parse config to find the files excluded inside - * tsconfig file - */ - const config = this.ts.parseConfig() - if (!config) { - this.logger.warning('Cannot start watcher because of errors in the config file') - this.watcherState = 'error' - this.renderServerIsReady() - return - } - - /** - * Stick file watcher - */ - const watcher = this.ts.tsCompiler.watcher(config, 'raw') - - /** - * Watcher is ready after first compile - */ - watcher.on('watcher:ready', () => { - this.logger.info('watching file system for changes') - this.watcherState = 'ready' - this.renderServerIsReady() - }) - - /** - * Source file removed - */ - watcher.on('source:unlink', async ({ absPath, relativePath }) => { - this.watchHelpers.clear(absPath) - this.logger.action('delete').succeeded(relativePath) - - /** - * Generate manifest when filePath is a commands path - */ - if (this.rcFile.isCommandsPath(relativePath)) { - this.manifest.generate() - } - - this.httpServer.restart() - }) - - /** - * Source file added - */ - watcher.on('source:add', async ({ absPath, relativePath }) => { - this.watchHelpers.clear(absPath) - this.logger.action('add').succeeded(relativePath) - - /** - * Generate manifest when filePath if file is in commands path - */ - if (this.rcFile.isCommandsPath(relativePath)) { - this.manifest.generate() - } - - this.httpServer.restart() - }) - - /** - * Source file changed - */ - watcher.on('source:change', async ({ absPath, relativePath }) => { - this.watchHelpers.clear(absPath) - this.logger.action('update').succeeded(relativePath) - - /** - * Generate manifest when filePath is a commands path - */ - if (this.rcFile.isCommandsPath(relativePath)) { - this.manifest.generate() - } - - this.httpServer.restart() - }) - - /** - * New file added - */ - watcher.on('add', async ({ relativePath }) => { - if (ENV_FILES.includes(relativePath)) { - this.logger.action('create').succeeded(relativePath) - this.httpServer.restart() - return - } - - const metaData = this.rcFile.getMetaData(relativePath) - if (!metaData.metaFile) { - return - } - - this.logger.action('create').succeeded(relativePath) - if (metaData.reload) { - this.httpServer.restart() - } - }) - - /** - * File changed - */ - watcher.on('change', async ({ relativePath }) => { - if (ENV_FILES.includes(relativePath)) { - this.logger.action('update').succeeded(relativePath) - this.httpServer.restart() - return - } - - const metaData = this.rcFile.getMetaData(relativePath) - if (!metaData.metaFile) { - return - } - - this.logger.action('update').succeeded(relativePath) - - if (metaData.reload || metaData.rcFile) { - this.httpServer.restart() - } - }) - - /** - * File removed - */ - watcher.on('unlink', async ({ relativePath }) => { - if (ENV_FILES.includes(relativePath)) { - this.logger.action('delete').succeeded(relativePath) - this.httpServer.restart() - return - } - - const metaData = this.rcFile.getMetaData(relativePath) - if (!metaData.metaFile) { - return - } - - if (metaData.rcFile) { - this.logger.info('cannot continue after deletion of .adonisrc.json file') - watcher.chokidar.close() - this.kill() - return - } - - this.logger.action('delete').succeeded(relativePath) - if (metaData.reload) { - this.httpServer.restart() - } - }) - - /** - * Start the watcher - */ - watcher.watch(['.'], { - usePolling: poll, - }) - - /** - * Kill when watcher recieves an error - */ - watcher.chokidar.on('error', (error) => { - this.logger.fatal(error) - this.kill() - }) - } -} diff --git a/src/EnvParser/index.ts b/src/EnvParser/index.ts deleted file mode 100644 index 19f503a..0000000 --- a/src/EnvParser/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { EnvParser as Parser, envLoader } from '@adonisjs/env' - -/** - * Parses the env file inside the project root. - */ -export class EnvParser { - private envContents: any = {} - - constructor() {} - - /** - * Parse .env file contents - */ - public async parse(rootDir: string) { - const { envContents, testEnvContent } = envLoader(rootDir) - const envVars = new Parser(true).parse(envContents) - const testEnvVars = new Parser(true).parse(testEnvContent) - this.envContents = { ...envVars, ...testEnvVars } - } - - /** - * Returns value for a key inside the `.env` file - */ - public get(key: string): string | undefined { - return this.envContents[key] - } - - /** - * Returns an env object for the keys that has defined values - */ - public asEnvObject(keys: string[]): { [key: string]: string } { - return keys.reduce((result, key) => { - const value = this.get(key) - if (value !== undefined) { - result[key] = value - } - return result - }, {}) - } -} diff --git a/src/HttpServer/index.ts b/src/HttpServer/index.ts deleted file mode 100644 index e7b94ac..0000000 --- a/src/HttpServer/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import execa from 'execa' -import Emittery from 'emittery' -import { logger as uiLogger } from '@poppinss/cliui' - -/** - * Exposes the API to start Node.js HTTP server as a child process. The - * child process is full managed and cleans up when parent process - * dies. - */ -export class HttpServer extends Emittery { - private childProcess?: execa.ExecaChildProcess - private nodeArgs: string[] = [] - - constructor( - private sourceFile: string, - private projectRoot: string, - nodeArgs: string[] = [], - private logger: typeof uiLogger, - private env: { [key: string]: string } = {} - ) { - super() - this.nodeArgs = nodeArgs.reduce((result, arg) => { - result = result.concat(arg.split(' ')) - return result - }, [] as string[]) - } - - /** - * Whether or not the underlying process is connected - */ - public get isConnected() { - return this.childProcess && this.childProcess.connected && !this.childProcess.killed - } - - /** - * Start the HTTP server as a child process. - */ - public start() { - if (this.isConnected) { - throw new Error('Http server is already connected. Call restart instead') - } - - this.logger.info(this.childProcess ? 're-starting http server...' : 'starting http server...') - - this.childProcess = execa.node(this.sourceFile, [], { - buffer: false, - stdio: 'inherit', - cwd: this.projectRoot, - env: { - FORCE_COLOR: 'true', - ...this.env, - }, - nodeOptions: ['-r', '@adonisjs/assembler/build/register'].concat(this.nodeArgs), - }) - - /** - * Notify about server events - */ - this.childProcess.on('message', (message) => { - if (message && message['isAdonisJS'] && message['environment'] === 'web') { - this.emit('ready', message) - } - }) - this.childProcess.on('close', (code, signal) => this.emit('close', { code, signal })) - this.childProcess.on('exit', (code, signal) => this.emit('exit', { code, signal })) - } - - /** - * Stop the underlying process - */ - public stop() { - if (this.childProcess) { - this.childProcess.removeAllListeners() - this.childProcess.kill('SIGKILL') - } - } - - /** - * Restart the server by killing the old one - */ - public restart() { - this.stop() - this.start() - } -} diff --git a/src/Manifest/index.ts b/src/Manifest/index.ts deleted file mode 100644 index bdc1194..0000000 --- a/src/Manifest/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import execa from 'execa' -import { logger as uiLogger } from '@poppinss/cliui' - -const WARN_MESSAGE = [ - 'Unable to generate manifest file.', - 'Check the following error for more info', -].join(' ') - -/** - * Exposes the API to execute generate manifest file - */ -export class Manifest { - /** - * The maximum number of times we should attempt to generate - * the manifest file before giving up. - * - * This number may sound too big, but in real world scanerio, we - * have seen encountered malformed JSON between 10-12 times. - * - * The JSON gets malformed, when a parallel process (node ace serve --watch) - * is trying to update it. - */ - private maxAttempts = 15 - private attempts = 0 - - constructor(private appRoot: string, private logger: typeof uiLogger) {} - - /** - * Returns a boolean telling if the error message is pointing - * towards invalid or empty JSON file read attempt. - */ - private isMalformedJSONError(error: string) { - return error.includes('Unexpected end of JSON input') - } - - /** - * Generates the manifest file. We ignore `generate:manifest` errors for - * now, since it's a secondary task for us and one should run it - * in seperate process to find the actual errors. - */ - public async generate(): Promise { - try { - const response = await execa(process.execPath, ['ace', 'generate:manifest'], { - buffer: true, - cwd: this.appRoot, - env: { - FORCE_COLOR: 'true', - }, - }) - - /** - * Log success - */ - if (response.stdout) { - this.logger.log(response.stdout) - } - - return true - } catch (error) { - if (this.isMalformedJSONError(error.stderr) && this.attempts < this.maxAttempts) { - this.attempts++ - return this.generate() - } - - /** - * Print warning on error - */ - this.logger.warning(WARN_MESSAGE) - if (error.stderr) { - this.logger.logError(error.stderr) - } - - if (error.stdout) { - this.logger.logError(error.stdout) - } - - return false - } - } -} diff --git a/src/RcFile/index.ts b/src/RcFile/index.ts deleted file mode 100644 index 881f96a..0000000 --- a/src/RcFile/index.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import slash from 'slash' -import picomatch from 'picomatch' -import { join, relative } from 'path' -import { readJSONSync } from 'fs-extra' -import { Application } from '@adonisjs/application' -import { resolveFrom } from '@poppinss/utils/build/helpers' - -import { RCFILE_NAME, ACE_FILE_NAME } from '../../config/paths' - -/** - * Exposes the API to pull meta files from the `.adonisrc.json` file and - * also match relative file paths against the defined globs. - */ -export class RcFile { - public rcFilePath = resolveFrom(this.appRoot, `./${RCFILE_NAME}`) - - /** - * Raw rcfile contents - */ - public raw = this.getDiskContents() - - /** - * Reference to application - */ - public application = new Application(this.appRoot, 'console', this.raw) - - /** - * A matcher to know if a file is part of the meta files globs - */ - public isMetaFile: (filePath: string) => boolean = picomatch(this.getMetaFilesGlob()) - - /** - * A matcher to know if file is a test file or not - */ - public isTestsFile: (filePath: string) => boolean = picomatch(this.getTestsFileGlob()) - - /** - * A matcher to know if a file is part of the restart server files globs - */ - public isRestartServerFile: (filePath: string) => boolean = picomatch( - this.getRestartServerFilesGlob() - ) - - /** - * Commands match to know, if file path is part of the commands paths defined - * inside `.adonisrc.json` file - */ - public isCommandsPath: (filePath: string) => boolean = picomatch(this.commandsGlob()) - - constructor(private appRoot: string) {} - - /** - * Returns an array of globs for the meta files that has `reloadServer` - * set to true - */ - private getRestartServerFilesGlob(): string[] { - return this.application.rcFile.metaFiles - .filter(({ reloadServer, pattern }) => { - return reloadServer === true && ![RCFILE_NAME, ACE_FILE_NAME].includes(pattern) - }) - .map(({ pattern }) => pattern) - } - - /** - * Returns the commands glob for registered commands. We convert the - * command paths to glob pattern - */ - private commandsGlob(): string[] { - const commands = this.application.rcFile.commands.reduce((result: string[], commandPath) => { - if (/^(.){1,2}\//.test(commandPath)) { - commandPath = slash(relative(this.appRoot, join(this.appRoot, commandPath))) - result = result.concat([`${commandPath}.*`, `${commandPath}/**/*`]) - } - return result - }, []) - - return commands - } - - /** - * Returns true when file is `.adonisrc.json` itself - */ - private isRcFile(filePath: string) { - return filePath === RCFILE_NAME - } - - /** - * Returns an array of globs for the meta files - * to be copied - */ - public getMetaFilesGlob(): string[] { - return this.application.rcFile.metaFiles - .filter(({ pattern }) => ![RCFILE_NAME, ACE_FILE_NAME].includes(pattern)) - .map(({ pattern }) => pattern) - .concat([ACE_FILE_NAME]) - } - - /** - * Returns an array of globs for the test files - */ - public getTestsFileGlob(): string[] { - return this.application.rcFile.tests.suites.reduce((result, suite) => { - if (suite.files) { - result = result.concat(suite.files) - } - - return result - }, [] as string[]) - } - - /** - * Reloads the rcfile.json - */ - public getDiskContents(): any { - return readJSONSync(this.rcFilePath) - } - - /** - * Returns metadata for a given file path. The metadata can - * be used to execute certain actions during file watch. - */ - public getMetaData(filePath: string) { - /** - * File path === '.adonisrc.json' - */ - if (this.isRcFile(filePath)) { - return { - reload: true, - rcFile: true, - metaFile: true, - testFile: false, - } - } - - /** - * File is part of `reloadServer` metadata file globs - */ - if (this.isRestartServerFile(filePath)) { - return { - reload: true, - rcFile: false, - metaFile: true, - testFile: false, - } - } - - /** - * File is part of metadata file globs, but reload = false - */ - if (this.isMetaFile(filePath)) { - return { - reload: false, - rcFile: false, - metaFile: true, - testFile: false, - } - } - - /** - * File is part of one of the tests suite - */ - if (this.isTestsFile(filePath)) { - return { - reload: false, - rcFile: false, - metaFile: false, - testFile: true, - } - } - - /** - * Out of scope - */ - return { - reload: false, - rcFile: false, - metaFile: false, - testFile: false, - } - } -} diff --git a/src/Test/index.ts b/src/Test/index.ts deleted file mode 100644 index 2c932f7..0000000 --- a/src/Test/index.ts +++ /dev/null @@ -1,414 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { extname } from 'path' -import picomatch from 'picomatch' -import { logger as uiLogger } from '@poppinss/cliui' -import { getWatcherHelpers } from '@adonisjs/require-ts' - -import { Ts } from '../Ts' -import { RcFile } from '../RcFile' -import { Manifest } from '../Manifest' -import { TestProcess } from './process' -import { JapaFlags } from '../Contracts' - -import { ENV_FILES, TESTS_ENTRY_FILE } from '../../config/paths' -import { EnvParser } from '../EnvParser' -import getPort from 'get-port' - -/** - * Exposes the API to watch project for compilition changes and - * run/re-run tests - */ -export class TestsServer { - /** - * A boolean to know if we are watching for filesystem - */ - private watchingFileSystem: boolean = false - - /** - * Boolean to hold the current state of tests. This is avoid - * re-running the tests when one run is in progress - */ - private busy = false - - /** - * Reference to the typescript compiler - */ - private ts = new Ts(this.appRoot, this.logger) - - /** - * Reference to the RCFile - */ - private rcFile = new RcFile(this.appRoot) - - /** - * Manifest instance to generate ace manifest file - */ - private manifest = new Manifest(this.appRoot, this.logger) - - /** - * Require-ts watch helpers - */ - private watchHelpers = getWatcherHelpers(this.appRoot) - - /** - * A method to know if the file is part of the selected suites - * or not - */ - private isTestSuiteFile: (filePath: string) => boolean = picomatch( - this.getFilesForSelectedSuites() - ) - - /** - * Find if the test file part of the applied file filters - */ - private isTestFile = (filePath: string): boolean => { - if (!this.filters['--files']) { - return true - } - - const fileName = filePath.replace(extname(filePath), '') - return !!this.filters['--files'].find((filter) => { - if (filePath.endsWith(filter)) { - return true - } - - return fileName.endsWith(filter) || fileName.endsWith(`${filter}.spec`) - }) - } - - constructor( - private appRoot: string, - private filters: JapaFlags, - private nodeArgs: string[] = [], - private logger: typeof uiLogger = uiLogger - ) {} - - /** - * Clear terminal screen - */ - private clearScreen() { - process.stdout.write('\u001Bc') - } - - /** - * Returns the glob paths for test suites. Returns all if no - * filter is applied. Otherwise only the filtered suites - * are picked. - */ - private getFilesForSelectedSuites() { - return this.rcFile.application.rcFile.tests.suites.reduce((result, suite) => { - if (!suite.files) { - return result - } - - if (!this.filters['--suites'] || this.filters['--suites'].includes(suite.name)) { - result = result.concat(suite.files) - } - - return result - }, [] as string[]) - } - - /** - * Kill current process - */ - private kill() { - process.exit() - } - - /** - * Returns the HOST and the PORT environment variables - * for the HTTP server - */ - private async getEnvironmentVariables() { - const envParser = new EnvParser() - await envParser.parse(this.appRoot) - - const envOptions = envParser.asEnvObject(['PORT', 'TZ', 'HOST']) - const HOST = process.env.HOST || envOptions.HOST || '0.0.0.0' - let PORT = Number(process.env.PORT || envOptions.PORT) - - /** - * Use the port defined inside ".env.test" file or use - * a random port - */ - PORT = await getPort({ - port: !isNaN(PORT) ? [PORT] : [], - host: HOST, - }) - - return { HOST, PORT: String(PORT) } - } - - /** - * Run tests. Use [[watch]] to also watch for file - * changes - */ - public async run(filePath?: string) { - if (this.busy) { - return - } - - this.clearScreen() - const filters = { ...this.filters } - - /** - * Overwrite files filter when a specific file path - * is mentioned - */ - if (filePath) { - filters['--files'] = [filePath.replace(/\\/g, '/')] - } - - this.busy = true - const { hasErrors } = await new TestProcess( - TESTS_ENTRY_FILE, - this.appRoot, - filters, - this.nodeArgs, - this.logger, - await this.getEnvironmentVariables() - ).run() - - this.busy = false - if (!this.watchingFileSystem) { - if (hasErrors) { - process.exitCode = 1 - } - this.kill() - } - } - - /** - * Build and watch for file changes - */ - public async watch(poll = false) { - this.watchingFileSystem = true - - /** - * Clear require-ts cache - */ - this.watchHelpers.clear() - - /** - * Run tests - */ - await this.run() - - /** - * Parse config to find the files excluded inside - * tsconfig file - */ - const config = this.ts.parseConfig() - if (!config) { - this.logger.warning('Cannot start watcher because of errors in the tsconfig file') - return - } - - /** - * Stick file watcher - */ - const watcher = this.ts.tsCompiler.watcher(config, 'raw') - - /** - * Watcher is ready after first compile - */ - watcher.on('watcher:ready', () => { - this.logger.info('watching file system for changes') - }) - - /** - * Source file removed - */ - watcher.on('source:unlink', async ({ absPath, relativePath }) => { - this.watchHelpers.clear(absPath) - - if (this.busy) { - return - } - - this.logger.action('delete').succeeded(relativePath) - - /** - * Generate manifest when filePath is a commands path - */ - if (this.rcFile.isCommandsPath(relativePath)) { - this.manifest.generate() - } - - /** - * Run all tests when any of the source, except the - * test file changes - */ - if (!this.rcFile.isTestsFile(relativePath)) { - await this.run() - } - }) - - /** - * Source file added - */ - watcher.on('source:add', async ({ absPath, relativePath }) => { - this.watchHelpers.clear(absPath) - - if (this.busy) { - return - } - - this.logger.action('add').succeeded(relativePath) - - /** - * Run all tests when any of the source, except the - * test file changes - */ - if (!this.rcFile.isTestsFile(relativePath)) { - await this.run() - return - } - - /** - * Run only the changed file if it part of the test - * suites (respecting filters) - */ - if (this.isTestSuiteFile(relativePath) && this.isTestFile(relativePath)) { - await this.run(relativePath) - } - }) - - /** - * Source file changed - */ - watcher.on('source:change', async ({ absPath, relativePath }) => { - this.watchHelpers.clear(absPath) - - if (this.busy) { - return - } - - this.logger.action('update').succeeded(relativePath) - - /** - * Generate manifest when filePath is a commands path - */ - if (this.rcFile.isCommandsPath(relativePath)) { - this.manifest.generate() - } - - /** - * Run all tests when any of the source, except the - * test file changes - */ - if (!this.rcFile.isTestsFile(relativePath)) { - await this.run() - return - } - - /** - * Run only the changed file if it part of the test - * suites (respecting filters) - */ - if (this.isTestSuiteFile(relativePath) && this.isTestFile(relativePath)) { - await this.run(relativePath) - } - }) - - /** - * New file added - */ - watcher.on('add', async ({ relativePath }) => { - if (this.busy) { - return - } - - if (ENV_FILES.includes(relativePath)) { - this.logger.action('create').succeeded(relativePath) - await this.run() - return - } - - const metaData = this.rcFile.getMetaData(relativePath) - if (!metaData.metaFile) { - return - } - - this.logger.action('create').succeeded(relativePath) - await this.run() - }) - - /** - * File changed - */ - watcher.on('change', async ({ relativePath }) => { - if (this.busy) { - return - } - - if (ENV_FILES.includes(relativePath)) { - this.logger.action('update').succeeded(relativePath) - await this.run() - return - } - - const metaData = this.rcFile.getMetaData(relativePath) - if (!metaData.metaFile) { - return - } - - this.logger.action('update').succeeded(relativePath) - await this.run() - }) - - /** - * File removed - */ - watcher.on('unlink', async ({ relativePath }) => { - if (this.busy) { - return - } - - if (ENV_FILES.includes(relativePath)) { - this.logger.action('delete').succeeded(relativePath) - await this.run() - return - } - - const metaData = this.rcFile.getMetaData(relativePath) - if (!metaData.metaFile) { - return - } - - if (metaData.rcFile) { - this.logger.info('cannot continue after deletion of .adonisrc.json file') - watcher.chokidar.close() - this.kill() - return - } - - this.logger.action('delete').succeeded(relativePath) - await this.run() - }) - - /** - * Start the watcher - */ - watcher.watch(['.'], { - usePolling: poll, - }) - - /** - * Kill when watcher recieves an error - */ - watcher.chokidar.on('error', (error) => { - this.logger.fatal(error) - this.kill() - }) - } -} diff --git a/src/Test/process.ts b/src/Test/process.ts deleted file mode 100644 index 77f39f8..0000000 --- a/src/Test/process.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import execa from 'execa' -import { logger as uiLogger } from '@poppinss/cliui' - -import { JapaFlags } from '../Contracts' - -/** - * Exposes the API to run tests as a child process. - */ -export class TestProcess { - private nodeArgs: string[] - - constructor( - private sourceFile: string, - private projectRoot: string, - private filters: JapaFlags, - nodeArgs: string[] = [], - private logger: typeof uiLogger, - private env: { [key: string]: string } = {} - ) { - this.nodeArgs = nodeArgs.reduce((result, arg) => { - result = result.concat(arg.split(' ')) - return result - }, []) - } - - /** - * Start the HTTP server as a child process. - */ - public async run() { - this.logger.info('running tests...') - const filters = Object.keys(this.filters).reduce((result, filter) => { - const value = this.filters[filter] - - if (filter === '_') { - result.push(...value) - return result - } - - result.push(filter) - if (Array.isArray(value)) { - result.push(value.join(',')) - } else { - result.push(value) - } - - return result - }, []) - - try { - await execa.node(this.sourceFile, filters, { - stdio: 'inherit', - cwd: this.projectRoot, - env: { - FORCE_COLOR: 'true', - ...this.env, - }, - nodeOptions: ['-r', '@adonisjs/assembler/build/register'].concat(this.nodeArgs), - }) - return { hasErrors: false } - } catch { - return { hasErrors: true } - } - } -} diff --git a/src/Ts/index.ts b/src/Ts/index.ts deleted file mode 100644 index 46e32ff..0000000 --- a/src/Ts/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import tsStatic from 'typescript' -import { logger as uiLogger } from '@poppinss/cliui' -import { TypescriptCompiler } from '@poppinss/chokidar-ts' -import { resolveFrom } from '@poppinss/utils/build/helpers' - -import { TSCONFIG_FILE_NAME, DEFAULT_BUILD_DIR } from '../../config/paths' - -/** - * Exposes the API to work with the Typescript compiler API - */ -export class Ts { - /** - * Reference to the typescript compiler - */ - public tsCompiler = new TypescriptCompiler( - this.appRoot, - this.tsconfig, - require(resolveFrom(this.appRoot, 'typescript/lib/typescript')) - ) - - constructor( - private appRoot: string, - private logger: typeof uiLogger, - private tsconfig = TSCONFIG_FILE_NAME - ) {} - - /** - * Render ts diagnostics - */ - public renderDiagnostics(diagnostics: tsStatic.Diagnostic[], host: tsStatic.CompilerHost) { - console.error(this.tsCompiler.ts.formatDiagnosticsWithColorAndContext(diagnostics, host)) - } - - /** - * Parses the tsconfig file - */ - public parseConfig(): undefined | tsStatic.ParsedCommandLine { - const { error, config } = this.tsCompiler.configParser().parse() - - if (error) { - this.logger.error(`unable to parse ${this.tsconfig}`) - this.renderDiagnostics([error], this.tsCompiler.ts.createCompilerHost({})) - return - } - - if (config && config.errors.length) { - this.logger.error(`unable to parse ${this.tsconfig}`) - this.renderDiagnostics(config.errors, this.tsCompiler.ts.createCompilerHost(config.options)) - return - } - - config!.options.rootDir = config!.options.rootDir || this.appRoot - config!.options.outDir = config!.options.outDir || join(this.appRoot, DEFAULT_BUILD_DIR) - return config - } -} diff --git a/src/assets_dev_server.ts b/src/assets_dev_server.ts new file mode 100644 index 0000000..5721c8d --- /dev/null +++ b/src/assets_dev_server.ts @@ -0,0 +1,182 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { ExecaChildProcess } from 'execa' +import { type Logger, cliui } from '@poppinss/cliui' + +import { run } from './helpers.js' +import type { AssetsBundlerOptions } from './types.js' + +/** + * Instance of CLIUI + */ +const ui = cliui() + +/** + * Exposes the API to start the development server for processing assets during + * development. + * + * - Here we are running the assets dev server in a child process. + * - Piping the output from the child process and reformatting it before writing it to + * process streams. + * + * AssetsDevServer is agnostic and can run any assets dev server. Be it Vite or Encore or + * even Webpack directly. + */ +export class AssetsDevServer { + #cwd: URL + #logger = ui.logger + #options?: AssetsBundlerOptions + #devServer?: ExecaChildProcess + + /** + * Getting reference to colors library from logger + */ + get #colors() { + return this.#logger.getColors() + } + + constructor(cwd: URL, options?: AssetsBundlerOptions) { + this.#cwd = cwd + this.#options = options + } + + /** + * Logs messages from vite dev server stdout and stderr + */ + #logViteDevServerMessage(data: Buffer) { + const dataString = data.toString() + const lines = dataString.split('\n') + + /** + * Put a wrapper around vite network address log + */ + if (dataString.includes('Local') && dataString.includes('Network')) { + const sticker = ui.sticker().useColors(this.#colors).useRenderer(this.#logger.getRenderer()) + + lines.forEach((line: string) => { + if (line.trim()) { + sticker.add(line) + } + }) + + sticker.render() + 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 + */ + lines.forEach((line: string) => { + if (line.trim()) { + console.log(line) + } + }) + } + + /** + * Logs messages from assets dev server stdout and stderr + */ + #logAssetsDevServerMessage(data: Buffer) { + const dataString = data.toString() + const lines = dataString.split('\n') + lines.forEach((line: string) => { + if (line.trim()) { + console.log(line) + } + }) + } + + /** + * Set a custom CLI UI logger + */ + setLogger(logger: Logger) { + this.#logger = logger + return this + } + + /** + * Starts the assets bundler server. The assets bundler server process is + * considered as the secondary process and therefore we do not perform + * any cleanup if it dies. + */ + start() { + if (!this.#options?.enabled) { + return + } + + this.#logger.info(`starting "${this.#options.driver}" dev server...`) + + /** + * Create child process + */ + this.#devServer = run(this.#cwd, { + script: this.#options.cmd, + + /** + * We do not inherit the stdio for vite and encore, because in + * inherit mode they own the stdin and interrupts the + * `Ctrl + C` command. + */ + stdio: 'pipe', + scriptArgs: this.#options.args, + }) + + /** + * Log child process messages + */ + this.#devServer.stdout?.on('data', (data) => { + if (this.#options!.driver === 'vite') { + this.#logViteDevServerMessage(data) + } else { + this.#logAssetsDevServerMessage(data) + } + }) + + this.#devServer.stderr?.on('data', (data) => { + if (this.#options!.driver === 'vite') { + this.#logViteDevServerMessage(data) + } else { + this.#logAssetsDevServerMessage(data) + } + }) + + this.#devServer + .then((result) => { + this.#logger.warning( + `"${this.#options!.driver}" dev server closed with status code "${result.exitCode}"` + ) + }) + .catch((error) => { + this.#logger.warning(`unable to connect to "${this.#options!.driver}" dev server`) + this.#logger.fatal(error) + }) + } + + /** + * Stop the dev server + */ + stop() { + if (this.#devServer) { + this.#devServer.removeAllListeners() + this.#devServer.kill('SIGKILL') + this.#devServer = undefined + } + } +} diff --git a/src/bundler.ts b/src/bundler.ts new file mode 100644 index 0000000..a5d5367 --- /dev/null +++ b/src/bundler.ts @@ -0,0 +1,259 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import slash from 'slash' +import fs from 'node:fs/promises' +import { relative } from 'node:path' +import type tsStatic from 'typescript' +import { fileURLToPath } from 'node:url' +import { cliui, type Logger } from '@poppinss/cliui' +import { detectPackageManager } from '@antfu/install-pkg' + +import type { BundlerOptions } from './types.js' +import { run, parseConfig, copyFiles } from './helpers.js' + +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 + */ +const ui = cliui() + +/** + * The bundler class exposes the API to build an AdonisJS project. + */ +export class Bundler { + #cwd: URL + #cwdPath: string + #ts: typeof tsStatic + #logger = ui.logger + #options: BundlerOptions + + /** + * Getting reference to colors library from logger + */ + get #colors() { + return this.#logger.getColors() + } + + constructor(cwd: URL, ts: typeof tsStatic, options: BundlerOptions) { + this.#cwd = cwd + this.#cwdPath = fileURLToPath(this.#cwd) + this.#ts = ts + this.#options = options + } + + /** + * Returns the relative unix path for an absolute + * file path + */ + #getRelativeName(filePath: string) { + return slash(relative(this.#cwdPath, filePath)) + } + + /** + * Cleans up the build directory + */ + async #cleanupBuildDirectory(outDir: string) { + await fs.rm(outDir, { recursive: true, force: true, maxRetries: 5 }) + } + + /** + * Runs assets bundler command to build assets + */ + async #buildAssets(): Promise { + const assetsBundler = this.#options.assets + if (!assetsBundler?.enabled) { + return true + } + + try { + this.#logger.info('compiling frontend assets', { suffix: assetsBundler.cmd }) + await run(this.#cwd, { + stdio: 'inherit', + script: assetsBundler.cmd, + scriptArgs: assetsBundler.args, + }) + return true + } catch { + return false + } + } + + /** + * Runs tsc command to build the source. + */ + async #runTsc(outDir: string): Promise { + try { + await run(this.#cwd, { + stdio: 'inherit', + script: 'tsc', + scriptArgs: ['--outDir', outDir], + }) + return true + } catch { + return false + } + } + + /** + * Copy meta files to the output directory + */ + async #copyMetaFiles(outDir: string, additionalFilesToCopy: string[]) { + const metaFiles = (this.#options.metaFiles || []) + .map((file) => file.pattern) + .concat(additionalFilesToCopy) + + await copyFiles(metaFiles, this.#cwdPath, outDir) + } + + /** + * Detect the package manager used by the project + * and return the lockfile name and install command + * related to it. + */ + async #getPackageManager(client?: SupportedPackageManager) { + let pkgManager: string | null | undefined = client + + if (!pkgManager) { + pkgManager = await detectPackageManager(this.#cwdPath) + } + if (!pkgManager) { + pkgManager = 'npm' + } + + if (!Object.keys(SUPPORT_PACKAGE_MANAGERS).includes(pkgManager)) { + return null + } + + return SUPPORT_PACKAGE_MANAGERS[pkgManager as SupportedPackageManager] + } + + /** + * Set a custom CLI UI logger + */ + setLogger(logger: Logger) { + this.#logger = logger + return this + } + + /** + * Bundles the application to be run in production + */ + async bundle(stopOnError: boolean = true, client?: SupportedPackageManager): Promise { + /** + * Step 1: Parse config file to get the build output directory + */ + const config = parseConfig(this.#cwd, this.#ts) + if (!config) { + return false + } + + /** + * Step 2: Cleanup existing build directory (if any) + */ + const outDir = config.options.outDir || fileURLToPath(new URL('build/', this.#cwd)) + this.#logger.info('cleaning up output directory', { suffix: this.#getRelativeName(outDir) }) + await this.#cleanupBuildDirectory(outDir) + + /** + * Step 3: Build frontend assets + */ + if (!(await this.#buildAssets())) { + return false + } + + /** + * Step 4: Build typescript source code + */ + this.#logger.info('compiling typescript source', { suffix: 'tsc' }) + const buildCompleted = await this.#runTsc(outDir) + await copyFiles(['ace.js'], this.#cwdPath, outDir) + + /** + * Remove incomplete build directory when tsc build + * failed and stopOnError is set to true. + */ + if (!buildCompleted && stopOnError) { + await this.#cleanupBuildDirectory(outDir) + const instructions = ui + .sticker() + .fullScreen() + .drawBorder((borderChar, colors) => colors.red(borderChar)) + + instructions.add( + this.#colors.red('Cannot complete the build process as there are TypeScript errors.') + ) + instructions.add( + this.#colors.red( + 'Use "--ignore-ts-errors" flag to ignore TypeScript errors and continue the build.' + ) + ) + + this.#logger.logError(instructions.prepare()) + return false + } + + /** + * Step 5: Copy meta files to the build directory + */ + const pkgManager = await this.#getPackageManager(client) + const pkgFiles = pkgManager ? ['package.json', pkgManager.lockFile] : ['package.json'] + this.#logger.info('copying meta files to the output directory') + await this.#copyMetaFiles(outDir, pkgFiles) + + this.#logger.success('build completed') + this.#logger.log('') + + /** + * Next steps + */ + ui.instructions() + .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 ? pkgManager.installCommand : 'Install production dependencies' + ) + ) + .add(this.#colors.cyan('node bin/server.js')) + .render() + + return true + } +} diff --git a/src/code_transformer/main.ts b/src/code_transformer/main.ts new file mode 100644 index 0000000..44f0295 --- /dev/null +++ b/src/code_transformer/main.ts @@ -0,0 +1,382 @@ +/* + * @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 { fileURLToPath } from 'node:url' +import { installPackage, detectPackageManager } from '@antfu/install-pkg' +import { + Node, + Project, + QuoteKind, + SourceFile, + SyntaxKind, + CodeBlockWriter, + FormatCodeSettings, +} from 'ts-morph' + +import { RcFileTransformer } from './rc_file_transformer.js' +import type { MiddlewareNode, EnvValidationNode, BouncerPolicyNode } from '../types.js' + +/** + * This class is responsible for updating + */ +export class CodeTransformer { + /** + * Exporting utilities to install package and detect + * the package manager + */ + installPackage = installPackage + detectPackageManager = detectPackageManager + + /** + * Directory of the adonisjs project + */ + #cwd: URL + + /** + * The TsMorph project + */ + #project: Project + + /** + * Settings to use when persisting files + */ + #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) { + this.#cwd = cwd + this.#project = new Project({ + tsConfigFilePath: join(fileURLToPath(this.#cwd), 'tsconfig.json'), + manipulationSettings: { quoteKind: QuoteKind.Single }, + }) + } + + /** + * Add a new middleware to the middleware array of the + * given file + */ + #addToMiddlewareArray(file: SourceFile, target: string, middlewareEntry: MiddlewareNode) { + const callExpressions = file + .getDescendantsOfKind(SyntaxKind.CallExpression) + .filter((statement) => statement.getExpression().getText() === target) + + if (!callExpressions.length) { + throw new Error(`Cannot find ${target} statement in the file.`) + } + + const arrayLiteralExpression = callExpressions[0].getArguments()[0] + if (!arrayLiteralExpression || !Node.isArrayLiteralExpression(arrayLiteralExpression)) { + throw new Error(`Cannot find middleware array in ${target} statement.`) + } + + const middleware = `() => import('${middlewareEntry.path}')` + + /** + * Delete the existing middleware if it exists + */ + const existingMiddlewareIndex = arrayLiteralExpression + .getElements() + .findIndex((element) => element.getText() === middleware) + + if (existingMiddlewareIndex === -1) { + /** + * Add the middleware to the top or bottom of the array + */ + if (middlewareEntry.position === 'before') { + arrayLiteralExpression.insertElement(0, middleware) + } else { + arrayLiteralExpression.addElement(middleware) + } + } + } + + /** + * Add a new middleware to the named middleware of the given file + */ + #addToNamedMiddleware(file: SourceFile, middlewareEntry: MiddlewareNode) { + if (!middlewareEntry.name) { + throw new Error('Named middleware requires a name.') + } + + const callArguments = file + .getVariableDeclarationOrThrow('middleware') + .getInitializerIfKindOrThrow(SyntaxKind.CallExpression) + .getArguments() + + if (callArguments.length === 0) { + throw new Error('Named middleware call has no arguments.') + } + + const namedMiddlewareObject = callArguments[0] + if (!Node.isObjectLiteralExpression(namedMiddlewareObject)) { + throw new Error('The argument of the named middleware call is not an object literal.') + } + + /** + * Check if property is already defined. If so, remove it + */ + const existingProperty = namedMiddlewareObject.getProperty(middlewareEntry.name) + if (!existingProperty) { + /** + * Add the named middleware + */ + const middleware = `${middlewareEntry.name}: () => import('${middlewareEntry.path}')` + namedMiddlewareObject!.insertProperty(0, middleware) + } + } + + /** + * Add a policy to the list of pre-registered policy + */ + #addToPoliciesList(file: SourceFile, policyEntry: BouncerPolicyNode) { + const policiesObject = file + .getVariableDeclarationOrThrow('policies') + .getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression) + + /** + * Only define policy when one with the existing name does not + * exist. + */ + const existingProperty = policiesObject.getProperty(policyEntry.name) + if (!existingProperty) { + const policy = `${policyEntry.name}: () => import('${policyEntry.path}')` + policiesObject!.insertProperty(0, policy) + } + } + + /** + * Write a leading comment + */ + #addLeadingComment(writer: CodeBlockWriter, comment?: string) { + if (!comment) { + return writer.blankLine() + } + + return writer + .blankLine() + .writeLine('/*') + .writeLine(`|----------------------------------------------------------`) + .writeLine(`| ${comment}`) + .writeLine(`|----------------------------------------------------------`) + .writeLine(`*/`) + } + + /** + * Add new env variable validation in the + * `env.ts` file + */ + async defineEnvValidations(definition: EnvValidationNode) { + /** + * Get the `start/env.ts` source file + */ + const kernelUrl = fileURLToPath(new URL('./start/env.ts', this.#cwd)) + const file = this.#project.getSourceFileOrThrow(kernelUrl) + + /** + * Get the `Env.create` call expression + */ + const callExpressions = file + .getDescendantsOfKind(SyntaxKind.CallExpression) + .filter((statement) => statement.getExpression().getText() === 'Env.create') + + if (!callExpressions.length) { + throw new Error(`Cannot find Env.create statement in the file.`) + } + + const objectLiteralExpression = callExpressions[0].getArguments()[1] + if (!Node.isObjectLiteralExpression(objectLiteralExpression)) { + throw new Error(`The second argument of Env.create is not an object literal.`) + } + + let shouldAddComment = true + + /** + * Add each variable validation + */ + for (const [variable, validation] of Object.entries(definition.variables)) { + /** + * Check if the variable is already defined. If so, remove it + */ + const existingProperty = objectLiteralExpression.getProperty(variable) + + /** + * Do not add leading comment if one or more properties + * already exists + */ + if (existingProperty) { + shouldAddComment = false + } + + /** + * Add property only when the property does not exist + */ + if (!existingProperty) { + objectLiteralExpression.addPropertyAssignment({ + name: variable, + initializer: validation, + leadingTrivia: (writer) => { + if (!shouldAddComment) { + return + } + + shouldAddComment = false + return this.#addLeadingComment(writer, definition.leadingComment) + }, + }) + } + } + + file.formatText(this.#editorSettings) + await file.save() + } + + /** + * Define new middlewares inside the `start/kernel.ts` + * file + * + * This function is highly based on some assumptions + * and will not work if you significantly tweaked + * your `start/kernel.ts` file. + */ + async addMiddlewareToStack(stack: 'server' | 'router' | 'named', middleware: MiddlewareNode[]) { + /** + * Get the `start/kernel.ts` source file + */ + const kernelUrl = fileURLToPath(new URL('./start/kernel.ts', this.#cwd)) + const file = this.#project.getSourceFileOrThrow(kernelUrl) + + /** + * Process each middleware entry + */ + for (const middlewareEntry of middleware) { + if (stack === 'named') { + this.#addToNamedMiddleware(file, middlewareEntry) + } else { + this.#addToMiddlewareArray(file!, `${stack}.use`, middlewareEntry) + } + } + + file.formatText(this.#editorSettings) + await file.save() + } + + /** + * Update the `adonisrc.ts` file + */ + async updateRcFile(callback: (transformer: RcFileTransformer) => void) { + const rcFileTransformer = new RcFileTransformer(this.#cwd, this.#project) + callback(rcFileTransformer) + await rcFileTransformer.save() + } + + /** + * Add a new Japa plugin in the `tests/bootstrap.ts` file + */ + async addJapaPlugin( + pluginCall: string, + importDeclarations: { isNamed: boolean; module: string; identifier: string }[] + ) { + /** + * Get the `tests/bootstrap.ts` source file + */ + const testBootstrapUrl = fileURLToPath(new URL('./tests/bootstrap.ts', this.#cwd)) + const file = this.#project.getSourceFileOrThrow(testBootstrapUrl) + + /** + * Add the import declaration + */ + const existingImports = file.getImportDeclarations() + + importDeclarations.forEach((importDeclaration) => { + const existingImport = existingImports.find( + (mod) => mod.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, + }) + }) + + /** + * Insert the plugin call in the `plugins` array + */ + const pluginsArray = file + .getVariableDeclaration('plugins') + ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) + + /** + * 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() + } + + /** + * Adds a policy to the list of `policies` object configured + * inside the `app/policies/main.ts` file. + */ + async addPolicies(policies: BouncerPolicyNode[]) { + /** + * Get the `app/policies/main.ts` source file + */ + const kernelUrl = fileURLToPath(new URL('./app/policies/main.ts', this.#cwd)) + const file = this.#project.getSourceFileOrThrow(kernelUrl) + + /** + * Process each middleware entry + */ + for (const policy of policies) { + this.#addToPoliciesList(file, policy) + } + + 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 new file mode 100644 index 0000000..a2a70fe --- /dev/null +++ b/src/code_transformer/rc_file_transformer.ts @@ -0,0 +1,357 @@ +/* + * @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 { + Node, + Project, + SourceFile, + SyntaxKind, + CallExpression, + PropertyAssignment, + FormatCodeSettings, + ArrayLiteralExpression, +} from 'ts-morph' + +/** + * RcFileTransformer is used to transform the `adonisrc.ts` file + * for adding new commands, providers, meta files etc + */ +export class RcFileTransformer { + #cwd: URL + #project: Project + + /** + * Settings to use when persisting files + */ + #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) { + this.#cwd = cwd + this.#project = project + } + + /** + * Get the `adonisrc.ts` source file + */ + #getRcFileOrThrow() { + const kernelUrl = fileURLToPath(new URL('./adonisrc.ts', this.#cwd)) + return this.#project.getSourceFileOrThrow(kernelUrl) + } + + /** + * Check if environments array has a subset of available environments + */ + #isInSpecificEnvironment(environments?: AppEnvironments[]): boolean { + if (!environments) { + return false + } + + return !!(['web', 'console', 'test', 'repl'] as const).find( + (env) => !environments.includes(env) + ) + } + + /** + * Locate the `defineConfig` call inside the `adonisrc.ts` file + */ + #locateDefineConfigCallOrThrow(file: SourceFile) { + const call = file + .getDescendantsOfKind(SyntaxKind.CallExpression) + .find((statement) => statement.getExpression().getText() === 'defineConfig') + + if (!call) { + throw new Error('Could not locate the defineConfig call.') + } + + return call + } + + /** + * Return the ObjectLiteralExpression of the defineConfig call + */ + #getDefineConfigObjectOrThrow(defineConfigCall: CallExpression) { + const configObject = defineConfigCall + .getArguments()[0] + .asKindOrThrow(SyntaxKind.ObjectLiteralExpression) + + return configObject + } + + /** + * Check if the defineConfig() call has the property assignment + * inside it or not. If not, it will create one and return it. + */ + #getPropertyAssignmentInDefineConfigCall(propertyName: string, initializer: string) { + const file = this.#getRcFileOrThrow() + const defineConfigCall = this.#locateDefineConfigCallOrThrow(file) + const configObject = this.#getDefineConfigObjectOrThrow(defineConfigCall) + + let property = configObject.getProperty(propertyName) + + if (!property) { + configObject.addPropertyAssignment({ name: propertyName, initializer }) + property = configObject.getProperty(propertyName) + } + + return property as PropertyAssignment + } + + /** + * Extract list of imported modules from an ArrayLiteralExpression + * + * It assumes that the array can have two types of elements: + * + * - Simple lazy imported modules: [() => import('path/to/file')] + * - Or an object entry: [{ file: () => import('path/to/file'), environment: ['web', 'console'] }] + * where the `file` property is a lazy imported module. + */ + #extractModulesFromArray(array: ArrayLiteralExpression) { + const modules = array.getElements().map((element) => { + /** + * Simple lazy imported module + */ + if (Node.isArrowFunction(element)) { + const importExp = element.getFirstDescendantByKindOrThrow(SyntaxKind.CallExpression) + const literal = importExp.getFirstDescendantByKindOrThrow(SyntaxKind.StringLiteral) + return literal.getLiteralValue() + } + + /** + * Object entry + */ + if (Node.isObjectLiteralExpression(element)) { + const fileProp = element.getPropertyOrThrow('file') as PropertyAssignment + const arrowFn = fileProp.getFirstDescendantByKindOrThrow(SyntaxKind.ArrowFunction) + const importExp = arrowFn.getFirstDescendantByKindOrThrow(SyntaxKind.CallExpression) + const literal = importExp.getFirstDescendantByKindOrThrow(SyntaxKind.StringLiteral) + return literal.getLiteralValue() + } + }) + + return modules.filter(Boolean) as string[] + } + + /** + * Extract a specific property from an ArrayLiteralExpression + * that contains object entries. + * + * This function is mainly used for extractring the `pattern` property + * when adding a new meta files entry, or the `name` property when + * adding a new test suite. + */ + #extractPropertyFromArray(array: ArrayLiteralExpression, propertyName: string) { + const property = array.getElements().map((el) => { + if (!Node.isObjectLiteralExpression(el)) return + + const nameProp = el.getPropertyOrThrow(propertyName) + if (!Node.isPropertyAssignment(nameProp)) return + + const name = nameProp.getInitializerIfKindOrThrow(SyntaxKind.StringLiteral) + return name.getLiteralValue() + }) + + return property.filter(Boolean) as string[] + } + + /** + * Build a new module entry for the preloads and providers array + * based upon the environments specified + */ + #buildNewModuleEntry(modulePath: string, environments?: AppEnvironments[]) { + if (!this.#isInSpecificEnvironment(environments)) { + return `() => import('${modulePath}')` + } + + return `{ + file: () => import('${modulePath}'), + environment: [${environments?.map((env) => `'${env}'`).join(', ')}], + }` + } + + /** + * Add a new command to the rcFile + */ + addCommand(commandPath: string) { + const commandsProperty = this.#getPropertyAssignmentInDefineConfigCall('commands', '[]') + const commandsArray = commandsProperty.getInitializerIfKindOrThrow( + SyntaxKind.ArrayLiteralExpression + ) + + const commandString = `() => import('${commandPath}')` + + /** + * If the command already exists, do nothing + */ + if (commandsArray.getElements().some((el) => el.getText() === commandString)) { + return this + } + + /** + * Add the command to the array + */ + commandsArray.addElement(commandString) + return this + } + + /** + * Add a new preloaded file to the rcFile + */ + addPreloadFile(modulePath: string, environments?: AppEnvironments[]) { + const preloadsProperty = this.#getPropertyAssignmentInDefineConfigCall('preloads', '[]') + const preloadsArray = preloadsProperty.getInitializerIfKindOrThrow( + SyntaxKind.ArrayLiteralExpression + ) + + /** + * Check for duplicates + */ + const existingPreloadedFiles = this.#extractModulesFromArray(preloadsArray) + const isDuplicate = existingPreloadedFiles.includes(modulePath) + if (isDuplicate) { + return this + } + + /** + * Add the preloaded file to the array + */ + preloadsArray.addElement(this.#buildNewModuleEntry(modulePath, environments)) + return this + } + + /** + * Add a new provider to the rcFile + */ + addProvider(providerPath: string, environments?: AppEnvironments[]) { + const property = this.#getPropertyAssignmentInDefineConfigCall('providers', '[]') + const providersArray = property.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression) + + /** + * Check for duplicates + */ + const existingProviderPaths = this.#extractModulesFromArray(providersArray) + const isDuplicate = existingProviderPaths.includes(providerPath) + if (isDuplicate) { + return this + } + + /** + * Add the provider to the array + */ + providersArray.addElement(this.#buildNewModuleEntry(providerPath, environments)) + + return this + } + + /** + * Add a new meta file to the rcFile + */ + addMetaFile(globPattern: string, reloadServer = false) { + const property = this.#getPropertyAssignmentInDefineConfigCall('metaFiles', '[]') + const metaFilesArray = property.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression) + + /** + * Check for duplicates + */ + const alreadyDefinedPatterns = this.#extractPropertyFromArray(metaFilesArray, 'pattern') + if (alreadyDefinedPatterns.includes(globPattern)) { + return this + } + + /** + * Add the meta file to the array + */ + metaFilesArray.addElement( + `{ + pattern: '${globPattern}', + reloadServer: ${reloadServer}, + }` + ) + + return this + } + + /** + * Set directory name and path + */ + setDirectory(key: string, value: string) { + const property = this.#getPropertyAssignmentInDefineConfigCall('directories', '{}') + const directories = property.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression) + directories.addPropertyAssignment({ name: key, initializer: `'${value}'` }) + + return this + } + + /** + * Set command alias + */ + setCommandAlias(alias: string, command: string) { + const aliasProperty = this.#getPropertyAssignmentInDefineConfigCall('commandsAliases', '{}') + const aliases = aliasProperty.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression) + aliases.addPropertyAssignment({ name: alias, initializer: `'${command}'` }) + + return this + } + + /** + * Add a new test suite to the rcFile + */ + addSuite(suiteName: string, files: string | string[], timeout?: number) { + const testProperty = this.#getPropertyAssignmentInDefineConfigCall( + 'tests', + `{ suites: [], forceExit: true, timeout: 2000 }` + ) + + const property = testProperty + .getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression) + .getPropertyOrThrow('suites') as PropertyAssignment + + const suitesArray = property.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression) + + /** + * Check for duplicates + */ + const existingSuitesNames = this.#extractPropertyFromArray(suitesArray, 'name') + if (existingSuitesNames.includes(suiteName)) { + return this + } + + /** + * Add the suite to the array + */ + const filesArray = Array.isArray(files) ? files : [files] + suitesArray.addElement( + `{ + name: '${suiteName}', + files: [${filesArray.map((file) => `'${file}'`).join(', ')}], + timeout: ${timeout ?? 2000}, + }` + ) + + return this + } + + /** + * Save the adonisrc.ts file + */ + save() { + const file = this.#getRcFileOrThrow() + file.formatText(this.#editorSettings) + return file.save() + } +} diff --git a/register.ts b/src/debug.ts similarity index 54% rename from register.ts rename to src/debug.ts index b709b29..1884192 100644 --- a/register.ts +++ b/src/debug.ts @@ -1,11 +1,12 @@ /* * @adonisjs/assembler * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import register from './src/requireHook' -register(process.env.ADONIS_ACE_CWD || process.cwd()) +import { debuglog } from 'node:util' + +export default debuglog('adonisjs:assembler') diff --git a/src/dev_server.ts b/src/dev_server.ts new file mode 100644 index 0000000..51ee93b --- /dev/null +++ b/src/dev_server.ts @@ -0,0 +1,370 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import picomatch from 'picomatch' +import type tsStatic from 'typescript' +import prettyHrtime from 'pretty-hrtime' +import { type ExecaChildProcess } from 'execa' +import { cliui, type Logger } from '@poppinss/cliui' +import type { Watcher } from '@poppinss/chokidar-ts' + +import type { DevServerOptions } from './types.js' +import { AssetsDevServer } from './assets_dev_server.js' +import { getPort, isDotEnvFile, runNode, watch } from './helpers.js' + +/** + * Instance of CLIUI + */ +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. + * + * The Dev server performs the following actions + * + * - 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.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 + + /** + * Getting reference to colors library from logger + */ + get #colors() { + return this.#logger.getColors() + } + + constructor(cwd: URL, options: DevServerOptions) { + this.#cwd = cwd + this.#options = options + + this.#isMetaFileWithReloadsEnabled = picomatch( + (this.#options.metaFiles || []) + .filter(({ reloadServer }) => reloadServer === true) + .map(({ pattern }) => pattern) + ) + + this.#isMetaFileWithReloadsDisabled = picomatch( + (this.#options.metaFiles || []) + .filter(({ reloadServer }) => reloadServer !== true) + .map(({ pattern }) => pattern) + ) + } + + /** + * Inspect if child process message is from AdonisJS HTTP server + */ + #isAdonisJSReadyMessage(message: unknown): message is { + isAdonisJS: true + environment: 'web' + port: number + host: string + duration?: [number, number] + } { + return ( + message !== null && + typeof message === 'object' && + 'isAdonisJS' in message && + 'environment' in message && + message.environment === 'web' + ) + } + + /** + * Conditionally clear the terminal screen + */ + #clearScreen() { + if (this.#options.clearScreen) { + process.stdout.write('\u001Bc') + } + } + + /** + * Starts the HTTP server + */ + #startHTTPServer(port: string, mode: 'blocking' | 'nonblocking') { + this.#httpServer = runNode(this.#cwd, { + script: this.#scriptFile, + env: { PORT: port, ...this.#options.env }, + nodeArgs: this.#options.nodeArgs, + scriptArgs: this.#options.scriptArgs, + }) + + this.#httpServer.on('message', (message) => { + if (this.#isAdonisJSReadyMessage(message)) { + const host = message.host === '0.0.0.0' ? '127.0.0.1' : message.host + + const displayMessage = ui + .sticker() + .useColors(this.#colors) + .useRenderer(this.#logger.getRenderer()) + .add(`Server address: ${this.#colors.cyan(`http://${host}:${message.port}`)}`) + .add( + `File system watcher: ${this.#colors.cyan( + `${this.#isWatching ? 'enabled' : 'disabled'}` + )}` + ) + + if (message.duration) { + displayMessage.add(`Ready in: ${this.#colors.cyan(prettyHrtime(message.duration))}`) + } + + displayMessage.render() + } + }) + + this.#httpServer + .then((result) => { + if (mode === 'nonblocking') { + this.#onClose?.(result.exitCode) + this.#watcher?.close() + this.#assetsServer?.stop() + } else { + this.#logger.info('Underlying HTTP server closed. Still watching for changes') + } + }) + .catch((error) => { + if (mode === 'nonblocking') { + this.#onError?.(error) + this.#watcher?.close() + this.#assetsServer?.stop() + } else { + this.#logger.info('Underlying HTTP server died. Still watching for changes') + } + }) + } + + /** + * Starts the assets server + */ + #startAssetsServer() { + this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets) + this.#assetsServer.setLogger(this.#logger) + this.#assetsServer.start() + } + + /** + * Restarts the HTTP server in the watch mode. Do not call this + * method when not in watch mode + */ + #restartHTTPServer(port: string) { + if (this.#httpServer) { + this.#httpServer.removeAllListeners() + this.#httpServer.kill('SIGKILL') + } + + this.#startHTTPServer(port, 'blocking') + } + + /** + * Handles a non TypeScript file change + */ + #handleFileChange(action: string, port: string, relativePath: string) { + if (isDotEnvFile(relativePath)) { + this.#clearScreen() + this.#logger.log(`${this.#colors.green(action)} ${relativePath}`) + this.#restartHTTPServer(port) + return + } + + if (this.#isMetaFileWithReloadsEnabled(relativePath)) { + this.#clearScreen() + this.#logger.log(`${this.#colors.green(action)} ${relativePath}`) + this.#restartHTTPServer(port) + return + } + + if (this.#isMetaFileWithReloadsDisabled(relativePath)) { + this.#clearScreen() + this.#logger.log(`${this.#colors.green(action)} ${relativePath}`) + } + } + + /** + * Handles TypeScript source file change + */ + #handleSourceFileChange(action: string, port: string, relativePath: string) { + this.#clearScreen() + this.#logger.log(`${this.#colors.green(action)} ${relativePath}`) + this.#restartHTTPServer(port) + } + + /** + * Set a custom CLI UI logger + */ + setLogger(logger: Logger) { + this.#logger = logger + this.#assetsServer?.setLogger(logger) + return this + } + + /** + * Add listener to get notified when dev server is + * closed + */ + onClose(callback: (exitCode: number) => any): this { + this.#onClose = callback + return this + } + + /** + * Add listener to get notified when dev server exists + * with an error + */ + onError(callback: (error: any) => any): this { + this.#onError = callback + return this + } + + /** + * Close watchers and running child processes + */ + async close() { + await this.#watcher?.close() + this.#assetsServer?.stop() + if (this.#httpServer) { + this.#httpServer.removeAllListeners() + this.#httpServer.kill('SIGKILL') + } + } + + /** + * Start the development server + */ + async start() { + this.#clearScreen() + this.#logger.info('starting HTTP server...') + this.#startHTTPServer(String(await getPort(this.#cwd)), 'nonblocking') + this.#startAssetsServer() + } + + /** + * Start the development server in watch mode + */ + async startAndWatch(ts: typeof tsStatic, options?: { poll: boolean }) { + const port = String(await getPort(this.#cwd)) + this.#isWatching = true + + this.#clearScreen() + + this.#logger.info('starting HTTP server...') + this.#startHTTPServer(port, 'blocking') + + this.#startAssetsServer() + + /** + * Create watcher using tsconfig.json file + */ + const output = watch(this.#cwd, ts, options || {}) + if (!output) { + this.#onClose?.(1) + return + } + + /** + * Storing reference to watcher, so that we can close it + * when HTTP server exists with error + */ + this.#watcher = output.chokidar + + /** + * Notify the watcher is ready + */ + output.watcher.on('watcher:ready', () => { + this.#logger.info('watching file system for changes...') + }) + + /** + * Cleanup when watcher dies + */ + output.chokidar.on('error', (error) => { + this.#logger.warning('file system watcher failure') + this.#logger.fatal(error) + this.#onError?.(error) + output.chokidar.close() + }) + + /** + * Changes in TypeScript source file + */ + output.watcher.on('source:add', ({ relativePath }) => + this.#handleSourceFileChange('add', port, relativePath) + ) + output.watcher.on('source:change', ({ relativePath }) => + this.#handleSourceFileChange('update', port, relativePath) + ) + output.watcher.on('source:unlink', ({ relativePath }) => + this.#handleSourceFileChange('delete', port, relativePath) + ) + + /** + * Changes in non-TypeScript source files + */ + output.watcher.on('add', ({ relativePath }) => + this.#handleFileChange('add', port, relativePath) + ) + output.watcher.on('change', ({ relativePath }) => + this.#handleFileChange('update', port, relativePath) + ) + output.watcher.on('unlink', ({ relativePath }) => + this.#handleFileChange('delete', port, relativePath) + ) + } +} diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..969da8d --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,215 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { isJunk } from 'junk' +import fastGlob from 'fast-glob' +import getRandomPort from 'get-port' +import { existsSync } from 'node:fs' +import type tsStatic from 'typescript' +import { fileURLToPath } from 'node:url' +import { execaNode, execa } from 'execa' +import { copyFile, mkdir } from 'node:fs/promises' +import { EnvLoader, EnvParser } from '@adonisjs/env' +import { ConfigParser, Watcher } from '@poppinss/chokidar-ts' +import { basename, dirname, isAbsolute, join, relative } from 'node:path' + +import type { RunOptions, WatchOptions } from './types.js' +import debug from './debug.js' + +/** + * Default set of args to pass in order to run TypeScript + * source. Used by "run" and "runNode" scripts + */ +const DEFAULT_NODE_ARGS = [ + // Use ts-node/esm loader. The project must install it + '--loader=ts-node/esm', + // Enable source maps, since TSNode source maps are broken + '--enable-source-maps', +] + +/** + * Parses tsconfig.json and prints errors using typescript compiler + * host + */ +export function parseConfig(cwd: string | URL, ts: typeof tsStatic) { + const { config, error } = new ConfigParser(cwd, 'tsconfig.json', ts).parse() + if (error) { + const compilerHost = ts.createCompilerHost({}) + console.log(ts.formatDiagnosticsWithColorAndContext([error], compilerHost)) + return + } + + if (config!.errors.length) { + const compilerHost = ts.createCompilerHost({}) + console.log(ts.formatDiagnosticsWithColorAndContext(config!.errors, compilerHost)) + return + } + + return config +} + +/** + * Runs a Node.js script as a child process and inherits the stdio streams + */ +export function runNode(cwd: string | URL, options: RunOptions) { + const childProcess = execaNode(options.script, options.scriptArgs, { + nodeOptions: DEFAULT_NODE_ARGS.concat(options.nodeArgs), + preferLocal: true, + windowsHide: false, + localDir: cwd, + cwd, + buffer: false, + stdio: options.stdio || 'inherit', + env: { + ...(options.stdio === 'pipe' ? { FORCE_COLOR: 'true' } : {}), + ...options.env, + }, + }) + + return childProcess +} + +/** + * Runs a script as a child process and inherits the stdio streams + */ +export function run(cwd: string | URL, options: Omit) { + const childProcess = execa(options.script, options.scriptArgs, { + preferLocal: true, + windowsHide: false, + localDir: cwd, + cwd, + buffer: false, + stdio: options.stdio || 'inherit', + env: { + ...(options.stdio === 'pipe' ? { FORCE_COLOR: 'true' } : {}), + ...options.env, + }, + }) + + return childProcess +} + +/** + * Watches the file system using tsconfig file + */ +export function watch(cwd: string | URL, ts: typeof tsStatic, options: WatchOptions) { + const config = parseConfig(cwd, ts) + if (!config) { + return + } + + const watcher = new Watcher(typeof cwd === 'string' ? cwd : fileURLToPath(cwd), config!) + const chokidar = watcher.watch(['.'], { usePolling: options.poll }) + return { watcher, chokidar } +} + +/** + * Check if file is an .env file + */ +export function isDotEnvFile(filePath: string) { + if (filePath === '.env') { + return true + } + + return filePath.includes('.env.') +} + +/** + * Returns the port to use after inspect the dot-env files inside + * a given directory. + * + * A random port is used when the specified port is in use. Following + * is the logic for finding a specified port. + * + * - The "process.env.PORT" value is used if exists. + * - The dot-env files are loaded using the "EnvLoader" and the PORT + * value is used by iterating over all the loaded files. The + * iteration stops after first find. + */ +export async function getPort(cwd: URL): Promise { + /** + * Use existing port if exists + */ + if (process.env.PORT) { + return getRandomPort({ port: Number(process.env.PORT) }) + } + + /** + * Loop over files and use the port from their contents. Stops + * after first match + */ + const files = await new EnvLoader(cwd).load() + for (let file of files) { + const envVariables = new EnvParser(file.contents).parse() + if (envVariables.PORT) { + return getRandomPort({ port: Number(envVariables.PORT) }) + } + } + + /** + * Use 3333 as the port + */ + return getRandomPort({ port: 3333 }) +} + +/** + * Helper function to copy files from relative paths or glob + * patterns + */ +export async function copyFiles(files: string[], cwd: string, outDir: string) { + /** + * Looping over files and create a new collection with paths + * and glob patterns + */ + const { paths, patterns } = files.reduce<{ patterns: string[]; paths: string[] }>( + (result, file) => { + /** + * If file is a glob pattern, then push it to patterns + */ + if (fastGlob.isDynamicPattern(file)) { + result.patterns.push(file) + return result + } + + /** + * Otherwise, check if file exists and push it to paths to copy + */ + if (existsSync(join(cwd, file))) { + result.paths.push(file) + } + + return result + }, + { patterns: [], paths: [] } + ) + + debug('copyFiles inputs: %O, paths: %O, patterns: %O', files, paths, patterns) + + /** + * Getting list of relative paths from glob patterns + */ + const filePaths = paths.concat(await fastGlob(patterns, { cwd, dot: true })).filter((file) => { + return !isJunk(basename(file)) + }) + + /** + * Finally copy files to the destination by keeping the same + * directory structure and ignoring junk files + */ + debug('copying files %O to destination "%s"', filePaths, outDir) + const copyPromises = filePaths.map(async (file) => { + const src = isAbsolute(file) ? file : join(cwd, file) + const dest = join(outDir, relative(cwd, src)) + + await mkdir(dirname(dest), { recursive: true }) + return copyFile(src, dest) + }) + + return await Promise.all(copyPromises) +} diff --git a/src/requireHook/index.ts b/src/requireHook/index.ts deleted file mode 100644 index 34b7d71..0000000 --- a/src/requireHook/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { register } from '@adonisjs/require-ts' - -/** - * Exports the function to be used for registering require hook - * for AdonisJS applications - */ -export default function registerForAdonis(appRoot: string) { - return register(appRoot, { - cache: true, - transformers: { - after: [ - { - transform: '@adonisjs/assembler/build/src/requireHook/ioc-transformer', - }, - ], - }, - }) -} diff --git a/src/requireHook/ioc-transformer.ts b/src/requireHook/ioc-transformer.ts deleted file mode 100644 index a96ff90..0000000 --- a/src/requireHook/ioc-transformer.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import type tsStatic from 'typescript' -import { rcParser } from '@adonisjs/application' -import { iocTransformer } from '@adonisjs/ioc-transformer' - -/** - * Transformer to transform AdonisJS IoC container import - * statements - */ -export default function (ts: typeof tsStatic, appRoot: string) { - return iocTransformer(ts, rcParser.parse(require(join(appRoot, '.adonisrc.json')))) -} diff --git a/src/test_runner.ts b/src/test_runner.ts new file mode 100644 index 0000000..2a68536 --- /dev/null +++ b/src/test_runner.ts @@ -0,0 +1,439 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import picomatch from 'picomatch' +import type tsStatic from 'typescript' +import { type ExecaChildProcess } from 'execa' +import { cliui, type Logger } from '@poppinss/cliui' +import type { Watcher } from '@poppinss/chokidar-ts' + +import type { TestRunnerOptions } from './types.js' +import { AssetsDevServer } from './assets_dev_server.js' +import { getPort, isDotEnvFile, runNode, watch } from './helpers.js' + +/** + * Instance of CLIUI + */ +const ui = cliui() + +/** + * Exposes the API to run Japa tests and optionally watch for file + * changes to re-run the tests. + * + * 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. + * + */ +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[] + + /** + * In watch mode, after a file is changed, we wait for the current + * set of tests to finish before triggering a re-run. Therefore, + * we use this flag to know if we are already busy in running + * tests and ignore file-changes. + */ + #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 + + /** + * Getting reference to colors library from logger + */ + get #colors() { + return this.#logger.getColors() + } + + 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) + .flat(1) + ) + + this.#scriptArgs = this.#convertOptionsToArgs().concat(this.#options.scriptArgs) + this.#initialFiltersArgs = this.#convertFiltersToArgs(this.#options.filters) + } + + /** + * Convert test runner options to the CLI args + */ + #convertOptionsToArgs() { + const args: string[] = [] + + if (this.#options.reporters) { + args.push('--reporters') + args.push(this.#options.reporters.join(',')) + } + + if (this.#options.timeout !== undefined) { + args.push('--timeout') + args.push(String(this.#options.timeout)) + } + + if (this.#options.failed) { + args.push('--failed') + } + + if (this.#options.retries !== undefined) { + args.push('--retries') + args.push(String(this.#options.retries)) + } + + return args + } + + /** + * Converts all known filters to CLI args. + * + * The following code snippet may seem like repetitive code. But, it + * is done intentionally to have visibility around how each filter + * is converted to an arg. + */ + #convertFiltersToArgs(filters: TestRunnerOptions['filters']): string[] { + const args: string[] = [] + + if (filters.suites) { + args.push(...filters.suites) + } + + if (filters.files) { + args.push('--files') + args.push(filters.files.join(',')) + } + + if (filters.groups) { + args.push('--groups') + args.push(filters.groups.join(',')) + } + + if (filters.tags) { + args.push('--tags') + args.push(filters.tags.join(',')) + } + + if (filters.tests) { + args.push('--tests') + args.push(filters.tests.join(',')) + } + + return args + } + + /** + * Conditionally clear the terminal screen + */ + #clearScreen() { + if (this.#options.clearScreen) { + process.stdout.write('\u001Bc') + } + } + + /** + * Runs tests + */ + #runTests( + port: string, + mode: 'blocking' | 'nonblocking', + filters?: TestRunnerOptions['filters'] + ) { + 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) + + this.#testScript = runNode(this.#cwd, { + script: this.#scriptFile, + env: { PORT: port, ...this.#options.env }, + nodeArgs: this.#options.nodeArgs, + scriptArgs, + }) + + this.#testScript + .then((result) => { + if (mode === 'nonblocking') { + this.#onClose?.(result.exitCode) + this.close() + } + }) + .catch((error) => { + if (mode === 'nonblocking') { + this.#onError?.(error) + this.close() + } + }) + .finally(() => { + this.#isBusy = false + }) + } + + /** + * Re-run tests with additional inline filters. Should be + * executed in watch mode only. + */ + #rerunTests(port: string, filters?: TestRunnerOptions['filters']) { + if (this.#testScript) { + this.#testScript.removeAllListeners() + this.#testScript.kill('SIGKILL') + } + + this.#runTests(port, 'blocking', filters) + } + + /** + * Starts the assets server + */ + #startAssetsServer() { + this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets) + this.#assetsServer.setLogger(this.#logger) + this.#assetsServer.start() + } + + /** + * Handles a non TypeScript file change + */ + #handleFileChange(action: string, port: string, relativePath: string) { + if (this.#isBusy) { + return + } + + if (isDotEnvFile(relativePath) || this.#isMetaFile(relativePath)) { + this.#clearScreen() + this.#logger.log(`${this.#colors.green(action)} ${relativePath}`) + this.#rerunTests(port) + } + } + + /** + * Handles TypeScript source file change + */ + #handleSourceFileChange(action: string, port: string, relativePath: string) { + if (this.#isBusy) { + return + } + + this.#clearScreen() + this.#logger.log(`${this.#colors.green(action)} ${relativePath}`) + + /** + * If changed file is a test file after considering the initial filters, + * then only run that file + */ + if (this.#isTestFile(relativePath)) { + this.#rerunTests(port, { + ...this.#options.filters, + files: [relativePath], + }) + return + } + + this.#rerunTests(port) + } + + /** + * Set a custom CLI UI logger + */ + setLogger(logger: Logger) { + this.#logger = logger + this.#assetsServer?.setLogger(logger) + return this + } + + /** + * Add listener to get notified when dev server is + * closed + */ + onClose(callback: (exitCode: number) => any): this { + this.#onClose = callback + return this + } + + /** + * Add listener to get notified when dev server exists + * with an error + */ + onError(callback: (error: any) => any): this { + this.#onError = callback + return this + } + + /** + * Close watchers and running child processes + */ + async close() { + await this.#watcher?.close() + this.#assetsServer?.stop() + if (this.#testScript) { + this.#testScript.removeAllListeners() + this.#testScript.kill('SIGKILL') + } + } + + /** + * Runs tests + */ + async run() { + const port = String(await getPort(this.#cwd)) + + this.#clearScreen() + this.#startAssetsServer() + + this.#logger.info('booting application to run tests...') + this.#runTests(port, 'nonblocking') + } + + /** + * Run tests in watch mode + */ + async runAndWatch(ts: typeof tsStatic, options?: { poll: boolean }) { + const port = String(await getPort(this.#cwd)) + + this.#clearScreen() + this.#startAssetsServer() + + this.#logger.info('booting application to run tests...') + this.#runTests(port, 'blocking') + + /** + * Create watcher using tsconfig.json file + */ + const output = watch(this.#cwd, ts, options || {}) + if (!output) { + this.#onClose?.(1) + return + } + + /** + * Storing reference to watcher, so that we can close it + * when HTTP server exists with error + */ + this.#watcher = output.chokidar + + /** + * Notify the watcher is ready + */ + output.watcher.on('watcher:ready', () => { + this.#logger.info('watching file system for changes...') + }) + + /** + * Cleanup when watcher dies + */ + output.chokidar.on('error', (error) => { + this.#logger.warning('file system watcher failure') + this.#logger.fatal(error) + this.#onError?.(error) + output.chokidar.close() + }) + + /** + * Changes in TypeScript source file + */ + output.watcher.on('source:add', ({ relativePath }) => + this.#handleSourceFileChange('add', port, relativePath) + ) + output.watcher.on('source:change', ({ relativePath }) => + this.#handleSourceFileChange('update', port, relativePath) + ) + output.watcher.on('source:unlink', ({ relativePath }) => + this.#handleSourceFileChange('delete', port, relativePath) + ) + + /** + * Changes in non-TypeScript source files + */ + output.watcher.on('add', ({ relativePath }) => + this.#handleFileChange('add', port, relativePath) + ) + output.watcher.on('change', ({ relativePath }) => + this.#handleFileChange('update', port, relativePath) + ) + output.watcher.on('unlink', ({ relativePath }) => + this.#handleFileChange('delete', port, relativePath) + ) + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9004a1e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,273 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * 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 +} + +/** + * Watcher options + */ +export type WatchOptions = { + poll?: boolean +} + +/** + * Meta file config defined in "adonisrc.ts" file + */ +export type MetaFile = { + pattern: string + reloadServer: boolean +} + +/** + * Test suite defined in "adonisrc.ts" file + */ +export type Suite = { + name: string + files: string | string[] +} + +/** + * Options accepted by assets bundler + */ +export type AssetsBundlerOptions = + | { + enabled: false + driver?: string + cmd?: string + args?: string[] + } + | { + enabled: true + driver: string + cmd: string + args: string[] + } + +/** + * 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 +} + +/** + * Options accepted by the test runner + */ +export type TestRunnerOptions = { + /** + * 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 + + /** + * 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 +} + +/** + * Entry to add a middleware to a given middleware stack + * via the CodeTransformer + */ +export type MiddlewareNode = { + /** + * If you are adding a named middleware, then you must + * define the name. + */ + name?: string + + /** + * The path to the middleware file + * + * @example + * `@adonisjs/static/static_middleware` + * `#middlewares/silent_auth.js` + */ + path: string + + /** + * The position to add the middleware. If `before` + * middleware will be added at the first position and + * therefore will be run before all others + * + * @default 'after' + */ + position?: 'before' | 'after' +} + +/** + * Policy node to be added to the list of policies. + */ +export type BouncerPolicyNode = { + /** + * Policy name + */ + name: string + + /** + * Policy import path + */ + path: string +} + +/** + * Defines the structure of an environment variable validation + * definition + */ +export type EnvValidationNode = { + /** + * Write a leading comment on top of your variables + */ + leadingComment?: string + + /** + * A key-value pair of env variables and their validation + * + * @example + * MY_VAR: 'Env.schema.string.optional()' + */ + variables: Record +} diff --git a/templates/command.txt b/templates/command.txt deleted file mode 100644 index 48fbeac..0000000 --- a/templates/command.txt +++ /dev/null @@ -1,33 +0,0 @@ -import { BaseCommand } from '@adonisjs/core/build/standalone' - -export default class {{ filename }} extends BaseCommand { - /** - * Command name is used to run the command - */ - public static commandName = '{{#toCommandName}}{{ filename }}{{/toCommandName}}' - - /** - * Command description is displayed in the "help" output - */ - public static description = '' - - public static settings = { - /** - * Set the following value to true, if you want to load the application - * before running the command. Don't forget to call `node ace generate:manifest` - * afterwards. - */ - loadApp: false, - - /** - * Set the following value to true, if you want this command to keep running until - * you manually decide to exit the process. Don't forget to call - * `node ace generate:manifest` afterwards. - */ - stayAlive: false, - } - - public async run() { - this.logger.info('Hello world!') - } -} diff --git a/templates/controller.txt b/templates/controller.txt deleted file mode 100644 index 2d1829a..0000000 --- a/templates/controller.txt +++ /dev/null @@ -1,3 +0,0 @@ -// import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - -export default class {{ filename }} {} diff --git a/templates/event-listener.txt b/templates/event-listener.txt deleted file mode 100644 index b1ba63a..0000000 --- a/templates/event-listener.txt +++ /dev/null @@ -1,3 +0,0 @@ -import type { EventsList } from '@ioc:Adonis/Core/Event' - -export default class {{ filename }} {} diff --git a/templates/exception.txt b/templates/exception.txt deleted file mode 100644 index 2435d5f..0000000 --- a/templates/exception.txt +++ /dev/null @@ -1,15 +0,0 @@ -import { Exception } from '@adonisjs/core/build/standalone' - -/* -|-------------------------------------------------------------------------- -| Exception -|-------------------------------------------------------------------------- -| -| The Exception class imported from `@adonisjs/core` allows defining -| a status code and error code for every exception. -| -| @example -| new {{ filename }}('message', 500, 'E_RUNTIME_EXCEPTION') -| -*/ -export default class {{ filename }} extends Exception {} diff --git a/templates/middleware.txt b/templates/middleware.txt deleted file mode 100644 index e1adb5f..0000000 --- a/templates/middleware.txt +++ /dev/null @@ -1,8 +0,0 @@ -import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - -export default class {{ filename }} { - public async handle({}: HttpContextContract, next: () => Promise) { - // code for middleware goes here. ABOVE THE NEXT CALL - await next() - } -} diff --git a/templates/preload-file.txt b/templates/preload-file.txt deleted file mode 100644 index d2cfc89..0000000 --- a/templates/preload-file.txt +++ /dev/null @@ -1,9 +0,0 @@ -/* -|-------------------------------------------------------------------------- -| Preloaded File -|-------------------------------------------------------------------------- -| -| Any code written inside this file will be executed during the application -| boot. -| -*/ diff --git a/templates/provider.txt b/templates/provider.txt deleted file mode 100644 index 4f2447a..0000000 --- a/templates/provider.txt +++ /dev/null @@ -1,40 +0,0 @@ -import type { ApplicationContract } from '@ioc:Adonis/Core/Application' - -/* -|-------------------------------------------------------------------------- -| Provider -|-------------------------------------------------------------------------- -| -| Your application is not ready when this file is loaded by the framework. -| Hence, the top level imports relying on the IoC container will not work. -| You must import them inside the life-cycle methods defined inside -| the provider class. -| -| @example: -| -| public async ready () { -| const Database = this.app.container.resolveBinding('Adonis/Lucid/Database') -| const Event = this.app.container.resolveBinding('Adonis/Core/Event') -| Event.on('db:query', Database.prettyPrint) -| } -| -*/ -export default class {{ filename }} { - constructor(protected app: ApplicationContract) {} - - public register() { - // Register your own bindings - } - - public async boot() { - // All bindings are ready, feel free to use them - } - - public async ready() { - // App is ready - } - - public async shutdown() { - // Cleanup, since app is going down - } -} diff --git a/templates/resource-controller.txt b/templates/resource-controller.txt deleted file mode 100644 index 797f455..0000000 --- a/templates/resource-controller.txt +++ /dev/null @@ -1,17 +0,0 @@ -import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - -export default class {{ filename }} { - public async index({}: HttpContextContract) {} - - public async create({}: HttpContextContract) {} - - public async store({}: HttpContextContract) {} - - public async show({}: HttpContextContract) {} - - public async edit({}: HttpContextContract) {} - - public async update({}: HttpContextContract) {} - - public async destroy({}: HttpContextContract) {} -} diff --git a/templates/self-handle-exception.txt b/templates/self-handle-exception.txt deleted file mode 100644 index 226f41f..0000000 --- a/templates/self-handle-exception.txt +++ /dev/null @@ -1,32 +0,0 @@ -import { Exception } from '@adonisjs/core/build/standalone' -import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - -/* -|-------------------------------------------------------------------------- -| Exception -|-------------------------------------------------------------------------- -| -| The Exception class imported from `@adonisjs/core` allows defining -| a status code and error code for every exception. -| -| @example -| new {{ filename }}('message', 500, 'E_RUNTIME_EXCEPTION') -| -*/ -export default class {{ filename }} extends Exception { - /** - * The handle method allows you to self handle the exception and - * return an HTTP response. - * - * This is how it works under the hood. - * - * - You raise this exception - * - The exception goes uncatched/unhandled through out the entire HTTP request cycle. - * - Just before making the response. AdonisJS will call the `handle` method. - * Giving you a chance to convert the exception to response. - * - */ - public async handle(error: this, ctx: HttpContextContract) { - ctx.response.status(error.status || 500).send(error.message) - } -} diff --git a/templates/test-entrypoint.txt b/templates/test-entrypoint.txt deleted file mode 100644 index 93b9d8e..0000000 --- a/templates/test-entrypoint.txt +++ /dev/null @@ -1,45 +0,0 @@ -/* -|-------------------------------------------------------------------------- -| Tests -|-------------------------------------------------------------------------- -| -| The contents in this file boots the AdonisJS application and configures -| the Japa tests runner. -| -| For the most part you will never edit this file. The configuration -| for the tests can be controlled via ".adonisrc.json" and -| "tests/bootstrap.ts" files. -| -*/ - -process.env.NODE_ENV = 'test' - -import 'reflect-metadata' -import sourceMapSupport from 'source-map-support' -import { Ignitor } from '@adonisjs/core/build/standalone' -import { configure, processCliArgs, run, RunnerHooksHandler } from '@japa/runner' - -sourceMapSupport.install({ handleUncaughtExceptions: false }) - -const kernel = new Ignitor(__dirname).kernel('test') - -kernel - .boot() - .then(() => import('./tests/bootstrap')) - .then(({ runnerHooks, ...config }) => { - const app: RunnerHooksHandler[] = [() => kernel.start()] - - configure({ - ...kernel.application.rcFile.tests, - ...processCliArgs(process.argv.slice(2)), - ...config, - ...{ - importer: (filePath) => import(filePath), - setup: app.concat(runnerHooks.setup), - teardown: runnerHooks.teardown, - }, - cwd: kernel.application.appRoot - }) - - run() - }) diff --git a/templates/test.txt b/templates/test.txt deleted file mode 100644 index d83c9fc..0000000 --- a/templates/test.txt +++ /dev/null @@ -1,5 +0,0 @@ -import { test } from '@japa/runner' - -test.group('{{ name }}', () => { - // Write your test here -}) diff --git a/templates/tests-contract.txt b/templates/tests-contract.txt deleted file mode 100644 index adbcbe1..0000000 --- a/templates/tests-contract.txt +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Contract source: https://bit.ly/3DP1ypf - * - * Feel free to let us know via PR, if you find something broken in this contract - * file. - */ - -import '@japa/runner' - -declare module '@japa/runner' { - interface TestContext { - // Extend context - } - - interface Test { - // Extend test - } -} diff --git a/templates/tests/bootstrap.txt b/templates/tests/bootstrap.txt deleted file mode 100644 index d574211..0000000 --- a/templates/tests/bootstrap.txt +++ /dev/null @@ -1,69 +0,0 @@ -/** - * File source: https://bit.ly/3ukaHTz - * - * Feel free to let us know via PR, if you find something broken in this contract - * file. - */ - -import type { Config } from '@japa/runner' -import TestUtils from '@ioc:Adonis/Core/TestUtils' -import { assert, runFailedTests, specReporter, apiClient } from '@japa/preset-adonis' - -/* -|-------------------------------------------------------------------------- -| Japa Plugins -|-------------------------------------------------------------------------- -| -| Japa plugins allows you to add additional features to Japa. By default -| we register the assertion plugin. -| -| Feel free to remove existing plugins or add more. -| -*/ -export const plugins: Required['plugins'] = [assert(), runFailedTests(), apiClient()] - -/* -|-------------------------------------------------------------------------- -| Japa Reporters -|-------------------------------------------------------------------------- -| -| Japa reporters displays/saves the progress of tests as they are executed. -| By default, we register the spec reporter to show a detailed report -| of tests on the terminal. -| -*/ -export const reporters: Required['reporters'] = [specReporter()] - -/* -|-------------------------------------------------------------------------- -| Runner hooks -|-------------------------------------------------------------------------- -| -| Runner hooks are executed after booting the AdonisJS app and -| before the test files are imported. -| -| You can perform actions like starting the HTTP server or running migrations -| within the runner hooks -| -*/ -export const runnerHooks: Pick, 'setup' | 'teardown'> = { - setup: [() => TestUtils.ace().loadCommands()], - teardown: [], -} - -/* -|-------------------------------------------------------------------------- -| Configure individual suites -|-------------------------------------------------------------------------- -| -| The configureSuite method gets called for every test suite registered -| within ".adonisrc.json" file. -| -| You can use this method to configure suites. For example: Only start -| the HTTP server when it is a functional suite. -*/ -export const configureSuite: Required['configureSuite'] = (suite) => { - if (suite.name === 'functional') { - suite.setup(() => TestUtils.httpServer().start()) - } -} diff --git a/templates/tests/functional/hello_world_api.spec.txt b/templates/tests/functional/hello_world_api.spec.txt deleted file mode 100644 index 7fa3df4..0000000 --- a/templates/tests/functional/hello_world_api.spec.txt +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '@japa/runner' - -test('display welcome page', async ({ client }) => { - const response = await client.get('/') - - response.assertStatus(200) - response.assertBodyContains({ hello: 'world' }) -}) diff --git a/templates/tests/functional/hello_world_slim.spec.txt b/templates/tests/functional/hello_world_slim.spec.txt deleted file mode 100644 index 25377d0..0000000 --- a/templates/tests/functional/hello_world_slim.spec.txt +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '@japa/runner' - -test('display welcome page', async ({ client }) => { - const response = await client.get('/') - - response.assertStatus(200) - response.assertTextIncludes('Hello world') -}) diff --git a/templates/tests/functional/hello_world_web.spec.txt b/templates/tests/functional/hello_world_web.spec.txt deleted file mode 100644 index e4bbf6d..0000000 --- a/templates/tests/functional/hello_world_web.spec.txt +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '@japa/runner' - -test('display welcome page', async ({ client }) => { - const response = await client.get('/') - - response.assertStatus(200) - response.assertTextIncludes('

It Works!

') -}) diff --git a/templates/validator.txt b/templates/validator.txt deleted file mode 100644 index 3a6de0e..0000000 --- a/templates/validator.txt +++ /dev/null @@ -1,40 +0,0 @@ -import { schema, CustomMessages } from '@ioc:Adonis/Core/Validator' -import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - -export default class {{ filename }} { - constructor(protected ctx: HttpContextContract) {} - - /* - * Define schema to validate the "shape", "type", "formatting" and "integrity" of data. - * - * For example: - * 1. The username must be of data type string. But then also, it should - * not contain special characters or numbers. - * ``` - * schema.string([ rules.alpha() ]) - * ``` - * - * 2. The email must be of data type string, formatted as a valid - * email. But also, not used by any other user. - * ``` - * schema.string([ - * rules.email(), - * rules.unique({ table: 'users', column: 'email' }), - * ]) - * ``` - */ - public schema = schema.create({}) - - /** - * Custom messages for validation failures. You can make use of dot notation `(.)` - * for targeting nested fields and array expressions `(*)` for targeting all - * children of an array. For example: - * - * { - * 'profile.username.required': 'Username is required', - * 'scores.*.number': 'Define scores as valid numbers' - * } - * - */ - public messages: CustomMessages = {} -} diff --git a/templates/view.txt b/templates/view.txt deleted file mode 100644 index e69de29..0000000 diff --git a/templates/webpack.config.txt b/templates/webpack.config.txt deleted file mode 100644 index 914b586..0000000 --- a/templates/webpack.config.txt +++ /dev/null @@ -1,214 +0,0 @@ -const { join } = require('path') -const Encore = require('@symfony/webpack-encore') - -/* -|-------------------------------------------------------------------------- -| Encore runtime environment -|-------------------------------------------------------------------------- -*/ -if (!Encore.isRuntimeEnvironmentConfigured()) { - Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev') -} - -/* -|-------------------------------------------------------------------------- -| Output path -|-------------------------------------------------------------------------- -| -| The output path for writing the compiled files. It should always -| be inside the public directory, so that AdonisJS can serve it. -| -*/ -Encore.setOutputPath('./public/assets') - -/* -|-------------------------------------------------------------------------- -| Public URI -|-------------------------------------------------------------------------- -| -| The public URI to access the static files. It should always be -| relative from the "public" directory. -| -*/ -Encore.setPublicPath('/assets') - -/* -|-------------------------------------------------------------------------- -| Entrypoints -|-------------------------------------------------------------------------- -| -| Entrypoints are script files that boots your frontend application. Ideally -| a single entrypoint is used by majority of applications. However, feel -| free to add more (if required). -| -| Also, make sure to read the docs on "Assets bundler" to learn more about -| entrypoints. -| -*/ -Encore.addEntry('app', './resources/js/app.js') - -/* -|-------------------------------------------------------------------------- -| Copy assets -|-------------------------------------------------------------------------- -| -| Since the edge templates are not part of the Webpack compile lifecycle, any -| images referenced by it will not be processed by Webpack automatically. Hence -| we must copy them manually. -| -*/ -// Encore.copyFiles({ -// from: './resources/images', -// to: 'images/[path][name].[hash:8].[ext]', -// }) - -/* -|-------------------------------------------------------------------------- -| Split shared code -|-------------------------------------------------------------------------- -| -| Instead of bundling duplicate code in all the bundles, generate a separate -| bundle for the shared code. -| -| https://symfony.com/doc/current/frontend/encore/split-chunks.html -| https://webpack.js.org/plugins/split-chunks-plugin/ -| -*/ -// Encore.splitEntryChunks() - -/* -|-------------------------------------------------------------------------- -| Isolated entrypoints -|-------------------------------------------------------------------------- -| -| Treat each entry point and its dependencies as its own isolated module. -| -*/ -Encore.disableSingleRuntimeChunk() - -/* -|-------------------------------------------------------------------------- -| Cleanup output folder -|-------------------------------------------------------------------------- -| -| It is always nice to cleanup the build output before creating a build. It -| will ensure that all unused files from the previous build are removed. -| -*/ -Encore.cleanupOutputBeforeBuild() - -/* -|-------------------------------------------------------------------------- -| Source maps -|-------------------------------------------------------------------------- -| -| Enable source maps in production -| -*/ -Encore.enableSourceMaps(!Encore.isProduction()) - -/* -|-------------------------------------------------------------------------- -| Assets versioning -|-------------------------------------------------------------------------- -| -| Enable assets versioning to leverage lifetime browser and CDN cache -| -*/ -Encore.enableVersioning(Encore.isProduction()) - -/* -|-------------------------------------------------------------------------- -| Configure dev server -|-------------------------------------------------------------------------- -| -| Here we configure the dev server to enable live reloading for edge templates. -| Remember edge templates are not processed by Webpack and hence we need -| to watch them explicitly and livereload the browser. -| -*/ -Encore.configureDevServerOptions((options) => { - /** - * Normalize "options.static" property to an array - */ - if (!options.static) { - options.static = [] - } else if (!Array.isArray(options.static)) { - options.static = [options.static] - } - - /** - * Enable live reload and add views directory - */ - options.liveReload = true - options.static.push({ - directory: join(__dirname, './resources/views'), - watch: true, - }) -}) - -/* -|-------------------------------------------------------------------------- -| CSS precompilers support -|-------------------------------------------------------------------------- -| -| Uncomment one of the following lines of code to enable support for your -| favorite CSS precompiler -| -*/ -// Encore.enableSassLoader() -// Encore.enableLessLoader() -// Encore.enableStylusLoader() - -/* -|-------------------------------------------------------------------------- -| CSS loaders -|-------------------------------------------------------------------------- -| -| Uncomment one of the following line of code to enable support for -| PostCSS or CSS. -| -*/ -// Encore.enablePostCssLoader() -// Encore.configureCssLoader(() => {}) - -/* -|-------------------------------------------------------------------------- -| Enable Vue loader -|-------------------------------------------------------------------------- -| -| Uncomment the following lines of code to enable support for vue. Also make -| sure to install the required dependencies. -| -*/ -// Encore.enableVueLoader(() => {}, { -// version: 3, -// runtimeCompilerBuild: false, -// useJsx: false -// }) - -/* -|-------------------------------------------------------------------------- -| Configure logging -|-------------------------------------------------------------------------- -| -| To keep the terminal clean from unnecessary info statements , we only -| log warnings and errors. If you want all the logs, you can change -| the level to "info". -| -*/ -const config = Encore.getWebpackConfig() -config.infrastructureLogging = { - level: 'warn', -} -config.stats = 'errors-warnings' - -/* -|-------------------------------------------------------------------------- -| Export config -|-------------------------------------------------------------------------- -| -| Export config for webpack to do its job -| -*/ -module.exports = config diff --git a/test-helpers/index.ts b/test-helpers/index.ts deleted file mode 100644 index 64c26bb..0000000 --- a/test-helpers/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -export function toNewlineArray(contents: string): string[] { - return contents.split(/\r?\n/) -} - -export const info = '[ blue(info) ]' -export const success = '[ green(success) ]' -export const error = '[ red(error) ]' -export const warning = '[ yellow(warn) ]' -export const dimYellow = (value: string) => `dim(yellow(${value}))` diff --git a/test/compiler.spec.ts b/test/compiler.spec.ts deleted file mode 100644 index 793d5b7..0000000 --- a/test/compiler.spec.ts +++ /dev/null @@ -1,922 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import execa from 'execa' -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' -import { instantiate } from '@poppinss/cliui/build/api' - -import { Compiler } from '../src/Compiler' -import { success, info, warning, error, dimYellow } from '../test-helpers' - -const ui = instantiate(true) -const fs = new Filesystem(join(__dirname, '__app')) - -test.group('Compiler', (group) => { - group.setup(() => { - ui.logger.useRenderer(ui.testingRenderer) - }) - - group.each.teardown(() => { - ui.testingRenderer.logs = [] - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('build source files', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', '') - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile() - - const hasFiles = await Promise.all( - [ - 'build/.adonisrc.json', - 'build/ace', - 'build/src/foo.js', - 'build/public/styles/main.css', - 'build/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [true, true, true, true, true]) - - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('public/**/*.(js|css),ace => build')} }`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('.adonisrc.json => build')} }`, - stream: 'stdout', - }, - { - message: `${success} built successfully`, - stream: 'stdout', - }, - ]) - - assert.isFalse(require(join(fs.basePath, 'build', '.adonisrc.json')).typescript) - }).timeout(0) - - test('build source files with explicit outDir', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - outDir: 'build', - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', '') - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile() - - const hasFiles = await Promise.all( - [ - 'build/.adonisrc.json', - 'build/src/foo.js', - 'build/public/styles/main.css', - 'build/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [true, true, true, true]) - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('public/**/*.(js|css),ace => build')} }`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('.adonisrc.json => build')} }`, - stream: 'stdout', - }, - { - message: `${success} built successfully`, - stream: 'stdout', - }, - ]) - }).timeout(0) - - test('build source files with explicit rootDir', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - rootDir: './', - outDir: 'build', - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', '') - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile() - - const hasFiles = await Promise.all( - [ - 'build/.adonisrc.json', - 'build/src/foo.js', - 'build/public/styles/main.css', - 'build/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [true, true, true, true]) - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('public/**/*.(js|css),ace => build')} }`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('.adonisrc.json => build')} }`, - stream: 'stdout', - }, - { - message: `${success} built successfully`, - stream: 'stdout', - }, - ]) - }).timeout(0) - - test('build source files to nested outDir', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - rootDir: './', - outDir: 'build/dist', - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', '') - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile() - - const hasFiles = await Promise.all( - [ - 'build/dist/.adonisrc.json', - 'build/dist/src/foo.js', - 'build/dist/public/styles/main.css', - 'build/dist/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [true, true, true, true]) - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build/dist"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('public/**/*.(js|css),ace => build/dist')} }`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('.adonisrc.json => build/dist')} }`, - stream: 'stdout', - }, - { - message: `${success} built successfully`, - stream: 'stdout', - }, - ]) - }).timeout(0) - - test('do not build when config has errors', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - foo: 'bar', - rootDir: './', - outDir: 'build/dist', - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', "import path from 'path'") - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile() - - const hasFiles = await Promise.all( - [ - 'build/dist/.adonisrc.json', - 'build/dist/src/foo.js', - 'build/dist/public/styles/main.css', - 'build/dist/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [false, false, false, false]) - - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${error} unable to parse tsconfig.json`, - stream: 'stderr', - }, - ]) - }).timeout(0) - - test('catch and report typescript errors', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - rootDir: './', - outDir: 'build/dist', - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', "import path from 'path'") - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile(false) - - const hasFiles = await Promise.all( - [ - 'build/dist/.adonisrc.json', - 'build/dist/src/foo.js', - 'build/dist/public/styles/main.css', - 'build/dist/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [true, true, true, true]) - - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build/dist"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${error} typescript compiler errors`, - stream: 'stderr', - }, - { - message: `${info} copy { ${dimYellow('public/**/*.(js|css),ace => build/dist')} }`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('.adonisrc.json => build/dist')} }`, - stream: 'stdout', - }, - { - message: `${success} built successfully`, - stream: 'stdout', - }, - ]) - }).timeout(0) - - test('do not continue on error', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - rootDir: './', - outDir: 'build/dist', - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', "import path from 'path'") - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile(true) - - const hasFiles = await Promise.all( - [ - 'build/dist/.adonisrc.json', - 'build/dist/src/foo.js', - 'build/dist/public/styles/main.css', - 'build/dist/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [false, false, false, false]) - - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build/dist"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${error} typescript compiler errors`, - stream: 'stderr', - }, - { - message: '', - stream: 'stderr', - }, - { - message: `bgRed(Cannot complete the build process as there are typescript errors. Use "--ignore-ts-errors" flag to ignore Typescript errors)`, - stream: 'stderr', - }, - { - message: `${info} cleaning up ${dimYellow('"./build/dist"')} directory`, - stream: 'stdout', - }, - ]) - }).timeout(0) - - test('do not emit when noEmitOnError is true', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - rootDir: './', - outDir: 'build/dist', - noEmitOnError: true, - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', "import path from 'path'") - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile() - - const hasFiles = await Promise.all( - [ - 'build/dist/.adonisrc.json', - 'build/dist/src/foo.js', - 'build/dist/public/styles/main.css', - 'build/dist/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [false, false, false, false]) - - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build/dist"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${warning} typescript emit skipped`, - stream: 'stdout', - }, - { - message: `${error} typescript compiler errors`, - stream: 'stderr', - }, - ]) - }).timeout(0) - - test('build for production should copy package files to build folder', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'package.json', - JSON.stringify({ - name: 'my-dummy-app', - dependencies: { - lodash: 'latest', - }, - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - outDir: 'build', - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', '') - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - await execa('npm', ['install'], { - buffer: false, - cwd: fs.basePath, - stdio: 'inherit', - }) - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compileForProduction(false, 'npm') - - const hasFiles = await Promise.all( - [ - 'build/.adonisrc.json', - 'build/src/foo.js', - 'build/public/styles/main.css', - 'build/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [true, true, true, true]) - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow( - 'public/**/*.(js|css),ace,package.json,package-lock.json => build' - )} }`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('.adonisrc.json => build')} }`, - stream: 'stdout', - }, - { - message: `${success} built successfully`, - stream: 'stdout', - }, - { - message: '', - stream: 'stdout', - }, - ]) - - const hasPackageLock = await fs.fsExtra.pathExists( - join(fs.basePath, 'build', 'package-lock.json') - ) - assert.isTrue(hasPackageLock) - }).timeout(0) - - test('gracefully log error when ace file finishes with non-zero exit code', async ({ - assert, - }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', "console.error('foo');process.exit(1)") - await fs.add('src/foo.ts', '') - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile() - - const hasFiles = await Promise.all( - [ - 'build/.adonisrc.json', - 'build/ace', - 'build/src/foo.js', - 'build/public/styles/main.css', - 'build/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [false, false, false, false, false]) - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('public/**/*.(js|css),ace => build')} }`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('.adonisrc.json => build')} }`, - stream: 'stdout', - }, - { - message: `${warning} Unable to generate manifest file. Check the following error for more info`, - stream: 'stdout', - }, - { - message: 'foo', - stream: 'stderr', - }, - { - message: `${info} cleaning up ${dimYellow('"./build"')} directory`, - stream: 'stdout', - }, - ]) - - assert.isFalse(require(join(fs.basePath, 'build', '.adonisrc.json')).typescript) - }).timeout(0) - - test('ignore error when any of the meta file is missing', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/css/app.js'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - skipLibCheck: true, - }, - }) - ) - - await fs.add('src/foo.ts', '') - await fs.add('ace', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - await compiler.compile() - - const hasFiles = await Promise.all( - ['build/.adonisrc.json', 'build/ace', 'build/src/foo.js', 'build/public/css/app.js'].map( - (file) => fs.fsExtra.pathExists(join(fs.basePath, file)) - ) - ) - - ui.testingRenderer.logs.pop() - - assert.deepEqual(hasFiles, [true, true, true, false]) - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} cleaning up ${dimYellow('"./build"')} directory`, - stream: 'stdout', - }, - { - message: `${info} compiling typescript source files`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('public/css/app.js,ace => build')} }`, - stream: 'stdout', - }, - { - message: `${info} copy { ${dimYellow('.adonisrc.json => build')} }`, - stream: 'stdout', - }, - ]) - - assert.isFalse(require(join(fs.basePath, 'build', '.adonisrc.json')).typescript) - }).timeout(0) - - test('build should support custom tsconfig file', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - }) - ) - - await fs.add( - 'package.json', - JSON.stringify({ - name: 'my-dummy-app', - dependencies: {}, - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - outDir: 'build', - skipLibCheck: true, - }, - }) - ) - - await fs.add( - 'tsconfig.production.json', - JSON.stringify({ - extends: './tsconfig.json', - exclude: ['build', 'src/ignored.ts'], - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', '') - await fs.add('src/ignored.ts', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger, 'tsconfig.production.json') - await compiler.compileForProduction(false, 'npm') - - const hasFiles = await Promise.all( - ['build/.adonisrc.json', 'build/src/foo.js', 'build/src/ignored.js'].map((file) => - fs.fsExtra.pathExists(join(fs.basePath, file)) - ) - ) - - assert.deepEqual(hasFiles, [true, true, false]) - }).timeout(0) - - test('typecheck and report typescript errors', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - rootDir: './', - outDir: 'build/dist', - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', "import path from 'path'") - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - const isValid = await compiler.typeCheck() - - assert.isFalse(isValid) - const hasFiles = await Promise.all( - [ - 'build/dist/.adonisrc.json', - 'build/dist/src/foo.js', - 'build/dist/public/styles/main.css', - 'build/dist/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [false, false, false, false]) - - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} type checking typescript source files`, - stream: 'stdout', - }, - { - message: `${error} typescript compiler errors`, - stream: 'stderr', - }, - ]) - }).timeout(0) - - test('complete successfully when typechecking has no errors', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - typescript: true, - metaFiles: ['public/**/*.(js|css)'], - }) - ) - - await fs.add( - 'tsconfig.json', - JSON.stringify({ - include: ['**/*'], - exclude: ['build'], - compilerOptions: { - rootDir: './', - outDir: 'build/dist', - skipLibCheck: true, - }, - }) - ) - - await fs.add('ace', '') - await fs.add('src/foo.ts', "import 'path'") - await fs.add('public/styles/main.css', '') - await fs.add('public/scripts/main.js', '') - - const compiler = new Compiler(fs.basePath, [], false, ui.logger) - const isValid = await compiler.typeCheck() - - assert.isTrue(isValid) - const hasFiles = await Promise.all( - [ - 'build/dist/.adonisrc.json', - 'build/dist/src/foo.js', - 'build/dist/public/styles/main.css', - 'build/dist/public/scripts/main.js', - ].map((file) => fs.fsExtra.pathExists(join(fs.basePath, file))) - ) - - assert.deepEqual(hasFiles, [false, false, false, false]) - - assert.deepEqual(ui.testingRenderer.logs, [ - { - message: `${info} type checking typescript source files`, - stream: 'stdout', - }, - { - message: `${success} built successfully`, - stream: 'stdout', - }, - ]) - }).timeout(0) -}) diff --git a/test/configure-encore.spec.ts b/test/configure-encore.spec.ts deleted file mode 100644 index c01f59d..0000000 --- a/test/configure-encore.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import Invoke from '../commands/Invoke' - -const fs = new Filesystem(join(__dirname, '__app')) - -test.group('Configure Encore', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('setup encore', async ({ assert }) => { - await fs.add( - 'package.json', - JSON.stringify({ - name: 'sample_app', - }) - ) - - await fs.ensureRoot() - const app = new Application(fs.basePath, 'test', {}) - - const invoke = new Invoke(app, new Kernel(app).mockConsoleOutput()) - invoke.packages = ['encore'] - await invoke.run() - - const envFile = await fs.fsExtra.pathExists(join(fs.basePath, 'webpack.config.js')) - const envExampleFile = await fs.fsExtra.readFile( - join(fs.basePath, 'resources/js/app.js'), - 'utf-8' - ) - - const pkgFile = await fs.get('package.json') - assert.properties(JSON.parse(pkgFile).devDependencies, [ - '@babel/core', - '@babel/preset-env', - '@symfony/webpack-encore', - 'webpack', - 'webpack-cli', - ]) - - assert.isTrue(envFile) - assert.equal(envExampleFile.trim(), '// app entrypoint') - }) - .timeout(0) - .skip(!!process.env.CI) -}) diff --git a/test/configure-tests.spec.ts b/test/configure-tests.spec.ts deleted file mode 100644 index e73b51e..0000000 --- a/test/configure-tests.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import Invoke from '../commands/Invoke' - -const fs = new Filesystem(join(__dirname, '__app')) - -test.group('Configure Tests', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('setup tests', async ({ assert }) => { - await fs.add( - 'package.json', - JSON.stringify({ - name: 'sample_app', - }) - ) - - await fs.ensureRoot() - const app = new Application(fs.basePath, 'test', {}) - - const invoke = new Invoke(app, new Kernel(app).mockConsoleOutput()) - invoke.packages = ['tests'] - await invoke.run() - - assert.isTrue(await fs.fsExtra.pathExists(join(fs.basePath, 'test.ts'))) - assert.isTrue(await fs.fsExtra.pathExists(join(fs.basePath, 'tests/bootstrap.ts'))) - assert.isTrue( - await fs.fsExtra.pathExists(join(fs.basePath, 'tests/functional/hello_world.spec.ts')) - ) - assert.isTrue(await fs.fsExtra.pathExists(join(fs.basePath, 'contracts/tests.ts'))) - }) - .timeout(0) - .skip(!!process.env.CI) -}) diff --git a/test/env-parser.spec.ts b/test/env-parser.spec.ts deleted file mode 100644 index 02efc63..0000000 --- a/test/env-parser.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' - -import { EnvParser } from '../src/EnvParser' - -const fs = new Filesystem(join(__dirname, '__app')) - -test.group('EnvParser', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('ignore exception raised when unable to lookup .env file', async () => { - const envParser = new EnvParser() - await envParser.parse(fs.basePath) - }) - - test('get value for a key defined inside .env file', async ({ assert }) => { - await fs.add('.env', 'PORT=3333') - - const envParser = new EnvParser() - await envParser.parse(fs.basePath) - assert.equal(envParser.get('PORT'), '3333') - }) - - test('get an object of values for defined keys', async ({ assert }) => { - await fs.add('.env', ['PORT=3333', 'TZ=Asia/Calcutta'].join('\n')) - - const envParser = new EnvParser() - await envParser.parse(fs.basePath) - assert.deepEqual(envParser.asEnvObject(['PORT', 'TZ', 'HOST']), { - PORT: '3333', - TZ: 'Asia/Calcutta', - }) - }) -}) diff --git a/test/invoke-command.spec.ts b/test/invoke-command.spec.ts deleted file mode 100644 index 1098d52..0000000 --- a/test/invoke-command.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import Invoke from '../commands/Invoke' - -const fs = new Filesystem(join(__dirname, '__app')) - -test.group('Invoke', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('execute instructions defined in package.json file', async ({ assert }) => { - await fs.add( - 'node_modules/@adonisjs/sample/package.json', - JSON.stringify({ - name: '@adonisjs/sample', - adonisjs: { - env: { - PORT: '3333', - }, - }, - }) - ) - - const app = new Application(fs.basePath, 'test', {}) - - const invoke = new Invoke(app, new Kernel(app).mockConsoleOutput()) - invoke.packages = ['@adonisjs/sample'] - await invoke.run() - - const envFile = await fs.fsExtra.readFile(join(fs.basePath, '.env'), 'utf-8') - const envExampleFile = await fs.fsExtra.readFile(join(fs.basePath, '.env.example'), 'utf-8') - - assert.equal(envFile.trim(), 'PORT=3333') - assert.equal(envExampleFile.trim(), 'PORT=3333') - }) -}) diff --git a/test/make-command.spec.ts b/test/make-command.spec.ts deleted file mode 100644 index f2abf7f..0000000 --- a/test/make-command.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Kernel } from '@adonisjs/ace' -import { readJSONSync } from 'fs-extra' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { toNewlineArray } from '../test-helpers' -import MakeCommand from '../commands/Make/Command' - -const fs = new Filesystem(join(__dirname, '__app')) -const templates = new Filesystem(join(__dirname, '..', 'templates')) - -test.group('Make Command', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make a command inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const command = new MakeCommand(app, new Kernel(app).mockConsoleOutput()) - command.name = 'greet' - await command.run() - - const GreetCommand = await fs.get('commands/Greet.ts') - const CommandTemplate = await templates.get('command.txt') - assert.deepEqual( - toNewlineArray(GreetCommand), - toNewlineArray( - CommandTemplate.replace('{{ filename }}', 'Greet').replace( - '{{#toCommandName}}{{ filename }}{{/toCommandName}}', - 'greet' - ) - ) - ) - }) - - test('make a command inside a custom directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - directories: { - commands: './foo', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const command = new MakeCommand(app, new Kernel(app).mockConsoleOutput()) - command.name = 'greet' - await command.run() - - const GreetCommand = await fs.get('foo/Greet.ts') - const CommandTemplate = await templates.get('command.txt') - assert.deepEqual( - toNewlineArray(GreetCommand), - toNewlineArray( - CommandTemplate.replace('{{ filename }}', 'Greet').replace( - '{{#toCommandName}}{{ filename }}{{/toCommandName}}', - 'greet' - ) - ) - ) - }) - - test('convert camelcase command path to colon seperated name', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - directories: { - commands: './foo', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const command = new MakeCommand(app, new Kernel(app).mockConsoleOutput()) - command.name = 'RunInstructions' - await command.run() - - const GreetCommand = await fs.get('foo/RunInstructions.ts') - const CommandTemplate = await templates.get('command.txt') - assert.deepEqual( - toNewlineArray(GreetCommand), - toNewlineArray( - CommandTemplate.replace('{{ filename }}', 'RunInstructions').replace( - '{{#toCommandName}}{{ filename }}{{/toCommandName}}', - 'run:instructions' - ) - ) - ) - }) -}) diff --git a/test/make-controller.spec.ts b/test/make-controller.spec.ts deleted file mode 100644 index f6297ba..0000000 --- a/test/make-controller.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { readJSONSync } from 'fs-extra' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { toNewlineArray } from '../test-helpers' -import MakeController from '../commands/Make/Controller' - -const fs = new Filesystem(join(__dirname, '__app')) -const templates = new Filesystem(join(__dirname, '..', 'templates')) - -test.group('Make Controller', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make a controller inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const controller = new MakeController(app, new Kernel(app).mockConsoleOutput()) - controller.name = 'user' - await controller.run() - - const UsersController = await fs.get('app/Controllers/Http/UsersController.ts') - const ControllerTemplate = await templates.get('controller.txt') - assert.deepEqual( - toNewlineArray(UsersController), - toNewlineArray(ControllerTemplate.replace('{{ filename }}', 'UsersController')) - ) - }) - - test('make a resourceful controller inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const controller = new MakeController(app, new Kernel(app).mockConsoleOutput()) - controller.name = 'user' - controller.resource = true - await controller.run() - - const UsersController = await fs.get('app/Controllers/Http/UsersController.ts') - const ResourceTemplate = await templates.get('resource-controller.txt') - assert.deepEqual( - toNewlineArray(UsersController), - toNewlineArray(ResourceTemplate.replace('{{ filename }}', 'UsersController')) - ) - }) - - test('make a controller inside a custom directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - namespaces: { - httpControllers: 'App/Controllers', - }, - autoloads: { - App: './app', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const controller = new MakeController(app, new Kernel(app).mockConsoleOutput()) - controller.name = 'user' - await controller.run() - - const UsersController = await fs.get('app/Controllers/UsersController.ts') - const ControllerTemplate = await templates.get('controller.txt') - assert.deepEqual( - toNewlineArray(UsersController), - toNewlineArray(ControllerTemplate.replace('{{ filename }}', 'UsersController')) - ) - }) -}) diff --git a/test/make-exception.spec.ts b/test/make-exception.spec.ts deleted file mode 100644 index eb96bd4..0000000 --- a/test/make-exception.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { readJSONSync } from 'fs-extra' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { toNewlineArray } from '../test-helpers' -import MakeException from '../commands/Make/Exception' - -const fs = new Filesystem(join(__dirname, '__app')) -const templates = new Filesystem(join(__dirname, '..', 'templates')) - -test.group('Make Exception', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make an exception class inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const exception = new MakeException(app, new Kernel(app).mockConsoleOutput()) - exception.name = 'user' - await exception.run() - - const UserException = await fs.get('app/Exceptions/UserException.ts') - const ExceptionTemplate = await templates.get('exception.txt') - assert.deepEqual( - toNewlineArray(UserException), - toNewlineArray( - ExceptionTemplate.replace(new RegExp('\\{{ filename }}', 'g'), 'UserException') - ) - ) - }) - - test('make a self-handled exception class inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const exception = new MakeException(app, new Kernel(app).mockConsoleOutput()) - exception.name = 'user' - exception.selfHandle = true - await exception.run() - - const UserException = await fs.get('app/Exceptions/UserException.ts') - const ExceptionTemplate = await templates.get('self-handle-exception.txt') - assert.deepEqual( - toNewlineArray(UserException), - toNewlineArray( - ExceptionTemplate.replace(new RegExp('\\{{ filename }}', 'g'), 'UserException') - ) - ) - }) - - test('make an exception class inside a custom directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - namespaces: { - exceptions: 'App/Exceptions/Custom', - }, - aliases: { - App: './app', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const exception = new MakeException(app, new Kernel(app).mockConsoleOutput()) - exception.name = 'user' - exception.selfHandle = true - await exception.run() - - const UserException = await fs.get('app/Exceptions/Custom/UserException.ts') - const ExceptionTemplate = await templates.get('self-handle-exception.txt') - assert.deepEqual( - toNewlineArray(UserException), - toNewlineArray( - ExceptionTemplate.replace(new RegExp('\\{{ filename }}', 'g'), 'UserException') - ) - ) - }) -}) diff --git a/test/make-listener.spec.ts b/test/make-listener.spec.ts deleted file mode 100644 index 97e33c7..0000000 --- a/test/make-listener.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { readJSONSync } from 'fs-extra' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { toNewlineArray } from '../test-helpers' -import MakeListener from '../commands/Make/Listener' - -const fs = new Filesystem(join(__dirname, '__app')) -const templates = new Filesystem(join(__dirname, '..', 'templates')) - -test.group('Make Listener', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make a listener inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const listener = new MakeListener(app, new Kernel(app).mockConsoleOutput()) - listener.name = 'user' - await listener.run() - - const UserListener = await fs.get('app/Listeners/User.ts') - const ListenerTemplate = await templates.get('event-listener.txt') - assert.deepEqual( - toNewlineArray(UserListener), - toNewlineArray(ListenerTemplate.replace('{{ filename }}', 'User')) - ) - }) - - test('make a listener inside a custom directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - namespaces: { - eventListeners: 'App/Events/Listeners', - }, - aliases: { - App: './app', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const listener = new MakeListener(app, new Kernel(app).mockConsoleOutput()) - listener.name = 'user' - await listener.run() - - const UserListener = await fs.get('app/Events/Listeners/User.ts') - const ListenerTemplate = await templates.get('event-listener.txt') - assert.deepEqual( - toNewlineArray(UserListener), - toNewlineArray(ListenerTemplate.replace('{{ filename }}', 'User')) - ) - }) -}) diff --git a/test/make-middleware.spec.ts b/test/make-middleware.spec.ts deleted file mode 100644 index 4657e84..0000000 --- a/test/make-middleware.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { readJSONSync } from 'fs-extra' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { toNewlineArray } from '../test-helpers' -import MakeMiddleware from '../commands/Make/Middleware' - -const fs = new Filesystem(join(__dirname, '__app')) -const templates = new Filesystem(join(__dirname, '..', 'templates')) - -test.group('Make Middleware', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make a middleware inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const middleware = new MakeMiddleware(app, new Kernel(app).mockConsoleOutput()) - middleware.name = 'spoof_accept' - await middleware.run() - - const SpoofMiddleware = await fs.get('app/Middleware/SpoofAccept.ts') - const MiddlewareTemplate = await templates.get('middleware.txt') - assert.deepEqual( - toNewlineArray(SpoofMiddleware), - toNewlineArray(MiddlewareTemplate.replace('{{ filename }}', 'SpoofAccept')) - ) - }) - - test('make a middleware inside a custom directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - namespaces: { - middleware: 'App/Module/Testing/Middleware', - }, - autoloads: { - App: './app', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const middleware = new MakeMiddleware(app, new Kernel(app).mockConsoleOutput()) - middleware.name = 'spoof_accept' - await middleware.run() - - const SpoofMiddleware = await fs.get('app/Module/Testing/Middleware/SpoofAccept.ts') - const MiddlewareTemplate = await templates.get('middleware.txt') - assert.deepEqual( - toNewlineArray(SpoofMiddleware), - toNewlineArray(MiddlewareTemplate.replace('{{ filename }}', 'SpoofAccept')) - ) - }) -}) diff --git a/test/make-preloaded-file.spec.ts b/test/make-preloaded-file.spec.ts deleted file mode 100644 index 63bde1d..0000000 --- a/test/make-preloaded-file.spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { readJSONSync } from 'fs-extra' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { toNewlineArray } from '../test-helpers' -import PreloadFile from '../commands/Make/PreloadFile' - -const fs = new Filesystem(join(__dirname, '__app')) -const templates = new Filesystem(join(__dirname, '..', 'templates')) - -test.group('Make Preloaded File', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make a preload file inside the start directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const preloadFile = new PreloadFile(app, new Kernel(app).mockConsoleOutput()) - preloadFile.name = 'viewGlobals' - preloadFile.environment = ['console', 'web'] - await preloadFile.run() - - const viewGlobals = await fs.get('start/viewGlobals.ts') - const preloadTemplate = await templates.get('preload-file.txt') - assert.deepEqual(toNewlineArray(viewGlobals), toNewlineArray(preloadTemplate)) - - const rcRawContents = await fs.get('.adonisrc.json') - assert.deepEqual(JSON.parse(rcRawContents), { - preloads: [ - { - file: './start/viewGlobals', - environment: ['console', 'web'], - }, - ], - }) - }) - - test('make a preload file inside custom directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - directories: { - start: 'foo', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const preloadFile = new PreloadFile(app, new Kernel(app).mockConsoleOutput()) - preloadFile.name = 'viewGlobals' - preloadFile.environment = ['console', 'web'] - await preloadFile.run() - - const viewGlobals = await fs.get('foo/viewGlobals.ts') - const preloadTemplate = await templates.get('preload-file.txt') - assert.deepEqual(toNewlineArray(viewGlobals), toNewlineArray(preloadTemplate)) - - const rcRawContents = await fs.get('.adonisrc.json') - assert.deepEqual(JSON.parse(rcRawContents), { - directories: { start: 'foo' }, - preloads: [ - { - file: './foo/viewGlobals', - environment: ['console', 'web'], - }, - ], - }) - }) - - test('set preload file environment as repl', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const preloadFile = new PreloadFile(app, new Kernel(app).mockConsoleOutput()) - preloadFile.name = 'repl' - preloadFile.environment = ['repl'] - await preloadFile.run() - - const replFile = await fs.get('start/repl.ts') - const preloadTemplate = await templates.get('preload-file.txt') - assert.deepEqual(toNewlineArray(replFile), toNewlineArray(preloadTemplate)) - - const rcRawContents = await fs.get('.adonisrc.json') - assert.deepEqual(JSON.parse(rcRawContents), { - preloads: [ - { - file: './start/repl', - environment: ['repl'], - }, - ], - }) - }) - - test('prompt for environment when not explicitly defined', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const preloadFile = new PreloadFile(app, new Kernel(app).mockConsoleOutput()) - preloadFile.prompt.on('prompt', (question) => { - question.select(2) - }) - - preloadFile.name = 'repl' - await preloadFile.exec() - - const replFile = await fs.get('start/repl.ts') - const preloadTemplate = await templates.get('preload-file.txt') - assert.deepEqual(toNewlineArray(replFile), toNewlineArray(preloadTemplate)) - - const rcRawContents = await fs.get('.adonisrc.json') - assert.deepEqual(JSON.parse(rcRawContents), { - preloads: [ - { - file: './start/repl', - environment: ['repl'], - }, - ], - }) - }) - - test('do not set environment when all is selected', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const preloadFile = new PreloadFile(app, new Kernel(app).mockConsoleOutput()) - preloadFile.prompt.on('prompt', (question) => { - question.select(0) - }) - - preloadFile.name = 'events' - await preloadFile.exec() - - const replFile = await fs.get('start/events.ts') - const preloadTemplate = await templates.get('preload-file.txt') - assert.deepEqual(toNewlineArray(replFile), toNewlineArray(preloadTemplate)) - - const rcRawContents = await fs.get('.adonisrc.json') - assert.deepEqual(JSON.parse(rcRawContents), { - preloads: ['./start/events'], - }) - }) -}) diff --git a/test/make-provider.spec.ts b/test/make-provider.spec.ts deleted file mode 100644 index d979209..0000000 --- a/test/make-provider.spec.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { readJSONSync } from 'fs-extra' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { toNewlineArray } from '../test-helpers' -import MakeProvider from '../commands/Make/Provider' - -const fs = new Filesystem(join(__dirname, '__app')) -const templates = new Filesystem(join(__dirname, '..', 'templates')) - -test.group('Make Provider', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make a provider inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const provider = new MakeProvider(app, new Kernel(app).mockConsoleOutput()) - provider.name = 'app' - await provider.run() - - const AppProvider = await fs.get('providers/AppProvider.ts') - const ProviderTemplate = await templates.get('provider.txt') - assert.deepEqual( - toNewlineArray(AppProvider), - toNewlineArray(ProviderTemplate.replace('{{ filename }}', 'AppProvider')) - ) - - const rcRawContents = await fs.get('.adonisrc.json') - assert.deepEqual(JSON.parse(rcRawContents), { - providers: ['./providers/AppProvider'], - }) - }) - - test('make a provider inside a custom directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - directories: { - providers: 'foo', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const provider = new MakeProvider(app, new Kernel(app).mockConsoleOutput()) - provider.name = 'app' - await provider.run() - - const AppProvider = await fs.get('foo/AppProvider.ts') - const ProviderTemplate = await templates.get('provider.txt') - assert.deepEqual( - toNewlineArray(AppProvider), - toNewlineArray(ProviderTemplate.replace('{{ filename }}', 'AppProvider')) - ) - - const rcRawContents = await fs.get('.adonisrc.json') - assert.deepEqual(JSON.parse(rcRawContents), { - directories: { - providers: 'foo', - }, - providers: ['./foo/AppProvider'], - }) - }) - - test('setup correct path when nested provider is created', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const provider = new MakeProvider(app, new Kernel(app).mockConsoleOutput()) - provider.name = 'auth/app' - await provider.run() - - const AppProvider = await fs.get('providers/auth/AppProvider.ts') - const ProviderTemplate = await templates.get('provider.txt') - assert.deepEqual( - toNewlineArray(AppProvider), - toNewlineArray(ProviderTemplate.replace('{{ filename }}', 'AppProvider')) - ) - - const rcRawContents = await fs.get('.adonisrc.json') - assert.deepEqual(JSON.parse(rcRawContents), { - providers: ['./providers/auth/AppProvider'], - }) - }) - - test('make ace provider', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const provider = new MakeProvider(app, new Kernel(app).mockConsoleOutput()) - provider.name = 'app' - provider.ace = true - await provider.run() - - const AppProvider = await fs.get('providers/AppProvider.ts') - const ProviderTemplate = await templates.get('provider.txt') - assert.deepEqual( - toNewlineArray(AppProvider), - toNewlineArray(ProviderTemplate.replace('{{ filename }}', 'AppProvider')) - ) - - const rcRawContents = await fs.get('.adonisrc.json') - assert.deepEqual(JSON.parse(rcRawContents), { - aceProviders: ['./providers/AppProvider'], - }) - }) -}) diff --git a/test/make-suite.spec.ts b/test/make-suite.spec.ts deleted file mode 100644 index ce28f01..0000000 --- a/test/make-suite.spec.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' -import { files } from '@adonisjs/sink' -import CreateSuite from '../commands/Make/Suite' - -const fs = new Filesystem(join(__dirname, '__app')) - -test.group('Make Suite', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('Should add suite to RcFile and create sample test', async ({ assert }) => { - await fs.ensureRoot() - const app = new Application(fs.basePath, 'test', {}) - const suiteName = 'my-super-suite' - const createSuite = new CreateSuite(app, new Kernel(app).mockConsoleOutput()) - - createSuite.suite = suiteName - await createSuite.run() - - const sampleTestExist = fs.fsExtra.pathExistsSync( - join(fs.basePath, `tests/${suiteName}/test.spec.ts`) - ) - assert.isTrue(sampleTestExist) - - const rcFile = new files.AdonisRcFile(fs.basePath) - assert.deepEqual(rcFile.get('tests.suites'), [ - { - name: suiteName, - files: [`tests/${suiteName}/**/*.spec(.ts|.js)`], - timeout: 60000, - }, - ]) - }) - - test("Shouldn't add suite to RcFile if it already exists", async ({ assert }) => { - await fs.ensureRoot() - const app = new Application(fs.basePath, 'test', {}) - const suiteName = 'my-super-suite' - const createSuite = new CreateSuite(app, new Kernel(app).mockConsoleOutput()) - - createSuite.suite = suiteName - await createSuite.run() - await createSuite.run() - await createSuite.run() - - const rcFile = new files.AdonisRcFile(fs.basePath) - assert.deepEqual(rcFile.get('tests.suites'), [ - { - name: suiteName, - files: [`tests/${suiteName}/**/*.spec(.ts|.js)`], - timeout: 60000, - }, - ]) - }) - - test("Shouldn't add a sample file if specified", async ({ assert }) => { - await fs.ensureRoot() - const app = new Application(fs.basePath, 'test', {}) - - const suiteName = 'my-super-suite' - const createSuite = new CreateSuite(app, new Kernel(app).mockConsoleOutput()) - - createSuite.suite = suiteName - createSuite.withExampleTest = false - await createSuite.run() - - const sampleTestExist = fs.fsExtra.pathExistsSync( - join(fs.basePath, `tests/${suiteName}/test.spec.ts`) - ) - - assert.isFalse(sampleTestExist) - }) - - test('Custom location - {location}') - .with([ - { location: 'tests/unit/**.spec.ts', filePath: 'tests/unit/test.spec.ts' }, - { location: 'tests/a/**/*.spec.ts', filePath: 'tests/a/test.spec.ts' }, - { - location: 'tests/a/b/c/**.spec.ts', - filePath: 'tests/a/b/c/test.spec.ts', - }, - { - location: 'tests/my-tests', - globPattern: 'tests/my-tests/**/*.spec(.ts|.js)', - filePath: 'tests/my-tests/test.spec.ts', - }, - { - location: '', - globPattern: 'tests/my-super-suite/**/*.spec(.ts|.js)', - filePath: 'tests/my-super-suite/test.spec.ts', - }, - ]) - .run(async ({ assert }, { location, filePath, globPattern }) => { - await fs.ensureRoot() - const app = new Application(fs.basePath, 'test', {}) - const suiteName = 'my-super-suite' - const createSuite = new CreateSuite(app, new Kernel(app).mockConsoleOutput()) - - createSuite.suite = suiteName - createSuite.location = location - - await createSuite.run() - - const sampleTestExist = fs.fsExtra.pathExistsSync(join(fs.basePath, filePath)) - assert.isTrue(sampleTestExist) - - const rcFile = new files.AdonisRcFile(fs.basePath) - assert.deepEqual(rcFile.get('tests.suites'), [ - { - name: suiteName, - files: [globPattern || location], - timeout: 60000, - }, - ]) - }) -}) diff --git a/test/make-test.spec.ts b/test/make-test.spec.ts deleted file mode 100644 index ac02116..0000000 --- a/test/make-test.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { readJSONSync } from 'fs-extra' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import MakeTest from '../commands/Make/Test' -import { toNewlineArray } from '../test-helpers' - -const fs = new Filesystem(join(__dirname, '__app')) -const templates = new Filesystem(join(__dirname, '..', 'templates')) - -test.group('Make Test', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make a test inside the suite directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - tests: { - suites: [{ name: 'functional', files: ['tests/functional/**/*.spec.ts'] }], - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const makeTest = new MakeTest(app, new Kernel(app).mockConsoleOutput()) - makeTest.suite = 'functional' - makeTest.name = 'Users' - await makeTest.run() - - const testFile = await fs.get('tests/functional/user.spec.ts') - const testTemplate = await templates.get('test.txt') - assert.deepEqual( - toNewlineArray(testFile), - toNewlineArray(testTemplate.replace('{{ name }}', 'Users')) - ) - }) - - test('make a test inside nested directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - tests: { - suites: [{ name: 'functional', files: ['tests/functional/**/*.spec.ts'] }], - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const makeTest = new MakeTest(app, new Kernel(app).mockConsoleOutput()) - makeTest.suite = 'functional' - makeTest.name = 'users/index' - await makeTest.run() - - const testFile = await fs.get('tests/functional/users/index.spec.ts') - const testTemplate = await templates.get('test.txt') - assert.deepEqual( - toNewlineArray(testFile), - toNewlineArray(testTemplate.replace('{{ name }}', 'Users index')) - ) - }) - - test('return error when suite is not registered', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const makeTest = new MakeTest(app, new Kernel(app).mockConsoleOutput()) - makeTest.suite = 'functional' - makeTest.name = 'Users/index' - await makeTest.run() - - assert.deepEqual(makeTest.ui.testingRenderer.logs, [ - { - stream: 'stderr', - message: - '[ red(error) ] Invalid suite "functional". Make sure the suite is registered inside the .adonisrc.json file', - }, - ]) - - assert.isFalse(await fs.exists('tests/functional/users/index.spec.ts')) - }) -}) diff --git a/test/make-validator.spec.ts b/test/make-validator.spec.ts deleted file mode 100644 index c431de1..0000000 --- a/test/make-validator.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { readJSONSync } from 'fs-extra' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { toNewlineArray } from '../test-helpers' -import MakeValidator from '../commands/Make/Validator' - -const fs = new Filesystem(join(__dirname, '__app')) -const templates = new Filesystem(join(__dirname, '..', 'templates')) - -test.group('Make Validator', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make a model inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const validator = new MakeValidator(app, new Kernel(app).mockConsoleOutput()) - validator.name = 'user' - await validator.run() - - const UserValidator = await fs.get('app/Validators/UserValidator.ts') - const ValidatorTemplate = await templates.get('validator.txt') - assert.deepEqual( - toNewlineArray(UserValidator), - toNewlineArray( - ValidatorTemplate.replace(new RegExp('\\{{ filename }}', 'g'), 'UserValidator') - ) - ) - }) - - test('make a validator inside a custom directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - namespaces: { - validators: 'App', - }, - autoloads: { - App: './app', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const validator = new MakeValidator(app, new Kernel(app).mockConsoleOutput()) - validator.name = 'user' - await validator.run() - - const UserValidator = await fs.get('app/UserValidator.ts') - const ValidatorTemplate = await templates.get('validator.txt') - assert.deepEqual( - toNewlineArray(UserValidator), - toNewlineArray( - ValidatorTemplate.replace(new RegExp('\\{{ filename }}', 'g'), 'UserValidator') - ) - ) - }) -}) diff --git a/test/make-view.spec.ts b/test/make-view.spec.ts deleted file mode 100644 index 27f6f75..0000000 --- a/test/make-view.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { readJSONSync } from 'fs-extra' -import { Kernel } from '@adonisjs/ace' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import MakeView from '../commands/Make/View' - -const fs = new Filesystem(join(__dirname, '__app')) - -test.group('Make Command', (group) => { - group.setup(() => { - process.env.ADONIS_ACE_CWD = fs.basePath - }) - - group.teardown(() => { - delete process.env.ADONIS_ACE_CWD - }) - - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('make an empty view inside the default directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const view = new MakeView(app, new Kernel(app).mockConsoleOutput()) - view.name = 'welcome' - await view.run() - - const welcomeView = await fs.get('resources/views/welcome.edge') - assert.deepEqual(welcomeView.trim(), '') - }) - - test('make a view inside a nested directory', async ({ assert }) => { - await fs.add('.adonisrc.json', JSON.stringify({})) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const view = new MakeView(app, new Kernel(app).mockConsoleOutput()) - view.name = 'users/welcome' - await view.run() - - const welcomeView = await fs.get('resources/views/users/welcome.edge') - assert.deepEqual(welcomeView.trim(), '') - }) - - test('make an empty view inside custom directory', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - directories: { - views: 'public/views', - }, - }) - ) - - const rcContents = readJSONSync(join(fs.basePath, '.adonisrc.json')) - const app = new Application(fs.basePath, 'test', rcContents) - - const view = new MakeView(app, new Kernel(app).mockConsoleOutput()) - view.name = 'welcome' - await view.run() - - const welcomeView = await fs.get('public/views/welcome.edge') - assert.deepEqual(welcomeView.trim(), '') - }) -}) diff --git a/test/rc-file.spec.ts b/test/rc-file.spec.ts deleted file mode 100644 index dd86ae4..0000000 --- a/test/rc-file.spec.ts +++ /dev/null @@ -1,242 +0,0 @@ -/* - * @adonisjs/assembler - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' - -import { RcFile } from '../src/RcFile' - -const fs = new Filesystem(join(__dirname, '__app')) - -test.group('RcFile', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('get an array of meta file patterns from the rcfile', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: ['.env', 'public/**/*.(css|js)'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.deepEqual(rcFile.getMetaFilesGlob(), ['.env', 'public/**/*.(css|js)', 'ace']) - }) - - test('get info about a meta file', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: [{ pattern: '.env', reloadServer: false }, 'public/**/*.(css|js)'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.deepEqual(rcFile.getMetaData('.env'), { - metaFile: true, - reload: false, - testFile: false, - rcFile: false, - }) - - assert.deepEqual(rcFile.getMetaData('public/foo.js'), { - metaFile: true, - reload: true, - testFile: false, - rcFile: false, - }) - - assert.deepEqual(rcFile.getMetaData('schema/app.js'), { - metaFile: false, - reload: false, - testFile: false, - rcFile: false, - }) - }) - - test('match relative paths against meta files', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: [{ pattern: '.env', reloadServer: false }, 'public/**/*.(css|js)'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.isTrue(rcFile.isMetaFile('.env')) - assert.isTrue(rcFile.isMetaFile('public/style.css')) - assert.isTrue(rcFile.isMetaFile('public/script.js')) - assert.isFalse(rcFile.isMetaFile('public/script.sass')) - }) - - test('match relative paths against reloadServer meta files', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: [{ pattern: '.env', reloadServer: false }, 'public/**/*.(css|js)'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.isTrue(rcFile.isRestartServerFile('public/style.css')) - assert.isTrue(rcFile.isRestartServerFile('public/script.js')) - assert.isFalse(rcFile.isRestartServerFile('.env')) - }) - - test('filter .adonisrc.json file from files globs array', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: [ - '.adonisrc.json', - { pattern: '.env', reloadServer: false }, - 'public/**/*.(css|js)', - ], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.deepEqual(rcFile.getMetaFilesGlob(), ['.env', 'public/**/*.(css|js)', 'ace']) - }) - - test('filter ace file from files globs array', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: ['ace', { pattern: '.env', reloadServer: false }, 'public/**/*.(css|js)'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.deepEqual(rcFile.getMetaFilesGlob(), ['.env', 'public/**/*.(css|js)', 'ace']) - }) - - test('get metadata for files', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: [ - '.adonisrc.json', - { pattern: '.env', reloadServer: false }, - 'public/**/*.(css|js)', - ], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.deepEqual(rcFile.getMetaData('.adonisrc.json'), { - reload: true, - rcFile: true, - testFile: false, - metaFile: true, - }) - - assert.deepEqual(rcFile.getMetaData('public/style.css'), { - reload: true, - rcFile: false, - testFile: false, - metaFile: true, - }) - - assert.deepEqual(rcFile.getMetaData('.env'), { - reload: false, - rcFile: false, - testFile: false, - metaFile: true, - }) - - assert.deepEqual(rcFile.getMetaData('foo/bar.js'), { - reload: false, - rcFile: false, - testFile: false, - metaFile: false, - }) - }) - - test('match sub paths to the defined command path', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: [], - commands: ['./commands'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.isTrue(rcFile.isCommandsPath('commands/foo.ts')) - }) - - test('match actual path to the defined command path', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: [], - commands: ['./commands'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.isTrue(rcFile.isCommandsPath('commands.ts')) - }) - - test('do not work when commands refer to path outside the project root', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: [], - commands: ['../commands'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.isFalse(rcFile.isCommandsPath('commands.ts')) - assert.isFalse(rcFile.isCommandsPath('commands/foo.ts')) - }) - - test('do not work when commands refer to a package', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: [], - commands: ['@adonisjs/foo'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.isFalse(rcFile.isCommandsPath('@adonisjs/foo.ts')) - assert.isFalse(rcFile.isCommandsPath('@adonisjs/foo/foo.ts')) - }) - - test('read file from the disk by-passing the cache', async ({ assert }) => { - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: ['.env', 'public/**/*.(css|js)'], - }) - ) - - const rcFile = new RcFile(fs.basePath) - assert.deepEqual(rcFile.getDiskContents(), { - metaFiles: ['.env', 'public/**/*.(css|js)'], - }) - - await fs.add( - '.adonisrc.json', - JSON.stringify({ - metaFiles: ['.env'], - }) - ) - assert.deepEqual(rcFile.getDiskContents(), { - metaFiles: ['.env'], - }) - }) -}) diff --git a/tests/__snapshots__/code_transformer.spec.ts.cjs b/tests/__snapshots__/code_transformer.spec.ts.cjs new file mode 100644 index 0000000..9267556 --- /dev/null +++ b/tests/__snapshots__/code_transformer.spec.ts.cjs @@ -0,0 +1,400 @@ +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')) + +server.use([ + () => import('#foo/middleware.js'), + () => import('@adonisjs/static/static_middleware'), + () => import('#middleware/container_bindings_middleware'), + () => import('@adonisjs/session/session_middleware'), + () => import('#foo/middleware2.js') +]) + +router.use([ + () => import('@adonisjs/core/bodyparser_middleware'), + () => import('@adonisjs/shield/shield_middleware'), +]) + +export const middleware = router.named({}) +"` + +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')) + +server.use([ + () => import('#middleware/container_bindings_middleware'), + () => import('@adonisjs/session/session_middleware'), +]) + +router.use([ + () => import('#foo/bar.js'), + () => import('@adonisjs/core/bodyparser_middleware'), + () => import('@adonisjs/shield/shield_middleware'), + () => import('@adonisjs/random_middleware') +]) + +export const middleware = router.named({}) +"` + +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')) + +server.use([ + () => import('#middleware/container_bindings_middleware'), + () => import('@adonisjs/session/session_middleware'), + () => import('@adonisjs/random_middleware') +]) + +router.use([ + () => import('#foo/bar.js'), + () => import('@adonisjs/core/bodyparser_middleware'), + () => import('@adonisjs/shield/shield_middleware'), +]) + +export const middleware = router.named({}) +"` + +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), + PORT: Env.schema.number(), + + /* + |---------------------------------------------------------- + | Redis configuration + |---------------------------------------------------------- + */ + REDIS_HOST: Env.schema.string.optional(), + REDIS_PORT: Env.schema.number() +}) +"` + +exports[`Code transformer | addProvider > add provider to rc file with specific environments 1`] = `"import { defineConfig } from '@adonisjs/core/app' + +export default defineConfig({ + typescript: true, + preloads: [ + () => import('./start/routes.ts'), + { + file: () => import('./start/ace.ts'), + environment: ['console'], + }, + ], + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + { + file: () => import('@adonisjs/core/providers/repl_provider'), + environment: ['repl'], + }, + { + file: () => import('@adonisjs/redis-provider'), + environment: ['console', 'repl'], + } + ], + metaFiles: [ + { + pattern: 'public/**', + reloadServer: true + }, + ], + commands: [ + () => import('@adonisjs/core/commands') + ] +}) +"` + +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, + preloads: [ + () => import('./start/routes.ts'), + { + file: () => import('./start/ace.ts'), + environment: ['console'], + }, + ], + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + { + file: () => import('@adonisjs/core/providers/repl_provider'), + environment: ['repl'], + }, + () => import('@adonisjs/redis-provider') + ], + metaFiles: [ + { + pattern: 'public/**', + reloadServer: true + }, + ], + commands: [ + () => import('@adonisjs/core/commands') + ] +}) +"` + +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, + preloads: [ + () => import('./start/routes.ts'), + { + file: () => import('./start/ace.ts'), + environment: ['console'], + }, + ], + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + { + file: () => import('@adonisjs/core/providers/repl_provider'), + environment: ['repl'], + } + ], + metaFiles: [ + { + pattern: 'public/**', + reloadServer: true + }, + { + pattern: 'assets/**', + reloadServer: true, + } + ], + commands: [ + () => import('@adonisjs/core/commands') + ] +}) +"` + +exports[`Code transformer | setDirectory > set directory in rc file 1`] = `"import { defineConfig } from '@adonisjs/core/app' + +export default defineConfig({ + typescript: true, + preloads: [ + () => import('./start/routes.ts'), + { + file: () => import('./start/ace.ts'), + environment: ['console'], + }, + ], + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + { + file: () => import('@adonisjs/core/providers/repl_provider'), + environment: ['repl'], + } + ], + metaFiles: [ + { + pattern: 'public/**', + reloadServer: true + }, + ], + commands: [ + () => import('@adonisjs/core/commands') + ], + directories: { + views: 'templates' + } +}) +"` + +exports[`Code transformer | setCommandAlias > set command alias in rc file 1`] = `"import { defineConfig } from '@adonisjs/core/app' + +export default defineConfig({ + typescript: true, + preloads: [ + () => import('./start/routes.ts'), + { + file: () => import('./start/ace.ts'), + environment: ['console'], + }, + ], + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + { + file: () => import('@adonisjs/core/providers/repl_provider'), + environment: ['repl'], + } + ], + metaFiles: [ + { + pattern: 'public/**', + reloadServer: true + }, + ], + commands: [ + () => import('@adonisjs/core/commands') + ], + commandsAliases: { + migrate: 'migration:run' + } +}) +"` + +exports[`Code transformer | addPreloadFile > add preload file with specific environments 1`] = `"import { defineConfig } from '@adonisjs/core/app' + +export default defineConfig({ + typescript: true, + preloads: [ + () => import('./start/routes.ts'), + { + file: () => import('./start/ace.ts'), + environment: ['console'], + }, + { + file: () => import('#start/foo.js'), + environment: ['console', 'repl'], + } + ], + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + { + file: () => import('@adonisjs/core/providers/repl_provider'), + environment: ['repl'], + } + ], + metaFiles: [ + { + pattern: 'public/**', + reloadServer: true + }, + ], + commands: [ + () => import('@adonisjs/core/commands') + ] +}) +"` + +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')) + +server.use([ + () => import('#middleware/container_bindings_middleware'), + () => import('@adonisjs/session/session_middleware'), +]) + +router.use([ + () => import('@adonisjs/core/bodyparser_middleware'), + () => import('@adonisjs/shield/shield_middleware'), +]) + +export const middleware = router.named({ + auth: () => import('#foo/bar3.js') +}) +"` + +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')) + +server.use([ + () => import('#middleware/container_bindings_middleware'), + () => import('@adonisjs/session/session_middleware'), +]) + +router.use([ + () => import('@adonisjs/core/bodyparser_middleware'), + () => import('@adonisjs/shield/shield_middleware'), +]) + +export const middleware = router.named({ + auth: () => import('#foo/bar.js') +}) +"` + +exports[`Code transformer | addCommand > add command to rc file 1`] = `"import { defineConfig } from '@adonisjs/core/app' + +export default defineConfig({ + typescript: true, + preloads: [ + () => import('./start/routes.ts'), + { + file: () => import('./start/ace.ts'), + environment: ['console'], + }, + ], + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + { + file: () => import('@adonisjs/core/providers/repl_provider'), + environment: ['repl'], + } + ], + metaFiles: [ + { + pattern: 'public/**', + reloadServer: true + }, + ], + commands: [ + () => import('@adonisjs/core/commands'), + () => import('#foo/bar.js'), + () => import('#foo/bar2.js') + ] +}) +"` + +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, + commands: [() => import('#foo/bar.js')] +}) +"` + +exports[`Code transformer | addJapaPlugin > addJapaPlugin with named import 1`] = `" +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) +] +"` + +exports[`Code transformer | addJapaPlugin > addJapaPlugin with default import 1`] = `" +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() +] +"` + +exports[`Code transformer | addJapaPlugin > add named import 1`] = `" +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) +] +"` + +exports[`Code transformer | addJapaPlugin > add default import 1`] = `" +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() +] +"` + diff --git a/tests/bundler.spec.ts b/tests/bundler.spec.ts new file mode 100644 index 0000000..2d0b040 --- /dev/null +++ b/tests/bundler.spec.ts @@ -0,0 +1,139 @@ +/* + * @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' + +test.group('Bundler', () => { + test('should copy metafiles to the build directory', async ({ assert, fs }) => { + await Promise.all([ + fs.create( + 'tsconfig.json', + JSON.stringify({ compilerOptions: { outDir: 'build', skipLibCheck: true } }) + ), + fs.create('adonisrc.ts', 'export default {}'), + fs.create('package.json', '{}'), + fs.create('package-lock.json', '{}'), + + fs.create('resources/js/app.ts', ''), + fs.create('resources/views/app.edge', ''), + fs.create('resources/views/foo.edge', ''), + fs.create('resources/views/nested/bar.edge', ''), + fs.create('resources/views/nested/baz.edge', ''), + ]) + + const bundler = new Bundler(fs.baseUrl, ts, { + metaFiles: [ + { + pattern: 'resources/views/**/*.edge', + reloadServer: false, + }, + ], + }) + + await bundler.bundle(true, 'npm') + + await Promise.all([ + assert.fileExists('./build/resources/views/app.edge'), + assert.fileExists('./build/resources/views/foo.edge'), + assert.fileExists('./build/resources/views/nested/bar.edge'), + assert.fileExists('./build/resources/views/nested/baz.edge'), + assert.fileExists('./build/package.json'), + assert.fileExists('./build/adonisrc.js'), + assert.fileExists('./build/package-lock.json'), + ]) + }) + + test('should copy metafiles even if lock file is missing', async ({ assert, fs }) => { + await Promise.all([ + fs.create( + 'tsconfig.json', + JSON.stringify({ compilerOptions: { outDir: 'build', skipLibCheck: true } }) + ), + fs.create('adonisrc.ts', 'export default {}'), + fs.create('package.json', '{}'), + + fs.create('resources/views/app.edge', ''), + ]) + + const bundler = new Bundler(fs.baseUrl, ts, { + metaFiles: [ + { + pattern: 'resources/views/**/*.edge', + reloadServer: false, + }, + ], + }) + + await bundler.bundle(true, 'npm') + + await Promise.all([ + assert.fileExists('./build/resources/views/app.edge'), + assert.fileExists('./build/package.json'), + assert.fileExists('./build/adonisrc.js'), + ]) + }) + + test('use npm by default if not specified', async ({ assert, fs }) => { + await Promise.all([ + fs.create( + 'tsconfig.json', + JSON.stringify({ compilerOptions: { outDir: 'build', skipLibCheck: true } }) + ), + fs.create('adonisrc.ts', 'export default {}'), + fs.create('package.json', '{}'), + fs.create('package-lock.json', '{}'), + ]) + + const bundler = new Bundler(fs.baseUrl, ts, { + metaFiles: [ + { + pattern: 'resources/views/**/*.edge', + reloadServer: false, + }, + ], + }) + + await bundler.bundle(true) + + await Promise.all([ + assert.fileExists('./build/package.json'), + assert.fileExists('./build/package-lock.json'), + ]) + }) + + test('detect package manager if not specified', async ({ assert, fs }) => { + await Promise.all([ + fs.create( + 'tsconfig.json', + JSON.stringify({ compilerOptions: { outDir: 'build', skipLibCheck: true } }) + ), + fs.create('adonisrc.ts', 'export default {}'), + fs.create('package.json', '{}'), + fs.create('pnpm-lock.yaml', '{}'), + ]) + + const bundler = new Bundler(fs.baseUrl, ts, { + metaFiles: [ + { + pattern: 'resources/views/**/*.edge', + reloadServer: false, + }, + ], + }) + + await bundler.bundle(true) + + await Promise.all([ + assert.fileExists('./build/package.json'), + assert.fileExists('./build/pnpm-lock.yaml'), + ]) + }) +}) diff --git a/tests/code_transformer.spec.ts b/tests/code_transformer.spec.ts new file mode 100644 index 0000000..35b7356 --- /dev/null +++ b/tests/code_transformer.spec.ts @@ -0,0 +1,781 @@ +/* + * @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) { + await Promise.all([ + fs.createJson('tsconfig.json', { compilerOptions: {} }), + fs.create('start/kernel.ts', await readFile('./tests/fixtures/kernel.txt', 'utf-8')), + fs.create('adonisrc.ts', await readFile('./tests/fixtures/adonisrc.txt', 'utf-8')), + fs.create('start/env.ts', await readFile('./tests/fixtures/env.txt', 'utf-8')), + ]) +} + +test.group('Code transformer | addMiddlewareToStack', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('add a server middleware', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addMiddlewareToStack('server', [ + { path: '@adonisjs/static/static_middleware' }, + ]) + + assert.fileContains('start/kernel.ts', `() => import('@adonisjs/static/static_middleware')`) + }) + + test('add multiple server middleware', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addMiddlewareToStack('server', [ + { path: '@adonisjs/static/static_middleware' }, + { path: '#foo/middleware.js' }, + ]) + + assert.fileContains('start/kernel.ts', `() => import('@adonisjs/static/static_middleware')`) + assert.fileContains('start/kernel.ts', `() => import('#foo/middleware.js')`) + }) + + test('set correct position when defined', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addMiddlewareToStack('server', [ + { path: '@adonisjs/static/static_middleware', position: 'before' }, + { path: '#foo/middleware.js', position: 'before' }, + { path: '#foo/middleware2.js' }, + ]) + + const file = await fs.contents('start/kernel.ts') + assert.snapshot(file).match() + }) + + test('add a route middleware', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addMiddlewareToStack('router', [ + { path: '#foo/bar.js', position: 'before' }, + { path: '@adonisjs/random_middleware', position: 'after' }, + ]) + + const file = await fs.contents('start/kernel.ts') + assert.snapshot(file).match() + }) + + test('add route and server middleware', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addMiddlewareToStack('router', [{ path: '#foo/bar.js', position: 'before' }]) + await transformer.addMiddlewareToStack('server', [ + { path: '@adonisjs/random_middleware', position: 'after' }, + ]) + + const file = await fs.contents('start/kernel.ts') + assert.snapshot(file).match() + }) + + test('add named middleware', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addMiddlewareToStack('named', [ + { name: 'auth', path: '#foo/bar.js', position: 'before' }, + { name: 'rand', path: '@adonisjs/random_middleware', position: 'after' }, + ]) + + assert.fileContains('start/kernel.ts', `auth: () => import('#foo/bar.js')`) + assert.fileContains('start/kernel.ts', `rand: () => import('@adonisjs/random_middleware')`) + }) + + test('do not add duplicate router/server middleware', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addMiddlewareToStack('router', [ + { path: '@adonisjs/core/bodyparser_middleware' }, + ]) + + await transformer.addMiddlewareToStack('server', [ + { path: '#middleware/container_bindings_middleware' }, + ]) + + const file = await fs.contents('start/kernel.ts') + const occurrences = ( + file.match(/() => import\('@adonisjs\/core\/bodyparser_middleware'\)/g) || [] + ).length + + const occurrences2 = ( + file.match(/() => import\('#middleware\/container_bindings_middleware'\)/g) || [] + ).length + + assert.equal(occurrences, 1) + assert.equal(occurrences2, 1) + }) + + test('do not add duplicate named middleware', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addMiddlewareToStack('named', [{ name: 'auth', path: '#foo/bar.js' }]) + + await transformer.addMiddlewareToStack('named', [ + { name: 'auth', path: '#foo/bar2.js' }, + { name: 'auth', path: '#foo/bar3.js' }, + ]) + + const file = await fs.contents('start/kernel.ts') + assert.snapshot(file).match() + }) +}) + +test.group('Code transformer | defineEnvValidations', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + test('define new env validations', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.defineEnvValidations({ + variables: { + MY_VAR: 'Env.schema.string.optional()', + MY_VAR2: 'Env.schema.number()', + }, + }) + + assert.fileContains('start/env.ts', `MY_VAR: Env.schema.string.optional()`) + assert.fileContains('start/env.ts', `MY_VAR2: Env.schema.number()`) + }) + + test('add leading comment', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.defineEnvValidations({ + leadingComment: 'Redis configuration', + variables: { + REDIS_HOST: 'Env.schema.string.optional()', + REDIS_PORT: 'Env.schema.number()', + }, + }) + + const file = await fs.contents('start/env.ts') + assert.snapshot(file).match() + }) + + test('do not add duplicates', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.defineEnvValidations({ + leadingComment: 'Redis configuration', + variables: { + REDIS_HOST: 'Env.schema.string.optional()', + REDIS_PORT: 'Env.schema.number()', + }, + }) + + await transformer.defineEnvValidations({ + leadingComment: 'Redis configuration', + variables: { + REDIS_HOST: 'Env.schema.string.optional()', + 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 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() + }) + " + `) + }) +}) + +test.group('Code transformer | addCommand', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('add command to rc file', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addCommand('#foo/bar.js').addCommand('#foo/bar2.js') + }) + + const file = await fs.contents('adonisrc.ts') + assert.snapshot(file).match() + }) + + test('add command should not add duplicate', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addCommand('#foo/bar.js').addCommand('#foo/bar.js') + }) + + const file = await fs.contents('adonisrc.ts') + const occurrences = (file.match(/() => import\('#foo\/bar\.js'\)/g) || []).length + + assert.equal(occurrences, 1) + }) + + test('should add command even if commands property is missing', async ({ assert, fs }) => { + await fs.create( + 'adonisrc.ts', + dedent` + import { defineConfig } from '@adonisjs/core/app' + + export default defineConfig({ + typescript: true, + })` + ) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addCommand('#foo/bar.js') + }) + + const file = await fs.contents('adonisrc.ts') + assert.snapshot(file).match() + }) +}) + +test.group('Code transformer | addProvider', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('add provider to rc file', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/redis-provider') + }) + + assert.fileContains('adonisrc.ts', `() => import('@adonisjs/redis-provider')`) + }) + + test('add provider to rc file with specific environments', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/redis-provider', ['console', 'repl']) + }) + + const file = await fs.contents('adonisrc.ts') + assert.snapshot(file).match() + }) + + test('should add provider even if providers property is missing', async ({ assert, fs }) => { + await fs.create( + 'adonisrc.ts', + dedent` + import { defineConfig } from '@adonisjs/core/app' + + export default defineConfig({ + typescript: true, + })` + ) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/redis-provider') + }) + + assert.fileContains('adonisrc.ts', `() => import('@adonisjs/redis-provider')`) + }) + + test('should ignore provider duplicate', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/redis-provider').addProvider('@adonisjs/redis-provider') + }) + + const file = await fs.contents('adonisrc.ts') + const occurrences = (file.match(/() => import\('@adonisjs\/redis-provider'\)/g) || []).length + + assert.equal(occurrences, 1) + }) + + test('should ignore provider duplicate when using different environments', async ({ + assert, + fs, + }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile + .addProvider('@adonisjs/redis-provider', ['console']) + .addProvider('@adonisjs/redis-provider') + }) + + const file = await fs.contents('adonisrc.ts') + const occurrences = (file.match(/() => import\('@adonisjs\/redis-provider'\)/g) || []).length + + assert.equal(occurrences, 1) + }) + + test('do no add environments when they are all specified', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/redis-provider', ['console', 'repl', 'web', 'test']) + }) + + const file = await fs.contents('adonisrc.ts') + assert.snapshot(file).match() + }) +}) + +test.group('Code transformer | addMetaFile', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('add meta files to rc file', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addMetaFile('assets/**', false) + }) + + assert.fileContains('adonisrc.ts', `assets/**`) + }) + + test('add meta files to rc file with reload server', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addMetaFile('assets/**', true) + }) + + const file = await fs.contents('adonisrc.ts') + assert.snapshot(file).match() + }) +}) + +test.group('Code transformer | setDirectory', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('set directory in rc file', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.setDirectory('views', 'templates') + }) + + const file = await fs.contents('adonisrc.ts') + assert.snapshot(file).match() + }) + + test('set directory should overwrite if already defined', async ({ assert, fs }) => { + await fs.create( + 'adonisrc.ts', + dedent` + import { defineConfig } from '@adonisjs/core/app' + + export default defineConfig({ + directories: { + views: 'resources/views', + }, + })` + ) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.setDirectory('views', 'templates') + }) + + assert.fileContains('adonisrc.ts', `templates`) + }) +}) + +test.group('Code transformer | setCommandAlias', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('set command alias in rc file', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.setCommandAlias('migrate', 'migration:run') + }) + + const file = await fs.contents('adonisrc.ts') + assert.snapshot(file).match() + }) + + test('set commandAlias should overwrite if already defined', async ({ assert, fs }) => { + await fs.create( + 'adonisrc.ts', + dedent` + import { defineConfig } from '@adonisjs/core/app' + + export default defineConfig({ + commandsAliases: { + migrate: 'migration:run', + }, + })` + ) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.setCommandAlias('migrate', 'migration:run --force') + }) + + assert.fileContains('adonisrc.ts', `migration:run --force`) + }) +}) + +test.group('Code transformer | addSuite', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('add a new test suite to the rcFile', async ({ assert, fs }) => { + await fs.create( + 'adonisrc.ts', + dedent` + import { defineConfig } from '@adonisjs/core/build/standalone' + + export default defineConfig({ + commands: [], + }) + ` + ) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addSuite('unit', 'test/unit') + }) + + assert.fileContains('adonisrc.ts', `name: 'unit'`) + }) + + test('should ignore suite duplicate', async ({ assert, fs }) => { + await fs.create( + 'adonisrc.ts', + dedent` + import { defineConfig } from '@adonisjs/core/build/standalone' + + export default defineConfig({ + commands: [], + tests: { + suites: [ + { + name: 'unit', + files: ['test/unit'], + }, + ], + }, + }) + ` + ) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addSuite('unit', 'nope') + }) + + const file = await fs.contents('adonisrc.ts') + assert.include(file, `name: 'unit'`) + assert.notInclude(file, `nope`) + }) +}) + +test.group('Code transformer | addPreloadFile', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('add preload file', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addPreloadFile('#start/foo.js') + }) + + assert.fileContains('adonisrc.ts', `'#start/foo.js'`) + }) + + test('add preload file with specific environments', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addPreloadFile('#start/foo.js', ['console', 'repl']) + }) + + const file = await fs.contents('adonisrc.ts') + assert.snapshot(file).match() + }) + + test('do not add preload file when already defined', async ({ assert, fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.updateRcFile((rcFile) => { + rcFile.addPreloadFile('#start/foo.js').addPreloadFile('#start/foo.js') + }) + + const file = await fs.contents('adonisrc.ts') + const occurrences = (file.match(/'#start\/foo\.js'/g) || []).length + + assert.equal(occurrences, 1) + }) +}) + +test.group('Code transformer | addJapaPlugin', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('add named import', 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, + }, + ]) + + 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) + ] + " + `) + }) + + test('add default import', 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()', [ + { + module: '@adonisjs/foo/plugin/japa', + identifier: 'fooPlugin', + 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() + ] + " + `) + }) + + 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) + ] + " + `) + }) +}) + +test.group('Code transformer | addPolicies', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('add policies to polices/main.ts file', async ({ assert, fs }) => { + await fs.create('app/policies/main.ts', `export const policies = {}`) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addPolicies([ + { + name: 'PostPolicy', + path: '#policies/post_policy', + }, + { + name: 'UserPolicy', + path: '#policies/user_policy', + }, + ]) + + const file = await fs.contents('app/policies/main.ts') + assert.snapshot(file).matchInline(` + "export const policies = { + UserPolicy: () => import('#policies/user_policy'), + PostPolicy: () => import('#policies/post_policy') + } + " + `) + }) + + test('throw error when policies/main.ts file is missing', async ({ fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addPolicies([ + { + name: 'PostPolicy', + path: '#policies/post_policy', + }, + { + name: 'UserPolicy', + path: '#policies/user_policy', + }, + ]) + }).throws(/Could not find source file in project at the provided path:/) + + test('throw error when policies object is not defined', async ({ fs }) => { + await fs.create('app/policies/main.ts', `export const foo = {}`) + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addPolicies([ + { + name: 'PostPolicy', + path: '#policies/post_policy', + }, + { + name: 'UserPolicy', + path: '#policies/user_policy', + }, + ]) + }).throws(`Expected to find variable declaration named 'policies'.`) + + test('throw error when policies declaration is not an object', async ({ fs }) => { + await fs.create('app/policies/main.ts', `export const policies = []`) + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addPolicies([ + { + name: 'PostPolicy', + path: '#policies/post_policy', + }, + { + name: 'UserPolicy', + path: '#policies/user_policy', + }, + ]) + }).throws(/Expected to find an initializer of kind \'ObjectLiteralExpression\'./) +}) diff --git a/tests/copy.spec.ts b/tests/copy.spec.ts new file mode 100644 index 0000000..6f4ada2 --- /dev/null +++ b/tests/copy.spec.ts @@ -0,0 +1,86 @@ +/* + * @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' + +test.group('Copy files', () => { + test('expand glob patterns and copy files to the destination', async ({ assert, fs }) => { + await fs.create('resources/views/welcome.edge', '') + await fs.create('resources/views/about.edge', '') + await fs.create('resources/views/contact/main.edge', '') + + await fs.create('public/foo/test/a.json', '') + await fs.create('public/foo/test/b/a.json', '') + + await copyFiles( + ['resources/views/*.edge', 'public/**'], + fs.basePath, + join(fs.basePath, 'build') + ) + + await assert.fileExists('build/resources/views/welcome.edge') + await assert.fileExists('build/resources/views/about.edge') + await assert.fileNotExists('build/resources/views/contact/main.edge') + + await assert.fileExists('build/public/foo/test/a.json') + await assert.fileExists('build/public/foo/test/b/a.json') + }) + + test('copy relative file paths to the destination', async ({ fs, assert }) => { + await fs.create('resources/views/welcome.edge', '') + await fs.create('resources/views/about.edge', '') + await fs.create('package.json', '') + + await copyFiles( + ['resources/views/welcome.edge', 'resources/views/about.edge', 'package.json'], + fs.basePath, + join(fs.basePath, 'build') + ) + + await assert.fileExists('build/resources/views/welcome.edge') + await assert.fileExists('build/resources/views/about.edge') + await assert.fileExists('build/package.json') + }) + + test('ignore missing files at source', async ({ fs, assert }) => { + await fs.create('resources/views/welcome.edge', '') + await fs.create('resources/views/about.edge', '') + + await copyFiles( + ['resources/views/welcome.edge', 'resources/views/about.edge', 'package.json'], + fs.basePath, + join(fs.basePath, 'build') + ) + + await assert.fileExists('build/resources/views/welcome.edge') + await assert.fileExists('build/resources/views/about.edge') + }) + + test('ignore junk files', async ({ fs, assert }) => { + await fs.create('resources/views/welcome.edge', '') + await fs.create('resources/views/about.edge', '') + await fs.create('resources/views/.DS_Store', '') + + await copyFiles(['resources/views/*'], fs.basePath, join(fs.basePath, 'build')) + await assert.fileExists('build/resources/views/welcome.edge') + await assert.fileExists('build/resources/views/about.edge') + await assert.fileNotExists('build/resources/views/.DS_STORE') + }) + + test('glob pattern should pick dot-files and dot-folders', async ({ fs, assert }) => { + await fs.create('public/.vite/manifest.json', '') + await fs.create('public/.redirects', '') + + await copyFiles(['public/**'], fs.basePath, join(fs.basePath, 'build')) + await assert.fileExists('build/public/.vite/manifest.json') + await assert.fileExists('build/public/.redirects') + }) +}) diff --git a/tests/fixtures/adonisrc.txt b/tests/fixtures/adonisrc.txt new file mode 100644 index 0000000..a84a4fb --- /dev/null +++ b/tests/fixtures/adonisrc.txt @@ -0,0 +1,28 @@ +import { defineConfig } from '@adonisjs/core/app' + +export default defineConfig({ + typescript: true, + preloads: [ + () => import('./start/routes.ts'), + { + file: () => import('./start/ace.ts'), + environment: ['console'], + }, + ], + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + { + file: () => import('@adonisjs/core/providers/repl_provider'), + environment: ['repl'], + } + ], + metaFiles: [ + { + pattern: 'public/**', + reloadServer: true + }, + ], + commands: [ + () => import('@adonisjs/core/commands') + ] +}) diff --git a/tests/fixtures/env.txt b/tests/fixtures/env.txt new file mode 100644 index 0000000..361cc1e --- /dev/null +++ b/tests/fixtures/env.txt @@ -0,0 +1,6 @@ +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(), +}) diff --git a/tests/fixtures/kernel.txt b/tests/fixtures/kernel.txt new file mode 100644 index 0000000..9823edd --- /dev/null +++ b/tests/fixtures/kernel.txt @@ -0,0 +1,16 @@ +import router from '@adonisjs/core/services/router' +import server from '@adonisjs/core/services/server' + +server.errorHandler(() => import('#exceptions/handler')) + +server.use([ + () => import('#middleware/container_bindings_middleware'), + () => import('@adonisjs/session/session_middleware'), +]) + +router.use([ + () => import('@adonisjs/core/bodyparser_middleware'), + () => import('@adonisjs/shield/shield_middleware'), +]) + +export const middleware = router.named({}) diff --git a/tests/helpers.spec.ts b/tests/helpers.spec.ts new file mode 100644 index 0000000..4443840 --- /dev/null +++ b/tests/helpers.spec.ts @@ -0,0 +1,74 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import slash from 'slash' +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, [slash(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) + }) +}) diff --git a/tests/run.spec.ts b/tests/run.spec.ts new file mode 100644 index 0000000..0a056b4 --- /dev/null +++ b/tests/run.spec.ts @@ -0,0 +1,119 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { pEvent } from 'p-event' +import { test } from '@japa/runner' +import { runNode } from '../src/helpers.js' + +test.group('Child process', () => { + test('run typescript file as a child process', async ({ fs, assert }) => { + await fs.create( + 'foo.ts', + ` + process.send('ready') + ` + ) + + const childProcess = runNode(fs.basePath, { script: 'foo.ts', scriptArgs: [], nodeArgs: [] }) + const payload = await pEvent(childProcess, 'message', { rejectionEvents: ['error'] }) + + await pEvent(childProcess, 'close', { rejectionEvents: ['error'] }) + + assert.equal(childProcess.exitCode, 0) + assert.equal(payload, 'ready') + }) + + test('pass arguments to the script', async ({ fs, assert }) => { + await fs.create( + 'foo.ts', + ` + process.send({ args: process.argv.splice(2) }) + ` + ) + + const childProcess = runNode(fs.basePath, { + script: 'foo.ts', + scriptArgs: ['--watch', '--foo=bar'], + nodeArgs: [], + }) + const payload = await pEvent(childProcess, 'message', { rejectionEvents: ['error'] }) + await pEvent(childProcess, 'close', { rejectionEvents: ['error'] }) + + assert.equal(childProcess.exitCode, 0) + assert.deepEqual(payload, { args: ['--watch', '--foo=bar'] }) + }) + + test('pass arguments to node', async ({ assert, fs }) => { + await fs.create( + 'foo.ts', + ` + process.send({ args: process.execArgv }) + ` + ) + + const childProcess = runNode(fs.basePath, { + script: 'foo.ts', + scriptArgs: ['--watch', '--foo=bar'], + nodeArgs: ['--throw-deprecation'], + }) + + const payload = await pEvent(childProcess, 'message', { rejectionEvents: ['error'] }) + await pEvent(childProcess, 'close', { rejectionEvents: ['error'] }) + + assert.equal(childProcess.exitCode, 0) + assert.deepEqual(payload, { + args: ['--loader=ts-node/esm', '--enable-source-maps', '--throw-deprecation'], + }) + }) + + test('wait for child process to finish', async ({ fs, assert }) => { + await fs.create( + 'foo.ts', + ` + setTimeout(() => {}, 1000) + ` + ) + + const childProcess = runNode(fs.basePath, { script: 'foo.ts', scriptArgs: [], nodeArgs: [] }) + await pEvent(childProcess, 'close', { rejectionEvents: ['error'] }) + assert.equal(childProcess.exitCode, 0) + }) + + test('get child process exit code', async ({ fs, assert }) => { + await fs.create( + 'foo.ts', + ` + throw new Error('Something went wrong') + ` + ) + + const childProcess = runNode(fs.basePath, { script: 'foo.ts', scriptArgs: [], nodeArgs: [] }) + + await pEvent(childProcess, 'close', { rejectionEvents: ['error'] }) + assert.equal(childProcess.exitCode, 1) + }) + + test('await and get child process exit code', async ({ fs, assert }) => { + assert.plan(1) + + await fs.create( + 'foo.ts', + ` + throw new Error('Something went wrong') + ` + ) + + const childProcess = runNode(fs.basePath, { script: 'foo.ts', scriptArgs: [], nodeArgs: [] }) + try { + await childProcess + } catch { + assert.equal(childProcess.exitCode, 1) + } + }) +}) diff --git a/tests/watch.spec.ts b/tests/watch.spec.ts new file mode 100644 index 0000000..8814cf5 --- /dev/null +++ b/tests/watch.spec.ts @@ -0,0 +1,151 @@ +/* + * @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 { watch } from '../src/helpers.js' + +test.group('Watcher', (group) => { + group.tap((t) => t.disableTimeout()) + + test('watch files included by the tsconfig.json', async ({ fs, assert, cleanup }, done) => { + assert.plan(1) + + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['./**/*'], + }) + ) + await fs.create('foo.ts', '') + + const output = watch(fs.baseUrl, ts, {}) + cleanup(() => output!.chokidar.close()) + + output!.watcher.on('source:add', (file) => { + assert.equal(file.relativePath, 'bar.ts') + done() + }) + + output!.watcher.on('watcher:ready', async () => { + await fs.create('bar.ts', '') + }) + }) + .waitForDone() + .skip(!!process.env.CI, 'Skipping in CI. Cannot get chokidar to work') + + test('emit source:change when file is changed', async ({ fs, assert, cleanup }, done) => { + assert.plan(1) + + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['./**/*'], + }) + ) + await fs.create('foo.ts', '') + + const output = watch(fs.baseUrl, ts, {}) + cleanup(() => output!.chokidar.close()) + + output!.watcher.on('source:change', (file) => { + assert.equal(file.relativePath, 'foo.ts') + done() + }) + + output!.watcher.on('watcher:ready', async () => { + await fs.create('foo.ts', 'hello world') + }) + }) + .waitForDone() + .skip(!!process.env.CI, 'Skipping in CI. Cannot get chokidar to work') + + test('emit source:unlink when file is deleted', async ({ fs, assert, cleanup }, done) => { + assert.plan(1) + + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['./**/*'], + }) + ) + await fs.create('foo.ts', '') + + const output = watch(fs.baseUrl, ts, {}) + cleanup(() => output!.chokidar.close()) + + output!.watcher.on('source:unlink', (file) => { + assert.equal(file.relativePath, 'foo.ts') + done() + }) + + output!.watcher.on('watcher:ready', async () => { + await fs.remove('foo.ts') + }) + }) + .waitForDone() + .skip(!!process.env.CI, 'Skipping in CI. Cannot get chokidar to work') + + test('do not emit source:add when file is excluded by tsconfig.json', async ({ + fs, + assert, + cleanup, + }) => { + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['./**/*'], + exclude: ['./baz.ts'], + }) + ) + await fs.create('foo.ts', '') + + const output = watch(fs.baseUrl, ts, {}) + cleanup(() => output!.chokidar.close()) + + output!.watcher.on('source:add', () => { + assert.fail('Never expected to reach here') + }) + + output!.watcher.on('watcher:ready', async () => { + await fs.create('baz.ts', '') + }) + await new Promise((resolve) => setTimeout(resolve, 1000)) + }).skip(!!process.env.CI, 'Skipping in CI. Cannot get chokidar to work') + + test('emit add when files other than typescript source files are created', async ({ + fs, + assert, + cleanup, + }, done) => { + assert.plan(1) + + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['./**/*'], + }) + ) + await fs.create('foo.ts', '') + + const output = watch(fs.baseUrl, ts, {}) + cleanup(() => output!.chokidar.close()) + + output!.watcher.on('add', (file) => { + assert.equal(file.relativePath, 'foo.md') + done() + }) + + output!.watcher.on('watcher:ready', async () => { + await fs.create('foo.md', '') + }) + }) + .waitForDone() + .skip(!!process.env.CI, 'Skipping in CI. Cannot get chokidar to work') +}) diff --git a/tsconfig.json b/tsconfig.json index e0b0db1..ad0cc44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { - "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", + "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "skipLibCheck": true - }, - "files": [ - "./node_modules/@adonisjs/application/build/adonis-typings/index.d.ts" - ] + "rootDir": "./", + "outDir": "./build" + } }