├── .editorconfig ├── .github ├── funding.yml ├── labels.json ├── lock.yml ├── stale.yml └── workflows │ ├── checks.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore └── commit-msg ├── .npmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── bin └── test.ts ├── configure.ts ├── index.ts ├── package.json ├── providers └── vite_provider.ts ├── services └── vite.ts ├── src ├── client │ ├── config.ts │ ├── main.ts │ └── types.ts ├── define_config.ts ├── hooks │ └── build_hook.ts ├── plugins │ └── edge.ts ├── types.ts ├── utils.ts ├── vite.ts └── vite_middleware.ts ├── stubs ├── config │ └── vite.stub ├── js_entrypoint.stub ├── main.ts └── vite.config.stub ├── tests ├── backend │ ├── define_config.spec.ts │ ├── edge_plugin.spec.ts │ ├── fixtures │ │ └── adonis_packages_manifest.json │ ├── helpers.ts │ ├── middleware.spec.ts │ ├── provider.spec.ts │ └── vite.spec.ts ├── client │ └── config.spec.ts └── configure.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [thetutlage, Julien-R44] 2 | -------------------------------------------------------------------------------- /.github/labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Priority: Critical", 4 | "color": "ea0056", 5 | "description": "The issue needs urgent attention", 6 | "aliases": [] 7 | }, 8 | { 9 | "name": "Priority: High", 10 | "color": "5666ed", 11 | "description": "Look into this issue before picking up any new work", 12 | "aliases": [] 13 | }, 14 | { 15 | "name": "Priority: Medium", 16 | "color": "f4ff61", 17 | "description": "Try to fix the issue for the next patch/minor release", 18 | "aliases": [] 19 | }, 20 | { 21 | "name": "Priority: Low", 22 | "color": "87dfd6", 23 | "description": "Something worth considering, but not a top priority for the team", 24 | "aliases": [] 25 | }, 26 | { 27 | "name": "Semver: Alpha", 28 | "color": "008480", 29 | "description": "Will make it's way to the next alpha version of the package", 30 | "aliases": [] 31 | }, 32 | { 33 | "name": "Semver: Major", 34 | "color": "ea0056", 35 | "description": "Has breaking changes", 36 | "aliases": [] 37 | }, 38 | { 39 | "name": "Semver: Minor", 40 | "color": "fbe555", 41 | "description": "Mainly new features and improvements", 42 | "aliases": [] 43 | }, 44 | { 45 | "name": "Semver: Next", 46 | "color": "5666ed", 47 | "description": "Will make it's way to the bleeding edge version of the package", 48 | "aliases": [] 49 | }, 50 | { 51 | "name": "Semver: Patch", 52 | "color": "87dfd6", 53 | "description": "A bug fix", 54 | "aliases": [] 55 | }, 56 | { 57 | "name": "Status: Abandoned", 58 | "color": "ffffff", 59 | "description": "Dropped and not into consideration", 60 | "aliases": ["wontfix"] 61 | }, 62 | { 63 | "name": "Status: Accepted", 64 | "color": "e5fbf2", 65 | "description": "The proposal or the feature has been accepted for the future versions", 66 | "aliases": [] 67 | }, 68 | { 69 | "name": "Status: Blocked", 70 | "color": "ea0056", 71 | "description": "The work on the issue or the PR is blocked. Check comments for reasoning", 72 | "aliases": [] 73 | }, 74 | { 75 | "name": "Status: Completed", 76 | "color": "008672", 77 | "description": "The work has been completed, but not released yet", 78 | "aliases": [] 79 | }, 80 | { 81 | "name": "Status: In Progress", 82 | "color": "73dbc4", 83 | "description": "Still banging the keyboard", 84 | "aliases": ["in progress"] 85 | }, 86 | { 87 | "name": "Status: On Hold", 88 | "color": "f4ff61", 89 | "description": "The work was started earlier, but is on hold now. Check comments for reasoning", 90 | "aliases": ["On Hold"] 91 | }, 92 | { 93 | "name": "Status: Review Needed", 94 | "color": "fbe555", 95 | "description": "Review from the core team is required before moving forward", 96 | "aliases": [] 97 | }, 98 | { 99 | "name": "Status: Awaiting More Information", 100 | "color": "89f8ce", 101 | "description": "Waiting on the issue reporter or PR author to provide more information", 102 | "aliases": [] 103 | }, 104 | { 105 | "name": "Status: Need Contributors", 106 | "color": "7057ff", 107 | "description": "Looking for contributors to help us move forward with this issue or PR", 108 | "aliases": [] 109 | }, 110 | { 111 | "name": "Type: Bug", 112 | "color": "ea0056", 113 | "description": "The issue has indentified a bug", 114 | "aliases": ["bug"] 115 | }, 116 | { 117 | "name": "Type: Security", 118 | "color": "ea0056", 119 | "description": "Spotted security vulnerability and is a top priority for the core team", 120 | "aliases": [] 121 | }, 122 | { 123 | "name": "Type: Duplicate", 124 | "color": "00837e", 125 | "description": "Already answered or fixed previously", 126 | "aliases": ["duplicate"] 127 | }, 128 | { 129 | "name": "Type: Enhancement", 130 | "color": "89f8ce", 131 | "description": "Improving an existing feature", 132 | "aliases": ["enhancement"] 133 | }, 134 | { 135 | "name": "Type: Feature Request", 136 | "color": "483add", 137 | "description": "Request to add a new feature to the package", 138 | "aliases": [] 139 | }, 140 | { 141 | "name": "Type: Invalid", 142 | "color": "dbdbdb", 143 | "description": "Doesn't really belong here. Maybe use discussion threads?", 144 | "aliases": ["invalid"] 145 | }, 146 | { 147 | "name": "Type: Question", 148 | "color": "eceafc", 149 | "description": "Needs clarification", 150 | "aliases": ["help wanted", "question"] 151 | }, 152 | { 153 | "name": "Type: Documentation Change", 154 | "color": "7057ff", 155 | "description": "Documentation needs some improvements", 156 | "aliases": ["documentation"] 157 | }, 158 | { 159 | "name": "Type: Dependencies Update", 160 | "color": "00837e", 161 | "description": "Bump dependencies", 162 | "aliases": ["dependencies"] 163 | }, 164 | { 165 | "name": "Good First Issue", 166 | "color": "008480", 167 | "description": "Want to contribute? Just filter by this label", 168 | "aliases": ["good first issue"] 169 | } 170 | ] 171 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads-app 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 60 5 | 6 | # Skip issues and pull requests created before a given timestamp. Timestamp must 7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 8 | skipCreatedBefore: false 9 | 10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 11 | exemptLabels: ['Type: Security'] 12 | 13 | # Label to add before locking, such as `outdated`. Set to `false` to disable 14 | lockLabel: false 15 | 16 | # Comment to post before locking. Set to `false` to disable 17 | lockComment: > 18 | This thread has been automatically locked since there has not been 19 | any recent activity after it was closed. Please open a new issue for 20 | related bugs. 21 | 22 | # Assign `resolved` as the reason for locking. Set to `false` to disable 23 | setLockReason: false 24 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - 'Type: Security' 10 | 11 | # Label to use when marking an issue as stale 12 | staleLabel: 'Status: Abandoned' 13 | 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | test: 8 | uses: adonisjs/.github/.github/workflows/test.yml@main 9 | 10 | lint: 11 | uses: adonisjs/.github/.github/workflows/lint.yml@main 12 | 13 | typecheck: 14 | uses: adonisjs/.github/.github/workflows/typecheck.yml@main 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: workflow_dispatch 3 | 4 | permissions: 5 | contents: write 6 | id-token: write 7 | 8 | jobs: 9 | checks: 10 | uses: ./.github/workflows/checks.yml 11 | release: 12 | needs: checks 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | - name: git config 22 | run: | 23 | git config user.name "${GITHUB_ACTOR}" 24 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 25 | - name: Init npm config 26 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 27 | env: 28 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | - run: npm install 30 | - run: npm run release -- --ci 31 | env: 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | test/__app 4 | .DS_STORE 5 | .nyc_output 6 | .idea 7 | .vscode/ 8 | *.sublime-project 9 | *.sublime-workspace 10 | *.log 11 | build 12 | dist 13 | shrinkwrap.yaml 14 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | *.md 4 | config.json 5 | .eslintrc.json 6 | package.json 7 | *.html 8 | *.txt 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2023 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @adonisjs/vite 2 | 3 |
4 | 5 | [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] 6 | 7 | ## Introduction 8 | Package to add [Vite](https://vitejs.dev/) as an assets bundler to AdonisJS. 9 | 10 | ## Official Documentation 11 | The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/assets-bundling) 12 | 13 | ## Contributing 14 | One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. 15 | 16 | We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. 17 | 18 | ## Code of Conduct 19 | In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). 20 | 21 | ## License 22 | AdonisJS Vite middleware is open-sourced software licensed under the [MIT license](LICENSE.md). 23 | 24 | [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/vite/checks.yml?style=for-the-badge 25 | [gh-workflow-url]: https://github.com/adonisjs/vite/actions/workflows/checks.yml "Github action" 26 | 27 | [npm-image]: https://img.shields.io/npm/v/@adonisjs/vite/latest.svg?style=for-the-badge&logo=npm 28 | [npm-url]: https://www.npmjs.com/package/@adonisjs/vite/v/latest "npm" 29 | 30 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 31 | 32 | [license-url]: LICENSE.md 33 | [license-image]: https://img.shields.io/github/license/adonisjs/vite?style=for-the-badge 34 | -------------------------------------------------------------------------------- /bin/test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { assert } from '@japa/assert' 11 | import { snapshot } from '@japa/snapshot' 12 | import { fileSystem } from '@japa/file-system' 13 | import { processCLIArgs, configure, run } from '@japa/runner' 14 | 15 | import { BASE_URL } from '../tests/backend/helpers.js' 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Configure tests 20 | |-------------------------------------------------------------------------- 21 | | 22 | | The configure method accepts the configuration to configure the Japa 23 | | tests runner. 24 | | 25 | | The first method call "processCLIArgs" process the command line arguments 26 | | and turns them into a config object. Using this method is not mandatory. 27 | | 28 | | Please consult japa.dev/runner-config for the config docs. 29 | */ 30 | processCLIArgs(process.argv.slice(2)) 31 | configure({ 32 | files: ['tests/**/*.spec.ts'], 33 | plugins: [assert(), fileSystem({ basePath: BASE_URL }), snapshot()], 34 | }) 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Run tests 39 | |-------------------------------------------------------------------------- 40 | | 41 | | The following "run" method is required to execute all the tests. 42 | | 43 | */ 44 | run() 45 | -------------------------------------------------------------------------------- /configure.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type Configure from '@adonisjs/core/commands/configure' 11 | 12 | import { stubsRoot } from './stubs/main.js' 13 | 14 | /** 15 | * Configures the package 16 | */ 17 | export async function configure(command: Configure) { 18 | const codemods = await command.createCodemods() 19 | let shouldInstallPackages: boolean | undefined = command.parsedFlags.install 20 | 21 | /** 22 | * Publish stubs 23 | */ 24 | await codemods.makeUsingStub(stubsRoot, 'config/vite.stub', {}) 25 | await codemods.makeUsingStub(stubsRoot, 'vite.config.stub', {}) 26 | await codemods.makeUsingStub(stubsRoot, 'js_entrypoint.stub', {}) 27 | 28 | /** 29 | * Update RC file 30 | */ 31 | await codemods.updateRcFile((rcFile) => { 32 | rcFile.addProvider('@adonisjs/vite/vite_provider') 33 | rcFile.addMetaFile('public/**', false) 34 | rcFile.addAssemblerHook('onBuildStarting', '@adonisjs/vite/build_hook') 35 | }) 36 | 37 | /** 38 | * Add server middleware 39 | */ 40 | await codemods.registerMiddleware('server', [ 41 | { path: '@adonisjs/vite/vite_middleware', position: 'after' }, 42 | ]) 43 | 44 | /** 45 | * Prompt when `install` or `--no-install` flags are 46 | * not used 47 | */ 48 | if (shouldInstallPackages === undefined) { 49 | shouldInstallPackages = await command.prompt.confirm('Do you want to install "vite"?') 50 | } 51 | 52 | /** 53 | * Install dependency or list the command to install it 54 | */ 55 | if (shouldInstallPackages) { 56 | await codemods.installPackages([{ name: 'vite', isDevDependency: true }]) 57 | } else { 58 | await codemods.listPackagesToInstall([{ name: 'vite', isDevDependency: true }]) 59 | } 60 | 61 | /** 62 | * Add `assetsBundler: false` to the adonisrc file 63 | */ 64 | const tsMorph = await import('ts-morph') 65 | const project = await codemods.getTsMorphProject() 66 | const adonisRcFile = project?.getSourceFile('adonisrc.ts') 67 | const defineConfigCall = adonisRcFile 68 | ?.getDescendantsOfKind(tsMorph.SyntaxKind.CallExpression) 69 | .find((statement) => statement.getExpression().getText() === 'defineConfig') 70 | 71 | const configObject = defineConfigCall! 72 | .getArguments()[0] 73 | .asKindOrThrow(tsMorph.SyntaxKind.ObjectLiteralExpression) 74 | 75 | configObject.addPropertyAssignment({ 76 | name: 'assetsBundler', 77 | initializer: 'false', 78 | }) 79 | 80 | await adonisRcFile?.save() 81 | } 82 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export { Vite } from './src/vite.js' 11 | export { configure } from './configure.js' 12 | export { stubsRoot } from './stubs/main.js' 13 | export { defineConfig } from './src/define_config.js' 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adonisjs/vite", 3 | "description": "Vite plugin for AdonisJS", 4 | "version": "4.0.0", 5 | "engines": { 6 | "node": ">=20.6.0" 7 | }, 8 | "main": "build/index.js", 9 | "type": "module", 10 | "files": [ 11 | "build", 12 | "!build/bin", 13 | "!build/tests", 14 | "!build/tests_helpers" 15 | ], 16 | "exports": { 17 | ".": "./build/index.js", 18 | "./vite_provider": "./build/providers/vite_provider.js", 19 | "./plugins/edge": "./build/src/plugins/edge.js", 20 | "./vite_middleware": "./build/src/vite_middleware.js", 21 | "./build_hook": "./build/src/hooks/build_hook.js", 22 | "./services/main": "./build/services/vite.js", 23 | "./client": "./build/src/client/main.js", 24 | "./types": "./build/src/types.js" 25 | }, 26 | "scripts": { 27 | "clean": "del-cli build", 28 | "copy:templates": "copyfiles --up 1 \"stubs/**/*.stub\" build", 29 | "typecheck": "tsc --noEmit", 30 | "lint": "eslint . --ext=.ts", 31 | "format": "prettier --write .", 32 | "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts", 33 | "pretest": "npm run lint", 34 | "test": "c8 npm run quick:test", 35 | "prebuild": "npm run lint && npm run clean", 36 | "build": "tsup-node && tsc --emitDeclarationOnly --declaration", 37 | "postbuild": "npm run copy:templates", 38 | "release": "release-it", 39 | "version": "npm run build", 40 | "prepublishOnly": "npm run build" 41 | }, 42 | "devDependencies": { 43 | "@adonisjs/assembler": "^7.8.2", 44 | "@adonisjs/core": "6.16.0", 45 | "@adonisjs/eslint-config": "^1.3.0", 46 | "@adonisjs/prettier-config": "^1.4.0", 47 | "@adonisjs/session": "^7.5.0", 48 | "@adonisjs/shield": "^8.1.1", 49 | "@adonisjs/tsconfig": "^1.4.0", 50 | "@japa/assert": "3.0.0", 51 | "@japa/file-system": "^2.3.0", 52 | "@japa/runner": "3.1.4", 53 | "@japa/snapshot": "^2.0.6", 54 | "@release-it/conventional-changelog": "^9.0.3", 55 | "@swc/core": "^1.10.1", 56 | "@types/node": "^22.10.2", 57 | "@types/supertest": "^6.0.2", 58 | "c8": "^10.1.3", 59 | "copyfiles": "^2.4.1", 60 | "del-cli": "^6.0.0", 61 | "edge.js": "^6.2.0", 62 | "eslint": "^8.57.0", 63 | "prettier": "^3.4.2", 64 | "release-it": "^17.10.0", 65 | "supertest": "^7.0.0", 66 | "ts-morph": "^24.0.0", 67 | "ts-node": "^10.9.2", 68 | "tsup": "^8.3.5", 69 | "typescript": "~5.7.2", 70 | "vite": "^6.0.3" 71 | }, 72 | "dependencies": { 73 | "@poppinss/utils": "^6.8.3", 74 | "@vavite/multibuild": "^5.1.0", 75 | "edge-error": "^4.0.1", 76 | "vite-plugin-restart": "^0.4.2" 77 | }, 78 | "peerDependencies": { 79 | "@adonisjs/core": "^6.3.0", 80 | "@adonisjs/shield": "^8.0.0", 81 | "edge.js": "^6.0.1", 82 | "vite": "^6.0.0" 83 | }, 84 | "peerDependenciesMeta": { 85 | "edge.js": { 86 | "optional": true 87 | }, 88 | "@adonisjs/shield": { 89 | "optional": true 90 | } 91 | }, 92 | "author": "Julien Ripouteau ", 93 | "license": "MIT", 94 | "homepage": "https://github.com/adonisjs/vite#readme", 95 | "repository": { 96 | "type": "git", 97 | "url": "git+https://github.com/adonisjs/vite.git" 98 | }, 99 | "bugs": { 100 | "url": "https://github.com/adonisjs/vite/issues" 101 | }, 102 | "keywords": [ 103 | "vite", 104 | "adonisjs" 105 | ], 106 | "eslintConfig": { 107 | "extends": "@adonisjs/eslint-config/package" 108 | }, 109 | "prettier": "@adonisjs/prettier-config", 110 | "publishConfig": { 111 | "access": "public" 112 | }, 113 | "c8": { 114 | "reporter": [ 115 | "text", 116 | "html" 117 | ], 118 | "exclude": [ 119 | "tests/**", 120 | "tests_helpers/**" 121 | ] 122 | }, 123 | "tsup": { 124 | "entry": [ 125 | "./src/hooks/build_hook.ts", 126 | "./providers/vite_provider.ts", 127 | "./src/vite_middleware.ts", 128 | "./src/plugins/edge.ts", 129 | "./src/client/main.ts", 130 | "./services/vite.ts", 131 | "./src/types.ts", 132 | "./index.ts" 133 | ], 134 | "outDir": "./build", 135 | "clean": true, 136 | "format": "esm", 137 | "dts": false, 138 | "sourcemap": true, 139 | "target": "esnext" 140 | }, 141 | "release-it": { 142 | "git": { 143 | "requireUpstream": true, 144 | "commitMessage": "chore(release): ${version}", 145 | "tagAnnotation": "v${version}", 146 | "push": true, 147 | "tagName": "v${version}" 148 | }, 149 | "github": { 150 | "release": true 151 | }, 152 | "npm": { 153 | "publish": true, 154 | "skipChecks": true, 155 | "tag": "latest" 156 | }, 157 | "plugins": { 158 | "@release-it/conventional-changelog": { 159 | "preset": { 160 | "name": "angular" 161 | } 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /providers/vite_provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { ApplicationService } from '@adonisjs/core/types' 11 | import type { cspKeywords as ShieldCSPKeywords } from '@adonisjs/shield' 12 | 13 | import { Vite } from '../src/vite.js' 14 | import type { ViteOptions } from '../src/types.js' 15 | import ViteMiddleware from '../src/vite_middleware.js' 16 | 17 | declare module '@adonisjs/core/types' { 18 | interface ContainerBindings { 19 | vite: Vite 20 | } 21 | } 22 | 23 | export default class ViteProvider { 24 | #shouldRunVite: boolean 25 | 26 | constructor(protected app: ApplicationService) { 27 | /** 28 | * We should only run Vite in development and test environments 29 | */ 30 | const env = this.app.getEnvironment() 31 | this.#shouldRunVite = (this.app.inDev || this.app.inTest) && (env === 'web' || env === 'test') 32 | } 33 | 34 | /** 35 | * Registers edge plugin when edge is installed 36 | */ 37 | protected async registerEdgePlugin() { 38 | if (this.app.usingEdgeJS) { 39 | const edge = await import('edge.js') 40 | const vite = await this.app.container.make('vite') 41 | const { edgePluginVite } = await import('../src/plugins/edge.js') 42 | edge.default.use(edgePluginVite(vite)) 43 | } 44 | } 45 | 46 | /** 47 | * Registers CSP keywords when @adonisjs/shield is installed 48 | */ 49 | protected async registerShieldKeywords() { 50 | let cspKeywords: typeof ShieldCSPKeywords | null = null 51 | try { 52 | const shieldExports = await import('@adonisjs/shield') 53 | cspKeywords = shieldExports.cspKeywords 54 | } catch {} 55 | 56 | if (!cspKeywords) return 57 | 58 | const vite = await this.app.container.make('vite') 59 | 60 | /** 61 | * Registering the @viteUrl keyword for CSP directives. 62 | * Returns http URL to the dev or the CDN server, otherwise 63 | * an empty string 64 | */ 65 | cspKeywords.register('@viteUrl', function () { 66 | const assetsURL = vite.assetsUrl() 67 | if (!assetsURL || !assetsURL.startsWith('http://') || assetsURL.startsWith('https://')) { 68 | return '' 69 | } 70 | 71 | return assetsURL 72 | }) 73 | } 74 | 75 | /** 76 | * Register Vite bindings 77 | */ 78 | register() { 79 | const config = this.app.config.get('vite') 80 | 81 | const vite = new Vite(this.#shouldRunVite, config) 82 | this.app.container.bind('vite', () => vite) 83 | this.app.container.singleton(ViteMiddleware, () => new ViteMiddleware(vite)) 84 | } 85 | 86 | /** 87 | * - Register edge tags 88 | * - Start Vite server when running in development or test 89 | */ 90 | async boot() { 91 | await this.registerEdgePlugin() 92 | 93 | if (!this.#shouldRunVite) return 94 | 95 | const vite = await this.app.container.make('vite') 96 | await vite.createDevServer() 97 | } 98 | 99 | /** 100 | * Stop Vite server when running in development or test 101 | */ 102 | async shutdown() { 103 | if (!this.#shouldRunVite) return 104 | 105 | const vite = await this.app.container.make('vite') 106 | await vite.stopDevServer() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /services/vite.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import app from '@adonisjs/core/services/app' 11 | import type { Vite } from '../src/vite.js' 12 | 13 | let vite: Vite 14 | 15 | /** 16 | * Returns a singleton instance of Vite class 17 | * from the container 18 | */ 19 | await app.booted(async () => { 20 | vite = await app.container.make('vite') 21 | }) 22 | 23 | export { vite as default } 24 | -------------------------------------------------------------------------------- /src/client/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'node:path' 11 | import type { ConfigEnv, Plugin, UserConfig } from 'vite' 12 | 13 | import { addTrailingSlash } from '../utils.js' 14 | import type { PluginFullOptions } from './types.js' 15 | 16 | /** 17 | * Resolve the `config.base` value 18 | */ 19 | export function resolveBase( 20 | config: UserConfig, 21 | options: PluginFullOptions, 22 | command: 'build' | 'serve' 23 | ): string { 24 | if (config.base) return config.base 25 | if (command === 'build') { 26 | return addTrailingSlash(options.assetsUrl) 27 | } 28 | 29 | return '/' 30 | } 31 | 32 | /** 33 | * Vite config hook 34 | */ 35 | export function configHook( 36 | options: PluginFullOptions, 37 | userConfig: UserConfig, 38 | { command }: ConfigEnv 39 | ): UserConfig { 40 | const config: UserConfig = { 41 | publicDir: userConfig.publicDir ?? false, 42 | base: resolveBase(userConfig, options, command), 43 | 44 | /** 45 | * Disable the vite dev server cors handling. Otherwise, it will 46 | * override the cors settings defined by @adonisjs/cors 47 | * https://github.com/adonisjs/vite/issues/13 48 | */ 49 | server: { cors: userConfig.server?.cors ?? false }, 50 | 51 | build: { 52 | assetsDir: '', 53 | emptyOutDir: true, 54 | manifest: userConfig.build?.manifest ?? true, 55 | outDir: userConfig.build?.outDir ?? options.buildDirectory, 56 | assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? 0, 57 | 58 | rollupOptions: { 59 | input: options.entrypoints.map((entrypoint) => join(userConfig.root || '', entrypoint)), 60 | }, 61 | }, 62 | } 63 | 64 | return config 65 | } 66 | 67 | /** 68 | * Update the user vite config to match the Adonis requirements 69 | */ 70 | export const config = (options: PluginFullOptions): Plugin => { 71 | return { 72 | name: 'vite-plugin-adonis:config', 73 | enforce: 'post', 74 | config: configHook.bind(null, options), 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/client/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /// 11 | 12 | import { PluginOption } from 'vite' 13 | import PluginRestart from 'vite-plugin-restart' 14 | 15 | import { config } from './config.js' 16 | import type { PluginOptions } from './types.js' 17 | 18 | declare module 'vite' { 19 | interface ManifestChunk { 20 | integrity: string 21 | } 22 | } 23 | 24 | /** 25 | * Vite plugin for AdonisJS 26 | */ 27 | export default function adonisjs(options: PluginOptions): PluginOption[] { 28 | const fullOptions = Object.assign( 29 | { 30 | assetsUrl: '/assets', 31 | buildDirectory: 'public/assets', 32 | reload: ['./resources/views/**/*.edge'], 33 | }, 34 | options 35 | ) 36 | 37 | return [PluginRestart({ reload: fullOptions.reload }), config(fullOptions)] 38 | } 39 | -------------------------------------------------------------------------------- /src/client/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export interface PluginOptions { 11 | /** 12 | * The URL where the assets will be served. This is particularly 13 | * useful if you are using a CDN to deploy your assets. 14 | * 15 | * @default '' 16 | */ 17 | assetsUrl?: string 18 | 19 | /** 20 | * Files that should trigger a page reload when changed. 21 | * 22 | * @default ['./resources/views/** /*.edge'] 23 | */ 24 | reload?: string[] 25 | 26 | /** 27 | * Paths to the entrypoints files 28 | */ 29 | entrypoints: string[] 30 | 31 | /** 32 | * Public directory where the assets will be compiled. 33 | * 34 | * @default 'public/assets' 35 | */ 36 | buildDirectory?: string 37 | } 38 | 39 | export type PluginFullOptions = Required 40 | -------------------------------------------------------------------------------- /src/define_config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'node:path' 11 | 12 | import type { ViteOptions } from './types.js' 13 | 14 | /** 15 | * Define the backend config for Vite 16 | */ 17 | export function defineConfig(config: Partial): ViteOptions { 18 | return { 19 | buildDirectory: 'public/assets', 20 | assetsUrl: '/assets', 21 | manifestFile: config.buildDirectory 22 | ? join(config.buildDirectory, '.vite/manifest.json') 23 | : 'public/assets/.vite/manifest.json', 24 | ...config, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/build_hook.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { multibuild } from '@vavite/multibuild' 11 | import type { AssemblerHookHandler } from '@adonisjs/core/types/app' 12 | 13 | /** 14 | * This is an Assembler hook that should be executed when the application is 15 | * builded using the `node ace build` command. 16 | * 17 | * The hook is responsible for launching a Vite multi-build process. 18 | */ 19 | export default async function viteBuildHook({ logger }: Parameters[0]) { 20 | logger.info('building assets with vite') 21 | 22 | await multibuild(undefined, { 23 | onStartBuildStep: (step) => { 24 | if (!step.currentStep.description) return 25 | 26 | logger.info(step.currentStep.description) 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/plugins/edge.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { Edge } from 'edge.js' 11 | import { EdgeError } from 'edge-error' 12 | import type { PluginFn } from 'edge.js/types' 13 | 14 | import type { Vite } from '../vite.js' 15 | 16 | /** 17 | * The edge plugin for vite to share vite service with edge 18 | * and register custom tags 19 | */ 20 | export const edgePluginVite: (vite: Vite) => PluginFn = (vite) => { 21 | const edgeVite = (edge: Edge) => { 22 | edge.global('vite', vite) 23 | edge.global('asset', vite.assetPath.bind(vite)) 24 | 25 | edge.registerTag({ 26 | tagName: 'viteReactRefresh', 27 | seekable: true, 28 | block: false, 29 | compile(parser, buffer, token) { 30 | let attributes = '' 31 | if (token.properties.jsArg.trim()) { 32 | /** 33 | * Converting a single argument to a SequenceExpression so that we 34 | * work around the following edge cases. 35 | * 36 | * - If someone passes an object literal to the tag, ie { nonce: 'foo' } 37 | * it will be parsed as a LabeledStatement and not an object. 38 | * - If we wrap the object literal inside parenthesis, ie ({nonce: 'foo'}) 39 | * then we will end up messing other expressions like a variable reference 40 | * , or a member expression and so on. 41 | * - So the best bet is to convert user supplied argument to a sequence expression 42 | * and hence ignore it during stringification. 43 | */ 44 | const jsArg = `a,${token.properties.jsArg}` 45 | 46 | const parsed = parser.utils.transformAst( 47 | parser.utils.generateAST(jsArg, token.loc, token.filename), 48 | token.filename, 49 | parser 50 | ) 51 | attributes = parser.utils.stringify(parsed.expressions[1]) 52 | } 53 | 54 | /** 55 | * Get HMR script 56 | */ 57 | buffer.writeExpression( 58 | `const __vite_hmr_script = state.vite.getReactHmrScript(${attributes})`, 59 | token.filename, 60 | token.loc.start.line 61 | ) 62 | 63 | /** 64 | * Check if the script exists (only in hot mode) 65 | */ 66 | buffer.writeStatement('if(__vite_hmr_script) {', token.filename, token.loc.start.line) 67 | 68 | /** 69 | * Write output 70 | */ 71 | buffer.outputExpression( 72 | `__vite_hmr_script.toString()`, 73 | token.filename, 74 | token.loc.start.line, 75 | false 76 | ) 77 | 78 | /** 79 | * Close if block 80 | */ 81 | buffer.writeStatement('}', token.filename, token.loc.start.line) 82 | }, 83 | }) 84 | 85 | edge.registerTag({ 86 | tagName: 'vite', 87 | seekable: true, 88 | block: false, 89 | compile(parser, buffer, token) { 90 | /** 91 | * Ensure an argument is defined 92 | */ 93 | if (!token.properties.jsArg.trim()) { 94 | throw new EdgeError('Missing entrypoint name', 'E_RUNTIME_EXCEPTION', { 95 | filename: token.filename, 96 | line: token.loc.start.line, 97 | col: token.loc.start.col, 98 | }) 99 | } 100 | 101 | const parsed = parser.utils.transformAst( 102 | parser.utils.generateAST(token.properties.jsArg, token.loc, token.filename), 103 | token.filename, 104 | parser 105 | ) 106 | 107 | const entrypoints = parser.utils.stringify(parsed) 108 | const methodCall = 109 | parsed.type === 'SequenceExpression' 110 | ? `generateEntryPointsTags${entrypoints}` 111 | : `generateEntryPointsTags(${entrypoints})` 112 | 113 | buffer.outputExpression( 114 | `(await state.vite.${methodCall}).join('\\n')`, 115 | token.filename, 116 | token.loc.start.line, 117 | false 118 | ) 119 | }, 120 | }) 121 | } 122 | 123 | return edgeVite 124 | } 125 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /** 11 | * Parameters passed to the setAttributes callback 12 | */ 13 | export type SetAttributesCallbackParams = { 14 | src: string 15 | url: string 16 | } 17 | 18 | /** 19 | * Attributes to be set on the script/style tags. 20 | * Can be either a record or a callback that returns a record. 21 | */ 22 | export type SetAttributes = 23 | | Record 24 | | ((params: SetAttributesCallbackParams) => Record) 25 | 26 | /** 27 | * Representation of an AdonisJS Vite Element returned 28 | * by different tags generation APIs 29 | */ 30 | export type AdonisViteElement = 31 | | { 32 | tag: 'link' 33 | attributes: Record 34 | } 35 | | { 36 | tag: 'script' 37 | attributes: Record 38 | children: string[] 39 | } 40 | 41 | export interface ViteOptions { 42 | /** 43 | * Public directory where the assets will be compiled. 44 | * 45 | * @default 'public/assets' 46 | */ 47 | buildDirectory: string 48 | 49 | /** 50 | * Path to the manifest file relative from the root of 51 | * the application 52 | * 53 | * @default 'public/assets/.vite/manifest.json' 54 | */ 55 | manifestFile: string 56 | 57 | /** 58 | * The URL to prefix when generating assets URLs. For example: This 59 | * could the CDN URL when generating the production build 60 | * 61 | * @default '' 62 | */ 63 | assetsUrl?: string 64 | 65 | /** 66 | * A custom set of attributes to apply on all 67 | * script tags injected by edge `@vite` tag 68 | */ 69 | styleAttributes?: SetAttributes 70 | 71 | /** 72 | * A custom set of attributes to apply on all 73 | * style tags injected by edge `@vite` tag 74 | */ 75 | scriptAttributes?: SetAttributes 76 | } 77 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /** 11 | * Returns a new array with unique items by the given key 12 | */ 13 | export function uniqBy(array: T[], key: keyof T): T[] { 14 | const seen = new Set() 15 | return array.filter((item) => { 16 | const k = item[key] 17 | return seen.has(k) ? false : seen.add(k) 18 | }) 19 | } 20 | 21 | /** 22 | * Convert Record of attributes to a valid HTML string 23 | */ 24 | export function makeAttributes(attributes: Record) { 25 | return Object.keys(attributes) 26 | .map((key) => { 27 | const value = attributes[key] 28 | 29 | if (value === true) { 30 | return key 31 | } 32 | 33 | if (!value) { 34 | return null 35 | } 36 | 37 | return `${key}="${value}"` 38 | }) 39 | .filter((attr) => attr !== null) 40 | .join(' ') 41 | } 42 | 43 | /** 44 | * Add a trailing slash if missing 45 | */ 46 | export const addTrailingSlash = (url: string) => { 47 | return url.endsWith('/') ? url : url + '/' 48 | } 49 | -------------------------------------------------------------------------------- /src/vite.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'node:path' 11 | import { readFileSync } from 'node:fs' 12 | import { slash } from '@poppinss/utils' 13 | import { ModuleRunner } from 'vite/module-runner' 14 | import type { 15 | InlineConfig, 16 | Manifest, 17 | ModuleNode, 18 | ViteDevServer, 19 | ServerModuleRunnerOptions, 20 | } from 'vite' 21 | 22 | import { makeAttributes, uniqBy } from './utils.js' 23 | import type { AdonisViteElement, SetAttributes, ViteOptions } from './types.js' 24 | 25 | const styleFileRegex = /\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\?)/ 26 | 27 | /** 28 | * Vite class exposes the APIs to generate tags and URLs for 29 | * assets processed using vite. 30 | */ 31 | export class Vite { 32 | /** 33 | * We cache the manifest file content in production 34 | * to avoid reading the file multiple times 35 | */ 36 | #manifestCache?: Manifest 37 | #options: ViteOptions 38 | #devServer?: ViteDevServer 39 | #createServerPromise?: Promise 40 | 41 | constructor( 42 | protected isViteRunning: boolean, 43 | options: ViteOptions 44 | ) { 45 | this.#options = options 46 | this.#options.assetsUrl = (this.#options.assetsUrl || '/').replace(/\/$/, '') 47 | } 48 | 49 | /** 50 | * Reads the file contents as JSON 51 | */ 52 | #readFileAsJSON(filePath: string) { 53 | return JSON.parse(readFileSync(filePath, 'utf-8')) 54 | } 55 | 56 | /** 57 | * Generates a JSON element with a custom toString implementation 58 | */ 59 | #generateElement(element: AdonisViteElement) { 60 | return { 61 | ...element, 62 | toString() { 63 | const attributes = `${makeAttributes(element.attributes)}` 64 | if (element.tag === 'link') { 65 | return `<${element.tag} ${attributes}/>` 66 | } 67 | 68 | return `<${element.tag} ${attributes}>${element.children.join('\n')}` 69 | }, 70 | } 71 | } 72 | 73 | /** 74 | * Returns the script needed for the HMR working with Vite 75 | */ 76 | #getViteHmrScript(attributes?: Record) { 77 | return this.#generateElement({ 78 | tag: 'script', 79 | attributes: { 80 | type: 'module', 81 | src: '/@vite/client', 82 | ...attributes, 83 | }, 84 | children: [], 85 | }) 86 | } 87 | 88 | /** 89 | * Check if the given path is a CSS path 90 | */ 91 | #isCssPath(path: string) { 92 | return path.match(styleFileRegex) !== null 93 | } 94 | 95 | /** 96 | * If the module is a style module 97 | */ 98 | #isStyleModule(mod: ModuleNode) { 99 | if (this.#isCssPath(mod.url) || (mod.id && /\?vue&type=style/.test(mod.id))) { 100 | return true 101 | } 102 | return false 103 | } 104 | 105 | /** 106 | * Unwrap attributes from the user defined function or return 107 | * the attributes as it is 108 | */ 109 | #unwrapAttributes(src: string, url: string, attributes?: SetAttributes) { 110 | if (typeof attributes === 'function') { 111 | return attributes({ src, url }) 112 | } 113 | 114 | return attributes 115 | } 116 | 117 | /** 118 | * Create a style tag for the given path 119 | */ 120 | #makeStyleTag(src: string, url: string, attributes?: Record): AdonisViteElement { 121 | const customAttributes = this.#unwrapAttributes(src, url, this.#options?.styleAttributes) 122 | return this.#generateElement({ 123 | tag: 'link', 124 | attributes: { rel: 'stylesheet', ...customAttributes, ...attributes, href: url }, 125 | }) 126 | } 127 | 128 | /** 129 | * Create a script tag for the given path 130 | */ 131 | #makeScriptTag(src: string, url: string, attributes?: Record): AdonisViteElement { 132 | const customAttributes = this.#unwrapAttributes(src, url, this.#options?.scriptAttributes) 133 | return this.#generateElement({ 134 | tag: 'script', 135 | attributes: { type: 'module', ...customAttributes, ...attributes, src: url }, 136 | children: [], 137 | }) 138 | } 139 | 140 | /** 141 | * Generate an asset URL for a given asset path 142 | */ 143 | #generateAssetUrl(path: string): string { 144 | return `${this.#options.assetsUrl}/${path}` 145 | } 146 | 147 | /** 148 | * Generate a HTML tag for the given asset 149 | */ 150 | #generateTag(asset: string, attributes?: Record): AdonisViteElement { 151 | let url = '' 152 | if (this.isViteRunning) { 153 | url = `/${asset}` 154 | } else { 155 | url = this.#generateAssetUrl(asset) 156 | } 157 | 158 | if (this.#isCssPath(asset)) { 159 | return this.#makeStyleTag(asset, url, attributes) 160 | } 161 | 162 | return this.#makeScriptTag(asset, url, attributes) 163 | } 164 | 165 | /** 166 | * Collect CSS files from the module graph recursively 167 | */ 168 | #collectCss( 169 | mod: ModuleNode, 170 | styleUrls: Set, 171 | visitedModules: Set, 172 | importer?: ModuleNode 173 | ): void { 174 | if (!mod.url) return 175 | 176 | /** 177 | * Prevent visiting the same module twice 178 | */ 179 | if (visitedModules.has(mod.url)) return 180 | visitedModules.add(mod.url) 181 | 182 | if (this.#isStyleModule(mod) && (!importer || !this.#isStyleModule(importer))) { 183 | if (mod.url.startsWith('/')) { 184 | styleUrls.add(mod.url) 185 | } else if (mod.url.startsWith('\0')) { 186 | // virtual modules are prefixed with \0 187 | styleUrls.add(`/@id/__x00__${mod.url.substring(1)}`) 188 | } else { 189 | styleUrls.add(`/@id/${mod.url}`) 190 | } 191 | } 192 | 193 | mod.importedModules.forEach((dep) => this.#collectCss(dep, styleUrls, visitedModules, mod)) 194 | } 195 | 196 | /** 197 | * Generate style and script tags for the given entrypoints 198 | * Also adds the @vite/client script 199 | */ 200 | async #generateEntryPointsTagsForDevMode( 201 | entryPoints: string[], 202 | attributes?: Record 203 | ): Promise { 204 | const server = this.getDevServer()! 205 | 206 | const tags = entryPoints.map((entrypoint) => this.#generateTag(entrypoint, attributes)) 207 | const jsEntrypoints = entryPoints.filter((entrypoint) => !this.#isCssPath(entrypoint)) 208 | 209 | /** 210 | * If the module graph is empty, that means we didn't execute the entrypoint 211 | * yet : we just started the AdonisJS dev server. 212 | * So let's execute the entrypoints to populate the module graph 213 | */ 214 | if (server?.moduleGraph.idToModuleMap.size === 0) { 215 | await Promise.allSettled( 216 | jsEntrypoints.map((entrypoint) => server.warmupRequest(`/${entrypoint}`)) 217 | ) 218 | } 219 | 220 | /** 221 | * We need to collect the CSS files imported by the entrypoints 222 | * Otherwise, we gonna have a FOUC each time we full reload the page 223 | */ 224 | const preloadUrls = new Set() 225 | const visitedModules = new Set() 226 | const cssTagsElement = new Set() 227 | 228 | /** 229 | * Let's search for the CSS files by browsing the module graph 230 | * generated by Vite. 231 | */ 232 | for (const entryPoint of jsEntrypoints) { 233 | const filePath = join(server.config.root, entryPoint) 234 | const entryMod = server.moduleGraph.getModuleById(slash(filePath)) 235 | if (entryMod) this.#collectCss(entryMod, preloadUrls, visitedModules) 236 | } 237 | 238 | /** 239 | * Once we have the CSS files, generate associated tags 240 | * that will be injected into the HTML 241 | */ 242 | const elements = Array.from(preloadUrls).map((href) => 243 | this.#generateElement({ 244 | tag: 'link', 245 | attributes: { rel: 'stylesheet', as: 'style', href: href }, 246 | }) 247 | ) 248 | elements.forEach((element) => cssTagsElement.add(element)) 249 | 250 | const viteHmr = this.#getViteHmrScript(attributes) 251 | const result = [...cssTagsElement, viteHmr].concat(tags) 252 | 253 | return result.sort((tag) => (tag.tag === 'link' ? -1 : 1)) 254 | } 255 | 256 | /** 257 | * Get a chunk from the manifest file for a given file name 258 | */ 259 | #chunk(manifest: Manifest, entrypoint: string) { 260 | const chunk = manifest[entrypoint] 261 | 262 | if (!chunk) { 263 | throw new Error(`Cannot find "${entrypoint}" chunk in the manifest file`) 264 | } 265 | 266 | return chunk 267 | } 268 | 269 | /** 270 | * Get a list of chunks for a given filename 271 | */ 272 | #chunksByFile(manifest: Manifest, file: string) { 273 | return Object.entries(manifest) 274 | .filter(([, chunk]) => chunk.file === file) 275 | .map(([_, chunk]) => chunk) 276 | } 277 | 278 | /** 279 | * Generate preload tag for a given url 280 | */ 281 | #makePreloadTagForUrl(url: string) { 282 | const attributes = this.#isCssPath(url) 283 | ? { rel: 'preload', as: 'style', href: url } 284 | : { rel: 'modulepreload', href: url } 285 | 286 | return this.#generateElement({ tag: 'link', attributes }) 287 | } 288 | 289 | /** 290 | * Generate style and script tags for the given entrypoints 291 | * using the manifest file 292 | */ 293 | #generateEntryPointsTagsWithManifest( 294 | entryPoints: string[], 295 | attributes?: Record 296 | ): AdonisViteElement[] { 297 | const manifest = this.manifest() 298 | const tags: { path: string; tag: AdonisViteElement }[] = [] 299 | const preloads: Array<{ path: string }> = [] 300 | 301 | for (const entryPoint of entryPoints) { 302 | /** 303 | * 1. We generate tags + modulepreload for the entrypoint 304 | */ 305 | const chunk = this.#chunk(manifest, entryPoint) 306 | preloads.push({ path: this.#generateAssetUrl(chunk.file) }) 307 | tags.push({ 308 | path: chunk.file, 309 | tag: this.#generateTag(chunk.file, { ...attributes, integrity: chunk.integrity }), 310 | }) 311 | 312 | /** 313 | * 2. We go through the CSS files that are imported by the entrypoint 314 | * and generate tags + preload for them 315 | */ 316 | for (const css of chunk.css || []) { 317 | preloads.push({ path: this.#generateAssetUrl(css) }) 318 | tags.push({ path: css, tag: this.#generateTag(css) }) 319 | } 320 | 321 | /** 322 | * 3. We go through every import of the entrypoint and generate preload 323 | */ 324 | for (const importNode of chunk.imports || []) { 325 | preloads.push({ path: this.#generateAssetUrl(manifest[importNode].file) }) 326 | 327 | /** 328 | * 4. Finally, we generate tags + preload for the CSS files imported by the import 329 | * of the entrypoint 330 | */ 331 | for (const css of manifest[importNode].css || []) { 332 | const subChunk = this.#chunksByFile(manifest, css) 333 | 334 | preloads.push({ path: this.#generateAssetUrl(css) }) 335 | tags.push({ 336 | path: this.#generateAssetUrl(css), 337 | tag: this.#generateTag(css, { 338 | ...attributes, 339 | integrity: subChunk[0]?.integrity, 340 | }), 341 | }) 342 | } 343 | } 344 | } 345 | 346 | /** 347 | * We sort the preload to ensure that CSS files are preloaded first 348 | */ 349 | const preloadsElements = uniqBy(preloads, 'path') 350 | .sort((preload) => (this.#isCssPath(preload.path) ? -1 : 1)) 351 | .map((preload) => this.#makePreloadTagForUrl(preload.path)) 352 | 353 | /** 354 | * And finally, we return the preloads + script and link tags 355 | */ 356 | return preloadsElements.concat(tags.map(({ tag }) => tag)) 357 | } 358 | 359 | /** 360 | * Generate tags for the entry points 361 | */ 362 | async generateEntryPointsTags( 363 | entryPoints: string[] | string, 364 | attributes?: Record 365 | ): Promise { 366 | entryPoints = Array.isArray(entryPoints) ? entryPoints : [entryPoints] 367 | 368 | if (this.isViteRunning) { 369 | return this.#generateEntryPointsTagsForDevMode(entryPoints, attributes) 370 | } 371 | 372 | return this.#generateEntryPointsTagsWithManifest(entryPoints, attributes) 373 | } 374 | 375 | /** 376 | * Returns the explicitly configured assetsUrl 377 | */ 378 | assetsUrl() { 379 | return this.#options.assetsUrl 380 | } 381 | 382 | /** 383 | * Returns path to a given asset file using the manifest file 384 | */ 385 | assetPath(asset: string): string { 386 | if (this.isViteRunning) { 387 | return `/${asset}` 388 | } 389 | 390 | const chunk = this.#chunk(this.manifest(), asset) 391 | return this.#generateAssetUrl(chunk.file) 392 | } 393 | 394 | /** 395 | * Returns the manifest file contents 396 | * 397 | * @throws Will throw an exception when running in dev 398 | */ 399 | manifest(): Manifest { 400 | if (this.isViteRunning) { 401 | throw new Error('Cannot read the manifest file when running in dev mode') 402 | } 403 | 404 | if (!this.#manifestCache) { 405 | this.#manifestCache = this.#readFileAsJSON(this.#options.manifestFile) 406 | } 407 | 408 | return this.#manifestCache! 409 | } 410 | 411 | /** 412 | * Create the Vite Dev Server and runtime 413 | * 414 | * We lazy load the APIs to avoid loading it in production 415 | * since we don't need it 416 | */ 417 | async createDevServer(options?: InlineConfig) { 418 | const { createServer } = await import('vite') 419 | 420 | /** 421 | * We do not await the server creation since it will 422 | * slow down the boot process of AdonisJS 423 | */ 424 | this.#createServerPromise = createServer({ 425 | server: { middlewareMode: true }, 426 | appType: 'custom', 427 | ...options, 428 | }) 429 | 430 | this.#devServer = await this.#createServerPromise 431 | } 432 | 433 | /** 434 | * Create a serverModuleRunner instance 435 | * Will not be available when running in production since 436 | * it needs the Vite Dev server 437 | */ 438 | async createModuleRunner(options: ServerModuleRunnerOptions = {}): Promise { 439 | const { createServerModuleRunner } = await import('vite') 440 | return createServerModuleRunner(this.#devServer!.environments.ssr, options) 441 | } 442 | 443 | /** 444 | * Stop the Vite Dev server 445 | */ 446 | async stopDevServer() { 447 | await this.#createServerPromise 448 | await this.#devServer?.close() 449 | } 450 | 451 | /** 452 | * Get the Vite Dev server instance 453 | * Will not be available when running in production 454 | */ 455 | getDevServer() { 456 | return this.#devServer 457 | } 458 | 459 | /** 460 | * Returns the script needed for the HMR working with React 461 | */ 462 | getReactHmrScript(attributes?: Record): AdonisViteElement | null { 463 | if (!this.isViteRunning) { 464 | return null 465 | } 466 | 467 | return this.#generateElement({ 468 | tag: 'script', 469 | attributes: { 470 | type: 'module', 471 | ...attributes, 472 | }, 473 | children: [ 474 | '', 475 | `import RefreshRuntime from '/@react-refresh'`, 476 | `RefreshRuntime.injectIntoGlobalHook(window)`, 477 | `window.$RefreshReg$ = () => {}`, 478 | `window.$RefreshSig$ = () => (type) => type`, 479 | `window.__vite_plugin_react_preamble_installed__ = true`, 480 | '', 481 | ], 482 | }) 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /src/vite_middleware.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { ViteDevServer } from 'vite' 11 | import type { HttpContext } from '@adonisjs/core/http' 12 | import type { NextFn } from '@adonisjs/core/types/http' 13 | 14 | import type { Vite } from './vite.js' 15 | 16 | /** 17 | * Since Vite dev server is integrated within the AdonisJS process, this 18 | * middleware is used to proxy the requests to it. 19 | * 20 | * Some of the requests are directly handled by the Vite dev server, 21 | * like the one for the assets, while others are passed down to the 22 | * AdonisJS server. 23 | */ 24 | export default class ViteMiddleware { 25 | #devServer: ViteDevServer 26 | 27 | constructor(protected vite: Vite) { 28 | this.#devServer = this.vite.getDevServer()! 29 | } 30 | 31 | async handle({ request, response }: HttpContext, next: NextFn) { 32 | if (!this.#devServer) return next() 33 | 34 | /** 35 | * @adonisjs/cors should handle the CORS instead of Vite 36 | */ 37 | if (this.#devServer.config.server.cors === false) response.relayHeaders() 38 | 39 | /** 40 | * Proxy the request to the vite dev server 41 | */ 42 | await new Promise((resolve) => { 43 | this.#devServer.middlewares.handle(request.request, response.response, () => { 44 | return resolve(next()) 45 | }) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /stubs/config/vite.stub: -------------------------------------------------------------------------------- 1 | {{{ 2 | exports({ to: app.configPath('vite.ts') }) 3 | }}} 4 | import { defineConfig } from '@adonisjs/vite' 5 | 6 | const viteBackendConfig = defineConfig({ 7 | /** 8 | * The output of vite will be written inside this 9 | * directory. The path should be relative from 10 | * the application root. 11 | */ 12 | buildDirectory: 'public/assets', 13 | 14 | /** 15 | * The path to the manifest file generated by the 16 | * "vite build" command. 17 | */ 18 | manifestFile: 'public/assets/.vite/manifest.json', 19 | 20 | /** 21 | * Feel free to change the value of the "assetsUrl" to 22 | * point to a CDN in production. 23 | */ 24 | assetsUrl: '/assets', 25 | }) 26 | 27 | export default viteBackendConfig 28 | -------------------------------------------------------------------------------- /stubs/js_entrypoint.stub: -------------------------------------------------------------------------------- 1 | {{{ 2 | exports({ to: app.makePath('resources/js/app.js') }) 3 | }}} 4 | console.log('Log from JS entrypoint') 5 | -------------------------------------------------------------------------------- /stubs/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { getDirname } from '@poppinss/utils' 11 | export const stubsRoot = getDirname(import.meta.url) 12 | -------------------------------------------------------------------------------- /stubs/vite.config.stub: -------------------------------------------------------------------------------- 1 | {{{ 2 | exports({ to: app.makePath('vite.config.ts') }) 3 | }}} 4 | import { defineConfig } from 'vite' 5 | import adonisjs from '@adonisjs/vite/client' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | adonisjs({ 10 | /** 11 | * Entrypoints of your application. Each entrypoint will 12 | * result in a separate bundle. 13 | */ 14 | entrypoints: ['resources/js/app.js'], 15 | 16 | /** 17 | * Paths to watch and reload the browser on file change 18 | */ 19 | reload: ['resources/views/**/*.edge'], 20 | }), 21 | ], 22 | }) 23 | -------------------------------------------------------------------------------- /tests/backend/define_config.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { defineConfig } from '../../index.js' 12 | 13 | test.group('Define config', () => { 14 | test('merge defaults with user provided config', ({ assert }) => { 15 | assert.deepEqual(defineConfig({}), { 16 | buildDirectory: 'public/assets', 17 | assetsUrl: '/assets', 18 | manifestFile: 'public/assets/.vite/manifest.json', 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /tests/backend/edge_plugin.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Edge } from 'edge.js' 11 | import { test } from '@japa/runner' 12 | 13 | import { Vite } from '../../src/vite.js' 14 | import { createVite } from './helpers.js' 15 | import { defineConfig } from '../../src/define_config.js' 16 | import { edgePluginVite } from '../../src/plugins/edge.js' 17 | 18 | test.group('Edge plugin vite', () => { 19 | test('generate asset path within edge template', async ({ assert }) => { 20 | const edge = Edge.create() 21 | const vite = await createVite(defineConfig({})) 22 | edge.use(edgePluginVite(vite)) 23 | 24 | const html = await edge.renderRaw(`{{ asset('foo.png') }}`) 25 | assert.equal(html, '/foo.png') 26 | }) 27 | 28 | test('share vite instance with edge', async ({ assert }) => { 29 | const edge = Edge.create() 30 | const vite = await createVite(defineConfig({})) 31 | edge.use(edgePluginVite(vite)) 32 | 33 | const html = await edge.renderRaw(`{{ vite.assetPath('foo.png') }}`) 34 | assert.equal(html, '/foo.png') 35 | }) 36 | 37 | test('output reactHMRScript', async ({ assert }) => { 38 | const edge = Edge.create() 39 | const vite = await createVite(defineConfig({})) 40 | edge.use(edgePluginVite(vite)) 41 | 42 | const html = await edge.renderRaw(`@viteReactRefresh()`) 43 | assert.deepEqual(html.split('\n'), [ 44 | ``, 51 | ]) 52 | }) 53 | 54 | test('pass custom attributes to reactHMRScript', async ({ assert }) => { 55 | const edge = Edge.create() 56 | const vite = await createVite(defineConfig({})) 57 | edge.use(edgePluginVite(vite)) 58 | 59 | const html = await edge.renderRaw(`@viteReactRefresh({ nonce: 'foo' })`) 60 | assert.deepEqual(html.split('\n'), [ 61 | ``, 68 | ]) 69 | }) 70 | 71 | test('do not output hmrScript when not in hot mode', async ({ assert }) => { 72 | const edge = Edge.create() 73 | const vite = new Vite(false, defineConfig({})) 74 | edge.use(edgePluginVite(vite)) 75 | 76 | const html = await edge.renderRaw(`@viteReactRefresh()`) 77 | assert.deepEqual(html.split('\n'), ['']) 78 | }) 79 | 80 | test('output entrypoint tags', async ({ assert }) => { 81 | const edge = Edge.create() 82 | const vite = await createVite(defineConfig({})) 83 | edge.use(edgePluginVite(vite)) 84 | 85 | const html = await edge.renderRaw(`@vite(['resources/js/app.js'])`) 86 | assert.deepEqual(html.split('\n'), [ 87 | '', 88 | '', 89 | ]) 90 | }) 91 | 92 | test('output entrypoint tags with custom attributes', async ({ assert }) => { 93 | const edge = Edge.create() 94 | const vite = await createVite(defineConfig({})) 95 | edge.use(edgePluginVite(vite)) 96 | 97 | const html = await edge.renderRaw(`@vite(['resources/js/app.js'], { nonce: 'foo' })`) 98 | assert.deepEqual(html.split('\n'), [ 99 | '', 100 | '', 101 | ]) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /tests/backend/fixtures/adonis_packages_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "__plugin-vue_export-helper-DlAUqK2U.js": { 3 | "file": "_plugin-vue_export-helper-DlAUqK2U.js" 4 | }, 5 | "_default-!~{00h}~.js": { 6 | "file": "default-CzWQScon.css", 7 | "src": "_default-!~{00h}~.js" 8 | }, 9 | "_default-Do-xftcX.js": { 10 | "file": "default-Do-xftcX.js", 11 | "imports": ["resources/app.ts", "__plugin-vue_export-helper-DlAUqK2U.js"], 12 | "css": ["default-CzWQScon.css"] 13 | }, 14 | "_index-C1JNlH7D.js": { 15 | "file": "index-C1JNlH7D.js", 16 | "imports": ["resources/app.ts"] 17 | }, 18 | "_main_section-!~{004}~.js": { 19 | "file": "main_section-QGbeXyUe.css", 20 | "src": "_main_section-!~{004}~.js" 21 | }, 22 | "_main_section-CT1dtBDn.js": { 23 | "file": "main_section-CT1dtBDn.js", 24 | "isDynamicEntry": true, 25 | "imports": [ 26 | "resources/app.ts", 27 | "resources/pages/home/components/package_card.vue", 28 | "__plugin-vue_export-helper-DlAUqK2U.js" 29 | ], 30 | "css": ["main_section-QGbeXyUe.css"] 31 | }, 32 | "_package_stats-BzvH-KEP.js": { 33 | "file": "package_stats-BzvH-KEP.js", 34 | "imports": ["resources/app.ts", "__plugin-vue_export-helper-DlAUqK2U.js"] 35 | }, 36 | "resources/app.ts": { 37 | "file": "app-CGO3UiiC.js", 38 | "src": "resources/app.ts", 39 | "isEntry": true, 40 | "dynamicImports": [ 41 | "resources/pages/home/components/button_group.vue", 42 | "resources/pages/home/components/filters.vue", 43 | "_main_section-CT1dtBDn.js", 44 | "resources/pages/home/components/order.vue", 45 | "resources/pages/home/components/package_card.vue", 46 | "resources/pages/home/components/pagination.vue", 47 | "resources/pages/home/components/search_bar.vue", 48 | "resources/pages/home/components/select_menu.vue", 49 | "resources/pages/home/main.vue", 50 | "resources/pages/package/components/heading.vue", 51 | "resources/pages/package/components/links.vue", 52 | "resources/pages/package/components/toc.vue", 53 | "resources/pages/package/main.vue" 54 | ], 55 | "css": ["app-2kD3K4XR.css"], 56 | "assets": ["PolySans-Neutral-DB_poC01.ttf", "Graphik-Regular-Cn31DaBb.ttf"] 57 | }, 58 | "resources/assets/fonts/Graphik-Regular.ttf": { 59 | "file": "Graphik-Regular-Cn31DaBb.ttf", 60 | "src": "resources/assets/fonts/Graphik-Regular.ttf" 61 | }, 62 | "resources/assets/fonts/PolySans-Neutral.ttf": { 63 | "file": "PolySans-Neutral-DB_poC01.ttf", 64 | "src": "resources/assets/fonts/PolySans-Neutral.ttf" 65 | }, 66 | "resources/assets/noise.webp": { 67 | "file": "noise-C0lGURVU.webp", 68 | "src": "resources/assets/noise.webp" 69 | }, 70 | "resources/assets/topography.svg": { 71 | "file": "topography-CdPJiSxy.svg", 72 | "src": "resources/assets/topography.svg" 73 | }, 74 | "resources/pages/home/components/button_group.vue": { 75 | "file": "button_group-BrQ8CWuu.js", 76 | "src": "resources/pages/home/components/button_group.vue", 77 | "isDynamicEntry": true, 78 | "imports": ["resources/app.ts", "__plugin-vue_export-helper-DlAUqK2U.js"] 79 | }, 80 | "resources/pages/home/components/filters.vue": { 81 | "file": "filters-Dmvaqb5E.js", 82 | "src": "resources/pages/home/components/filters.vue", 83 | "isDynamicEntry": true, 84 | "imports": ["resources/app.ts", "__plugin-vue_export-helper-DlAUqK2U.js"] 85 | }, 86 | "resources/pages/home/components/order.vue": { 87 | "file": "order-D6tpsh_Z.js", 88 | "src": "resources/pages/home/components/order.vue", 89 | "isDynamicEntry": true, 90 | "imports": ["resources/app.ts", "__plugin-vue_export-helper-DlAUqK2U.js"] 91 | }, 92 | "resources/pages/home/components/package_card.vue": { 93 | "file": "package_card-COzCRM-8.js", 94 | "src": "resources/pages/home/components/package_card.vue", 95 | "isDynamicEntry": true, 96 | "imports": [ 97 | "resources/app.ts", 98 | "__plugin-vue_export-helper-DlAUqK2U.js", 99 | "_package_stats-BzvH-KEP.js" 100 | ], 101 | "css": ["package_card-JrVjtBKi.css"] 102 | }, 103 | "resources/pages/home/components/pagination.vue": { 104 | "file": "pagination-FizlBWAv.js", 105 | "src": "resources/pages/home/components/pagination.vue", 106 | "isDynamicEntry": true, 107 | "imports": ["resources/app.ts", "__plugin-vue_export-helper-DlAUqK2U.js"] 108 | }, 109 | "resources/pages/home/components/search_bar.vue": { 110 | "file": "search_bar-DO-fkAsH.js", 111 | "src": "resources/pages/home/components/search_bar.vue", 112 | "isDynamicEntry": true, 113 | "imports": ["resources/app.ts", "__plugin-vue_export-helper-DlAUqK2U.js"] 114 | }, 115 | "resources/pages/home/components/select_menu.vue": { 116 | "file": "select_menu-SuftI0p0.js", 117 | "src": "resources/pages/home/components/select_menu.vue", 118 | "isDynamicEntry": true, 119 | "imports": ["resources/app.ts", "__plugin-vue_export-helper-DlAUqK2U.js"] 120 | }, 121 | "resources/pages/home/main.vue": { 122 | "file": "main-CKiOIoD7.js", 123 | "src": "resources/pages/home/main.vue", 124 | "isDynamicEntry": true, 125 | "imports": [ 126 | "resources/app.ts", 127 | "_index-C1JNlH7D.js", 128 | "_main_section-CT1dtBDn.js", 129 | "__plugin-vue_export-helper-DlAUqK2U.js", 130 | "_default-Do-xftcX.js", 131 | "resources/pages/home/components/order.vue", 132 | "resources/pages/home/components/filters.vue", 133 | "resources/pages/home/components/search_bar.vue", 134 | "resources/pages/home/components/pagination.vue", 135 | "resources/pages/home/components/select_menu.vue", 136 | "resources/pages/home/components/button_group.vue", 137 | "resources/pages/home/components/package_card.vue", 138 | "_package_stats-BzvH-KEP.js" 139 | ], 140 | "css": ["main-BcGYH63d.css"], 141 | "assets": ["noise-C0lGURVU.webp", "topography-CdPJiSxy.svg"] 142 | }, 143 | "resources/pages/package/components/heading.vue": { 144 | "file": "heading-CsYW2tca.js", 145 | "src": "resources/pages/package/components/heading.vue", 146 | "isDynamicEntry": true, 147 | "imports": [ 148 | "resources/app.ts", 149 | "_package_stats-BzvH-KEP.js", 150 | "__plugin-vue_export-helper-DlAUqK2U.js" 151 | ] 152 | }, 153 | "resources/pages/package/components/links.vue": { 154 | "file": "links-Dp_Qnu9y.js", 155 | "src": "resources/pages/package/components/links.vue", 156 | "isDynamicEntry": true, 157 | "imports": ["resources/app.ts", "__plugin-vue_export-helper-DlAUqK2U.js"] 158 | }, 159 | "resources/pages/package/components/toc.vue": { 160 | "file": "toc-C4T6XXuW.js", 161 | "src": "resources/pages/package/components/toc.vue", 162 | "isDynamicEntry": true, 163 | "imports": ["resources/app.ts", "_index-C1JNlH7D.js", "__plugin-vue_export-helper-DlAUqK2U.js"] 164 | }, 165 | "resources/pages/package/main.vue": { 166 | "file": "main-Sqg_PXSY.js", 167 | "src": "resources/pages/package/main.vue", 168 | "isDynamicEntry": true, 169 | "imports": [ 170 | "resources/app.ts", 171 | "resources/pages/package/components/toc.vue", 172 | "_default-Do-xftcX.js", 173 | "resources/pages/package/components/links.vue", 174 | "resources/pages/package/components/heading.vue", 175 | "__plugin-vue_export-helper-DlAUqK2U.js", 176 | "_index-C1JNlH7D.js", 177 | "_package_stats-BzvH-KEP.js" 178 | ], 179 | "css": ["main-LIhZCNSE.css"], 180 | "assets": ["noise-C0lGURVU.webp", "topography-CdPJiSxy.svg"] 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/backend/helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { getActiveTest } from '@japa/runner' 11 | 12 | import { Vite } from '../../index.js' 13 | import { ViteOptions } from '../../src/types.js' 14 | import { InlineConfig } from 'vite' 15 | 16 | export const BASE_URL = new URL('./../__app/', import.meta.url) 17 | 18 | /** 19 | * Create an instance of AdonisJS Vite class, run the dev server 20 | * and auto close it when the test ends 21 | */ 22 | export async function createVite(config: ViteOptions, viteConfig: InlineConfig = {}) { 23 | const test = getActiveTest() 24 | if (!test) { 25 | throw new Error('Cannot create vite instance outside of a test') 26 | } 27 | 28 | /** 29 | * Create a dummy file to ensure the root directory exists 30 | * otherwise Vite will throw an error 31 | */ 32 | await test.context.fs.create('dummy.txt', 'dummy') 33 | 34 | const vite = new Vite(true, config) 35 | 36 | await vite.createDevServer({ 37 | logLevel: 'silent', 38 | clearScreen: false, 39 | root: test.context.fs.basePath, 40 | ...viteConfig, 41 | }) 42 | 43 | test.cleanup(() => vite.stopDevServer()) 44 | 45 | return vite 46 | } 47 | -------------------------------------------------------------------------------- /tests/backend/middleware.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import supertest from 'supertest' 11 | import { test } from '@japa/runner' 12 | import { createServer } from 'node:http' 13 | import { RequestFactory, ResponseFactory, HttpContextFactory } from '@adonisjs/core/factories/http' 14 | 15 | import { Vite } from '../../index.js' 16 | import { createVite } from './helpers.js' 17 | import adonisjs from '../../src/client/main.js' 18 | import ViteMiddleware from '../../src/vite_middleware.js' 19 | 20 | test.group('Vite Middleware', () => { 21 | test('if route is handled by vite, relay cors headers', async ({ assert, fs }) => { 22 | await fs.create('resources/js/app.ts', 'console.log("Hello world")') 23 | 24 | const vite = await createVite( 25 | { buildDirectory: 'foo', manifestFile: 'bar.json' }, 26 | { plugins: [adonisjs({ entrypoints: ['./resources/js/app.ts'] })] } 27 | ) 28 | 29 | const server = createServer(async (req, res) => { 30 | const middleware = new ViteMiddleware(vite) 31 | 32 | const request = new RequestFactory().merge({ req, res }).create() 33 | const response = new ResponseFactory().merge({ req, res }).create() 34 | const ctx = new HttpContextFactory().merge({ request, response }).create() 35 | 36 | response.header('access-control-allow-origin', 'http://test-origin.com') 37 | 38 | await middleware.handle(ctx, () => {}) 39 | 40 | ctx.response.finish() 41 | }) 42 | 43 | const res = await supertest(server).get('/resources/js/app.ts') 44 | assert.equal(res.headers['access-control-allow-origin'], 'http://test-origin.com') 45 | 46 | const resOptions = await supertest(server).options('/resources/js/app.ts') 47 | assert.equal(resOptions.headers['access-control-allow-origin'], 'http://test-origin.com') 48 | }) 49 | 50 | test('if vite dev server is not available, call next middleware', async ({ assert }) => { 51 | class FakeVite extends Vite { 52 | getDevServer() { 53 | return undefined 54 | } 55 | } 56 | 57 | const vite = new FakeVite(false, { buildDirectory: 'foo', manifestFile: 'bar.json' }) 58 | 59 | const server = createServer(async (req, res) => { 60 | const middleware = new ViteMiddleware(vite) 61 | 62 | const request = new RequestFactory().merge({ req, res }).create() 63 | const response = new ResponseFactory().merge({ req, res }).create() 64 | const ctx = new HttpContextFactory().merge({ request, response }).create() 65 | 66 | await middleware.handle(ctx, () => { 67 | ctx.response.status(200).send('handled by next middleware') 68 | }) 69 | 70 | ctx.response.finish() 71 | }) 72 | 73 | const res = await supertest(server).get('/resources/js/app.ts') 74 | assert.equal(res.text, 'handled by next middleware') 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /tests/backend/provider.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { setTimeout } from 'node:timers/promises' 12 | import { IgnitorFactory } from '@adonisjs/core/factories' 13 | 14 | import { defineConfig } from '../../index.js' 15 | import ViteMiddleware from '../../src/vite_middleware.js' 16 | 17 | const BASE_URL = new URL('./tmp/', import.meta.url) 18 | const IMPORTER = (filePath: string) => { 19 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 20 | return import(new URL(filePath, BASE_URL).href) 21 | } 22 | return import(filePath) 23 | } 24 | 25 | test.group('Vite Provider', () => { 26 | test('register vite middleware singleton', async ({ assert }) => { 27 | process.env.NODE_ENV = 'development' 28 | 29 | const ignitor = new IgnitorFactory() 30 | .merge({ rcFileContents: { providers: [() => import('../../providers/vite_provider.js')] } }) 31 | .withCoreConfig() 32 | .withCoreProviders() 33 | .merge({ config: { vite: defineConfig({}) } }) 34 | .create(BASE_URL, { importer: IMPORTER }) 35 | 36 | const app = ignitor.createApp('web') 37 | await app.init() 38 | await app.boot() 39 | 40 | assert.instanceOf(await app.container.make(ViteMiddleware), ViteMiddleware) 41 | 42 | await app.terminate() 43 | }) 44 | 45 | test('launch dev server in dev mode', async ({ assert }) => { 46 | process.env.NODE_ENV = 'development' 47 | 48 | const ignitor = new IgnitorFactory() 49 | .merge({ rcFileContents: { providers: [() => import('../../providers/vite_provider.js')] } }) 50 | .withCoreConfig() 51 | .withCoreProviders() 52 | .merge({ config: { vite: defineConfig({}) } }) 53 | .create(BASE_URL, { importer: IMPORTER }) 54 | 55 | const app = ignitor.createApp('web') 56 | await app.init() 57 | await app.boot() 58 | 59 | const vite = await app.container.make('vite') 60 | 61 | await setTimeout(200) 62 | assert.isDefined(vite.getDevServer()?.restart) 63 | 64 | await app.terminate() 65 | }) 66 | 67 | test('doesnt launch dev server in prod', async ({ assert }) => { 68 | process.env.NODE_ENV = 'production' 69 | 70 | const ignitor = new IgnitorFactory() 71 | .merge({ rcFileContents: { providers: [() => import('../../providers/vite_provider.js')] } }) 72 | .withCoreConfig() 73 | .withCoreProviders() 74 | .merge({ config: { vite: defineConfig({}) } }) 75 | .create(BASE_URL, { importer: IMPORTER }) 76 | 77 | const app = ignitor.createApp('web') 78 | await app.init() 79 | await app.boot() 80 | 81 | const vite = await app.container.make('vite') 82 | assert.isUndefined(vite.getDevServer()) 83 | 84 | await app.terminate() 85 | }) 86 | 87 | test('run dev server in test', async ({ assert }) => { 88 | process.env.NODE_ENV = 'test' 89 | 90 | const ignitor = new IgnitorFactory() 91 | .merge({ rcFileContents: { providers: [() => import('../../providers/vite_provider.js')] } }) 92 | .withCoreConfig() 93 | .withCoreProviders() 94 | .merge({ config: { vite: defineConfig({}) } }) 95 | .create(BASE_URL, { importer: IMPORTER }) 96 | 97 | const app = ignitor.createApp('test') 98 | await app.init() 99 | await app.boot() 100 | 101 | const vite = await app.container.make('vite') 102 | await setTimeout(200) 103 | assert.isDefined(vite.getDevServer()?.restart) 104 | 105 | await app.terminate() 106 | }) 107 | 108 | test('doesnt launch dev server in console environment', async ({ assert }) => { 109 | const ignitor = new IgnitorFactory() 110 | .merge({ rcFileContents: { providers: [() => import('../../providers/vite_provider.js')] } }) 111 | .withCoreConfig() 112 | .withCoreProviders() 113 | .merge({ config: { vite: defineConfig({}) } }) 114 | .create(BASE_URL, { importer: IMPORTER }) 115 | 116 | const app = ignitor.createApp('console') 117 | await app.init() 118 | await app.boot() 119 | 120 | const vite = await app.container.make('vite') 121 | assert.isUndefined(vite.getDevServer()) 122 | 123 | await app.terminate() 124 | }) 125 | 126 | test('register edge plugin', async ({ assert }) => { 127 | process.env.NODE_ENV = 'development' 128 | 129 | const ignitor = new IgnitorFactory() 130 | .merge({ 131 | rcFileContents: { 132 | providers: [ 133 | () => import('../../providers/vite_provider.js'), 134 | () => import('@adonisjs/core/providers/edge_provider'), 135 | ], 136 | }, 137 | }) 138 | .withCoreConfig() 139 | .withCoreProviders() 140 | .merge({ config: { vite: defineConfig({}) } }) 141 | .create(BASE_URL, { importer: IMPORTER }) 142 | 143 | const app = ignitor.createApp('web') 144 | await app.init() 145 | await app.boot() 146 | 147 | const edge = await import('edge.js') 148 | await edge.default.renderRaw('') 149 | 150 | assert.isDefined(edge.default.tags.vite) 151 | 152 | await app.terminate() 153 | }) 154 | 155 | test('register edge plugin in production', async ({ assert }) => { 156 | process.env.NODE_ENV = 'production' 157 | 158 | const ignitor = new IgnitorFactory() 159 | .merge({ 160 | rcFileContents: { 161 | providers: [ 162 | () => import('../../providers/vite_provider.js'), 163 | () => import('@adonisjs/core/providers/edge_provider'), 164 | ], 165 | }, 166 | }) 167 | .withCoreConfig() 168 | .withCoreProviders() 169 | .merge({ config: { vite: defineConfig({}) } }) 170 | .create(BASE_URL, { importer: IMPORTER }) 171 | 172 | const app = ignitor.createApp('web') 173 | await app.init() 174 | await app.boot() 175 | 176 | const edge = await import('edge.js') 177 | await edge.default.renderRaw('') 178 | 179 | assert.isDefined(edge.default.tags.vite) 180 | 181 | await app.terminate() 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /tests/backend/vite.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'node:path' 11 | import { test } from '@japa/runner' 12 | import { fileURLToPath } from 'node:url' 13 | 14 | import { Vite } from '../../src/vite.js' 15 | import { createVite } from './helpers.js' 16 | import { defineConfig } from '../../src/define_config.js' 17 | 18 | test.group('Vite | dev', () => { 19 | test('generate entrypoints tags for a file', async ({ assert, fs }) => { 20 | const vite = await createVite( 21 | defineConfig({ buildDirectory: join(fs.basePath, 'public/assets') }) 22 | ) 23 | 24 | const output = await vite.generateEntryPointsTags('test.js') 25 | 26 | assert.containsSubset(output, [ 27 | { 28 | tag: 'script', 29 | attributes: { type: 'module', src: '/@vite/client' }, 30 | children: [], 31 | }, 32 | { 33 | tag: 'script', 34 | attributes: { type: 'module', src: '/test.js' }, 35 | children: [], 36 | }, 37 | ]) 38 | assert.deepEqual( 39 | output.map((element) => String(element)), 40 | [ 41 | '', 42 | '', 43 | ] 44 | ) 45 | }) 46 | 47 | test('ignore assetsUrl in dev mode', async ({ assert, fs }) => { 48 | const vite = await createVite( 49 | defineConfig({ 50 | buildDirectory: join(fs.basePath, 'public/assets'), 51 | assetsUrl: 'https://cdn.url.com', 52 | }) 53 | ) 54 | 55 | const output = await vite.generateEntryPointsTags('test.js') 56 | 57 | assert.containsSubset(output, [ 58 | { 59 | tag: 'script', 60 | attributes: { type: 'module', src: '/@vite/client' }, 61 | children: [], 62 | }, 63 | { 64 | tag: 'script', 65 | attributes: { type: 'module', src: '/test.js' }, 66 | children: [], 67 | }, 68 | ]) 69 | assert.deepEqual( 70 | output.map((element) => String(element)), 71 | [ 72 | '', 73 | '', 74 | ] 75 | ) 76 | }) 77 | 78 | test('raise exception when trying to access manifest file in dev mode', async ({ fs }) => { 79 | const vite = await createVite( 80 | defineConfig({ buildDirectory: join(fs.basePath, 'public/assets') }) 81 | ) 82 | 83 | vite.manifest() 84 | }).throws('Cannot read the manifest file when running in dev mode') 85 | 86 | test('get asset path', async ({ fs, assert }) => { 87 | const vite = await createVite( 88 | defineConfig({ buildDirectory: join(fs.basePath, 'public/assets') }) 89 | ) 90 | 91 | assert.equal(vite.assetPath('test.js'), '/test.js') 92 | }) 93 | 94 | test('ignore custom assetsUrl in dev mode', async ({ fs, assert }) => { 95 | const vite = await createVite( 96 | defineConfig({ 97 | buildDirectory: join(fs.basePath, 'public/assets'), 98 | assetsUrl: 'https://cdn.url.com', 99 | }) 100 | ) 101 | 102 | assert.equal(vite.assetPath('test.js'), '/test.js') 103 | }) 104 | 105 | test('get viteHMRScript for React', async ({ fs, assert }) => { 106 | const vite = await createVite( 107 | defineConfig({ 108 | buildDirectory: join(fs.basePath, 'public/assets'), 109 | assetsUrl: 'https://cdn.url.com', 110 | }) 111 | ) 112 | 113 | assert.containsSubset(vite.getReactHmrScript(), { 114 | tag: 'script', 115 | attributes: { 116 | type: 'module', 117 | }, 118 | children: [ 119 | '', 120 | `import RefreshRuntime from '/@react-refresh'`, 121 | `RefreshRuntime.injectIntoGlobalHook(window)`, 122 | `window.$RefreshReg$ = () => {}`, 123 | `window.$RefreshSig$ = () => (type) => type`, 124 | `window.__vite_plugin_react_preamble_installed__ = true`, 125 | '', 126 | ], 127 | }) 128 | }) 129 | 130 | test('add custom attributes to the entrypoints script tags', async ({ assert, fs }) => { 131 | const vite = await createVite( 132 | defineConfig({ 133 | buildDirectory: join(fs.basePath, 'public/assets'), 134 | assetsUrl: 'https://cdn.url.com', 135 | scriptAttributes: { 136 | 'data-test': 'test', 137 | }, 138 | }) 139 | ) 140 | 141 | const output = await vite.generateEntryPointsTags('test.js') 142 | 143 | assert.containsSubset(output, [ 144 | { 145 | tag: 'script', 146 | attributes: { type: 'module', src: '/@vite/client' }, 147 | children: [], 148 | }, 149 | { 150 | tag: 'script', 151 | attributes: { 152 | 'type': 'module', 153 | 'data-test': 'test', 154 | 'src': '/test.js', 155 | }, 156 | children: [], 157 | }, 158 | ]) 159 | assert.deepEqual( 160 | output.map((element) => String(element)), 161 | [ 162 | '', 163 | '', 164 | ] 165 | ) 166 | }) 167 | 168 | test('add custom attributes to the entrypoints style tags', async ({ assert, fs }) => { 169 | const vite = await createVite( 170 | defineConfig({ 171 | buildDirectory: join(fs.basePath, 'public/assets'), 172 | assetsUrl: 'https://cdn.url.com', 173 | styleAttributes: { 174 | 'data-test': 'test', 175 | }, 176 | }) 177 | ) 178 | 179 | const output = await vite.generateEntryPointsTags('app.css') 180 | 181 | assert.containsSubset(output, [ 182 | { 183 | tag: 'script', 184 | attributes: { type: 'module', src: '/@vite/client' }, 185 | children: [], 186 | }, 187 | { 188 | tag: 'link', 189 | attributes: { 190 | 'rel': 'stylesheet', 191 | 'data-test': 'test', 192 | 'href': '/app.css', 193 | }, 194 | }, 195 | ]) 196 | assert.deepEqual( 197 | output.map((element) => String(element)), 198 | [ 199 | '', 200 | '', 201 | ] 202 | ) 203 | }) 204 | }) 205 | 206 | test.group('Vite | manifest', () => { 207 | test('generate entrypoints tags for a file', async ({ assert, fs, cleanup }) => { 208 | const vite = new Vite( 209 | false, 210 | defineConfig({ 211 | buildDirectory: join(fs.basePath, 'public/assets'), 212 | }) 213 | ) 214 | 215 | await vite.createDevServer() 216 | cleanup(() => vite.stopDevServer()) 217 | 218 | await fs.create( 219 | 'public/assets/.vite/manifest.json', 220 | JSON.stringify({ 'test.js': { file: 'test-12345.js', src: 'test.js' } }) 221 | ) 222 | 223 | const output = await vite.generateEntryPointsTags('test.js') 224 | 225 | assert.containsSubset(output, [ 226 | { 227 | tag: 'script', 228 | attributes: { type: 'module', src: '/assets/test-12345.js' }, 229 | children: [], 230 | }, 231 | ]) 232 | assert.containsSubset( 233 | output.map((element) => String(element)), 234 | [''] 235 | ) 236 | }) 237 | 238 | test('generate entrypoints with css imported inside js', async ({ assert, fs }) => { 239 | const vite = new Vite( 240 | false, 241 | defineConfig({ 242 | buildDirectory: join(fs.basePath, 'public/assets'), 243 | }) 244 | ) 245 | 246 | await fs.create( 247 | 'public/assets/.vite/manifest.json', 248 | JSON.stringify({ 249 | 'test.js': { file: 'test-12345.js', src: 'test.js', css: ['main.b82dbe22.css'] }, 250 | 'main.css': { file: 'main.b82dbe22.css', src: 'main.css' }, 251 | }) 252 | ) 253 | 254 | const output = await vite.generateEntryPointsTags('test.js') 255 | 256 | assert.containsSubset(output, [ 257 | { 258 | tag: 'link', 259 | attributes: { rel: 'stylesheet', href: '/assets/main.b82dbe22.css' }, 260 | }, 261 | { 262 | tag: 'script', 263 | attributes: { type: 'module', src: '/assets/test-12345.js' }, 264 | children: [], 265 | }, 266 | ]) 267 | assert.containsSubset( 268 | output.map((element) => String(element)), 269 | [ 270 | '', 271 | '', 272 | ] 273 | ) 274 | }) 275 | 276 | test('prefix assetsUrl', async ({ assert, fs }) => { 277 | const vite = new Vite( 278 | false, 279 | defineConfig({ 280 | buildDirectory: join(fs.basePath, 'public/assets'), 281 | assetsUrl: 'https://cdn.url.com', 282 | }) 283 | ) 284 | 285 | await fs.create( 286 | 'public/assets/.vite/manifest.json', 287 | JSON.stringify({ 'test.js': { file: 'test-12345.js', src: 'test.js' } }) 288 | ) 289 | 290 | const output = await vite.generateEntryPointsTags('test.js') 291 | 292 | assert.containsSubset(output, [ 293 | { 294 | tag: 'script', 295 | attributes: { type: 'module', src: 'https://cdn.url.com/test-12345.js' }, 296 | children: [], 297 | }, 298 | ]) 299 | assert.containsSubset( 300 | output.map((element) => String(element)), 301 | [''] 302 | ) 303 | }) 304 | 305 | test('access manifest file', async ({ fs, assert }) => { 306 | const vite = new Vite( 307 | false, 308 | defineConfig({ 309 | buildDirectory: join(fs.basePath, 'public/assets'), 310 | }) 311 | ) 312 | 313 | await fs.create( 314 | 'public/assets/.vite/manifest.json', 315 | JSON.stringify({ 'test.js': { file: 'test-12345.js', src: 'test.js' } }) 316 | ) 317 | 318 | assert.deepEqual(vite.manifest(), { 'test.js': { file: 'test-12345.js', src: 'test.js' } }) 319 | }) 320 | 321 | test('get asset path', async ({ fs, assert }) => { 322 | const vite = new Vite( 323 | false, 324 | defineConfig({ 325 | buildDirectory: join(fs.basePath, 'public/assets'), 326 | }) 327 | ) 328 | 329 | await fs.create( 330 | 'public/assets/.vite/manifest.json', 331 | JSON.stringify({ 'test.js': { file: 'test-12345.js', src: 'test.js' } }) 332 | ) 333 | 334 | assert.equal(vite.assetPath('test.js'), '/assets/test-12345.js') 335 | }) 336 | 337 | test('throw error when manifest does not have the chunk', async ({ fs }) => { 338 | const vite = new Vite( 339 | false, 340 | defineConfig({ 341 | buildDirectory: join(fs.basePath, 'public/assets'), 342 | }) 343 | ) 344 | 345 | await fs.create( 346 | 'public/assets/.vite/manifest.json', 347 | JSON.stringify({ 'test.js': { file: 'test-12345.js', src: 'test.js' } }) 348 | ) 349 | 350 | vite.assetPath('app.css') 351 | }).throws('Cannot find "app.css" chunk in the manifest file') 352 | 353 | test('prefix custom assetsUrl to the assetPath', async ({ fs, assert }) => { 354 | const vite = new Vite( 355 | false, 356 | defineConfig({ 357 | buildDirectory: join(fs.basePath, 'public/assets'), 358 | assetsUrl: 'https://cdn.url.com', 359 | }) 360 | ) 361 | 362 | await fs.create( 363 | 'public/assets/.vite/manifest.json', 364 | JSON.stringify({ 'test.js': { file: 'test-12345.js', src: 'test.js' } }) 365 | ) 366 | 367 | assert.equal(vite.assetPath('test.js'), 'https://cdn.url.com/test-12345.js') 368 | }) 369 | 370 | test('return null for viteHMRScript when not in hot mode', async ({ fs, assert }) => { 371 | const vite = new Vite( 372 | false, 373 | defineConfig({ 374 | buildDirectory: join(fs.basePath, 'public/assets'), 375 | assetsUrl: 'https://cdn.url.com', 376 | }) 377 | ) 378 | 379 | await fs.create( 380 | 'public/assets/.vite/manifest.json', 381 | JSON.stringify({ 'test.js': { file: 'test-12345.js', src: 'test.js' } }) 382 | ) 383 | 384 | assert.isNull(vite.getReactHmrScript()) 385 | }) 386 | 387 | test('add custom attributes to the entrypoints script tags', async ({ assert, fs }) => { 388 | const vite = new Vite( 389 | false, 390 | defineConfig({ 391 | buildDirectory: join(fs.basePath, 'public/assets'), 392 | assetsUrl: 'https://cdn.url.com', 393 | scriptAttributes: () => { 394 | return { 395 | 'data-test': 'test', 396 | } 397 | }, 398 | }) 399 | ) 400 | 401 | await fs.create( 402 | 'public/assets/.vite/manifest.json', 403 | JSON.stringify({ 'test.js': { file: 'test-12345.js', src: 'test.js' } }) 404 | ) 405 | 406 | const output = await vite.generateEntryPointsTags('test.js') 407 | 408 | assert.containsSubset(output, [ 409 | { 410 | tag: 'script', 411 | attributes: { 412 | 'type': 'module', 413 | 'data-test': 'test', 414 | 'src': 'https://cdn.url.com/test-12345.js', 415 | }, 416 | children: [], 417 | }, 418 | ]) 419 | assert.containsSubset( 420 | output.map((element) => String(element)), 421 | [''] 422 | ) 423 | }) 424 | 425 | test('add custom attributes to the entrypoints link tags', async ({ assert, fs }) => { 426 | const vite = new Vite( 427 | false, 428 | defineConfig({ 429 | buildDirectory: join(fs.basePath, 'public/assets'), 430 | assetsUrl: 'https://cdn.url.com', 431 | styleAttributes: { 432 | 'data-test': 'test', 433 | }, 434 | }) 435 | ) 436 | 437 | await fs.create( 438 | 'public/assets/.vite/manifest.json', 439 | JSON.stringify({ 'app.css': { file: 'app-12345.css', src: 'app.css' } }) 440 | ) 441 | 442 | const output = await vite.generateEntryPointsTags('app.css') 443 | 444 | assert.containsSubset(output, [ 445 | { 446 | tag: 'link', 447 | attributes: { 448 | 'rel': 'stylesheet', 449 | 'data-test': 'test', 450 | 'href': 'https://cdn.url.com/app-12345.css', 451 | }, 452 | }, 453 | ]) 454 | assert.containsSubset( 455 | output.map((element) => String(element)), 456 | [''] 457 | ) 458 | }) 459 | 460 | test('add integrity attribute to entrypoint tags', async ({ assert, fs }) => { 461 | const vite = new Vite( 462 | false, 463 | defineConfig({ 464 | buildDirectory: join(fs.basePath, 'public/assets'), 465 | assetsUrl: 'https://cdn.url.com', 466 | }) 467 | ) 468 | 469 | await fs.create( 470 | 'public/assets/.vite/manifest.json', 471 | JSON.stringify({ 472 | 'test.js': { 473 | file: 'test-12345.js', 474 | src: 'test.js', 475 | integrity: 'sha384-hNF0CSk1Cqwkjmpb374DXqtYJ/rDp5SqV6ttpKEnqyjT/gDHGHuYsj3XzBcMke15', 476 | css: ['app-12345.css'], 477 | }, 478 | }) 479 | ) 480 | 481 | const output = await vite.generateEntryPointsTags('test.js') 482 | 483 | assert.containsSubset(output, [ 484 | { 485 | tag: 'link', 486 | attributes: { 487 | rel: 'stylesheet', 488 | href: 'https://cdn.url.com/app-12345.css', 489 | }, 490 | }, 491 | { 492 | tag: 'script', 493 | attributes: { 494 | type: 'module', 495 | integrity: 'sha384-hNF0CSk1Cqwkjmpb374DXqtYJ/rDp5SqV6ttpKEnqyjT/gDHGHuYsj3XzBcMke15', 496 | src: 'https://cdn.url.com/test-12345.js', 497 | }, 498 | children: [], 499 | }, 500 | ]) 501 | assert.containsSubset( 502 | output.map((element) => String(element)), 503 | [ 504 | '', 505 | '', 506 | ] 507 | ) 508 | }) 509 | 510 | test('return path to assets directory', async ({ assert, fs }) => { 511 | const vite = new Vite( 512 | false, 513 | defineConfig({ 514 | buildDirectory: join(fs.basePath, 'public/assets'), 515 | }) 516 | ) 517 | 518 | assert.equal(vite.assetsUrl(), '/assets') 519 | }) 520 | }) 521 | 522 | test.group('Preloading', () => { 523 | const config = defineConfig({ 524 | manifestFile: fileURLToPath(new URL('fixtures/adonis_packages_manifest.json', import.meta.url)), 525 | }) 526 | 527 | test('Preload root entrypoints', async ({ assert }) => { 528 | const vite = new Vite(false, config) 529 | const entrypoints = await vite.generateEntryPointsTags('resources/pages/home/main.vue') 530 | 531 | const result = entrypoints.map((tag) => tag.toString()) 532 | 533 | assert.include(result, '') 534 | }) 535 | 536 | test('Preload files imported from entrypoints', async ({ assert }) => { 537 | const vite = new Vite(false, config) 538 | const entrypoints = await vite.generateEntryPointsTags('resources/pages/home/main.vue') 539 | 540 | const result = entrypoints.map((tag) => tag.toString()) 541 | 542 | assert.includeMembers(result, [ 543 | '', 544 | '', 545 | '', 546 | '', 547 | '', 548 | '', 549 | '', 550 | ]) 551 | }) 552 | 553 | test('Preload entrypoints css files', async ({ assert }) => { 554 | const vite = new Vite(false, config) 555 | const entrypoints = await vite.generateEntryPointsTags('resources/pages/home/main.vue') 556 | 557 | const result = entrypoints.map((tag) => tag.toString()) 558 | 559 | assert.includeMembers(result, [ 560 | '', 561 | ]) 562 | }) 563 | 564 | test('Preload css files of imported files of entrypoint', async ({ assert }) => { 565 | const vite = new Vite(false, config) 566 | const entrypoints = await vite.generateEntryPointsTags('resources/pages/home/main.vue') 567 | 568 | const result = entrypoints.map((tag) => tag.toString()) 569 | 570 | assert.includeMembers(result, [ 571 | '', 572 | '', 573 | '', 574 | '', 575 | '', 576 | ]) 577 | }) 578 | 579 | test('css preload should be ordered before js preload', async ({ assert }) => { 580 | const vite = new Vite(false, config) 581 | const entrypoints = await vite.generateEntryPointsTags('resources/pages/home/main.vue') 582 | 583 | const result = entrypoints.map((tag) => tag.toString()) 584 | 585 | const cssPreloadIndex = result.findIndex((tag) => tag.includes('rel="preload" as="style"')) 586 | const jsPreloadIndex = result.findIndex((tag) => tag.includes('rel="modulepreload"')) 587 | 588 | assert.isTrue(cssPreloadIndex < jsPreloadIndex) 589 | }) 590 | 591 | test('preloads should use assetsUrl when defined', async ({ assert }) => { 592 | const vite = new Vite(false, defineConfig({ ...config, assetsUrl: 'https://cdn.url.com' })) 593 | const entrypoints = await vite.generateEntryPointsTags('resources/pages/home/main.vue') 594 | 595 | const result = entrypoints.map((tag) => tag.toString()) 596 | 597 | assert.includeMembers(result, [ 598 | '', 599 | '', 600 | '', 601 | '', 602 | '', 603 | '', 604 | '', 605 | '', 606 | ]) 607 | }) 608 | }) 609 | 610 | test.group('Vite | collect css', () => { 611 | test('collect and preload css files of entrypoint', async ({ assert, fs }) => { 612 | const vite = await createVite(defineConfig({}), { 613 | build: { rollupOptions: { input: 'foo.ts' } }, 614 | }) 615 | 616 | await fs.create('foo.ts', `import './style.css'`) 617 | await fs.create('style.css', 'body { color: red }') 618 | 619 | const result = await vite.generateEntryPointsTags('foo.ts') 620 | 621 | assert.deepEqual( 622 | result.map((tag) => tag.toString()), 623 | [ 624 | '', 625 | '', 626 | '', 627 | ] 628 | ) 629 | }) 630 | 631 | test('collect recursively css files of entrypoint', async ({ assert, fs }) => { 632 | const vite = await createVite(defineConfig({}), { 633 | build: { rollupOptions: { input: 'foo.ts' } }, 634 | }) 635 | 636 | await fs.create( 637 | 'foo.ts', 638 | ` 639 | import './foo2.ts' 640 | import './style.css' 641 | ` 642 | ) 643 | 644 | await fs.create('foo2.ts', `import './style2.css'`) 645 | await fs.create('style.css', 'body { color: red }') 646 | await fs.create('style2.css', 'body { color: blue }') 647 | 648 | const result = await vite.generateEntryPointsTags('foo.ts') 649 | 650 | assert.deepEqual( 651 | result.map((tag) => tag.toString()), 652 | [ 653 | '', 654 | '', 655 | '', 656 | '', 657 | ] 658 | ) 659 | }).skip( 660 | true, 661 | 'Doesnt work since we moved from executeEntrypoint to transformRequest, but in real application it seems to work fine ?' 662 | ) 663 | 664 | test('collect css rendered page', async ({ assert, fs }) => { 665 | const vite = await createVite(defineConfig({}), { 666 | build: { rollupOptions: { input: 'foo.ts' } }, 667 | }) 668 | 669 | await fs.create( 670 | 'app.ts', 671 | ` 672 | import './style.css' 673 | import.meta.glob('./pages/**/*.tsx') 674 | ` 675 | ) 676 | await fs.create('style.css', 'body { color: red }') 677 | 678 | await fs.create('./pages/home/main.tsx', `import './style2.css'`) 679 | await fs.create('./pages/home/style2.css', 'body { color: blue }') 680 | 681 | const result = await vite.generateEntryPointsTags(['app.ts', 'pages/home/main.tsx']) 682 | 683 | assert.deepEqual( 684 | result.map((tag) => tag.toString()), 685 | [ 686 | '', 687 | '', 688 | '', 689 | '', 690 | '', 691 | ] 692 | ) 693 | }) 694 | }) 695 | -------------------------------------------------------------------------------- /tests/client/config.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { Plugin, build } from 'vite' 12 | import adonisjs from '../../src/client/main.js' 13 | 14 | test.group('Vite plugin', () => { 15 | test('build the assets', async ({ fs, assert }) => { 16 | await fs.create('resources/js/app.ts', 'console.log("hello")') 17 | 18 | await build({ 19 | root: fs.basePath, 20 | logLevel: 'warn', 21 | plugins: [adonisjs({ entrypoints: ['./resources/js/app.ts'] })], 22 | }) 23 | 24 | await assert.fileContains('public/assets/.vite/manifest.json', 'resources/js/app.ts') 25 | }) 26 | 27 | test('build the assets with custom manifest filename', async ({ fs, assert }) => { 28 | await fs.create('resources/js/app.ts', 'console.log("hello")') 29 | 30 | await build({ 31 | root: fs.basePath, 32 | logLevel: 'warn', 33 | plugins: [adonisjs({ entrypoints: ['./resources/js/app.ts'] })], 34 | build: { manifest: 'foo.json' }, 35 | }) 36 | 37 | await assert.fileContains('public/assets/foo.json', 'resources/js/app.ts') 38 | }) 39 | 40 | test('define the asset url', async ({ assert }) => { 41 | const plugin = adonisjs({ 42 | entrypoints: ['./resources/js/app.ts'], 43 | assetsUrl: 'https://cdn.com', 44 | buildDirectory: 'my-assets', 45 | })[1] as Plugin 46 | 47 | // @ts-ignore 48 | const config = plugin!.config!({}, { command: 'build' }) 49 | assert.deepEqual(config.base, 'https://cdn.com/') 50 | }) 51 | 52 | test('disable vite dev server cors handling', async ({ assert }) => { 53 | const plugin = adonisjs({ 54 | entrypoints: ['./resources/js/app.ts'], 55 | })[1] as Plugin 56 | 57 | // @ts-ignore 58 | const config = plugin!.config!({}, { command: 'serve' }) 59 | assert.deepEqual(config.server?.cors, false) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/configure.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/vite 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { IgnitorFactory } from '@adonisjs/core/factories' 12 | import Configure from '@adonisjs/core/commands/configure' 13 | 14 | import { BASE_URL } from './backend/helpers.js' 15 | 16 | test.group('Configure', (group) => { 17 | group.each.disableTimeout() 18 | 19 | test('create config file and register provider and middleware', async ({ assert, fs }) => { 20 | const ignitor = new IgnitorFactory() 21 | .withCoreProviders() 22 | .withCoreConfig() 23 | .create(BASE_URL, { 24 | importer: (filePath) => { 25 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 26 | return import(new URL(filePath, BASE_URL).href) 27 | } 28 | 29 | return import(filePath) 30 | }, 31 | }) 32 | 33 | await fs.create('.env', '') 34 | await fs.createJson('tsconfig.json', {}) 35 | await fs.create('adonisrc.ts', `export default defineConfig({})`) 36 | await fs.create('start/kernel.ts', `server.use([])`) 37 | 38 | const app = ignitor.createApp('web') 39 | await app.init() 40 | await app.boot() 41 | 42 | const ace = await app.container.make('ace') 43 | const command = await ace.create(Configure, ['../../index.js']) 44 | command.ui.switchMode('raw') 45 | command.prompt.trap('Do you want to install "vite"?').reject() 46 | 47 | await command.exec() 48 | 49 | await assert.fileExists('vite.config.ts') 50 | await assert.fileExists('resources/js/app.js') 51 | await assert.fileContains('adonisrc.ts', '@adonisjs/vite/vite_provider') 52 | await assert.fileContains('vite.config.ts', `import adonisjs from '@adonisjs/vite/client'`) 53 | await assert.fileContains('adonisrc.ts', `pattern: 'public/**'`) 54 | await assert.fileContains('adonisrc.ts', `reloadServer: false`) 55 | await assert.fileContains('start/kernel.ts', '@adonisjs/vite/vite_middleware') 56 | await assert.fileContains('adonisrc.ts', `@adonisjs/vite/build_hook`) 57 | await assert.fileContains('adonisrc.ts', `assetsBundler: false`) 58 | }) 59 | 60 | test('install package when --install flag is used', async ({ assert, fs }) => { 61 | const ignitor = new IgnitorFactory() 62 | .withCoreProviders() 63 | .withCoreConfig() 64 | .create(BASE_URL, { 65 | importer: (filePath) => { 66 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 67 | return import(new URL(filePath, BASE_URL).href) 68 | } 69 | 70 | return import(filePath) 71 | }, 72 | }) 73 | 74 | await fs.create('.env', '') 75 | await fs.createJson('tsconfig.json', {}) 76 | await fs.createJson('package.json', {}) 77 | await fs.create('adonisrc.ts', `export default defineConfig({})`) 78 | await fs.create('start/kernel.ts', `server.use([])`) 79 | 80 | const app = ignitor.createApp('web') 81 | await app.init() 82 | await app.boot() 83 | 84 | const ace = await app.container.make('ace') 85 | const command = await ace.create(Configure, ['../../index.js', '--install']) 86 | await command.exec() 87 | 88 | await assert.fileExists('node_modules/vite/package.json') 89 | }) 90 | 91 | test('do not prompt when --no-install flag is used', async ({ assert, fs }) => { 92 | const ignitor = new IgnitorFactory() 93 | .withCoreProviders() 94 | .withCoreConfig() 95 | .create(BASE_URL, { 96 | importer: (filePath) => { 97 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 98 | return import(new URL(filePath, BASE_URL).href) 99 | } 100 | 101 | return import(filePath) 102 | }, 103 | }) 104 | 105 | await fs.create('.env', '') 106 | await fs.createJson('tsconfig.json', {}) 107 | await fs.createJson('package.json', {}) 108 | await fs.create('adonisrc.ts', `export default defineConfig({})`) 109 | await fs.create('start/kernel.ts', `server.use([])`) 110 | 111 | const app = ignitor.createApp('web') 112 | await app.init() 113 | await app.boot() 114 | 115 | const ace = await app.container.make('ace') 116 | const command = await ace.create(Configure, ['../../index.js', '--no-install']) 117 | await command.exec() 118 | 119 | await assert.fileNotExists('node_modules/vite/package.json') 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@adonisjs/tsconfig/tsconfig.package.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "./build" 6 | } 7 | } 8 | --------------------------------------------------------------------------------