feat(nitro, vite): use native module (#252)

Co-authored-by: Daniel Roe <daniel@roe.dev>
This commit is contained in:
pooya parsa 2021-07-15 11:38:06 +02:00 committed by GitHub
parent 569d4f3cb3
commit 6318438415
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 608 additions and 1814 deletions

View File

@ -43,7 +43,4 @@ jobs:
run: yarn build run: yarn build
- name: Test - name: Test
run: TEST_COMPAT=1 yarn jest --ci run: TEST_COMPAT=1 yarn test:presets
# - name: Coverage
# uses: codecov/codecov-action@v1

View File

@ -44,10 +44,7 @@ jobs:
run: yarn build run: yarn build
- name: Test - name: Test
run: yarn jest --ci run: yarn test:presets
# - name: Coverage
# uses: codecov/codecov-action@v1
- name: Release Edge - name: Release Edge
if: github.event_name == 'push' if: github.event_name == 'push'

View File

@ -2,9 +2,9 @@
> How to deploy Nuxt to a Node.js host with Nuxt Nitro > How to deploy Nuxt to a Node.js host with Nuxt Nitro
- Support for ultra-minimal SSR build - Support for ultra-minimal SSR build
- Zero millisecond cold start - Zero millisecond cold start
- More configuration required - More configuration required
## Setup ## Setup
@ -28,7 +28,7 @@ This `.output` folder can be deployed to your Node.js host and the server can be
To start the server in production mode, run: To start the server in production mode, run:
```bash ```bash
node .output/server node .output/server/index.mjs
``` ```
For example, using `pm2`: For example, using `pm2`:

View File

@ -1,8 +1,8 @@
# Node.js server # Node.js server
- Default preset if none is specified or auto-detected - Default preset if none is specified or auto-detected
- Loads only the chunks required to render the request for optimal cold start timing - Loads only the chunks required to render the request for optimal cold start timing
- Useful for debugging - Useful for debugging
### Entrypoint ### Entrypoint
@ -11,7 +11,7 @@ With `{ preset: 'server' }` the result will be an entrypoint that launches a rea
#### Example #### Example
```bash ```bash
node .output/server node .output/server/index.mjs
# > Load chunks/nitro/server (10.405923ms) # > Load chunks/nitro/server (10.405923ms)
# > Cold Start (26.289817ms) # > Cold Start (26.289817ms)
# Listening on http://localhost:3000 # Listening on http://localhost:3000

View File

@ -7,6 +7,6 @@
"scripts": { "scripts": {
"dev": "nu dev", "dev": "nu dev",
"build": "nu build", "build": "nu build",
"start": "node .output/server" "start": "node .output/server/index.mjs"
} }
} }

View File

@ -7,6 +7,6 @@
"scripts": { "scripts": {
"dev": "nu dev", "dev": "nu dev",
"build": "nu build", "build": "nu build",
"start": "node .output/server" "start": "node .output/server/index.mjs"
} }
} }

View File

@ -7,6 +7,6 @@
"scripts": { "scripts": {
"dev": "nu dev", "dev": "nu dev",
"build": "nu build", "build": "nu build",
"start": "node .output/server" "start": "node .output/server/index.mjs"
} }
} }

View File

@ -7,6 +7,6 @@
"scripts": { "scripts": {
"dev": "nu dev", "dev": "nu dev",
"build": "nu build", "build": "nu build",
"start": "node .output/server" "start": "node .output/server/index.mjs"
} }
} }

View File

@ -7,6 +7,6 @@
"scripts": { "scripts": {
"dev": "nu dev", "dev": "nu dev",
"build": "nu build", "build": "nu build",
"start": "node .output/server" "start": "node .output/server/index.mjs"
} }
} }

View File

@ -7,6 +7,6 @@
"scripts": { "scripts": {
"dev": "nu dev", "dev": "nu dev",
"build": "nu build", "build": "nu build",
"start": "node .output/server" "start": "node .output/server/index.mjs"
} }
} }

View File

@ -7,6 +7,6 @@
"scripts": { "scripts": {
"dev": "nu dev", "dev": "nu dev",
"build": "nu build", "build": "nu build",
"start": "node .output/server" "start": "node .output/server/index.mjs"
} }
} }

View File

@ -8,6 +8,6 @@
"scripts": { "scripts": {
"dev": "nu dev", "dev": "nu dev",
"build": "nu build", "build": "nu build",
"start": "node .output/server" "start": "node .output/server/index.mjs"
} }
} }

View File

@ -1,9 +0,0 @@
module.exports = {
testEnvironment: 'node',
transform: {
'\\.[jt]sx?$': './scripts/jest-transform.mjs'
},
testPathIgnorePatterns: [
'.output/.*'
]
}

View File

