├── .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')}${element.tag}>`
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 |
--------------------------------------------------------------------------------