diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..73bb887 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +node_modules +dist +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..ee3ede1 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,31 @@ +module.exports = { + root: true, + env: { + node: true, + es6: true, + }, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], + rules: { + 'no-trailing-spaces': 'error', + quotes: [ + 'error', + 'single', + { + avoidEscape: true, + allowTemplateLiterals: true, + }, + ], + }, + overrides: [ + { + // enable the rule specifically for TypeScript files + files: ['*.ts', '*.mts'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/indent': 'off', + }, + }, + ], +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 73a2579..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - root: true, - env: { - node: true, - }, - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint"], - extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - rules: { - "no-trailing-spaces": "error", - }, -}; diff --git a/.gitattributes b/.gitattributes index 7dda1e6..f35942e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -* text=auto +* text=auto eol=lf /.github export-ignore /tests export-ignore diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md index dbfbf53..29bb633 100644 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.md +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -1,5 +1,5 @@ --- -name: "Bug report" +name: 'Bug report' about: "Report something that's broken. Please ensure your version is still supported: https://laravel.com/docs/releases#support-policy" --- @@ -16,5 +16,4 @@ about: "Report something that's broken. Please ensure your version is still supp ### Description: - ### Steps To Reproduce: diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md index 28672b3..6708ddb 100644 --- a/.github/ISSUE_TEMPLATE/2_Feature_request.md +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -1,4 +1,4 @@ --- -name: "Feature request" +name: 'Feature request' about: 'For ideas or feature requests, please make a pull request or open an issue' --- diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d4770c3..0d34616 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,10 +13,10 @@ name: codeql on: push: - branches: [ master ] + branches: [master] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master] schedule: - cron: '44 23 * * 1' @@ -28,40 +28,40 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] + language: ['javascript'] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v3 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index f37b03e..6b1f614 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ /inertia-helpers/ /npm-debug.log /package-lock.json +.eslintcache +/pnpm-lock.yaml +/yarn.lock diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a3a0fed --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +node_modules +dist +src/dev-server-index.html diff --git a/.prettierrc.cjs b/.prettierrc.cjs new file mode 100644 index 0000000..97348f9 --- /dev/null +++ b/.prettierrc.cjs @@ -0,0 +1,22 @@ +module.exports = { + semi: false, + trailingComma: 'all', + singleQuote: true, + tabWidth: 2, + printWidth: 120, + overrides: [ + { + files: ['*.yml'], + options: { + singleQuote: true, + }, + }, + { + files: ['*.json'], + options: { + singleQuote: false, + quoteProps: 'preserve', + }, + }, + ], +} diff --git a/UPGRADE.md b/UPGRADE.md index 987031f..36afc39 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -36,28 +36,25 @@ npm install --save-dev @vitejs/plugin-react Create a `vite.config.js` file in the root of your project: ```js -import { defineConfig } from 'vite'; -import laravel from 'laravel-vite-plugin'; +import { defineConfig } from 'vite' +import laravel from 'laravel-vite-plugin' // import react from '@vitejs/plugin-react'; // import vue from '@vitejs/plugin-vue'; export default defineConfig({ - plugins: [ - laravel([ - 'resources/css/app.css', - 'resources/js/app.js', - ]), - // react(), - // vue({ - // template: { - // transformAssetUrls: { - // base: null, - // includeAbsolute: false, - // }, - // }, - // }), - ], -}); + plugins: [ + laravel(['resources/css/app.css', 'resources/js/app.js']), + // react(), + // vue({ + // template: { + // transformAssetUrls: { + // base: null, + // includeAbsolute: false, + // }, + // }, + // }), + ], +}) ``` If you are building an SPA, you will get a better developer experience by removing the CSS entry point above and [importing your CSS from Javascript](#importing-your-css-from-your-javascript-entry-points). @@ -68,18 +65,13 @@ If you are migrating aliases from your `webpack.mix.js` file to your `vite.confi ```js export default defineConfig({ - plugins: [ - laravel([ - 'resources/css/app.css', - 'resources/js/app.js', - ]), - ], - resolve: { - alias: { - '@': '/resources/js' - } - } -}); + plugins: [laravel(['resources/css/app.css', 'resources/js/app.js'])], + resolve: { + alias: { + '@': '/resources/js', + }, + }, +}) ``` For your convenience, the Laravel Vite plugin automatically adds an `@` alias for your `/resources/js` directory. If you do not need to customize your aliases, you may omit this section from your `vite.config.js` file. @@ -184,11 +176,10 @@ The entry points should match those used in your `vite.config.js`. #### React -If you are using React and hot-module replacement, you will need to include an additional directive *before* the `@vite` directive: +If you are using React and hot-module replacement, you will need to include an additional directive _before_ the `@vite` directive: ```html -@viteReactRefresh -@vite('resources/js/app.jsx') +@viteReactRefresh @vite('resources/js/app.jsx') ``` This loads a React "refresh runtime" in development mode only, which is required for hot module replacement to work correctly. @@ -248,7 +239,6 @@ Next, if you are using the Vapor asset helper in your application, you only need If you want to use the asset helper with your Vite project, you will also need to ensure you have updated to the latest version: - ```sh npm install laravel-vapor@latest ``` @@ -303,17 +293,17 @@ rm webpack.ssr.mix.js In most cases, you won't need a dedicated SSR configuration file when using Vite. You can specify your SSR entry point by passing a configuration option to the Laravel plugin: ```js -import { defineConfig } from 'vite'; -import laravel from 'laravel-vite-plugin'; +import { defineConfig } from 'vite' +import laravel from 'laravel-vite-plugin' export default defineConfig({ - plugins: [ - laravel({ - input: 'resources/js/app.js', - ssr: 'resources/js/ssr.js', - }), - ], -}); + plugins: [ + laravel({ + input: 'resources/js/app.js', + ssr: 'resources/js/ssr.js', + }), + ], +}) ``` You may wish to add the following additional scripts to your `package.json`: diff --git a/package.json b/package.json index 0cc37ae..3c0fc79 100644 --- a/package.json +++ b/package.json @@ -1,49 +1,72 @@ { - "name": "laravel-vite-plugin", - "version": "0.7.4", - "description": "Laravel plugin for Vite.", - "keywords": [ - "laravel", - "vite", - "vite-plugin" - ], - "homepage": "https://github.com/laravel/vite-plugin", - "repository": { - "type": "git", - "url": "https://github.com/laravel/vite-plugin" + "name": "laravel-vite-plugin", + "version": "0.7.4", + "description": "Laravel plugin for Vite.", + "type": "module", + "keywords": [ + "laravel", + "vite", + "vite-plugin" + ], + "homepage": "https://github.com/laravel/vite-plugin", + "repository": { + "type": "git", + "url": "https://github.com/laravel/vite-plugin" + }, + "license": "MIT", + "author": "Laravel", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.cjs", + "import": "./dist/index.mjs" }, - "license": "MIT", - "author": "Laravel", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "/dist", - "/inertia-helpers" - ], - "scripts": { - "build": "npm run build-plugin && npm run build-inertia-helpers", - "build-plugin": "rm -rf dist && tsc && cp src/dev-server-index.html dist/", - "build-inertia-helpers": "rm -rf inertia-helpers && tsc --project tsconfig.inertia-helpers.json", - "lint": "eslint --ext .ts ./src ./tests", - "test": "vitest run" + "./inertia-helpers": { + "types": "./dist/inertia-helpers/index.d.ts", + "require": "./dist/inertia-helpers/index.cjs", + "import": "./dist/inertia-helpers/index.mjs" }, - "devDependencies": { - "@types/node": "^18.11.9", - "@typescript-eslint/eslint-plugin": "^5.21.0", - "@typescript-eslint/parser": "^5.21.0", - "eslint": "^8.14.0", - "typescript": "^4.6.4", - "vite": "^4.0.0", - "vitest": "^0.25.2" - }, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "dependencies": { - "picocolors": "^1.0.0", - "vite-plugin-full-reload": "^1.0.5" - } + "./*": "./*" + }, + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "/dist", + "*.d.ts" + ], + "scripts": { + "build": "npm run typecheck && tsup && cp src/dev-server-index.html dist/", + "build:watch": "tsup --watch", + "build-plugin": "rm -rf dist && tsc && cp src/dev-server-index.html dist/", + "build-inertia-helpers": "rm -rf inertia-helpers && tsc --project tsconfig.inertia-helpers.json", + "lint": "eslint --cache --ext .ts ./src ./tests", + "lint:fix": "eslint . --fix", + "format": "prettier --write --cache .", + "test": "vitest run", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "devDependencies": { + "@types/node": "^18.13.0", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "eslint": "^8.34.0", + "eslint-config-prettier": "^8.6.0", + "prettier": "^2.8.4", + "tsup": "^6.6.0", + "typescript": "^4.9.5", + "vite": "^4.1.1", + "vitest": "^0.28.4" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.0.5" + } } diff --git a/src/index.ts b/src/index.ts index fbf951a..4130d56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,84 +7,84 @@ import { Plugin, loadEnv, UserConfig, ConfigEnv, ResolvedConfig, SSROptions, Plu import fullReload, { Config as FullReloadConfig } from 'vite-plugin-full-reload' interface PluginConfig { - /** - * The path or paths of the entry points to compile. - */ - input: string|string[] - - /** - * Laravel's public directory. - * - * @default 'public' - */ - publicDirectory?: string - - /** - * The public subdirectory where compiled assets should be written. - * - * @default 'build' - */ - buildDirectory?: string - - /** - * The path to the "hot" file. - * - * @default `${publicDirectory}/hot` - */ - hotFile?: string - - /** - * The path of the SSR entry point. - */ - ssr?: string|string[] - - /** - * The directory where the SSR bundle should be written. - * - * @default 'bootstrap/ssr' - */ - ssrOutputDirectory?: string - - /** - * Configuration for performing full page refresh on blade (or other) file changes. - * - * {@link https://github.com/ElMassimo/vite-plugin-full-reload} - * @default false - */ - refresh?: boolean|string|string[]|RefreshConfig|RefreshConfig[] - - /** - * Utilise the valet TLS certificates. - * - * @default false - */ - valetTls?: string|boolean, - - /** - * Transform the code while serving. - */ - transformOnServe?: (code: string, url: DevServerUrl) => string, + /** + * The path or paths of the entry points to compile. + */ + input: string | string[] + + /** + * Laravel's public directory. + * + * @default 'public' + */ + publicDirectory?: string + + /** + * The public subdirectory where compiled assets should be written. + * + * @default 'build' + */ + buildDirectory?: string + + /** + * The path to the "hot" file. + * + * @default `${publicDirectory}/hot` + */ + hotFile?: string + + /** + * The path of the SSR entry point. + */ + ssr?: string | string[] + + /** + * The directory where the SSR bundle should be written. + * + * @default 'bootstrap/ssr' + */ + ssrOutputDirectory?: string + + /** + * Configuration for performing full page refresh on blade (or other) file changes. + * + * {@link https://github.com/ElMassimo/vite-plugin-full-reload} + * @default false + */ + refresh?: boolean | string | string[] | RefreshConfig | RefreshConfig[] + + /** + * Utilise the valet TLS certificates. + * + * @default false + */ + valetTls?: string | boolean + + /** + * Transform the code while serving. + */ + transformOnServe?: (code: string, url: DevServerUrl) => string } interface RefreshConfig { - paths: string[], - config?: FullReloadConfig, + paths: string[] + config?: FullReloadConfig } interface LaravelPlugin extends Plugin { - config: (config: UserConfig, env: ConfigEnv) => UserConfig + config: (config: UserConfig, env: ConfigEnv) => UserConfig } -type DevServerUrl = `${'http'|'https'}://${string}:${number}` +type DevServerUrl = `${'http' | 'https'}://${string}:${number}` let exitHandlersBound = false export const refreshPaths = [ - 'app/View/Components/**', - 'resources/views/**', - 'resources/lang/**', - 'lang/**', - 'routes/**', + 'app/View/Components/**', + 'resources/views/**', + 'resources/lang/**', + 'lang/**', + 'routes/**', ] /** @@ -92,334 +92,367 @@ export const refreshPaths = [ * * @param config - A config object or relative path(s) of the scripts to be compiled. */ -export default function laravel(config: string|string[]|PluginConfig): [LaravelPlugin, ...Plugin[]] { - const pluginConfig = resolvePluginConfig(config) +export default function laravel(config: string | string[] | PluginConfig): [LaravelPlugin, ...Plugin[]] { + const pluginConfig = resolvePluginConfig(config) - return [ - resolveLaravelPlugin(pluginConfig), - ...resolveFullReloadConfig(pluginConfig) as Plugin[], - ]; + return [resolveLaravelPlugin(pluginConfig), ...(resolveFullReloadConfig(pluginConfig) as Plugin[])] } /** * Resolve the Laravel Plugin configuration. */ function resolveLaravelPlugin(pluginConfig: Required): LaravelPlugin { - let viteDevServerUrl: DevServerUrl - let resolvedConfig: ResolvedConfig - - const defaultAliases: Record = { - '@': '/resources/js', - }; - - return { - name: 'laravel', - enforce: 'post', - config: (userConfig, { command, mode }) => { - const ssr = !! userConfig.build?.ssr - const env = loadEnv(mode, userConfig.envDir || process.cwd(), '') - const assetUrl = env.ASSET_URL ?? '' - const serverConfig = command === 'serve' - ? (resolveValetServerConfig(pluginConfig.valetTls) ?? resolveEnvironmentServerConfig(env)) - : undefined - - ensureCommandShouldRunInEnvironment(command, env) - - return { - base: userConfig.base ?? (command === 'build' ? resolveBase(pluginConfig, assetUrl) : ''), - publicDir: userConfig.publicDir ?? false, - build: { - manifest: userConfig.build?.manifest ?? !ssr, - outDir: userConfig.build?.outDir ?? resolveOutDir(pluginConfig, ssr), - rollupOptions: { - input: userConfig.build?.rollupOptions?.input ?? resolveInput(pluginConfig, ssr) - }, - assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? 0, - }, - server: { - origin: userConfig.server?.origin ?? '__laravel_vite_placeholder__', - ...(process.env.LARAVEL_SAIL ? { - host: userConfig.server?.host ?? '0.0.0.0', - port: userConfig.server?.port ?? (env.VITE_PORT ? parseInt(env.VITE_PORT) : 5173), - strictPort: userConfig.server?.strictPort ?? true, - } : undefined), - ...(serverConfig ? { - host: userConfig.server?.host ?? serverConfig.host, - hmr: userConfig.server?.hmr === false ? false : { - ...serverConfig.hmr, - ...(userConfig.server?.hmr === true ? {} : userConfig.server?.hmr), - }, - https: userConfig.server?.https === false ? false : { - ...serverConfig.https, - ...(userConfig.server?.https === true ? {} : userConfig.server?.https), - }, - } : undefined), - }, - resolve: { - alias: Array.isArray(userConfig.resolve?.alias) - ? [ - ...userConfig.resolve?.alias ?? [], - ...Object.keys(defaultAliases).map(alias => ({ - find: alias, - replacement: defaultAliases[alias] - })) - ] - : { - ...defaultAliases, - ...userConfig.resolve?.alias, - } - }, - ssr: { - noExternal: noExternalInertiaHelpers(userConfig), - }, - } + let viteDevServerUrl: DevServerUrl + let resolvedConfig: ResolvedConfig + + const defaultAliases: Record = { + '@': '/resources/js', + } + + return { + name: 'laravel', + enforce: 'post', + config: (userConfig, { command, mode }) => { + const ssr = !!userConfig.build?.ssr + const env = loadEnv(mode, userConfig.envDir || process.cwd(), '') + const assetUrl = env.ASSET_URL ?? '' + const serverConfig = + command === 'serve' + ? resolveValetServerConfig(pluginConfig.valetTls) ?? resolveEnvironmentServerConfig(env) + : undefined + + ensureCommandShouldRunInEnvironment(command, env) + + return { + base: userConfig.base ?? (command === 'build' ? resolveBase(pluginConfig, assetUrl) : ''), + publicDir: userConfig.publicDir ?? false, + build: { + manifest: userConfig.build?.manifest ?? !ssr, + outDir: userConfig.build?.outDir ?? resolveOutDir(pluginConfig, ssr), + rollupOptions: { + input: userConfig.build?.rollupOptions?.input ?? resolveInput(pluginConfig, ssr), + }, + assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? 0, }, - configResolved(config) { - resolvedConfig = config + server: { + origin: userConfig.server?.origin ?? '__laravel_vite_placeholder__', + ...(process.env.LARAVEL_SAIL + ? { + host: userConfig.server?.host ?? '0.0.0.0', + port: userConfig.server?.port ?? (env.VITE_PORT ? parseInt(env.VITE_PORT) : 5173), + strictPort: userConfig.server?.strictPort ?? true, + } + : undefined), + ...(serverConfig + ? { + host: userConfig.server?.host ?? serverConfig.host, + hmr: + userConfig.server?.hmr === false + ? false + : { + ...serverConfig.hmr, + ...(userConfig.server?.hmr === true ? {} : userConfig.server?.hmr), + }, + https: + userConfig.server?.https === false + ? false + : { + ...serverConfig.https, + ...(userConfig.server?.https === true ? {} : userConfig.server?.https), + }, + } + : undefined), }, - transform(code) { - if (resolvedConfig.command === 'serve') { - code = code.replace(/__laravel_vite_placeholder__/g, viteDevServerUrl) - - return pluginConfig.transformOnServe(code, viteDevServerUrl) - } + resolve: { + alias: Array.isArray(userConfig.resolve?.alias) + ? [ + ...(userConfig.resolve?.alias ?? []), + ...Object.keys(defaultAliases).map((alias) => ({ + find: alias, + replacement: defaultAliases[alias], + })), + ] + : { + ...defaultAliases, + ...userConfig.resolve?.alias, + }, + }, + ssr: { + noExternal: noExternalInertiaHelpers(userConfig), }, - configureServer(server) { - const envDir = resolvedConfig.envDir || process.cwd() - const appUrl = loadEnv(resolvedConfig.mode, envDir, 'APP_URL').APP_URL ?? 'undefined' - - server.httpServer?.once('listening', () => { - const address = server.httpServer?.address() - - const isAddressInfo = (x: string|AddressInfo|null|undefined): x is AddressInfo => typeof x === 'object' - if (isAddressInfo(address)) { - viteDevServerUrl = resolveDevServerUrl(address, server.config) - fs.writeFileSync(pluginConfig.hotFile, viteDevServerUrl) - - setTimeout(() => { - server.config.logger.info(`\n ${colors.red(`${colors.bold('LARAVEL')} ${laravelVersion()}`)} ${colors.dim('plugin')} ${colors.bold(`v${pluginVersion()}`)}`) - server.config.logger.info('') - server.config.logger.info(` ${colors.green('➜')} ${colors.bold('APP_URL')}: ${colors.cyan(appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`))}`) - }, 100) - } - }) - - if (! exitHandlersBound) { - const clean = () => { - if (fs.existsSync(pluginConfig.hotFile)) { - fs.rmSync(pluginConfig.hotFile) - } - } - - process.on('exit', clean) - process.on('SIGINT', process.exit) - process.on('SIGTERM', process.exit) - process.on('SIGHUP', process.exit) - - exitHandlersBound = true - } - - return () => server.middlewares.use((req, res, next) => { - if (req.url === '/index.html') { - res.statusCode = 404 - - res.end( - fs.readFileSync(path.join(__dirname, 'dev-server-index.html')).toString().replace(/{{ APP_URL }}/g, appUrl) - ) - } - - next() - }) + } + }, + configResolved(config) { + resolvedConfig = config + }, + transform(code) { + if (resolvedConfig.command === 'serve') { + code = code.replace(/__laravel_vite_placeholder__/g, viteDevServerUrl) + + return pluginConfig.transformOnServe(code, viteDevServerUrl) + } + }, + configureServer(server) { + const envDir = resolvedConfig.envDir || process.cwd() + const appUrl = loadEnv(resolvedConfig.mode, envDir, 'APP_URL').APP_URL ?? 'undefined' + + server.httpServer?.once('listening', () => { + const address = server.httpServer?.address() + + const isAddressInfo = (x: string | AddressInfo | null | undefined): x is AddressInfo => typeof x === 'object' + if (isAddressInfo(address)) { + viteDevServerUrl = resolveDevServerUrl(address, server.config) + fs.writeFileSync(pluginConfig.hotFile, viteDevServerUrl) + + setTimeout(() => { + server.config.logger.info( + `\n ${colors.red(`${colors.bold('LARAVEL')} ${laravelVersion()}`)} ${colors.dim( + 'plugin', + )} ${colors.bold(`v${pluginVersion()}`)}`, + ) + server.config.logger.info('') + server.config.logger.info( + ` ${colors.green('➜')} ${colors.bold('APP_URL')}: ${colors.cyan( + appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`), + )}`, + ) + }, 100) } - } + }) + + if (!exitHandlersBound) { + const clean = () => { + if (fs.existsSync(pluginConfig.hotFile)) { + fs.rmSync(pluginConfig.hotFile) + } + } + + process.on('exit', clean) + process.on('SIGINT', process.exit) + process.on('SIGTERM', process.exit) + process.on('SIGHUP', process.exit) + + exitHandlersBound = true + } + + return () => + server.middlewares.use((req, res, next) => { + if (req.url === '/index.html') { + res.statusCode = 404 + + res.end( + fs + .readFileSync(path.join(__dirname, 'dev-server-index.html')) + .toString() + .replace(/{{ APP_URL }}/g, appUrl), + ) + } + + next() + }) + }, + } } /** * Validate the command can run in the given environment. */ -function ensureCommandShouldRunInEnvironment(command: 'build'|'serve', env: Record): void { - if (command === 'build' || env.LARAVEL_BYPASS_ENV_CHECK === '1') { - return; - } - - if (typeof env.LARAVEL_VAPOR !== 'undefined') { - throw Error('You should not run the Vite HMR server on Vapor. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1'); - } - - if (typeof env.LARAVEL_FORGE !== 'undefined') { - throw Error('You should not run the Vite HMR server in your Forge deployment script. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1'); - } - - if (typeof env.LARAVEL_ENVOYER !== 'undefined') { - throw Error('You should not run the Vite HMR server in your Envoyer hook. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1') - } - - if (typeof env.CI !== 'undefined') { - throw Error('You should not run the Vite HMR server in CI environments. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1') - } +function ensureCommandShouldRunInEnvironment(command: 'build' | 'serve', env: Record): void { + if (command === 'build' || env.LARAVEL_BYPASS_ENV_CHECK === '1') { + return + } + + if (typeof env.LARAVEL_VAPOR !== 'undefined') { + throw Error( + 'You should not run the Vite HMR server on Vapor. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1', + ) + } + + if (typeof env.LARAVEL_FORGE !== 'undefined') { + throw Error( + 'You should not run the Vite HMR server in your Forge deployment script. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1', + ) + } + + if (typeof env.LARAVEL_ENVOYER !== 'undefined') { + throw Error( + 'You should not run the Vite HMR server in your Envoyer hook. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1', + ) + } + + if (typeof env.CI !== 'undefined') { + throw Error( + 'You should not run the Vite HMR server in CI environments. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1', + ) + } } /** * The version of Laravel being run. */ function laravelVersion(): string { - try { - const composer = JSON.parse(fs.readFileSync('composer.lock').toString()) - - return composer.packages?.find((composerPackage: {name: string}) => composerPackage.name === 'laravel/framework')?.version ?? '' - } catch { - return '' - } + try { + const composer = JSON.parse(fs.readFileSync('composer.lock').toString()) + + return ( + composer.packages?.find((composerPackage: { name: string }) => composerPackage.name === 'laravel/framework') + ?.version ?? '' + ) + } catch { + return '' + } } /** * The version of the Laravel Vite plugin being run. */ function pluginVersion(): string { - try { - return JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json')).toString())?.version - } catch { - return '' - } + try { + return JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json')).toString())?.version + } catch { + return '' + } } /** * Convert the users configuration into a standard structure with defaults. */ -function resolvePluginConfig(config: string|string[]|PluginConfig): Required { - if (typeof config === 'undefined') { - throw new Error('laravel-vite-plugin: missing configuration.') - } - - if (typeof config === 'string' || Array.isArray(config)) { - config = { input: config, ssr: config } - } - - if (typeof config.input === 'undefined') { - throw new Error('laravel-vite-plugin: missing configuration for "input".') - } - - if (typeof config.publicDirectory === 'string') { - config.publicDirectory = config.publicDirectory.trim().replace(/^\/+/, '') - - if (config.publicDirectory === '') { - throw new Error('laravel-vite-plugin: publicDirectory must be a subdirectory. E.g. \'public\'.') - } - } - - if (typeof config.buildDirectory === 'string') { - config.buildDirectory = config.buildDirectory.trim().replace(/^\/+/, '').replace(/\/+$/, '') - - if (config.buildDirectory === '') { - throw new Error('laravel-vite-plugin: buildDirectory must be a subdirectory. E.g. \'build\'.') - } - } - - if (typeof config.ssrOutputDirectory === 'string') { - config.ssrOutputDirectory = config.ssrOutputDirectory.trim().replace(/^\/+/, '').replace(/\/+$/, '') - } - - if (config.refresh === true) { - config.refresh = [{ paths: refreshPaths }] - } - - return { - input: config.input, - publicDirectory: config.publicDirectory ?? 'public', - buildDirectory: config.buildDirectory ?? 'build', - ssr: config.ssr ?? config.input, - ssrOutputDirectory: config.ssrOutputDirectory ?? 'bootstrap/ssr', - refresh: config.refresh ?? false, - hotFile: config.hotFile ?? path.join((config.publicDirectory ?? 'public'), 'hot'), - valetTls: config.valetTls ?? false, - transformOnServe: config.transformOnServe ?? ((code) => code), - } +function resolvePluginConfig(config: string | string[] | PluginConfig): Required { + if (typeof config === 'undefined') { + throw new Error('laravel-vite-plugin: missing configuration.') + } + + if (typeof config === 'string' || Array.isArray(config)) { + config = { input: config, ssr: config } + } + + if (typeof config.input === 'undefined') { + throw new Error('laravel-vite-plugin: missing configuration for "input".') + } + + if (typeof config.publicDirectory === 'string') { + config.publicDirectory = config.publicDirectory.trim().replace(/^\/+/, '') + + if (config.publicDirectory === '') { + throw new Error("laravel-vite-plugin: publicDirectory must be a subdirectory. E.g. 'public'.") + } + } + + if (typeof config.buildDirectory === 'string') { + config.buildDirectory = config.buildDirectory.trim().replace(/^\/+/, '').replace(/\/+$/, '') + + if (config.buildDirectory === '') { + throw new Error("laravel-vite-plugin: buildDirectory must be a subdirectory. E.g. 'build'.") + } + } + + if (typeof config.ssrOutputDirectory === 'string') { + config.ssrOutputDirectory = config.ssrOutputDirectory.trim().replace(/^\/+/, '').replace(/\/+$/, '') + } + + if (config.refresh === true) { + config.refresh = [{ paths: refreshPaths }] + } + + return { + input: config.input, + publicDirectory: config.publicDirectory ?? 'public', + buildDirectory: config.buildDirectory ?? 'build', + ssr: config.ssr ?? config.input, + ssrOutputDirectory: config.ssrOutputDirectory ?? 'bootstrap/ssr', + refresh: config.refresh ?? false, + hotFile: config.hotFile ?? path.join(config.publicDirectory ?? 'public', 'hot'), + valetTls: config.valetTls ?? false, + transformOnServe: config.transformOnServe ?? ((code) => code), + } } /** * Resolve the Vite base option from the configuration. */ function resolveBase(config: Required, assetUrl: string): string { - return assetUrl + (! assetUrl.endsWith('/') ? '/' : '') + config.buildDirectory + '/' + return assetUrl + (!assetUrl.endsWith('/') ? '/' : '') + config.buildDirectory + '/' } /** * Resolve the Vite input path from the configuration. */ -function resolveInput(config: Required, ssr: boolean): string|string[]|undefined { - if (ssr) { - return config.ssr - } +function resolveInput(config: Required, ssr: boolean): string | string[] | undefined { + if (ssr) { + return config.ssr + } - return config.input + return config.input } /** * Resolve the Vite outDir path from the configuration. */ -function resolveOutDir(config: Required, ssr: boolean): string|undefined { - if (ssr) { - return config.ssrOutputDirectory - } +function resolveOutDir(config: Required, ssr: boolean): string | undefined { + if (ssr) { + return config.ssrOutputDirectory + } - return path.join(config.publicDirectory, config.buildDirectory) + return path.join(config.publicDirectory, config.buildDirectory) } -function resolveFullReloadConfig({ refresh: config }: Required): PluginOption[]{ - if (typeof config === 'boolean') { - return []; - } +function resolveFullReloadConfig({ refresh: config }: Required): PluginOption[] { + if (typeof config === 'boolean') { + return [] + } - if (typeof config === 'string') { - config = [{ paths: [config]}] - } + if (typeof config === 'string') { + config = [{ paths: [config] }] + } - if (! Array.isArray(config)) { - config = [config] - } + if (!Array.isArray(config)) { + config = [config] + } - if (config.some(c => typeof c === 'string')) { - config = [{ paths: config }] as RefreshConfig[] - } + if (config.some((c) => typeof c === 'string')) { + config = [{ paths: config }] as RefreshConfig[] + } - return (config as RefreshConfig[]).flatMap(c => { - const plugin = fullReload(c.paths, c.config) + return (config as RefreshConfig[]).flatMap((c) => { + const plugin = fullReload(c.paths, c.config) - /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ - /** @ts-ignore */ - plugin.__laravel_plugin_config = c + /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ + /** @ts-ignore */ + plugin.__laravel_plugin_config = c - return plugin - }) + return plugin + }) } /** * Resolve the dev server URL from the server address and configuration. */ function resolveDevServerUrl(address: AddressInfo, config: ResolvedConfig): DevServerUrl { - const configHmrProtocol = typeof config.server.hmr === 'object' ? config.server.hmr.protocol : null - const clientProtocol = configHmrProtocol ? (configHmrProtocol === 'wss' ? 'https' : 'http') : null - const serverProtocol = config.server.https ? 'https' : 'http' - const protocol = clientProtocol ?? serverProtocol + const configHmrProtocol = typeof config.server.hmr === 'object' ? config.server.hmr.protocol : null + const clientProtocol = configHmrProtocol ? (configHmrProtocol === 'wss' ? 'https' : 'http') : null + const serverProtocol = config.server.https ? 'https' : 'http' + const protocol = clientProtocol ?? serverProtocol - const configHmrHost = typeof config.server.hmr === 'object' ? config.server.hmr.host : null - const configHost = typeof config.server.host === 'string' ? config.server.host : null - const serverAddress = isIpv6(address) ? `[${address.address}]` : address.address - const host = configHmrHost ?? configHost ?? serverAddress + const configHmrHost = typeof config.server.hmr === 'object' ? config.server.hmr.host : null + const configHost = typeof config.server.host === 'string' ? config.server.host : null + const serverAddress = isIpv6(address) ? `[${address.address}]` : address.address + const host = configHmrHost ?? configHost ?? serverAddress - const configHmrClientPort = typeof config.server.hmr === 'object' ? config.server.hmr.clientPort : null - const port = configHmrClientPort ?? address.port + const configHmrClientPort = typeof config.server.hmr === 'object' ? config.server.hmr.clientPort : null + const port = configHmrClientPort ?? address.port - return `${protocol}://${host}:${port}` + return `${protocol}://${host}:${port}` } function isIpv6(address: AddressInfo): boolean { - return address.family === 'IPv6' - // In node >=18.0 <18.4 this was an integer value. This was changed in a minor version. - // See: https://github.com/laravel/vite-plugin/issues/103 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-next-line - || address.family === 6; + return ( + address.family === 'IPv6' || + // In node >=18.0 <18.4 this was an integer value. This was changed in a minor version. + // See: https://github.com/laravel/vite-plugin/issues/103 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-next-line + address.family === 6 + ) } /** @@ -427,113 +460,116 @@ function isIpv6(address: AddressInfo): boolean { * * @see https://vitejs.dev/guide/ssr.html#ssr-externals */ -function noExternalInertiaHelpers(config: UserConfig): true|Array { - /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ - /* @ts-ignore */ - const userNoExternal = (config.ssr as SSROptions|undefined)?.noExternal - const pluginNoExternal = ['laravel-vite-plugin'] +function noExternalInertiaHelpers(config: UserConfig): true | Array { + /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ + /* @ts-ignore */ + const userNoExternal = (config.ssr as SSROptions | undefined)?.noExternal + const pluginNoExternal = ['laravel-vite-plugin'] - if (userNoExternal === true) { - return true - } + if (userNoExternal === true) { + return true + } - if (typeof userNoExternal === 'undefined') { - return pluginNoExternal - } + if (typeof userNoExternal === 'undefined') { + return pluginNoExternal + } - return [ - ...(Array.isArray(userNoExternal) ? userNoExternal : [userNoExternal]), - ...pluginNoExternal, - ] + return [...(Array.isArray(userNoExternal) ? userNoExternal : [userNoExternal]), ...pluginNoExternal] } /** * Resolve the server config from the environment. */ -function resolveEnvironmentServerConfig(env: Record): { - hmr?: { host: string } - host?: string, - https?: { cert: Buffer, key: Buffer } -}|undefined { - if (! env.VITE_DEV_SERVER_KEY && ! env.VITE_DEV_SERVER_CERT) { - return - } - - if (! fs.existsSync(env.VITE_DEV_SERVER_KEY) || ! fs.existsSync(env.VITE_DEV_SERVER_CERT)) { - throw Error(`Unable to find the certificate files specified in your environment. Ensure you have correctly configured VITE_DEV_SERVER_KEY: [${env.VITE_DEV_SERVER_KEY}] and VITE_DEV_SERVER_CERT: [${env.VITE_DEV_SERVER_CERT}].`) - } - - const host = resolveHostFromEnv(env) - - if (! host) { - throw Error(`Unable to determine the host from the environment's APP_URL: [${env.APP_URL}].`) - } - - return { - hmr: { host }, - host, - https: { - key: fs.readFileSync(env.VITE_DEV_SERVER_KEY), - cert: fs.readFileSync(env.VITE_DEV_SERVER_CERT), - }, - } +function resolveEnvironmentServerConfig(env: Record): + | { + hmr?: { host: string } + host?: string + https?: { cert: Buffer; key: Buffer } + } + | undefined { + if (!env.VITE_DEV_SERVER_KEY && !env.VITE_DEV_SERVER_CERT) { + return + } + + if (!fs.existsSync(env.VITE_DEV_SERVER_KEY) || !fs.existsSync(env.VITE_DEV_SERVER_CERT)) { + throw Error( + `Unable to find the certificate files specified in your environment. Ensure you have correctly configured VITE_DEV_SERVER_KEY: [${env.VITE_DEV_SERVER_KEY}] and VITE_DEV_SERVER_CERT: [${env.VITE_DEV_SERVER_CERT}].`, + ) + } + + const host = resolveHostFromEnv(env) + + if (!host) { + throw Error(`Unable to determine the host from the environment's APP_URL: [${env.APP_URL}].`) + } + + return { + hmr: { host }, + host, + https: { + key: fs.readFileSync(env.VITE_DEV_SERVER_KEY), + cert: fs.readFileSync(env.VITE_DEV_SERVER_CERT), + }, + } } /** * Resolve the host name from the environment. */ -function resolveHostFromEnv(env: Record): string|undefined -{ - try { - return new URL(env.APP_URL).host - } catch { - return - } +function resolveHostFromEnv(env: Record): string | undefined { + try { + return new URL(env.APP_URL).host + } catch { + return + } } - /** * Resolve the valet server config for the given host. */ -function resolveValetServerConfig(host: string|boolean): { - hmr?: { host: string } - host?: string, - https?: { cert: Buffer, key: Buffer } -}|undefined { - if (host === false) { - return - } - - host = host === true ? resolveValetHost() : host - - const keyPath = path.resolve(os.homedir(), `.config/valet/Certificates/${host}.key`) - const certPath = path.resolve(os.homedir(), `.config/valet/Certificates/${host}.crt`) - - if (! fs.existsSync(keyPath) || ! fs.existsSync(certPath)) { - throw Error(`Unable to find Valet certificate files for your host [${host}]. Ensure you have run "valet secure".`) - } - - return { - hmr: { host }, - host, - https: { - key: fs.readFileSync(keyPath), - cert: fs.readFileSync(certPath), - }, - } +function resolveValetServerConfig(host: string | boolean): + | { + hmr?: { host: string } + host?: string + https?: { cert: Buffer; key: Buffer } + } + | undefined { + if (host === false) { + return + } + + host = host === true ? resolveValetHost() : host + + const keyPath = path.resolve(os.homedir(), `.config/valet/Certificates/${host}.key`) + const certPath = path.resolve(os.homedir(), `.config/valet/Certificates/${host}.crt`) + + if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) { + throw Error(`Unable to find Valet certificate files for your host [${host}]. Ensure you have run "valet secure".`) + } + + return { + hmr: { host }, + host, + https: { + key: fs.readFileSync(keyPath), + cert: fs.readFileSync(certPath), + }, + } } /** * Resolve the valet valet host for the current directory. */ function resolveValetHost(): string { - const configPath = os.homedir() + `/.config/valet/config.json` + const configPath = os.homedir() + `/.config/valet/config.json` - if (! fs.existsSync(configPath)) { - throw Error('Unable to find the Valet configuration file. You will need to manually specify the host in the `valetTls` configuration option.') - } + if (!fs.existsSync(configPath)) { + throw Error( + 'Unable to find the Valet configuration file. You will need to manually specify the host in the `valetTls` configuration option.', + ) + } - const config: { tld: string } = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + const config: { tld: string } = JSON.parse(fs.readFileSync(configPath, 'utf-8')) - return path.basename(process.cwd()) + '.' + config.tld + return path.basename(process.cwd()) + '.' + config.tld } diff --git a/src/inertia-helpers/index.ts b/src/inertia-helpers/index.ts index 043f001..2cb4ac0 100644 --- a/src/inertia-helpers/index.ts +++ b/src/inertia-helpers/index.ts @@ -1,9 +1,12 @@ -export async function resolvePageComponent(path: string, pages: Record | (() => Promise)>): Promise { - const page = pages[path] +export async function resolvePageComponent( + path: string, + pages: Record | (() => Promise)>, +): Promise { + const page = pages[path] - if (typeof page === 'undefined') { - throw new Error(`Page not found: ${path}`) - } + if (typeof page === 'undefined') { + throw new Error(`Page not found: ${path}`) + } - return typeof page === 'function' ? page() : page + return typeof page === 'function' ? page() : page } diff --git a/tests/index.test.ts b/tests/index.test.ts index 66ff7cc..1ffb31b 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,379 +1,395 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import laravel from '../src' -import { resolvePageComponent } from '../src/inertia-helpers'; +import { resolvePageComponent } from '../src/inertia-helpers' describe('laravel-vite-plugin', () => { - afterEach(() => { - vi.clearAllMocks() + afterEach(() => { + vi.clearAllMocks() + }) + + it('handles missing configuration', () => { + /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ + /* @ts-ignore */ + expect(() => laravel()).toThrowError('laravel-vite-plugin: missing configuration.') + + /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ + /* @ts-ignore */ + expect(() => laravel({})).toThrowError('laravel-vite-plugin: missing configuration for "input".') + }) + + it('accepts a single input', () => { + const plugin = laravel('resources/js/app.ts')[0] + + const config = plugin.config({}, { command: 'build', mode: 'production' }) + expect(config.build.rollupOptions.input).toBe('resources/js/app.ts') + + const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) + expect(ssrConfig.build.rollupOptions.input).toBe('resources/js/app.ts') + }) + + it('accepts an array of inputs', () => { + const plugin = laravel(['resources/js/app.ts', 'resources/js/other.js'])[0] + + const config = plugin.config({}, { command: 'build', mode: 'production' }) + expect(config.build.rollupOptions.input).toEqual(['resources/js/app.ts', 'resources/js/other.js']) + + const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) + expect(ssrConfig.build.rollupOptions.input).toEqual(['resources/js/app.ts', 'resources/js/other.js']) + }) + + it('accepts a full configuration', () => { + const plugin = laravel({ + input: 'resources/js/app.ts', + publicDirectory: 'other-public', + buildDirectory: 'other-build', + ssr: 'resources/js/ssr.ts', + ssrOutputDirectory: 'other-ssr-output', + })[0] + + const config = plugin.config({}, { command: 'build', mode: 'production' }) + expect(config.base).toBe('/other-build/') + expect(config.build.manifest).toBe(true) + expect(config.build.outDir).toBe('other-public/other-build') + expect(config.build.rollupOptions.input).toBe('resources/js/app.ts') + + const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) + expect(ssrConfig.base).toBe('/other-build/') + expect(ssrConfig.build.manifest).toBe(false) + expect(ssrConfig.build.outDir).toBe('other-ssr-output') + expect(ssrConfig.build.rollupOptions.input).toBe('resources/js/ssr.ts') + }) + + it('respects users base config option', () => { + const plugin = laravel({ + input: 'resources/js/app.ts', + })[0] + + const userConfig = { base: '/foo/' } + + const config = plugin.config(userConfig, { command: 'build', mode: 'production' }) + + expect(config.base).toBe('/foo/') + }) + + it('accepts a partial configuration', () => { + const plugin = laravel({ + input: 'resources/js/app.js', + ssr: 'resources/js/ssr.js', + })[0] + + const config = plugin.config({}, { command: 'build', mode: 'production' }) + expect(config.base).toBe('/build/') + expect(config.build.manifest).toBe(true) + expect(config.build.outDir).toBe('public/build') + expect(config.build.rollupOptions.input).toBe('resources/js/app.js') + + const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) + expect(ssrConfig.base).toBe('/build/') + expect(ssrConfig.build.manifest).toBe(false) + expect(ssrConfig.build.outDir).toBe('bootstrap/ssr') + expect(ssrConfig.build.rollupOptions.input).toBe('resources/js/ssr.js') + }) + + it('uses the default entry point when ssr entry point is not provided', () => { + // This is support users who may want a dedicated Vite config for SSR. + const plugin = laravel('resources/js/ssr.js')[0] + + const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) + expect(ssrConfig.build.rollupOptions.input).toBe('resources/js/ssr.js') + }) + + it('prefixes the base with ASSET_URL in production mode', () => { + process.env.ASSET_URL = 'http://example.com' + const plugin = laravel('resources/js/app.js')[0] + + const devConfig = plugin.config({}, { command: 'serve', mode: 'development' }) + expect(devConfig.base).toBe('') + + const prodConfig = plugin.config({}, { command: 'build', mode: 'production' }) + expect(prodConfig.base).toBe('http://example.com/build/') + + delete process.env.ASSET_URL + }) + + it('prevents setting an empty publicDirectory', () => { + expect(() => laravel({ input: 'resources/js/app.js', publicDirectory: '' })[0]).toThrowError( + 'publicDirectory must be a subdirectory', + ) + }) + + it('prevents setting an empty buildDirectory', () => { + expect(() => laravel({ input: 'resources/js/app.js', buildDirectory: '' })[0]).toThrowError( + 'buildDirectory must be a subdirectory', + ) + }) + + it('handles surrounding slashes on directories', () => { + const plugin = laravel({ + input: 'resources/js/app.js', + publicDirectory: '/public/test/', + buildDirectory: '/build/test/', + ssrOutputDirectory: '/ssr-output/test/', + })[0] + + const config = plugin.config({}, { command: 'build', mode: 'production' }) + expect(config.base).toBe('/build/test/') + expect(config.build.outDir).toBe('public/test/build/test') + + const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) + expect(ssrConfig.build.outDir).toBe('ssr-output/test') + }) + + it('provides an @ alias by default', () => { + const plugin = laravel('resources/js/app.js')[0] + + const config = plugin.config({}, { command: 'build', mode: 'development' }) + + expect(config.resolve.alias['@']).toBe('/resources/js') + }) + + it('respects a users existing @ alias', () => { + const plugin = laravel('resources/js/app.js')[0] + + const config = plugin.config( + { + resolve: { + alias: { + '@': '/somewhere/else', + }, + }, + }, + { command: 'build', mode: 'development' }, + ) + + expect(config.resolve.alias['@']).toBe('/somewhere/else') + }) + + it('appends an Alias object when using an alias array', () => { + const plugin = laravel('resources/js/app.js')[0] + + const config = plugin.config( + { + resolve: { + alias: [{ find: '@', replacement: '/something/else' }], + }, + }, + { command: 'build', mode: 'development' }, + ) + + expect(config.resolve.alias).toEqual([ + { find: '@', replacement: '/something/else' }, + { find: '@', replacement: '/resources/js' }, + ]) + }) + + it('configures the Vite server when inside a Sail container', () => { + process.env.LARAVEL_SAIL = '1' + const plugin = laravel('resources/js/app.js')[0] + + const config = plugin.config({}, { command: 'serve', mode: 'development' }) + expect(config.server.host).toBe('0.0.0.0') + expect(config.server.port).toBe(5173) + expect(config.server.strictPort).toBe(true) + + delete process.env.LARAVEL_SAIL + }) + + it('allows the Vite port to be configured when inside a Sail container', () => { + process.env.LARAVEL_SAIL = '1' + process.env.VITE_PORT = '1234' + const plugin = laravel('resources/js/app.js')[0] + + const config = plugin.config({}, { command: 'serve', mode: 'development' }) + expect(config.server.host).toBe('0.0.0.0') + expect(config.server.port).toBe(1234) + expect(config.server.strictPort).toBe(true) + + delete process.env.LARAVEL_SAIL + delete process.env.VITE_PORT + }) + + it('allows the server configuration to be overridden inside a Sail container', () => { + process.env.LARAVEL_SAIL = '1' + const plugin = laravel('resources/js/app.js')[0] + + const config = plugin.config( + { + server: { + host: 'example.com', + port: 1234, + strictPort: false, + }, + }, + { command: 'serve', mode: 'development' }, + ) + expect(config.server.host).toBe('example.com') + expect(config.server.port).toBe(1234) + expect(config.server.strictPort).toBe(false) + + delete process.env.LARAVEL_SAIL + }) + + it('prevents the Inertia helpers from being externalized', () => { + /* eslint-disable @typescript-eslint/ban-ts-comment */ + const plugin = laravel('resources/js/app.js')[0] + + const noSsrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) + /* @ts-ignore */ + expect(noSsrConfig.ssr.noExternal).toEqual(['laravel-vite-plugin']) + + /* @ts-ignore */ + const nothingExternalConfig = plugin.config( + { ssr: { noExternal: true }, build: { ssr: true } }, + { command: 'build', mode: 'production' }, + ) + /* @ts-ignore */ + expect(nothingExternalConfig.ssr.noExternal).toBe(true) + + /* @ts-ignore */ + const arrayNoExternalConfig = plugin.config( + { ssr: { noExternal: ['foo'] }, build: { ssr: true } }, + { command: 'build', mode: 'production' }, + ) + /* @ts-ignore */ + expect(arrayNoExternalConfig.ssr.noExternal).toEqual(['foo', 'laravel-vite-plugin']) + + /* @ts-ignore */ + const stringNoExternalConfig = plugin.config( + { ssr: { noExternal: 'foo' }, build: { ssr: true } }, + { command: 'build', mode: 'production' }, + ) + /* @ts-ignore */ + expect(stringNoExternalConfig.ssr.noExternal).toEqual(['foo', 'laravel-vite-plugin']) + }) + + it('does not configure full reload when configuration it not an object', () => { + const plugins = laravel('resources/js/app.js') + + expect(plugins.length).toBe(1) + }) + + it('does not configure full reload when refresh is not present', () => { + const plugins = laravel({ + input: 'resources/js/app.js', }) - it('handles missing configuration', () => { - /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ - /* @ts-ignore */ - expect(() => laravel()) - .toThrowError('laravel-vite-plugin: missing configuration.'); + expect(plugins.length).toBe(1) + }) - /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ - /* @ts-ignore */ - expect(() => laravel({})) - .toThrowError('laravel-vite-plugin: missing configuration for "input".'); + it('does not configure full reload when refresh is set to undefined', () => { + const plugins = laravel({ + input: 'resources/js/app.js', + refresh: undefined, }) + expect(plugins.length).toBe(1) + }) - it('accepts a single input', () => { - const plugin = laravel('resources/js/app.ts')[0] - - const config = plugin.config({}, { command: 'build', mode: 'production' }) - expect(config.build.rollupOptions.input).toBe('resources/js/app.ts') - - const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) - expect(ssrConfig.build.rollupOptions.input).toBe('resources/js/app.ts') + it('does not configure full reload when refresh is false', () => { + const plugins = laravel({ + input: 'resources/js/app.js', + refresh: false, }) - it('accepts an array of inputs', () => { - const plugin = laravel([ - 'resources/js/app.ts', - 'resources/js/other.js', - ])[0] + expect(plugins.length).toBe(1) + }) - const config = plugin.config({}, { command: 'build', mode: 'production' }) - expect(config.build.rollupOptions.input).toEqual(['resources/js/app.ts', 'resources/js/other.js']) - - const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) - expect(ssrConfig.build.rollupOptions.input).toEqual(['resources/js/app.ts', 'resources/js/other.js']) + it('configures full reload with routes and views when refresh is true', () => { + const plugins = laravel({ + input: 'resources/js/app.js', + refresh: true, }) - it('accepts a full configuration', () => { - const plugin = laravel({ - input: 'resources/js/app.ts', - publicDirectory: 'other-public', - buildDirectory: 'other-build', - ssr: 'resources/js/ssr.ts', - ssrOutputDirectory: 'other-ssr-output', - })[0] - - const config = plugin.config({}, { command: 'build', mode: 'production' }) - expect(config.base).toBe('/other-build/') - expect(config.build.manifest).toBe(true) - expect(config.build.outDir).toBe('other-public/other-build') - expect(config.build.rollupOptions.input).toBe('resources/js/app.ts') - - const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) - expect(ssrConfig.base).toBe('/other-build/') - expect(ssrConfig.build.manifest).toBe(false) - expect(ssrConfig.build.outDir).toBe('other-ssr-output') - expect(ssrConfig.build.rollupOptions.input).toBe('resources/js/ssr.ts') + expect(plugins.length).toBe(2) + /** @ts-ignore */ + expect(plugins[1].__laravel_plugin_config).toEqual({ + paths: ['app/View/Components/**', 'resources/views/**', 'resources/lang/**', 'lang/**', 'routes/**'], }) + }) - it('respects users base config option', () => { - const plugin = laravel({ - input: 'resources/js/app.ts', - })[0] - - const userConfig = { base: '/foo/' } - - const config = plugin.config(userConfig, { command: 'build', mode: 'production' }) - - expect(config.base).toBe('/foo/') + it('configures full reload when refresh is a single path', () => { + const plugins = laravel({ + input: 'resources/js/app.js', + refresh: 'path/to/watch/**', }) - it('accepts a partial configuration', () => { - const plugin = laravel({ - input: 'resources/js/app.js', - ssr: 'resources/js/ssr.js', - })[0] - - const config = plugin.config({}, { command: 'build', mode: 'production' }) - expect(config.base).toBe('/build/') - expect(config.build.manifest).toBe(true) - expect(config.build.outDir).toBe('public/build') - expect(config.build.rollupOptions.input).toBe('resources/js/app.js') - - const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) - expect(ssrConfig.base).toBe('/build/') - expect(ssrConfig.build.manifest).toBe(false) - expect(ssrConfig.build.outDir).toBe('bootstrap/ssr') - expect(ssrConfig.build.rollupOptions.input).toBe('resources/js/ssr.js') + expect(plugins.length).toBe(2) + /** @ts-ignore */ + expect(plugins[1].__laravel_plugin_config).toEqual({ + paths: ['path/to/watch/**'], }) + }) - it('uses the default entry point when ssr entry point is not provided', () => { - // This is support users who may want a dedicated Vite config for SSR. - const plugin = laravel('resources/js/ssr.js')[0] - - const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) - expect(ssrConfig.build.rollupOptions.input).toBe('resources/js/ssr.js') + it('configures full reload when refresh is an array of paths', () => { + const plugins = laravel({ + input: 'resources/js/app.js', + refresh: ['path/to/watch/**', 'another/to/watch/**'], }) - it('prefixes the base with ASSET_URL in production mode', () => { - process.env.ASSET_URL = 'http://example.com' - const plugin = laravel('resources/js/app.js')[0] - - const devConfig = plugin.config({}, { command: 'serve', mode: 'development' }) - expect(devConfig.base).toBe('') - - const prodConfig = plugin.config({}, { command: 'build', mode: 'production' }) - expect(prodConfig.base).toBe('http://example.com/build/') - - delete process.env.ASSET_URL + expect(plugins.length).toBe(2) + /** @ts-ignore */ + expect(plugins[1].__laravel_plugin_config).toEqual({ + paths: ['path/to/watch/**', 'another/to/watch/**'], }) - - it('prevents setting an empty publicDirectory', () => { - expect(() => laravel({ input: 'resources/js/app.js', publicDirectory: '' })[0]) - .toThrowError('publicDirectory must be a subdirectory'); + }) + + it('configures full reload when refresh is a complete configuration to proxy', () => { + const plugins = laravel({ + input: 'resources/js/app.js', + refresh: { + paths: ['path/to/watch/**', 'another/to/watch/**'], + config: { delay: 987 }, + }, }) - it('prevents setting an empty buildDirectory', () => { - expect(() => laravel({ input: 'resources/js/app.js', buildDirectory: '' })[0]) - .toThrowError('buildDirectory must be a subdirectory'); + expect(plugins.length).toBe(2) + /** @ts-ignore */ + expect(plugins[1].__laravel_plugin_config).toEqual({ + paths: ['path/to/watch/**', 'another/to/watch/**'], + config: { delay: 987 }, }) - - it('handles surrounding slashes on directories', () => { - const plugin = laravel({ - input: 'resources/js/app.js', - publicDirectory: '/public/test/', - buildDirectory: '/build/test/', - ssrOutputDirectory: '/ssr-output/test/', - })[0] - - const config = plugin.config({}, { command: 'build', mode: 'production' }) - expect(config.base).toBe('/build/test/') - expect(config.build.outDir).toBe('public/test/build/test') - - const ssrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) - expect(ssrConfig.build.outDir).toBe('ssr-output/test') + }) + + it('configures full reload when refresh is an array of complete configurations to proxy', () => { + const plugins = laravel({ + input: 'resources/js/app.js', + refresh: [ + { + paths: ['path/to/watch/**'], + config: { delay: 987 }, + }, + { + paths: ['another/to/watch/**'], + config: { delay: 123 }, + }, + ], }) - it('provides an @ alias by default', () => { - const plugin = laravel('resources/js/app.js')[0] - - const config = plugin.config({}, { command: 'build', mode: 'development' }) - - expect(config.resolve.alias['@']).toBe('/resources/js') + expect(plugins.length).toBe(3) + /** @ts-ignore */ + expect(plugins[1].__laravel_plugin_config).toEqual({ + paths: ['path/to/watch/**'], + config: { delay: 987 }, }) - - it('respects a users existing @ alias', () => { - const plugin = laravel('resources/js/app.js')[0] - - const config = plugin.config({ - resolve: { - alias: { - '@': '/somewhere/else' - } - } - }, { command: 'build', mode: 'development' }) - - expect(config.resolve.alias['@']).toBe('/somewhere/else') - }) - - it('appends an Alias object when using an alias array', () => { - const plugin = laravel('resources/js/app.js')[0] - - const config = plugin.config({ - resolve: { - alias: [ - { find: '@', replacement: '/something/else' } - ], - } - }, { command: 'build', mode: 'development' }) - - expect(config.resolve.alias).toEqual([ - { find: '@', replacement: '/something/else' }, - { find: '@', replacement: '/resources/js' }, - ]) - }) - - it('configures the Vite server when inside a Sail container', () => { - process.env.LARAVEL_SAIL = '1' - const plugin = laravel('resources/js/app.js')[0] - - const config = plugin.config({}, { command: 'serve', mode: 'development' }) - expect(config.server.host).toBe('0.0.0.0') - expect(config.server.port).toBe(5173) - expect(config.server.strictPort).toBe(true) - - delete process.env.LARAVEL_SAIL - }) - - it('allows the Vite port to be configured when inside a Sail container', () => { - process.env.LARAVEL_SAIL = '1' - process.env.VITE_PORT = '1234' - const plugin = laravel('resources/js/app.js')[0] - - const config = plugin.config({}, { command: 'serve', mode: 'development' }) - expect(config.server.host).toBe('0.0.0.0') - expect(config.server.port).toBe(1234) - expect(config.server.strictPort).toBe(true) - - delete process.env.LARAVEL_SAIL - delete process.env.VITE_PORT - }) - - it('allows the server configuration to be overridden inside a Sail container', () => { - process.env.LARAVEL_SAIL = '1' - const plugin = laravel('resources/js/app.js')[0] - - const config = plugin.config({ - server: { - host: 'example.com', - port: 1234, - strictPort: false, - } - }, { command: 'serve', mode: 'development' }) - expect(config.server.host).toBe('example.com') - expect(config.server.port).toBe(1234) - expect(config.server.strictPort).toBe(false) - - delete process.env.LARAVEL_SAIL - }) - - it('prevents the Inertia helpers from being externalized', () => { - /* eslint-disable @typescript-eslint/ban-ts-comment */ - const plugin = laravel('resources/js/app.js')[0] - - const noSsrConfig = plugin.config({ build: { ssr: true } }, { command: 'build', mode: 'production' }) - /* @ts-ignore */ - expect(noSsrConfig.ssr.noExternal).toEqual(['laravel-vite-plugin']) - - /* @ts-ignore */ - const nothingExternalConfig = plugin.config({ ssr: { noExternal: true }, build: { ssr: true } }, { command: 'build', mode: 'production' }) - /* @ts-ignore */ - expect(nothingExternalConfig.ssr.noExternal).toBe(true) - - /* @ts-ignore */ - const arrayNoExternalConfig = plugin.config({ ssr: { noExternal: ['foo'] }, build: { ssr: true } }, { command: 'build', mode: 'production' }) - /* @ts-ignore */ - expect(arrayNoExternalConfig.ssr.noExternal).toEqual(['foo', 'laravel-vite-plugin']) - - /* @ts-ignore */ - const stringNoExternalConfig = plugin.config({ ssr: { noExternal: 'foo' }, build: { ssr: true } }, { command: 'build', mode: 'production' }) - /* @ts-ignore */ - expect(stringNoExternalConfig.ssr.noExternal).toEqual(['foo', 'laravel-vite-plugin']) - }) - - it('does not configure full reload when configuration it not an object', () => { - const plugins = laravel('resources/js/app.js') - - expect(plugins.length).toBe(1) - }) - - it('does not configure full reload when refresh is not present', () => { - const plugins = laravel({ - input: 'resources/js/app.js', - }) - - expect(plugins.length).toBe(1) - }) - - it('does not configure full reload when refresh is set to undefined', () => { - const plugins = laravel({ - input: 'resources/js/app.js', - refresh: undefined, - }) - expect(plugins.length).toBe(1) - }) - - it('does not configure full reload when refresh is false', () => { - const plugins = laravel({ - input: 'resources/js/app.js', - refresh: false, - }) - - expect(plugins.length).toBe(1) - }) - - it('configures full reload with routes and views when refresh is true', () => { - const plugins = laravel({ - input: 'resources/js/app.js', - refresh: true, - }) - - expect(plugins.length).toBe(2) - /** @ts-ignore */ - expect(plugins[1].__laravel_plugin_config).toEqual({ - paths: ['app/View/Components/**', 'resources/views/**', 'resources/lang/**', 'lang/**', 'routes/**'], - }) - }) - - it('configures full reload when refresh is a single path', () => { - const plugins = laravel({ - input: 'resources/js/app.js', - refresh: 'path/to/watch/**', - }) - - expect(plugins.length).toBe(2) - /** @ts-ignore */ - expect(plugins[1].__laravel_plugin_config).toEqual({ - paths: ['path/to/watch/**'], - }) - }) - - it('configures full reload when refresh is an array of paths', () => { - const plugins = laravel({ - input: 'resources/js/app.js', - refresh: ['path/to/watch/**', 'another/to/watch/**'], - }) - - expect(plugins.length).toBe(2) - /** @ts-ignore */ - expect(plugins[1].__laravel_plugin_config).toEqual({ - paths: ['path/to/watch/**', 'another/to/watch/**'], - }) - }) - - it('configures full reload when refresh is a complete configuration to proxy', () => { - const plugins = laravel({ - input: 'resources/js/app.js', - refresh: { - paths: ['path/to/watch/**', 'another/to/watch/**'], - config: { delay: 987 } - }, - }) - - expect(plugins.length).toBe(2) - /** @ts-ignore */ - expect(plugins[1].__laravel_plugin_config).toEqual({ - paths: ['path/to/watch/**', 'another/to/watch/**'], - config: { delay: 987 } - }) - }) - - it('configures full reload when refresh is an array of complete configurations to proxy', () => { - const plugins = laravel({ - input: 'resources/js/app.js', - refresh: [ - { - paths: ['path/to/watch/**'], - config: { delay: 987 } - }, - { - paths: ['another/to/watch/**'], - config: { delay: 123 } - }, - ], - }) - - expect(plugins.length).toBe(3) - /** @ts-ignore */ - expect(plugins[1].__laravel_plugin_config).toEqual({ - paths: ['path/to/watch/**'], - config: { delay: 987 } - }) - /** @ts-ignore */ - expect(plugins[2].__laravel_plugin_config).toEqual({ - paths: ['another/to/watch/**'], - config: { delay: 123 } - }) + /** @ts-ignore */ + expect(plugins[2].__laravel_plugin_config).toEqual({ + paths: ['another/to/watch/**'], + config: { delay: 123 }, }) + }) }) describe('inertia-helpers', () => { - const path = './__data__/dummy.ts' - it('pass glob value to resolvePageComponent', async () => { - const file = await resolvePageComponent<{ default: string }>(path, import.meta.glob('./__data__/*.ts')) - expect(file.default).toBe('Dummy File') - }) - - it('pass eagerly globed value to resolvePageComponent', async () => { - const file = await resolvePageComponent<{ default: string }>(path, import.meta.glob('./__data__/*.ts', { eager: true })) - expect(file.default).toBe('Dummy File') - }) + const path = './__data__/dummy.ts' + it('pass glob value to resolvePageComponent', async () => { + const file = await resolvePageComponent<{ default: string }>(path, import.meta.glob('./__data__/*.ts')) + expect(file.default).toBe('Dummy File') + }) + + it('pass eagerly globed value to resolvePageComponent', async () => { + const file = await resolvePageComponent<{ default: string }>( + path, + import.meta.glob('./__data__/*.ts', { eager: true }), + ) + expect(file.default).toBe('Dummy File') + }) }) diff --git a/tsconfig.inertia-helpers.json b/tsconfig.inertia-helpers.json index b6531ff..794d71a 100644 --- a/tsconfig.inertia-helpers.json +++ b/tsconfig.inertia-helpers.json @@ -1,11 +1,9 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "inertia-helpers", - "target": "ES2020", - "module": "ES2020", - }, - "include": [ - "./src/inertia-helpers/index.ts" - ] + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "inertia-helpers", + "target": "ES2020", + "module": "ES2020" + }, + "include": ["./src/inertia-helpers/index.ts"] } diff --git a/tsconfig.json b/tsconfig.json index be715c9..be87aad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,16 @@ { - "compilerOptions": { - "outDir": "./dist", - "target": "ES2019", - "module": "commonjs", - "moduleResolution": "node", - "resolveJsonModule": true, - "strict": true, - "declaration": true, - "esModuleInterop": true - }, - "include": [ - "./src/index.ts" - ] + "compilerOptions": { + "outDir": "./dist", + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "noUnusedLocals": true, + "strict": true, + "declaration": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["node", "vite/client"] + }, + "include": ["./src/index.ts"] } diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..18a900a --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,14 @@ +import { builtinModules } from 'module' +import { defineConfig } from 'tsup' +import pkg from './package.json' + +export default defineConfig({ + entry: ['./src/index.ts', './src/inertia-helpers/index.ts'], + format: ['cjs', 'esm'], + target: ['esnext'], + outDir: 'dist', + clean: true, + dts: true, + treeshake: true, + external: [...builtinModules, ...Object.keys(pkg.peerDependencies || {})], +})