@ -16,8 +16,9 @@
"example": "yarn workspace example-$0 dev", "example": "yarn workspace example-$0 dev",
"example:build": "yarn workspace example-$0 build", "example:build": "yarn workspace example-$0 build",
"lint": "eslint --ext .vue,.ts,.js .", "lint": "eslint --ext .vue,.ts,.js .",
"test": "yarn lint && jest", "test": "yarn lint && yarn test:presets",
"test:compat": "TEST_COMPAT=1 jest", "test:presets": "mocha test/presets/*.mjs",
"test:compat": "TEST_COMPAT=1 yarn test:presets",
"version": "yarn && git add yarn.lock" "version": "yarn && git add yarn.lock"
}, },
"resolutions": { "resolutions": {
@ -26,17 +27,23 @@
"devDependencies": { "devDependencies": {
"@nuxtjs/eslint-config": "^6.0.1", "@nuxtjs/eslint-config": "^6.0.1",
"@nuxtjs/eslint-config-typescript": "^6.0.1", "@nuxtjs/eslint-config-typescript": "^6.0.1",
"@types/jest": "^26.0.24", "@types/chai": "^4.2.21",
"@types/jsdom": "^16",
"@types/mocha": "^8.2.3",
"@types/node": "^14.17.5", "@types/node": "^14.17.5",
"@types/object-hash": "^2", "@types/object-hash": "^2",
"chai": "^4.3.4",
"esbuild": "^0.12.15", "esbuild": "^0.12.15",
"eslint": "^7.30.0", "eslint": "^7.30.0",
"eslint-plugin-jsdoc": "^35.4.3", "eslint-plugin-jsdoc": "^35.4.3",
"execa": "^5.1.1",
"globby": "^11.0.4", "globby": "^11.0.4",
"jest": "^27.0.6",
"jiti": "^1.10.1", "jiti": "^1.10.1",
"jsdom": "^16.6.0",
"lerna": "^4.0.0", "lerna": "^4.0.0",
"mocha": "^9.0.2",
"object-hash": "^2.2.0", "object-hash": "^2.2.0",
"ts-mocha": "^8.0.0",
"typescript": "^4.3.5", "typescript": "^4.3.5",
"unbuild": "^0.3.2", "unbuild": "^0.3.2",
"upath": "^2.0.1" "upath": "^2.0.1"

View File

@ -1,10 +1,8 @@
import head from '#app/plugins/head'
import preload from '#app/plugins/preload.server' import preload from '#app/plugins/preload.server'
<%= utils.importSources(app.plugins.map(p => p.src)) %> <%= utils.importSources(app.plugins.map(p => p.src)) %>
const commonPlugins = [ const commonPlugins = [
head,
<%= app.plugins.filter(p => !p.mode || p.mode === 'all').map(p => utils.importName(p.src)).join(',\n ') %> <%= app.plugins.filter(p => !p.mode || p.mode === 'all').map(p => utils.importName(p.src)).join(',\n ') %>
] ]

View File

@ -47,10 +47,10 @@ export async function build (nitroContext: NitroContext) {
// Compile html template // Compile html template
const htmlSrc = resolve(nitroContext._nuxt.buildDir, `views/${{ 2: 'app', 3: 'document' }[2]}.template.html`) const htmlSrc = resolve(nitroContext._nuxt.buildDir, `views/${{ 2: 'app', 3: 'document' }[2]}.template.html`)
const htmlTemplate = { src: htmlSrc, contents: '', dst: '', compiled: '' } const htmlTemplate = { src: htmlSrc, contents: '', dst: '', compiled: '' }
htmlTemplate.dst = htmlTemplate.src.replace(/.html$/, '.js').replace('app.', 'document.') htmlTemplate.dst = htmlTemplate.src.replace(/.html$/, '.mjs').replace('app.', 'document.')
htmlTemplate.contents = await readFile(htmlTemplate.src, 'utf-8') htmlTemplate.contents = await readFile(htmlTemplate.src, 'utf-8')
await nitroContext._internal.hooks.callHook('nitro:document', htmlTemplate) await nitroContext._internal.hooks.callHook('nitro:document', htmlTemplate)
htmlTemplate.compiled = 'module.exports = ' + serializeTemplate(htmlTemplate.contents) htmlTemplate.compiled = 'export default ' + serializeTemplate(htmlTemplate.contents)
await writeFile(htmlTemplate.dst, htmlTemplate.compiled) await writeFile(htmlTemplate.dst, htmlTemplate.compiled)
nitroContext.rollupConfig = getRollupConfig(nitroContext) nitroContext.rollupConfig = getRollupConfig(nitroContext)

View File

@ -1,6 +1,6 @@
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { resolve } from 'upath' import { resolve } from 'upath'
import { resolveModule } from '@nuxt/kit' import { readFile, writeFile } from 'fs-extra'
import { build, generate, prepare } from './build' import { build, generate, prepare } from './build'
import { getNitroContext, NitroContext } from './context' import { getNitroContext, NitroContext } from './context'
import { createDevServer } from './server/dev' import { createDevServer } from './server/dev'
@ -55,16 +55,25 @@ export default function nuxt2CompatModule () {
// Nitro client plugin // Nitro client plugin
this.addPlugin({ this.addPlugin({
fileName: 'nitro.client.js', fileName: 'nitro.client.mjs',
src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.js') src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.mjs')
}) })
// Fix module resolution // Fix module resolution
nuxt.hook('webpack:config', (configs) => { nuxt.hook('webpack:config', (configs) => {
for (const config of configs) { for (const config of configs) {
if (config.name === 'client') { config.resolve.alias.ufo = 'ufo/dist/index.mjs'
config.resolve.alias.ufo = resolveModule('ufo/dist/index.mjs') config.resolve.alias.ohmyfetch = 'ohmyfetch/dist/index.mjs'
} }
})
// Generate mjs resources
nuxt.hook('build:compiled', async ({ name }) => {
if (name === 'server') {
await writeFile(resolve(nuxt.options.buildDir, 'dist/server/server.mjs'), 'export { default } from "./server.js"', 'utf8')
} else if (name === 'client') {
const manifest = await readFile(resolve(nuxt.options.buildDir, 'dist/server/client.manifest.json'), 'utf8')
await writeFile(resolve(nuxt.options.buildDir, 'dist/server/client.manifest.mjs'), 'export default ' + manifest, 'utf8')
} }
}) })

View File

@ -22,7 +22,7 @@ if ('serviceWorker' in navigator) {
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="prefetch" href="${routerBase}sw.js"> <link rel="prefetch" href="${routerBase}sw.js">
<link rel="prefetch" href="${routerBase}_server/index.js"> <link rel="prefetch" href="${routerBase}_server/index.mjs">
<script> <script>
async function register () { async function register () {
const registration = await navigator.serviceWorker.register('${routerBase}sw.js') const registration = await navigator.serviceWorker.register('${routerBase}sw.js')
@ -65,7 +65,7 @@ if ('serviceWorker' in navigator) {
tmpl.compiled = tmpl.compiled.replace('</body>', script + '</body>') tmpl.compiled = tmpl.compiled.replace('</body>', script + '</body>')
}, },
async 'nitro:compiled' ({ output }: NitroContext) { async 'nitro:compiled' ({ output }: NitroContext) {
await writeFile(resolve(output.publicDir, 'sw.js'), `self.importScripts('${input._nuxt.routerBase}_server/index.js');`) await writeFile(resolve(output.publicDir, 'sw.js'), `self.importScripts('${input._nuxt.routerBase}_server/index.mjs');`)
// Temp fix // Temp fix
await writeFile(resolve(output.publicDir, 'index.html'), html) await writeFile(resolve(output.publicDir, 'index.html'), html)

View File

@ -11,7 +11,7 @@ export const cloudflare: NitroPreset = extendPreset(worker, {
], ],
hooks: { hooks: {
async 'nitro:compiled' ({ output, _nuxt }: NitroContext) { async 'nitro:compiled' ({ output, _nuxt }: NitroContext) {
await writeFile(resolve(output.dir, 'package.json'), JSON.stringify({ private: true, main: './server/index.js' }, null, 2)) 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)) await writeFile(resolve(output.dir, 'package-lock.json'), JSON.stringify({ lockfileVersion: 1 }, null, 2))
let inDir = prettyPath(_nuxt.rootDir) let inDir = prettyPath(_nuxt.rootDir)
if (inDir) { if (inDir) {

View File

@ -8,7 +8,7 @@ export const server: NitroPreset = extendPreset(node, {
serveStatic: true, serveStatic: true,
hooks: { hooks: {
'nitro:compiled' ({ output }: NitroContext) { 'nitro:compiled' ({ output }: NitroContext) {
consola.success('Ready to run', hl('node ' + prettyPath(output.serverDir))) consola.success('Ready to run', hl('node ' + prettyPath(output.serverDir) + '/index.mjs'))
} }
} }
}) })

View File

@ -60,7 +60,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
delete env.alias['node-fetch'] // FIX ME delete env.alias['node-fetch'] // FIX ME
if (nitroContext.sourceMap) { if (nitroContext.sourceMap) {
env.polyfill.push('source-map-support/register') env.polyfill.push('source-map-support/register.js')
} }
const buildServerDir = join(nitroContext._nuxt.buildDir, 'dist/server') const buildServerDir = join(nitroContext._nuxt.buildDir, 'dist/server')
@ -70,7 +70,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
input: resolvePath(nitroContext, nitroContext.entry), input: resolvePath(nitroContext, nitroContext.entry),
output: { output: {
dir: nitroContext.output.serverDir, dir: nitroContext.output.serverDir,
entryFileNames: 'index.js', entryFileNames: 'index.mjs',
chunkFileNames (chunkInfo) { chunkFileNames (chunkInfo) {
let prefix = '' let prefix = ''
const modules = Object.keys(chunkInfo.modules) const modules = Object.keys(chunkInfo.modules)
@ -88,10 +88,10 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
} else if (lastModule.includes('assets')) { } else if (lastModule.includes('assets')) {
prefix = 'assets' prefix = 'assets'
} }
return join('chunks', prefix, '[name].js') return join('chunks', prefix, '[name].mjs')
}, },
inlineDynamicImports: nitroContext.inlineDynamicImports, inlineDynamicImports: nitroContext.inlineDynamicImports,
format: 'cjs', format: 'esm',
exports: 'auto', exports: 'auto',
intro: '', intro: '',
outro: '', outro: '',
@ -232,6 +232,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
'~~', '~~',
'@@/', '@@/',
'virtual:', 'virtual:',
'ohmyfetch', // TODO: Webpack externals forces default import!
nitroContext._internal.runtimeDir, nitroContext._internal.runtimeDir,
nitroContext._nuxt.srcDir, nitroContext._nuxt.srcDir,
nitroContext._nuxt.rootDir, nitroContext._nuxt.rootDir,
@ -241,7 +242,8 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
], ],
traceOptions: { traceOptions: {
base: '/', base: '/',
processCwd: nitroContext._nuxt.rootDir processCwd: nitroContext._nuxt.rootDir,
exportsOnly: true
} }
}))) })))
} }
@ -252,7 +254,13 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
preferBuiltins: true, preferBuiltins: true,
rootDir: nitroContext._nuxt.rootDir, rootDir: nitroContext._nuxt.rootDir,
moduleDirectories, moduleDirectories,
mainFields: ['main'] // Force resolve CJS (@vue/runtime-core ssrUtils) // 'module' is intentionally not supported because of externals
mainFields: ['main'],
exportConditions: [
'default',
'module',
'import'
]
})) }))
// Automatically mock unresolved externals // Automatically mock unresolved externals

