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
- name: Test
run: TEST_COMPAT=1 yarn jest --ci
# - name: Coverage
# uses: codecov/codecov-action@v1
run: TEST_COMPAT=1 yarn test:presets

View File

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

View File

@ -2,9 +2,9 @@
> How to deploy Nuxt to a Node.js host with Nuxt Nitro
- Support for ultra-minimal SSR build
- Zero millisecond cold start
- More configuration required
- Support for ultra-minimal SSR build
- Zero millisecond cold start
- More configuration required
## 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:
```bash
node .output/server
node .output/server/index.mjs
```
For example, using `pm2`:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,8 @@
import head from '#app/plugins/head'
import preload from '#app/plugins/preload.server'
<%= utils.importSources(app.plugins.map(p => p.src)) %>
const commonPlugins = [
head,
<%= 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
const htmlSrc = resolve(nitroContext._nuxt.buildDir, `views/${{ 2: 'app', 3: 'document' }[2]}.template.html`)
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')
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)
nitroContext.rollupConfig = getRollupConfig(nitroContext)

View File

@ -1,6 +1,6 @@
import fetch from 'node-fetch'
import { resolve } from 'upath'
import { resolveModule } from '@nuxt/kit'
import { readFile, writeFile } from 'fs-extra'
import { build, generate, prepare } from './build'
import { getNitroContext, NitroContext } from './context'
import { createDevServer } from './server/dev'
@ -55,16 +55,25 @@ export default function nuxt2CompatModule () {
// Nitro client plugin
this.addPlugin({
fileName: 'nitro.client.js',
src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.js')
fileName: 'nitro.client.mjs',
src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.mjs')
})
// Fix module resolution
nuxt.hook('webpack:config', (configs) => {
for (const config of configs) {
if (config.name === 'client') {
config.resolve.alias.ufo = resolveModule('ufo/dist/index.mjs')
}
config.resolve.alias.ufo = '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>
<meta charset="utf-8">
<link rel="prefetch" href="${routerBase}sw.js">
<link rel="prefetch" href="${routerBase}_server/index.js">
<link rel="prefetch" href="${routerBase}_server/index.mjs">
<script>
async function register () {
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>')
},
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
await writeFile(resolve(output.publicDir, 'index.html'), html)

View File

@ -11,7 +11,7 @@ export const cloudflare: NitroPreset = extendPreset(worker, {
],
hooks: {
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))
let inDir = prettyPath(_nuxt.rootDir)
if (inDir) {

View File

@ -8,7 +8,7 @@ export const server: NitroPreset = extendPreset(node, {
serveStatic: true,
hooks: {
'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
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')
@ -70,7 +70,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
input: resolvePath(nitroContext, nitroContext.entry),
output: {
dir: nitroContext.output.serverDir,
entryFileNames: 'index.js',
entryFileNames: 'index.mjs',
chunkFileNames (chunkInfo) {
let prefix = ''
const modules = Object.keys(chunkInfo.modules)
@ -88,10 +88,10 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
} else if (lastModule.includes('assets')) {
prefix = 'assets'
}
return join('chunks', prefix, '[name].js')
return join('chunks', prefix, '[name].mjs')
},
inlineDynamicImports: nitroContext.inlineDynamicImports,
format: 'cjs',
format: 'esm',
exports: 'auto',
intro: '',
outro: '',
@ -232,6 +232,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
'~~',
'@@/',
'virtual:',
'ohmyfetch', // TODO: Webpack externals forces default import!
nitroContext._internal.runtimeDir,
nitroContext._nuxt.srcDir,
nitroContext._nuxt.rootDir,
@ -241,7 +242,8 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
],
traceOptions: {
base: '/',
processCwd: nitroContext._nuxt.rootDir
processCwd: nitroContext._nuxt.rootDir,
exportsOnly: true
}
})))
}
@ -252,7 +254,13 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
preferBuiltins: true,
rootDir: nitroContext._nuxt.rootDir,
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

View File

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

View File

@ -103,16 +103,20 @@ export default function dynamicRequire(id) {
function TMPL_LAZY ({ chunks }: TemplateContext) {
return `
function dynamicWebpackModule(id, getChunk) {
function dynamicWebpackModule(id, getChunk, ids) {
return function (module, exports, require) {
const r = getChunk()
if (r instanceof Promise) {
if (typeof r.then === 'function') {
module.exports = r.then(r => {
const realModule = { exports: {}, 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;
});
} else {
} else if (r && typeof r.modules[id] === 'function') {
r.modules[id](module, exports, require);
}
};
@ -121,7 +125,7 @@ function dynamicWebpackModule(id, getChunk) {
function webpackChunk (meta, getChunk) {
const chunk = { ...meta, modules: {} };
for (const id of meta.moduleIds) {
chunk.modules[id] = dynamicWebpackModule(id, getChunk);
chunk.modules[id] = dynamicWebpackModule(id, getChunk, meta.moduleIds);
};
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 { nodeFileTrace, NodeFileTraceOptions } from '@vercel/nft'
import type { Plugin } from 'rollup'
@ -13,11 +13,11 @@ export interface NodeExternalsOptions {
}
export function externals (opts: NodeExternalsOptions): Plugin {
const resolvedExternals = new Set<string>()
const trackedExternals = new Set<string>()
return {
name: 'node-externals',
resolveId (id) {
async resolveId (id, importer, options) {
// Internals
if (!id || id.startsWith('\x00') || id.includes('?') || id.startsWith('#')) {
return null
@ -44,11 +44,10 @@ export function externals (opts: NodeExternalsOptions): Plugin {
}
}
// Try to resolve for nft
// Track externals
if (opts.trace !== false) {
let _resolvedId = _id
try { _resolvedId = normalize(require.resolve(_resolvedId, { paths: opts.moduleDirectories })) } catch (_err) { }
resolvedExternals.add(_resolvedId)
const resolved = await this.resolve(id, importer, { ...options, skipSelf: true }).then(r => r.id)
trackedExternals.add(resolved)
}
return {
@ -58,14 +57,25 @@ export function externals (opts: NodeExternalsOptions): Plugin {
},
async buildEnd () {
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.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) => {
if (!file.includes('node_modules')) {
return
}
// TODO: Minify package.json
const src = resolve(opts.traceOptions.base, file)
const dst = resolve(opts.outDir, 'node_modules', file.split('node_modules/').pop())
await mkdirp(dirname(dst))

View File

@ -32,8 +32,12 @@ export function staticAssets (context: NitroContext) {
'#static': `
import { promises } from 'fs'
import { resolve } from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import assets from '#static-assets'
const mainDir = dirname(fileURLToPath(globalThis.entryURL))
export function readAsset (id) {
return promises.readFile(resolve(mainDir, getAsset(id).path))
}
@ -50,7 +54,7 @@ export function dirnames (): Plugin {
name: 'dirnames',
renderChunk (code, chunk) {
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
}
}

View File

@ -2,7 +2,7 @@ import { createRenderer } from 'vue-bundle-renderer'
import devalue from '@nuxt/devalue'
import { runtimeConfig } from './config'
// @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 }
@ -17,9 +17,9 @@ async function loadRenderer () {
// @ts-ignore
const { renderToString } = await import('#nitro-renderer')
// @ts-ignore
const createApp = await import('#build/dist/server/server')
const createApp = await import('#build/dist/server/server.mjs')
// @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), {
clientManifest: _interopDefault(clientManifest),
renderToString

View File

@ -14,7 +14,7 @@ import type { NitroContext } from '../context'
export function createDevServer (nitroContext: NitroContext) {
// 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 activeWorker: Worker
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[]> {
const pattern = '**/*.{js,ts}'
const pattern = '**/*.{ts,mjs,js,cjs}'
const globalDir = resolve(serverDir, 'middleware')
const apiDir = resolve(serverDir, 'api')
@ -78,7 +78,7 @@ export function resolveMiddleware (nuxt: Nuxt) {
middleware.push({
...m,
handle: tryResolvePath(handle, {
extensions: ['.ts', '.js'],
extensions: ['.ts', '.mjs', '.js', '.cjs'],
alias: nuxt.options.alias,
base: nuxt.options.srcDir
}),

View File

@ -32,6 +32,11 @@ export async function buildServer (ctx: ViteBuildContext) {
ssr: true,
rollupOptions: {
input: resolve(ctx.nuxt.options.buildDir, 'entry.mjs'),
output: {
entryFileNames: 'server.mjs',
preferConst: true,
format: 'module'
},
onwarn (warning, rollupWarn) {
if (!['UNUSED_EXTERNAL_IMPORT'].includes(warning.code)) {
rollupWarn(warning)
@ -51,8 +56,8 @@ export async function buildServer (ctx: ViteBuildContext) {
const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')
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.mjs'), 'export default false', 'utf8')
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 writeFile(this.options.filename, src)
const mjsSrc = 'export default ' + src
await writeFile(this.options.filename.replace('.json', '.mjs'), mjsSrc)
// assets[this.options.filename] = {
// source: () => src,
// size: () => src.length

View File

@ -74,6 +74,21 @@ export default class VueSSRServerPlugin {
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()
})
})

View File

@ -7,6 +7,6 @@
"scripts": {
"dev": "nu dev",
"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'
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({
buildModules: [
'@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 { readFile } from 'fs-extra'
import { resolve } from 'path'
import { promises as fsp } from 'fs'
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
describe.skip('nitro:preset:cloudflare', () => {
const ctx = setupTest()
testNitroBuild(ctx, 'cloudflare')
describe('nitro:preset:cloudflare', () => {
const ctx = setupTest('cloudflare')
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(
`<!DOCTYPE html>
<html>

View File

@ -1,11 +1,10 @@
import { resolve } from 'upath'
import { testNitroBuild, setupTest, testNitroBehavior } from './_utils'
import { resolve } from 'path'
import { setupTest, testNitroBehavior, importModule } from './_tests.mjs'
describe('nitro:preset:lambda', () => {
const ctx = setupTest()
testNitroBuild(ctx, 'lambda')
const ctx = setupTest('lambda')
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 }) => {
// creating new URL object to parse query easier
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 { testNitroBuild, setupTest, startServer, testNitroBehavior } from './_utils'
import { resolve } from 'path'
import { setupTest, startServer, testNitroBehavior, importModule } from './_tests.mjs'
describe('nitro:preset:vercel', () => {
const ctx = setupTest()
testNitroBuild(ctx, 'vercel')
const ctx = setupTest('vercel')
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)
await startServer(ctx, handle)
return async ({ url }) => {

View File

@ -1,15 +1,27 @@
import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from 'fs'
import { execSync } from 'child_process'
import { resolve, dirname } from 'upath'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import defu from 'defu'
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)
}
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 buildDir = resolve(opts.rootDir, '.nuxt', buildId)
const { loadNuxt } = await import('@nuxt/kit')
@ -17,7 +29,7 @@ export async function loadFixture (opts: LoadNuxtOptions, unhashedConfig?: NuxtC
return nuxt
}
export async function buildFixture (opts: LoadNuxtOptions) {
export async function buildFixture (opts) {
const buildId = hash(opts)
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()
}

View File

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

1896
yarn.lock

File diff suppressed because it is too large Load Diff