diff --git a/docs/content/1.getting-started/5.commands.md b/docs/content/1.getting-started/5.commands.md index e9c22519441..709bc2a2d9c 100644 --- a/docs/content/1.getting-started/5.commands.md +++ b/docs/content/1.getting-started/5.commands.md @@ -66,6 +66,16 @@ npm run build Nuxt will create a [`.output`](/docs/directory-structure/output) directory with all your application, server and dependencies ready to be deployed. Check out the [deployment](/docs/deployment) section to learn where and how you can deploy a Nuxt application using Nitro. +## Previewing your production build + +Once you've built your Nuxt application, you can preview it locally: + +```bash +npx nuxi preview +``` + +If you're using a supported preset, this will start a local server (for testing purposes only). + ## Upgrade Nuxt3 version To upgrade Nuxt3 version: diff --git a/packages/nitro/src/build.ts b/packages/nitro/src/build.ts index be13632507c..c2b3cad51fa 100644 --- a/packages/nitro/src/build.ts +++ b/packages/nitro/src/build.ts @@ -4,7 +4,7 @@ import * as rollup from 'rollup' import fse from 'fs-extra' import { printFSTree } from './utils/tree' import { getRollupConfig } from './rollup/config' -import { hl, prettyPath, serializeTemplate, writeFile, isDirectory, readDirRecursively } from './utils' +import { hl, prettyPath, serializeTemplate, writeFile, isDirectory, readDirRecursively, replaceAll } from './utils' import { NitroContext } from './context' import { scanMiddleware } from './server/middleware' @@ -107,10 +107,35 @@ async function _build (nitroContext: NitroContext) { consola.start('Writing server bundle...') await build.write(nitroContext.rollupConfig.output) + const rewriteBuildPaths = (input: unknown, to: string) => + typeof input === 'string' ? replaceAll(input, nitroContext.output.dir, to) : undefined + + // Write build info + const nitroConfigPath = resolve(nitroContext.output.dir, 'nitro.json') + const buildInfo = { + date: new Date(), + preset: nitroContext.preset, + commands: { + preview: rewriteBuildPaths(nitroContext.commands.preview, '.'), + deploy: rewriteBuildPaths(nitroContext.commands.deploy, '.') + } + } + await writeFile(nitroConfigPath, JSON.stringify(buildInfo, null, 2)) + consola.success('Server built') await printFSTree(nitroContext.output.serverDir) await nitroContext._internal.hooks.callHook('nitro:compiled', nitroContext) + // Show deploy and preview hints + const rOutDir = relative(process.cwd(), nitroContext.output.dir) + if (nitroContext.commands.preview) { + // consola.info(`You can preview this build using \`${rewriteBuildPaths(nitroContext.commands.preview, rOutDir)}\``) + consola.info('You can preview this build using `nuxi preview`') + } + if (nitroContext.commands.deploy) { + consola.info(`You can deploy this build using \`${rewriteBuildPaths(nitroContext.commands.deploy, rOutDir)}\``) + } + return { entry: resolve(nitroContext.rollupConfig.output.dir, nitroContext.rollupConfig.output.entryFileNames as string) } diff --git a/packages/nitro/src/context.ts b/packages/nitro/src/context.ts index 7d82b5d1f2c..ff062057d42 100644 --- a/packages/nitro/src/context.ts +++ b/packages/nitro/src/context.ts @@ -5,7 +5,7 @@ import { createHooks, Hookable, NestedHooks } from 'hookable' import type { Preset } from 'unenv' import type { NuxtHooks, NuxtOptions } from '@nuxt/schema' import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer' -import { tryImport, resolvePath, detectTarget, extendPreset } from './utils' +import { tryImport, resolvePath, detectTarget, extendPreset, evalTemplate } from './utils' import * as PRESETS from './presets' import type { NodeExternalsOptions } from './rollup/plugins/externals' import type { StorageOptions } from './rollup/plugins/storage' @@ -40,6 +40,10 @@ export interface NitroContext { experiments?: { wasm?: boolean } + commands: { + preview: string | ((config: NitroContext) => string) + deploy: string | ((config: NitroContext) => string) + }, moduleSideEffects: string[] renderer: string serveStatic: boolean @@ -104,6 +108,10 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N moduleSideEffects: ['unenv/runtime/polyfill/'], renderer: undefined, serveStatic: undefined, + commands: { + preview: undefined, + deploy: undefined + }, middleware: [], scannedMiddleware: [], ignore: [], @@ -164,6 +172,13 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N nitroContext.output.publicDir = resolvePath(nitroContext, nitroContext.output.publicDir) nitroContext.output.serverDir = resolvePath(nitroContext, nitroContext.output.serverDir) + if (nitroContext.commands.preview) { + nitroContext.commands.preview = evalTemplate(nitroContext, nitroContext.commands.preview) + } + if (nitroContext.commands.deploy) { + nitroContext.commands.deploy = evalTemplate(nitroContext, nitroContext.commands.deploy) + } + nitroContext._internal.hooks.addHooks(nitroContext.hooks) // Dev-only storage diff --git a/packages/nitro/src/presets/azure.ts b/packages/nitro/src/presets/azure.ts index 150b7e938ec..5d7774b776e 100644 --- a/packages/nitro/src/presets/azure.ts +++ b/packages/nitro/src/presets/azure.ts @@ -1,8 +1,7 @@ -import consola from 'consola' import fse from 'fs-extra' import globby from 'globby' import { join, resolve } from 'pathe' -import { hl, prettyPath, writeFile } from '../utils' +import { writeFile } from '../utils' import { NitroPreset, NitroContext } from '../context' export const azure: NitroPreset = { @@ -11,6 +10,9 @@ export const azure: NitroPreset = { output: { serverDir: '{{ output.dir }}/server/functions' }, + commands: { + preview: 'npx @azure/static-web-apps-cli start {{ output.publicDir }} --api-location {{ output.serverDir }}/..' + }, hooks: { async 'nitro:compiled' (ctx: NitroContext) { await writeRoutes(ctx) @@ -101,8 +103,4 @@ async function writeRoutes ({ output }: NitroContext) { if (!indexFileExists) { await writeFile(indexPath, '') } - - const apiDir = resolve(output.serverDir, '..') - - consola.success('Ready to run', hl('npx @azure/static-web-apps-cli start ' + prettyPath(output.publicDir) + ' --api-location ' + prettyPath(apiDir)), 'for local testing') } diff --git a/packages/nitro/src/presets/azure_functions.ts b/packages/nitro/src/presets/azure_functions.ts index 104b903c485..30d67fec16c 100644 --- a/packages/nitro/src/presets/azure_functions.ts +++ b/packages/nitro/src/presets/azure_functions.ts @@ -1,8 +1,7 @@ import { createWriteStream } from 'fs' import archiver from 'archiver' -import consola from 'consola' import { join, resolve } from 'pathe' -import { prettyPath, writeFile } from '../utils' +import { writeFile } from '../utils' import { NitroPreset, NitroContext } from '../context' // eslint-disable-next-line @@ -10,6 +9,9 @@ export const azure_functions: NitroPreset = { serveStatic: true, entry: '{{ _internal.runtimeDir }}/entries/azure_functions', externals: true, + commands: { + deploy: 'az functionapp deployment source config-zip -g -n --src {{ output.dir }}/deploy.zip' + }, hooks: { async 'nitro:compiled' (ctx: NitroContext) { await writeRoutes(ctx) @@ -67,9 +69,5 @@ async function writeRoutes ({ output: { dir, serverDir } }: NitroContext) { await writeFile(resolve(serverDir, 'function.json'), JSON.stringify(functionDefinition)) await writeFile(resolve(dir, 'host.json'), JSON.stringify(host)) - await zipDirectory(dir, join(dir, 'deploy.zip')) - const zipPath = prettyPath(resolve(dir, 'deploy.zip')) - - consola.success(`Ready to run \`az functionapp deployment source config-zip -g -n --src ${zipPath}\``) } diff --git a/packages/nitro/src/presets/cloudflare.ts b/packages/nitro/src/presets/cloudflare.ts index 093b7cdb000..f8a4405a36d 100644 --- a/packages/nitro/src/presets/cloudflare.ts +++ b/packages/nitro/src/presets/cloudflare.ts @@ -1,6 +1,5 @@ import { resolve } from 'pathe' -import consola from 'consola' -import { extendPreset, writeFile, prettyPath, hl } from '../utils' +import { extendPreset, writeFile } from '../utils' import { NitroContext, NitroPreset } from '../context' import { worker } from './worker' @@ -9,15 +8,14 @@ export const cloudflare: NitroPreset = extendPreset(worker, { ignore: [ 'wrangler.toml' ], + commands: { + preview: 'npx miniflare {{ output.serverDir }}/index.mjs --site {{ output.publicDir }}', + deploy: 'cd {{ output.serverDir }} && npx wrangler publish' + }, hooks: { async 'nitro:compiled' ({ output, _nuxt }: NitroContext) { await writeFile(resolve(output.dir, 'package.json'), JSON.stringify({ private: true, main: './server/index.mjs' }, null, 2)) await writeFile(resolve(output.dir, 'package-lock.json'), JSON.stringify({ lockfileVersion: 1 }, null, 2)) - let inDir = prettyPath(_nuxt.rootDir) - if (inDir) { - inDir = 'in ' + inDir - } - consola.success('Ready to run', hl('npx wrangler publish ' + inDir), 'or', hl('npx miniflare ' + prettyPath(output.serverDir) + '/index.mjs --site ' + prettyPath(output.publicDir)), 'for local testing') } } }) diff --git a/packages/nitro/src/presets/firebase.ts b/packages/nitro/src/presets/firebase.ts index 4049732179e..8acaadd7e08 100644 --- a/packages/nitro/src/presets/firebase.ts +++ b/packages/nitro/src/presets/firebase.ts @@ -1,7 +1,6 @@ import { createRequire } from 'module' import { join, relative, resolve } from 'pathe' import fse from 'fs-extra' -import consola from 'consola' import globby from 'globby' import { readPackageJSON } from 'pkg-types' import { writeFile } from '../utils' @@ -10,6 +9,9 @@ import { NitroPreset, NitroContext } from '../context' export const firebase: NitroPreset = { entry: '{{ _internal.runtimeDir }}/entries/firebase', externals: true, + commands: { + deploy: 'npx firebase deploy' + }, hooks: { async 'nitro:compiled' (ctx: NitroContext) { await writeRoutes(ctx) @@ -85,6 +87,4 @@ async function writeRoutes ({ output: { publicDir, serverDir }, _nuxt: { rootDir 2 ) ) - - consola.success('Ready to run `firebase deploy`') } diff --git a/packages/nitro/src/presets/server.ts b/packages/nitro/src/presets/server.ts index 474c3e4641a..86ea46cdeb6 100644 --- a/packages/nitro/src/presets/server.ts +++ b/packages/nitro/src/presets/server.ts @@ -1,14 +1,11 @@ -import consola from 'consola' -import { extendPreset, hl, prettyPath } from '../utils' -import { NitroPreset, NitroContext } from '../context' +import { extendPreset } from '../utils' +import { NitroPreset } from '../context' import { node } from './node' export const server: NitroPreset = extendPreset(node, { entry: '{{ _internal.runtimeDir }}/entries/server', serveStatic: true, - hooks: { - 'nitro:compiled' ({ output }: NitroContext) { - consola.success('Ready to run', hl('node ' + prettyPath(output.serverDir) + '/index.mjs')) - } + commands: { + preview: 'node {{ output.serverDir }}/index.mjs' } }) diff --git a/packages/nitro/src/utils/index.ts b/packages/nitro/src/utils/index.ts index bd75ba5eb65..3326f936aac 100644 --- a/packages/nitro/src/utils/index.ts +++ b/packages/nitro/src/utils/index.ts @@ -51,18 +51,22 @@ export async function writeFile (file: string, contents: string, log = false) { } } -export function resolvePath (nitroContext: NitroInput, path: string | ((nitroContext: NitroInput) => string), resolveBase: string = ''): string { - if (typeof path === 'function') { - path = path(nitroContext) +export function evalTemplate (ctx, input: string | ((ctx) => string)): string { + if (typeof input === 'function') { + input = input(ctx) } - - if (typeof path !== 'string') { - throw new TypeError('Invalid path: ' + path) + if (typeof input !== 'string') { + throw new TypeError('Invalid template: ' + input) } + return compileTemplate(input)(ctx) +} - path = compileTemplate(path)(nitroContext) +export function resolvePath (nitroContext: NitroInput, input: string | ((nitroContext: NitroInput) => string), resolveBase: string = ''): string { + return resolve(resolveBase, evalTemplate(nitroContext, input)) +} - return resolve(resolveBase, path) +export function replaceAll (input: string, from: string, to: string) { + return input.replace(new RegExp(from, 'g'), to) } export function detectTarget () { diff --git a/packages/nuxi/src/commands/index.ts b/packages/nuxi/src/commands/index.ts index e232af14141..0d8e3987fe5 100644 --- a/packages/nuxi/src/commands/index.ts +++ b/packages/nuxi/src/commands/index.ts @@ -5,6 +5,8 @@ const _rDefault = r => r.default || r export const commands = { dev: () => import('./dev').then(_rDefault), build: () => import('./build').then(_rDefault), + preview: () => import('./preview').then(_rDefault), + start: () => import('./preview').then(_rDefault), analyze: () => import('./analyze').then(_rDefault), generate: () => import('./generate').then(_rDefault), prepare: () => import('./prepare').then(_rDefault), diff --git a/packages/nuxi/src/commands/preview.ts b/packages/nuxi/src/commands/preview.ts new file mode 100644 index 00000000000..8d55d334e4f --- /dev/null +++ b/packages/nuxi/src/commands/preview.ts @@ -0,0 +1,42 @@ +import { existsSync, promises as fsp } from 'fs' +import { dirname, relative } from 'path' +import { execa } from 'execa' +import { resolve } from 'pathe' +import consola from 'consola' + +import { defineNuxtCommand } from './index' + +export default defineNuxtCommand({ + meta: { + name: 'preview', + usage: 'npx nuxi preview|start [rootDir]', + description: 'Launches nitro server for local testing after `nuxi build`.' + }, + async invoke (args) { + process.env.NODE_ENV = process.env.NODE_ENV || 'production' + const rootDir = resolve(args._[0] || '.') + + const nitroJSONPaths = ['.output/nitro.json', 'nitro.json'].map(p => resolve(rootDir, p)) + const nitroJSONPath = nitroJSONPaths.find(p => existsSync(p)) + if (!nitroJSONPath) { + consola.error('Cannot find `nitro.json`. Did you run `nuxi build` first? Search path:\n', nitroJSONPaths) + process.exit(1) + } + const outputPath = dirname(nitroJSONPath) + const nitroJSON = JSON.parse(await fsp.readFile(nitroJSONPath, 'utf-8')) + + consola.info('Node.js version:', process.versions.node) + consola.info('Preset:', nitroJSON.preset) + consola.info('Working dir:', relative(process.cwd(), outputPath)) + + if (!nitroJSON.commands.preview) { + consola.error('Preview is not supported for this build.') + process.exit(1) + } + + consola.info('Starting preview command:', nitroJSON.commands.preview) + const [command, ...commandArgs] = nitroJSON.commands.preview.split(' ') + consola.log('') + await execa(command, commandArgs, { stdio: 'inherit', cwd: outputPath }) + } +})