View File

@ -1,5 +1,4 @@
import consola from 'consola' import consola from 'consola'
import { normalize } from 'upath'
const internalRegex = /^\.|\?|\.[mc]?js$|.ts$|.json$/ const internalRegex = /^\.|\?|\.[mc]?js$|.ts$|.json$/
@ -10,7 +9,7 @@ export function autoMock () {
if (src && !internalRegex.test(src)) { if (src && !internalRegex.test(src)) {
consola.warn('Auto mock external ', src) consola.warn('Auto mock external ', src)
return { return {
id: normalize(require.resolve('unenv/runtime/mock/proxy')) id: 'unenv/runtime/mock/proxy'
} }
} }
return null return null

View File

@ -103,16 +103,20 @@ export default function dynamicRequire(id) {
function TMPL_LAZY ({ chunks }: TemplateContext) { function TMPL_LAZY ({ chunks }: TemplateContext) {
return ` return `
function dynamicWebpackModule(id, getChunk) { function dynamicWebpackModule(id, getChunk, ids) {
return function (module, exports, require) { return function (module, exports, require) {
const r = getChunk() const r = getChunk()
if (r instanceof Promise) { if (typeof r.then === 'function') {
module.exports = r.then(r => { module.exports = r.then(r => {
const realModule = { exports: {}, require }; const realModule = { exports: {}, require };
r.modules[id](realModule, realModule.exports, realModule.require); r.modules[id](realModule, realModule.exports, realModule.require);
for (const _id of ids) {
if (_id === id) continue;
r.modules[_id](realModule, realModule.exports, realModule.require);
}
return realModule.exports; return realModule.exports;
}); });
} else { } else if (r && typeof r.modules[id] === 'function') {
r.modules[id](module, exports, require); r.modules[id](module, exports, require);
} }
}; };
@ -121,7 +125,7 @@ function dynamicWebpackModule(id, getChunk) {
function webpackChunk (meta, getChunk) { function webpackChunk (meta, getChunk) {
const chunk = { ...meta, modules: {} }; const chunk = { ...meta, modules: {} };
for (const id of meta.moduleIds) { for (const id of meta.moduleIds) {
chunk.modules[id] = dynamicWebpackModule(id, getChunk); chunk.modules[id] = dynamicWebpackModule(id, getChunk, meta.moduleIds);
}; };
return chunk; return chunk;
}; };

View File

@ -1,4 +1,4 @@
import { resolve, dirname, normalize } from 'upath' import { resolve, dirname } from 'upath'
import { copyFile, mkdirp } from 'fs-extra' import { copyFile, mkdirp } from 'fs-extra'
import { nodeFileTrace, NodeFileTraceOptions } from '@vercel/nft' import { nodeFileTrace, NodeFileTraceOptions } from '@vercel/nft'
import type { Plugin } from 'rollup' import type { Plugin } from 'rollup'
@ -13,11 +13,11 @@ export interface NodeExternalsOptions {
} }
export function externals (opts: NodeExternalsOptions): Plugin { export function externals (opts: NodeExternalsOptions): Plugin {
const resolvedExternals = new Set<string>() const trackedExternals = new Set<string>()
return { return {
name: 'node-externals', name: 'node-externals',
resolveId (id) { async resolveId (id, importer, options) {
// Internals // Internals
if (!id || id.startsWith('\x00') || id.includes('?') || id.startsWith('#')) { if (!id || id.startsWith('\x00') || id.includes('?') || id.startsWith('#')) {
return null return null
@ -44,11 +44,10 @@ export function externals (opts: NodeExternalsOptions): Plugin {
} }
} }
// Try to resolve for nft // Track externals
if (opts.trace !== false) { if (opts.trace !== false) {
let _resolvedId = _id const resolved = await this.resolve(id, importer, { ...options, skipSelf: true }).then(r => r.id)
try { _resolvedId = normalize(require.resolve(_resolvedId, { paths: opts.moduleDirectories })) } catch (_err) { } trackedExternals.add(resolved)
resolvedExternals.add(_resolvedId)
} }
return { return {
@ -58,14 +57,25 @@ export function externals (opts: NodeExternalsOptions): Plugin {
}, },
async buildEnd () { async buildEnd () {
if (opts.trace !== false) { if (opts.trace !== false) {
const tracedFiles = await nodeFileTrace(Array.from(resolvedExternals), opts.traceOptions) const tracedFiles = await nodeFileTrace(Array.from(trackedExternals), opts.traceOptions)
.then(r => r.fileList.map(f => resolve(opts.traceOptions.base, f))) .then(r => r.fileList.map(f => resolve(opts.traceOptions.base, f)))
.then(r => r.filter(file => file.includes('node_modules')))
// // Find all unique package names
const pkgs = new Set<string>()
for (const file of tracedFiles) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, baseDir, pkgName, _importPath] = /(.+\/node_modules\/)([^/]+)\/(.*)/.exec(file)
pkgs.add(resolve(baseDir, pkgName, 'package.json'))
}
for (const pkg of pkgs) {
if (!tracedFiles.includes(pkg)) {
tracedFiles.push(pkg)
}
}
await Promise.all(tracedFiles.map(async (file) => { await Promise.all(tracedFiles.map(async (file) => {
if (!file.includes('node_modules')) {
return
}
// TODO: Minify package.json
const src = resolve(opts.traceOptions.base, file) const src = resolve(opts.traceOptions.base, file)
const dst = resolve(opts.outDir, 'node_modules', file.split('node_modules/').pop()) const dst = resolve(opts.outDir, 'node_modules', file.split('node_modules/').pop())
await mkdirp(dirname(dst)) await mkdirp(dirname(dst))

View File

@ -32,8 +32,12 @@ export function staticAssets (context: NitroContext) {
'#static': ` '#static': `
import { promises } from 'fs' import { promises } from 'fs'
import { resolve } from 'path' import { resolve } from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import assets from '#static-assets' import assets from '#static-assets'
const mainDir = dirname(fileURLToPath(globalThis.entryURL))
export function readAsset (id) { export function readAsset (id) {
return promises.readFile(resolve(mainDir, getAsset(id).path)) return promises.readFile(resolve(mainDir, getAsset(id).path))
} }
@ -50,7 +54,7 @@ export function dirnames (): Plugin {
name: 'dirnames', name: 'dirnames',
renderChunk (code, chunk) { renderChunk (code, chunk) {
return { return {
code: code + (chunk.isEntry ? 'globalThis.mainDir="undefined"!=typeof __dirname?__dirname:require.main.filename;' : ''), code: code + (chunk.isEntry ? 'globalThis.entryURL = import.meta.url' : ''),
map: null map: null
} }
} }

View File

@ -2,7 +2,7 @@ import { createRenderer } from 'vue-bundle-renderer'
import devalue from '@nuxt/devalue' import devalue from '@nuxt/devalue'
import { runtimeConfig } from './config' import { runtimeConfig } from './config'
// @ts-ignore // @ts-ignore
import htmlTemplate from '#build/views/document.template.js' import htmlTemplate from '#build/views/document.template.mjs'
function _interopDefault (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e } function _interopDefault (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e }
@ -17,9 +17,9 @@ async function loadRenderer () {
// @ts-ignore // @ts-ignore
const { renderToString } = await import('#nitro-renderer') const { renderToString } = await import('#nitro-renderer')
// @ts-ignore // @ts-ignore
const createApp = await import('#build/dist/server/server') const createApp = await import('#build/dist/server/server.mjs')
// @ts-ignore // @ts-ignore
const clientManifest = await import('#build/dist/server/client.manifest.json') const clientManifest = await import('#build/dist/server/client.manifest.mjs')
_renderer = createRenderer(_interopDefault(createApp), { _renderer = createRenderer(_interopDefault(createApp), {
clientManifest: _interopDefault(clientManifest), clientManifest: _interopDefault(clientManifest),
renderToString renderToString

View File

@ -14,7 +14,7 @@ import type { NitroContext } from '../context'
export function createDevServer (nitroContext: NitroContext) { export function createDevServer (nitroContext: NitroContext) {
// Worker // Worker
const workerEntry = resolve(nitroContext.output.dir, nitroContext.output.serverDir, 'index.js') const workerEntry = resolve(nitroContext.output.dir, nitroContext.output.serverDir, 'index.mjs')
let pendingWorker: Worker | null let pendingWorker: Worker | null
let activeWorker: Worker let activeWorker: Worker
let workerAddress: string | null let workerAddress: string | null

View File

@ -36,7 +36,7 @@ function filesToMiddleware (files: string[], baseDir: string, basePath: string,
} }
export function scanMiddleware (serverDir: string, onChange?: (results: ServerMiddleware[], event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string) => void): Promise<ServerMiddleware[]> { export function scanMiddleware (serverDir: string, onChange?: (results: ServerMiddleware[], event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string) => void): Promise<ServerMiddleware[]> {
const pattern = '**/*.{js,ts}' const pattern = '**/*.{ts,mjs,js,cjs}'
const globalDir = resolve(serverDir, 'middleware') const globalDir = resolve(serverDir, 'middleware')
const apiDir = resolve(serverDir, 'api') const apiDir = resolve(serverDir, 'api')
@ -78,7 +78,7 @@ export function resolveMiddleware (nuxt: Nuxt) {
middleware.push({ middleware.push({
...m, ...m,
handle: tryResolvePath(handle, { handle: tryResolvePath(handle, {
extensions: ['.ts', '.js'], extensions: ['.ts', '.mjs', '.js', '.cjs'],
alias: nuxt.options.alias, alias: nuxt.options.alias,
base: nuxt.options.srcDir base: nuxt.options.srcDir
}), }),

View File

@ -32,6 +32,11 @@ export async function buildServer (ctx: ViteBuildContext) {
ssr: true, ssr: true,
rollupOptions: { rollupOptions: {
input: resolve(ctx.nuxt.options.buildDir, 'entry.mjs'), input: resolve(ctx.nuxt.options.buildDir, 'entry.mjs'),
output: {
entryFileNames: 'server.mjs',
preferConst: true,
format: 'module'
},
onwarn (warning, rollupWarn) { onwarn (warning, rollupWarn) {
if (!['UNUSED_EXTERNAL_IMPORT'].includes(warning.code)) { if (!['UNUSED_EXTERNAL_IMPORT'].includes(warning.code)) {
rollupWarn(warning) rollupWarn(warning)
@ -51,8 +56,8 @@ export async function buildServer (ctx: ViteBuildContext) {
const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server') const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')
await mkdirp(serverDist) await mkdirp(serverDist)
await writeFile(resolve(serverDist, 'server.js'), 'module.exports = require("./entry")', 'utf8')
await writeFile(resolve(serverDist, 'client.manifest.json'), 'false', 'utf8') await writeFile(resolve(serverDist, 'client.manifest.json'), 'false', 'utf8')
await writeFile(resolve(serverDist, 'client.manifest.mjs'), 'export default false', 'utf8')
const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs) const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)

View File

@ -113,6 +113,10 @@ export default class VueSSRClientPlugin {
await mkdirp(dirname(this.options.filename)) await mkdirp(dirname(this.options.filename))
await writeFile(this.options.filename, src) await writeFile(this.options.filename, src)
const mjsSrc = 'export default ' + src
await writeFile(this.options.filename.replace('.json', '.mjs'), mjsSrc)
// assets[this.options.filename] = { // assets[this.options.filename] = {
// source: () => src, // source: () => src,
// size: () => src.length // size: () => src.length

View File

@ -74,6 +74,21 @@ export default class VueSSRServerPlugin {
size: () => src.length size: () => src.length
} }
const mjsSrc = 'export default ' + src
assets[this.options.filename.replace('.json', '.mjs')] = {
source: () => mjsSrc,
map: () => null,
size: () => mjsSrc.length
}
// TODO: Workaround for webpack
const serverJS = 'export { default } from "./server.js"'
assets['server.mjs'] = {
source: () => serverJS,
map: () => null,
size: () => serverJS.length
}
cb() cb()
}) })
}) })

View File

@ -7,6 +7,6 @@
"scripts": { "scripts": {
"dev": "nu dev", "dev": "nu dev",
"build": "nu build", "build": "nu build",
"start": "node .output/server" "start": "node .output/server/index.mjs"
} }
} }

View File

@ -1,33 +0,0 @@
import { transformSync } from 'esbuild'
// https://jestjs.io/docs/next/code-transformation
export default {
process (src, path, _opts) {
const r = transformSync(src, {
target: 'node14',
format: 'cjs',
sourcefile: path,
loader: path.endsWith('.ts') ? 'ts' : 'default'
})
r.code = r.code.replace(/import(\(.*\))/g, (_, id) => {
let openBrackets = 0
for (let pos = 0; pos < id.length; pos++) {
const char = id[pos]
switch (char) {
case '(':
openBrackets++
break
case ')':
openBrackets--
if (!openBrackets) {
return 'Promise.resolve(require' + id.slice(0, pos) + ')' + id.slice(pos)
}
break
}
}
return 'Promise.resolve(require' + id + ')'
})
return r
}
}

View File

@ -1,3 +1,8 @@
import { defineNuxtConfig } from '@nuxt/kit' import { defineNuxtConfig } from '@nuxt/kit'
export default defineNuxtConfig({}) export default defineNuxtConfig({
buildDir: process.env.NITRO_BUILD_DIR,
nitro: {
output: { dir: process.env.NITRO_OUTPUT_DIR }
}
})

View File

@ -6,5 +6,9 @@ global.__NUXT_PREPATHS__ = (global.__NUXT_PREPATHS__ || []).concat(__dirname)
export default defineNuxtConfig({ export default defineNuxtConfig({
buildModules: [ buildModules: [
'@nuxt/nitro/compat' '@nuxt/nitro/compat'
] ],
buildDir: process.env.NITRO_BUILD_DIR,
nitro: {
output: { dir: process.env.NITRO_OUTPUT_DIR }
}
}) })

70
test/presets/_tests.mjs Normal file
View File

@ -0,0 +1,70 @@
import { resolve } from 'path'
import destr from 'destr'
import { listen } from 'listhen'
import { $fetch } from 'ohmyfetch/node'
import execa from 'execa'
import { expect } from 'chai'
import { fixtureDir, resolveWorkspace } from '../utils.mjs'
const isCompat = Boolean(process.env.TEST_COMPAT)
export function importModule (path) {
return import(path)
}
export function setupTest (preset) {
const fixture = isCompat ? 'compat' : 'basic'
const rootDir = fixtureDir(fixture)
const buildDir = resolve(rootDir, '.nuxt-' + preset)
const ctx = {
rootDir,
outDir: resolve(buildDir, 'output'),
fetch: url => $fetch(url, { baseURL: ctx.server.url })
}
it('nitro build', async () => {
const nuxtCLI = isCompat
? resolve(ctx.rootDir, 'node_modules/nuxt/bin/nuxt.js')
: resolveWorkspace('packages/cli/bin/nuxt.js')
await execa('node', [nuxtCLI, 'build', ctx.rootDir], {
env: {
NITRO_PRESET: preset,
NITRO_BUILD_DIR: buildDir,
NITRO_OUTPUT_DIR: ctx.outDir,
NODE_ENV: 'production'
}
})
}).timeout(60000)
after('Cleanup', async () => {
if (ctx.server) {
await ctx.server.close()
}
})
return ctx
}
export async function startServer (ctx, handle) {
ctx.server = await listen(handle)
}
export function testNitroBehavior (ctx, getHandler) {
let handler
it('setup handler', async () => {
handler = await getHandler()
})
it('SSR Works', async () => {
const { data } = await handler({ url: '/' })
expect(data).to.have.string('Hello Vue')
})
it('API Works', async () => {
const { data } = await handler({ url: '/api/hello' })
expect(destr(data)).to.have.string('Hello API')
})
}

View File

@ -1,103 +0,0 @@
import { RequestListener } from 'http'
import { resolve } from 'upath'
import destr from 'destr'
import consola from 'consola'
import { Listener, listen } from 'listhen'
import { $fetch } from 'ohmyfetch/node'
import createRequire from 'create-require'
import type { LoadNuxtOptions } from '@nuxt/kit'
import { fixtureDir, buildFixture, loadFixture } from '../utils'
const isCompat = Boolean(process.env.TEST_COMPAT)
export interface TestContext {
rootDir: string
outDir: string
nuxt?: any
fetch: (url: string) => Promise<any>
server?: Listener
}
export interface AbstractRequest {
url: string
headers?: any
method?: string
body?: any
}
export interface AbstractResponse {
data: string
}
export type AbstractHandler = (req: AbstractRequest) => Promise<AbstractResponse>
export function setupTest (): TestContext {
const fixture = isCompat ? 'compat' : 'basic'
const rootDir = fixtureDir(fixture)
const outDir = resolve(__dirname, '.output', fixture)
const ctx: TestContext = {
rootDir,
outDir,
fetch: url => $fetch<any>(url, { baseURL: ctx.server.url })
}
beforeAll(() => {
jest.mock('jiti', () => createRequire)
consola.wrapAll()
consola.mock(() => jest.fn())
})
afterAll(async () => {
if (ctx.nuxt) {
await ctx.nuxt.close()
}
if (ctx.server) {
await ctx.server.close()
}
})
return ctx
}
export function testNitroBuild (ctx: TestContext, preset: string) {
test('nitro build', async () => {
ctx.outDir = resolve(ctx.outDir, preset)
const loadOpts: LoadNuxtOptions = { rootDir: ctx.rootDir, dev: false, version: isCompat ? 2 : 3 }
await buildFixture(loadOpts)
const nuxt = await loadFixture(loadOpts, {
nitro: {
preset,
minify: false,
serveStatic: false,
externals: preset === 'cloudflare' ? false : { trace: false },
output: { dir: ctx.outDir }
}
})
await nuxt.callHook('build:done', {})
ctx.nuxt = nuxt
}, 60000)
}
export async function startServer (ctx: TestContext, handle: RequestListener) {
ctx.server = await listen(handle)
}
export function testNitroBehavior (_ctx: TestContext, getHandler: () => Promise<AbstractHandler>) {
let handler
test('setup handler', async () => {
handler = await getHandler()
})
test('SSR Works', async () => {
const { data } = await handler({ url: '/' })
expect(data).toMatch('Hello Vue')
}, 10000)
test('API Works', async () => {
const { data } = await handler({ url: '/api/hello' })
expect(destr(data)).toEqual('Hello API')
})
}

View File

@ -1,15 +1,14 @@
import { resolve } from 'upath' import { resolve } from 'path'
import { readFile } from 'fs-extra' import { promises as fsp } from 'fs'
import { JSDOM } from 'jsdom' import { JSDOM } from 'jsdom'
import { setupTest, testNitroBuild, testNitroBehavior } from './_utils' import { setupTest, testNitroBehavior } from './_tests.mjs'
// TODO: fix SyntaxError: Unexpected end of input on script executation // TODO: fix SyntaxError: Unexpected end of input on script executation
describe.skip('nitro:preset:cloudflare', () => { describe('nitro:preset:cloudflare', () => {
const ctx = setupTest() const ctx = setupTest('cloudflare')
testNitroBuild(ctx, 'cloudflare')
testNitroBehavior(ctx, async () => { testNitroBehavior(ctx, async () => {
const script = await readFile(resolve(ctx.outDir, 'server/index.js'), 'utf-8') const script = await fsp.readFile(resolve(ctx.outDir, 'server/index.mjs'), 'utf-8')
const dom = new JSDOM( const dom = new JSDOM(
`<!DOCTYPE html> `<!DOCTYPE html>
<html> <html>

View File

@ -1,11 +1,10 @@
import { resolve } from 'upath' import { resolve } from 'path'
import { testNitroBuild, setupTest, testNitroBehavior } from './_utils' import { setupTest, testNitroBehavior, importModule } from './_tests.mjs'
describe('nitro:preset:lambda', () => { describe('nitro:preset:lambda', () => {
const ctx = setupTest() const ctx = setupTest('lambda')
testNitroBuild(ctx, 'lambda')
testNitroBehavior(ctx, async () => { testNitroBehavior(ctx, async () => {
const { handler } = await import(resolve(ctx.outDir, 'server/index.js')) const { handler } = await importModule(resolve(ctx.outDir, 'server/index.mjs'))
return async ({ url: rawRelativeUrl, headers, method, body }) => { return async ({ url: rawRelativeUrl, headers, method, body }) => {
// creating new URL object to parse query easier // creating new URL object to parse query easier
const url = new URL(`https://example.com${rawRelativeUrl}`) const url = new URL(`https://example.com${rawRelativeUrl}`)

View File

@ -0,0 +1,16 @@
import { resolve } from 'path'
import { startServer, setupTest, testNitroBehavior, importModule } from './_tests.mjs'
describe('nitro:preset:node', () => {
const ctx = setupTest('node')
testNitroBehavior(ctx, async () => {
const { handle } = await importModule(resolve(ctx.outDir, 'server/index.mjs'))
await startServer(ctx, handle)
return async ({ url }) => {
const data = await ctx.fetch(url)
return {
data
}
}
})
})

View File

@ -1,17 +0,0 @@
import { resolve } from 'upath'
import { testNitroBuild, startServer, setupTest, testNitroBehavior } from './_utils'
describe('nitro:preset:node', () => {
const ctx = setupTest()
testNitroBuild(ctx, 'node')
testNitroBehavior(ctx, async () => {
const { handle } = await import(resolve(ctx.outDir, 'server/index.js'))
await startServer(ctx, handle)
return async ({ url }) => {
const data = await ctx.fetch(url)
return {
data
}
}
})
})

View File

@ -1,11 +1,10 @@
import { resolve } from 'upath' import { resolve } from 'path'
import { testNitroBuild, setupTest, startServer, testNitroBehavior } from './_utils' import { setupTest, startServer, testNitroBehavior, importModule } from './_tests.mjs'
describe('nitro:preset:vercel', () => { describe('nitro:preset:vercel', () => {
const ctx = setupTest() const ctx = setupTest('vercel')
testNitroBuild(ctx, 'vercel')
testNitroBehavior(ctx, async () => { testNitroBehavior(ctx, async () => {
const handle = await import(resolve(ctx.outDir, 'functions/node/server/index.js')) const handle = await importModule(resolve(ctx.outDir, 'functions/node/server/index.mjs'))
.then(r => r.default || r) .then(r => r.default || r)
await startServer(ctx, handle) await startServer(ctx, handle)
return async ({ url }) => { return async ({ url }) => {

View File

@ -1,15 +1,27 @@
import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from 'fs' import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from 'fs'
import { execSync } from 'child_process' import { execSync } from 'child_process'
import { resolve, dirname } from 'upath' import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import defu from 'defu' import defu from 'defu'
import hash from 'object-hash' import hash from 'object-hash'
import type { LoadNuxtOptions, NuxtConfig } from '@nuxt/kit' import execa from 'execa'
export function fixtureDir (name: string) { const __dirname = dirname(fileURLToPath(import.meta.url))
export function resolveWorkspace (name) {
return resolve(__dirname, '../', name)
}
export function fixtureDir (name) {
return resolve(__dirname, 'fixtures', name) return resolve(__dirname, 'fixtures', name)
} }
export async function loadFixture (opts: LoadNuxtOptions, unhashedConfig?: NuxtConfig) { export async function execNuxtCLI (args, opts) {
const nuxtCLI = resolveWorkspace('packages/cli/bin/nuxt.js')
await execa('node', [nuxtCLI, ...args], opts)
}
export async function loadFixture (opts, unhashedConfig) {
const buildId = hash(opts) const buildId = hash(opts)
const buildDir = resolve(opts.rootDir, '.nuxt', buildId) const buildDir = resolve(opts.rootDir, '.nuxt', buildId)
const { loadNuxt } = await import('@nuxt/kit') const { loadNuxt } = await import('@nuxt/kit')
@ -17,7 +29,7 @@ export async function loadFixture (opts: LoadNuxtOptions, unhashedConfig?: NuxtC
return nuxt return nuxt
} }
export async function buildFixture (opts: LoadNuxtOptions) { export async function buildFixture (opts) {
const buildId = hash(opts) const buildId = hash(opts)
const buildDir = resolve(opts.rootDir, '.nuxt', buildId) const buildDir = resolve(opts.rootDir, '.nuxt', buildId)
@ -75,6 +87,6 @@ function waitWhile (check, interval = 100, timeout = 30000) {
}) })
} }
function gitHead (): string { function gitHead () {
return execSync('git rev-parse HEAD').toString('utf8').trim() return execSync('git rev-parse HEAD').toString('utf8').trim()
} }

View File

@ -11,7 +11,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"types": [ "types": [
"node", "node",
"jest", "mocha",
"chai",
"@nuxt/app", "@nuxt/app",
"@nuxt/nitro", "@nuxt/nitro",
] ]

1896
yarn.lock

File diff suppressed because it is too large Load Diff