diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index c506b0d9e039c2..b62a200ceb586e 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -5984,6 +5984,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "preview-server", + "path": "/packages/vite/executors/preview-server", + "name": "preview-server", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/packages.json b/docs/generated/manifests/packages.json index 6f578802941f0d..cf91f2eab7fc25 100644 --- a/docs/generated/manifests/packages.json +++ b/docs/generated/manifests/packages.json @@ -2633,6 +2633,15 @@ "originalFilePath": "/packages/vite/src/executors/test/schema.json", "path": "/packages/vite/executors/test", "type": "executor" + }, + "/packages/vite/executors/preview-server": { + "description": "Vite preview server", + "file": "generated/packages/vite/executors/preview-server.json", + "hidden": false, + "name": "preview-server", + "originalFilePath": "/packages/vite/src/executors/preview-server/schema.json", + "path": "/packages/vite/executors/preview-server", + "type": "executor" } }, "generators": { diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index ac4c0e486dc914..dde3bb14f400e3 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2602,6 +2602,15 @@ "originalFilePath": "/packages/vite/src/executors/test/schema.json", "path": "vite/executors/test", "type": "executor" + }, + { + "description": "Vite preview server", + "file": "generated/packages/vite/executors/preview-server.json", + "hidden": false, + "name": "preview-server", + "originalFilePath": "/packages/vite/src/executors/preview-server/schema.json", + "path": "vite/executors/preview-server", + "type": "executor" } ], "generators": [ diff --git a/docs/generated/packages/vite/executors/preview-server.json b/docs/generated/packages/vite/executors/preview-server.json new file mode 100644 index 00000000000000..079b5d9378060e --- /dev/null +++ b/docs/generated/packages/vite/executors/preview-server.json @@ -0,0 +1,55 @@ +{ + "name": "preview-server", + "implementation": "/packages/vite/src/executors/preview-server/preview-server.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "version": 2, + "cli": "nx", + "title": "Vite Preview Server", + "description": "Preview Server for Vite.", + "type": "object", + "presets": [ + { "name": "Default minimum setup", "keys": ["buildTarget"] }, + { "name": "Using a Different Port", "keys": ["buildTarget", "port"] } + ], + "properties": { + "buildTarget": { + "type": "string", + "description": "Target which builds the application." + }, + "proxyConfig": { + "type": "string", + "description": "Path to the proxy configuration file.", + "x-completion-type": "file" + }, + "port": { "type": "number", "description": "Port to listen on." }, + "host": { + "description": "Specify which IP addresses the server should listen on.", + "oneOf": [{ "type": "boolean" }, { "type": "string" }] + } + }, + "https": { "type": "boolean", "description": "Serve using HTTPS." }, + "open": { + "description": "Automatically open the app in the browser on server start. When the value is a string, it will be used as the URL's pathname.", + "oneOf": [{ "type": "boolean" }, { "type": "string" }] + }, + "logLevel": { + "type": "string", + "description": "Adjust console output verbosity.", + "enum": ["info", "warn", "error", "silent"] + }, + "mode": { "type": "string", "description": "Mode to run the server in." }, + "clearScreen": { + "description": "Set to false to prevent Vite from clearing the terminal screen when logging certain messages.", + "type": "boolean" + }, + "definitions": {}, + "required": ["buildTarget"], + "examplesFile": "`project.json`:\n\n```json\n//...\n\"my-app\": {\n \"targets\": {\n //...\n \"preview\": {\n \"executor\": \"@nrwl/vite:preview-server\",\n \"defaultConfiguration\": \"development\",\n \"options\": {\n \"buildTarget\": \"my-app:build\",\n },\n \"configurations\": {\n ...\n }\n },\n }\n}\n```\n\n```bash\nnx preview my-app\n```\n\n## Examples\n\n{% tabs %}\n{% tab label=\"Set up a custom port\" %}\n\nYou can always set the port in your `vite.config.ts` file. However, you can also set it directly in your `project.json` file, in the `preview` target options:\n\n```json\n//...\n\"my-app\": {\n \"targets\": {\n //...\n \"preview\": {\n \"executor\": \"@nrwl/vite:preview-server\",\n \"defaultConfiguration\": \"development\",\n \"options\": {\n \"buildTarget\": \"my-app:build\",\n \"port\": 4200,\n },\n \"configurations\": {\n ...\n }\n },\n }\n}\n```\n\n{% /tab %}\n{% tab label=\"Specify a proxyConfig\" %}\n\nYou can specify a proxy config by pointing to the path of your proxy configuration file:\n\n```json\n//...\n\"my-app\": {\n \"targets\": {\n //...\n \"preview\": {\n \"executor\": \"@nrwl/vite:preview-server\",\n \"defaultConfiguration\": \"development\",\n \"options\": {\n \"buildTarget\": \"my-app:build\",\n \"proxyConfig\": \"apps/my-app/proxy.conf.json\"\n },\n \"configurations\": {\n ...\n }\n },\n }\n}\n```\n\n{% /tab %}\n\n{% /tabs %}\n" + }, + "description": "Vite preview server", + "aliases": [], + "hidden": false, + "path": "/packages/vite/src/executors/preview-server/schema.json", + "type": "executor" +} diff --git a/packages/vite/docs/preview-server-examples.md b/packages/vite/docs/preview-server-examples.md new file mode 100644 index 00000000000000..023103d4badd67 --- /dev/null +++ b/packages/vite/docs/preview-server-examples.md @@ -0,0 +1,80 @@ +`project.json`: + +```json +//... +"my-app": { + "targets": { + //... + "preview": { + "executor": "@nrwl/vite:preview-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "my-app:build", + }, + "configurations": { + ... + } + }, + } +} +``` + +```bash +nx preview my-app +``` + +## Examples + +{% tabs %} +{% tab label="Set up a custom port" %} + +You can always set the port in your `vite.config.ts` file. However, you can also set it directly in your `project.json` file, in the `preview` target options: + +```json +//... +"my-app": { + "targets": { + //... + "preview": { + "executor": "@nrwl/vite:preview-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "my-app:build", + "port": 4200, + }, + "configurations": { + ... + } + }, + } +} +``` + +{% /tab %} +{% tab label="Specify a proxyConfig" %} + +You can specify a proxy config by pointing to the path of your proxy configuration file: + +```json +//... +"my-app": { + "targets": { + //... + "preview": { + "executor": "@nrwl/vite:preview-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "my-app:build", + "proxyConfig": "apps/my-app/proxy.conf.json" + }, + "configurations": { + ... + } + }, + } +} +``` + +{% /tab %} + +{% /tabs %} diff --git a/packages/vite/executors.json b/packages/vite/executors.json index 17ee34b90f4bf1..20037b8be34a52 100644 --- a/packages/vite/executors.json +++ b/packages/vite/executors.json @@ -14,6 +14,11 @@ "implementation": "./src/executors/test/compat", "schema": "./src/executors/test/schema.json", "description": "Test with Vitest" + }, + "preview-server": { + "implementation": "./src/executors/preview-server/compat", + "schema": "./src/executors/preview-server/schema.json", + "description": "Vite preview server" } }, "executors": { @@ -31,6 +36,11 @@ "implementation": "./src/executors/test/vitest.impl", "schema": "./src/executors/test/schema.json", "description": "Test with Vitest" + }, + "preview-server": { + "implementation": "./src/executors/preview-server/preview-server.impl", + "schema": "./src/executors/preview-server/schema.json", + "description": "Vite preview server" } } } diff --git a/packages/vite/src/executors/dev-server/dev-server.impl.ts b/packages/vite/src/executors/dev-server/dev-server.impl.ts index 82f2a068173f05..a4edcb4c0440cf 100644 --- a/packages/vite/src/executors/dev-server/dev-server.impl.ts +++ b/packages/vite/src/executors/dev-server/dev-server.impl.ts @@ -63,7 +63,7 @@ export default async function* viteDevServerExecutor( } // This Promise intentionally never resolves, leaving the process running - await new Promise<{ success: boolean }>(() => {}); + await new Promise(() => {}); } async function runViteDevServer(server: ViteDevServer): Promise { diff --git a/packages/vite/src/executors/dev-server/schema.d.ts b/packages/vite/src/executors/dev-server/schema.d.ts index d20933266aab94..e2c9df62f7cec9 100644 --- a/packages/vite/src/executors/dev-server/schema.d.ts +++ b/packages/vite/src/executors/dev-server/schema.d.ts @@ -1,4 +1,3 @@ -import type { FileReplacement } from '../../plugins/rollup-replace-files.plugin'; export interface ViteDevServerExecutorOptions { buildTarget: string; proxyConfig?: string; @@ -8,7 +7,7 @@ export interface ViteDevServerExecutorOptions { hmr?: boolean; open?: string | boolean; cors?: boolean; - logLevel?: info | warn | error | silent; + logLevel?: 'info' | 'warn' | 'error' | 'silent'; mode?: string; clearScreen?: boolean; force?: boolean; diff --git a/packages/vite/src/executors/preview-server/compat.ts b/packages/vite/src/executors/preview-server/compat.ts new file mode 100644 index 00000000000000..a7ff6313d660d0 --- /dev/null +++ b/packages/vite/src/executors/preview-server/compat.ts @@ -0,0 +1,4 @@ +import { convertNxExecutor } from '@nrwl/devkit'; +import vitePreviewServerExecutor from './preview-server.impl'; + +export default convertNxExecutor(vitePreviewServerExecutor); diff --git a/packages/vite/src/executors/preview-server/preview-server.impl.ts b/packages/vite/src/executors/preview-server/preview-server.impl.ts new file mode 100644 index 00000000000000..8c50426f78ab71 --- /dev/null +++ b/packages/vite/src/executors/preview-server/preview-server.impl.ts @@ -0,0 +1,88 @@ +import { ExecutorContext, parseTargetString, runExecutor } from '@nrwl/devkit'; +import { InlineConfig, mergeConfig, preview } from 'vite'; +import { + getNxTargetOptions, + getViteSharedConfig, + getViteBuildOptions, + getVitePreviewOptions, +} from '../../utils/options-utils'; +import { ViteBuildExecutorOptions } from '../build/schema'; +import { VitePreviewServerExecutorOptions } from './schema'; + +export default async function* vitePreviewServerExecutor( + options: VitePreviewServerExecutorOptions, + context: ExecutorContext +) { + // Retrieve the option for the configured buildTarget. + const buildTargetOptions: ViteBuildExecutorOptions = getNxTargetOptions( + options.buildTarget, + context + ); + + // Merge the options from the build and preview-serve targets. + // The latter takes precedence. + const mergedOptions = { + ...buildTargetOptions, + ...options, + }; + + // Launch the build target. + const target = parseTargetString(options.buildTarget, context.projectGraph); + const build = await runExecutor(target, mergedOptions, context); + for await (const result of build) { + if (!result.success) { + return result; + } + } + + // Launch the server. + const serverConfig: InlineConfig = mergeConfig( + getViteSharedConfig(mergedOptions, options.clearScreen, context), + { + build: getViteBuildOptions(mergedOptions, context), + preview: getVitePreviewOptions(mergedOptions, context), + } + ); + + if (serverConfig.mode === 'production') { + console.warn('WARNING: preview is not meant to be run in production!'); + } + + try { + const server = await preview(serverConfig); + server.printUrls(); + + const processOnExit = async () => { + const { httpServer } = server; + // closeAllConnections was added in Node v18.2.0 + httpServer.closeAllConnections && httpServer.closeAllConnections(); + httpServer.close(() => { + process.off('SIGINT', processOnExit); + process.off('SIGTERM', processOnExit); + process.off('exit', processOnExit); + }); + }; + + process.on('SIGINT', processOnExit); + process.on('SIGTERM', processOnExit); + process.on('exit', processOnExit); + + const resolvedUrls = [ + ...server.resolvedUrls.local, + ...server.resolvedUrls.network, + ]; + + yield { + success: true, + baseUrl: resolvedUrls[0] ?? '', + }; + } catch (e) { + console.error(e); + yield { + success: false, + baseUrl: '', + }; + } + + await new Promise(() => {}); +} diff --git a/packages/vite/src/executors/preview-server/schema.d.ts b/packages/vite/src/executors/preview-server/schema.d.ts new file mode 100644 index 00000000000000..aa529a39755147 --- /dev/null +++ b/packages/vite/src/executors/preview-server/schema.d.ts @@ -0,0 +1,11 @@ +export interface VitePreviewServerExecutorOptions { + buildTarget: string; + proxyConfig?: string; + port?: number; + host?: string | boolean; + https?: boolean; + open?: string | boolean; + logLevel?: 'info' | 'warn' | 'error' | 'silent'; + mode?: string; + clearScreen?: boolean; +} diff --git a/packages/vite/src/executors/preview-server/schema.json b/packages/vite/src/executors/preview-server/schema.json new file mode 100644 index 00000000000000..0b4d4e120fc363 --- /dev/null +++ b/packages/vite/src/executors/preview-server/schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/schema", + "version": 2, + "cli": "nx", + "title": "Vite Preview Server", + "description": "Preview Server for Vite.", + "type": "object", + "presets": [ + { + "name": "Default minimum setup", + "keys": ["buildTarget"] + }, + { + "name": "Using a Different Port", + "keys": ["buildTarget", "port"] + } + ], + "properties": { + "buildTarget": { + "type": "string", + "description": "Target which builds the application." + }, + "proxyConfig": { + "type": "string", + "description": "Path to the proxy configuration file.", + "x-completion-type": "file" + }, + "port": { + "type": "number", + "description": "Port to listen on." + }, + "host": { + "description": "Specify which IP addresses the server should listen on.", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + } + }, + "https": { + "type": "boolean", + "description": "Serve using HTTPS." + }, + "open": { + "description": "Automatically open the app in the browser on server start. When the value is a string, it will be used as the URL's pathname.", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "logLevel": { + "type": "string", + "description": "Adjust console output verbosity.", + "enum": ["info", "warn", "error", "silent"] + }, + "mode": { + "type": "string", + "description": "Mode to run the server in." + }, + "clearScreen": { + "description": "Set to false to prevent Vite from clearing the terminal screen when logging certain messages.", + "type": "boolean" + }, + "definitions": {}, + "required": ["buildTarget"], + "examplesFile": "../../../docs/preview-server-examples.md" +} diff --git a/packages/vite/src/utils/options-utils.ts b/packages/vite/src/utils/options-utils.ts index c2ad8a159e3438..729161fefe96f7 100644 --- a/packages/vite/src/utils/options-utils.ts +++ b/packages/vite/src/utils/options-utils.ts @@ -11,13 +11,57 @@ import { BuildOptions, InlineConfig, PluginOption, + PreviewOptions, searchForWorkspaceRoot, ServerOptions, } from 'vite'; import { ViteDevServerExecutorOptions } from '../executors/dev-server/schema'; +import { VitePreviewServerExecutorOptions } from '../executors/preview-server/schema'; import replaceFiles from '../../plugins/rollup-replace-files.plugin'; import { ViteBuildExecutorOptions } from '../executors/build/schema'; +/** + * Returns the path to the vite config file or undefined when not found. + */ +export function normalizeViteConfigFilePath( + projectRoot: string, + configFile?: string +): string | undefined { + return configFile && existsSync(joinPathFragments(configFile)) + ? configFile + : existsSync(joinPathFragments(`${projectRoot}/vite.config.ts`)) + ? joinPathFragments(`${projectRoot}/vite.config.ts`) + : existsSync(joinPathFragments(`${projectRoot}/vite.config.js`)) + ? joinPathFragments(`${projectRoot}/vite.config.js`) + : undefined; +} + +/** + * Returns the path to the proxy configuration file or undefined when not found. + */ +export function getViteServerProxyConfigPath( + nxProxyConfig: string | undefined, + context: ExecutorContext +): string | undefined { + if (nxProxyConfig) { + const projectRoot = + context.projectsConfigurations.projects[context.projectName].root; + + const proxyConfigPath = nxProxyConfig + ? join(context.root, nxProxyConfig) + : join(projectRoot, 'proxy.conf.json'); + + if (existsSync(proxyConfigPath)) { + return proxyConfigPath; + } + } +} + +/** + * Builds the shared options for vite. + * + * Most shared options are derived from the build target. + */ export function getViteSharedConfig( options: ViteBuildExecutorOptions, clearScreen: boolean | undefined, @@ -38,19 +82,9 @@ export function getViteSharedConfig( }; } -export function normalizeViteConfigFilePath( - projectRoot: string, - configFile?: string -): string { - return configFile && existsSync(joinPathFragments(configFile)) - ? configFile - : existsSync(joinPathFragments(`${projectRoot}/vite.config.ts`)) - ? joinPathFragments(`${projectRoot}/vite.config.ts`) - : existsSync(joinPathFragments(`${projectRoot}/vite.config.js`)) - ? joinPathFragments(`${projectRoot}/vite.config.js`) - : undefined; -} - +/** + * Builds the options for the vite dev server. + */ export function getViteServerOptions( options: ViteDevServerExecutorOptions, context: ExecutorContext @@ -64,33 +98,29 @@ export function getViteServerOptions( hmr: options.hmr, open: options.open, cors: options.cors, + fs: { + allow: [ + searchForWorkspaceRoot(joinPathFragments(projectRoot)), + joinPathFragments(context.root, 'node_modules/vite'), + ], + }, }; - if (options.proxyConfig) { - const proxyConfigPath = options.proxyConfig - ? join(context.root, options.proxyConfig) - : join(projectRoot, 'proxy.conf.json'); - - if (existsSync(proxyConfigPath)) { - logger.info(`Loading proxy configuration from: ${proxyConfigPath}`); - serverOptions.proxy = require(proxyConfigPath); - serverOptions.fs = { - allow: [ - searchForWorkspaceRoot(joinPathFragments(projectRoot)), - joinPathFragments(context.root, 'node_modules/vite'), - ], - }; - } + const proxyConfigPath = getViteServerProxyConfigPath( + options.proxyConfig, + context + ); + if (proxyConfigPath) { + logger.info(`Loading proxy configuration from: ${proxyConfigPath}`); + serverOptions.proxy = require(proxyConfigPath); } return serverOptions; } -export function getNxTargetOptions(target: string, context: ExecutorContext) { - const targetObj = parseTargetString(target, context.projectGraph); - return readTargetOptions(targetObj, context); -} - +/** + * Builds the build options for the vite. + */ export function getViteBuildOptions( options: ViteBuildExecutorOptions, context: ExecutorContext @@ -114,3 +144,36 @@ export function getViteBuildOptions( ssr: options.ssr, }; } + +/** + * Builds the options for the vite preview server. + */ +export function getVitePreviewOptions( + options: VitePreviewServerExecutorOptions, + context: ExecutorContext +): PreviewOptions { + const projectRoot = + context.projectsConfigurations.projects[context.projectName].root; + const serverOptions: ServerOptions = { + host: options.host, + port: options.port, + https: options.https, + open: options.open, + }; + + const proxyConfigPath = getViteServerProxyConfigPath( + options.proxyConfig, + context + ); + if (proxyConfigPath) { + logger.info(`Loading proxy configuration from: ${proxyConfigPath}`); + serverOptions.proxy = require(proxyConfigPath); + } + + return serverOptions; +} + +export function getNxTargetOptions(target: string, context: ExecutorContext) { + const targetObj = parseTargetString(target, context.projectGraph); + return readTargetOptions(targetObj, context); +}