feat: use virtual filesystem for templates (#292)

Co-authored-by: Daniel Roe <daniel@roe.dev>
This commit is contained in:
pooya parsa 2021-07-15 12:18:34 +02:00 committed by GitHub
parent ea0d2788a4
commit bec2720930
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 136 additions and 40 deletions

View File

@ -1 +0,0 @@
export { default } from '#app/entry'

View File

@ -6,6 +6,6 @@
<body {{ BODY_ATTRS }}>
{{ APP }}
<% if (nuxt.options.vite && nuxt.options.dev) { %><script type="module" src="/@vite/client"></script>
<script type="module" src="/__build/entry.mjs"></script><% } %>
<script type="module" src="/__app/entry.mjs"></script><% } %>
</body>
</html>

View File

@ -12,7 +12,6 @@ import type { TemplateOpts, PluginTemplateOpts } from '../types/module'
* If a fileName is not provided or the template is string, target file name defaults to
* [dirName].[fileName].[pathHash].[ext].
*
* This file is available to import with `#build/${filename}`
*/
export function addTemplate (tmpl: TemplateOpts | string) {
const nuxt = useNuxt()

View File

@ -21,6 +21,8 @@ export interface Nuxt {
/** The production or development server */
server?: any
vfs: Record<string, string>
}
export interface NuxtPlugin {

View File

@ -48,7 +48,7 @@ export async function build (nitroContext: NitroContext) {
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$/, '.mjs').replace('app.', 'document.')
htmlTemplate.contents = await readFile(htmlTemplate.src, 'utf-8')
htmlTemplate.contents = nitroContext.vfs[htmlTemplate.src] || await readFile(htmlTemplate.src, 'utf-8')
await nitroContext._internal.hooks.callHook('nitro:document', htmlTemplate)
htmlTemplate.compiled = 'export default ' + serializeTemplate(htmlTemplate.contents)
await writeFile(htmlTemplate.dst, htmlTemplate.compiled)

View File

@ -29,6 +29,7 @@ export interface NitroContext {
nuxtHooks: configHooksT
ignore: string[]
env: Preset
vfs: Record<string, string>
output: {
dir: string
serverDir: string
@ -82,6 +83,7 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N
scannedMiddleware: [],
ignore: [],
env: {},
vfs: {},
hooks: {},
nuxtHooks: {},
output: {

View File

@ -1,9 +1,9 @@
import { resolve, join, relative, dirname } from 'upath'
import { resolve, join, relative } from 'upath'
import globby from 'globby'
import lodashTemplate from 'lodash/template'
import defu from 'defu'
import { tryResolvePath, resolveFiles, Nuxt, NuxtApp, NuxtTemplate, NuxtPlugin } from '@nuxt/kit'
import { mkdirp, writeFile, readFile } from 'fs-extra'
import { readFile } from 'fs-extra'
import * as templateUtils from './template.utils'
export function createApp (nuxt: Nuxt, options: Partial<NuxtApp> = {}): NuxtApp {
@ -43,12 +43,17 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp) {
// Extend templates
await nuxt.callHook('app:templates', app)
// Generate templates
await Promise.all(app.templates.map(t => generateTemplate(t, nuxt.options.buildDir, {
utils: templateUtils,
nuxt,
app
})))
// Compile templates into vfs
const templateContext = { utils: templateUtils, nuxt, app }
await Promise.all(app.templates.map(async (template) => {
const contents = await compileTemplate(template, templateContext)
const fullPath = resolve(nuxt.options.buildDir, template.path)
nuxt.vfs[fullPath] = contents
const aliasPath = '#build/' + template.path.replace(/\.\w+$/, '')
nuxt.vfs[aliasPath] = contents
}))
await nuxt.callHook('app:templatesGenerated', app)
}
@ -78,23 +83,21 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
await nuxt.callHook('app:resolve', app)
}
async function generateTemplate (tmpl: NuxtTemplate, destDir: string, ctx) {
let compiledSrc: string = ''
async function compileTemplate (tmpl: NuxtTemplate, ctx: any) {
const data = { ...ctx, ...tmpl.data }
if (tmpl.src) {
try {
const srcContents = await readFile(tmpl.src, 'utf-8')
compiledSrc = lodashTemplate(srcContents, {})(data)
return lodashTemplate(srcContents, {})(data)
} catch (err) {
console.error('Error compiling template: ', tmpl)
throw err
}
} else if (tmpl.compile) {
compiledSrc = tmpl.compile(data)
}
const dest = join(destDir, tmpl.path)
await mkdirp(dirname(dest))
await writeFile(dest, compiledSrc)
if (tmpl.compile) {
return tmpl.compile(data)
}
throw new Error('Invalid template:' + tmpl)
}
async function resolvePlugins (nuxt: Nuxt) {

View File

@ -10,6 +10,11 @@ export function initNitro (nuxt: Nuxt) {
nuxt.server = createDevServer(nitroDevContext)
if (nuxt.vfs) {
nitroContext.vfs = nuxt.vfs
nitroDevContext.vfs = nuxt.vfs
}
// Connect hooks
// @ts-ignore
nuxt.hooks.addHooks(nitroContext.nuxtHooks)

View File

@ -12,7 +12,8 @@ export function createNuxt (options: NuxtOptions): Nuxt {
callHook: hooks.callHook,
hook: hooks.hook,
ready: () => initNuxt(nuxt),
close: () => Promise.resolve(hooks.callHook('close', nuxt))
close: () => Promise.resolve(hooks.callHook('close', nuxt)),
vfs: {}
}
return nuxt

View File

@ -1,4 +1,3 @@
import { resolve } from 'upath'
import * as vite from 'vite'
import vitePlugin from '@vitejs/plugin-vue'
import { cacheDirPlugin } from './plugins/cache-dir'
@ -16,10 +15,7 @@ export async function buildClient (ctx: ViteBuildContext) {
},
build: {
outDir: 'dist/client',
assetsDir: '.',
rollupOptions: {
input: resolve(ctx.nuxt.options.buildDir, 'client.js')
}
assetsDir: '.'
},
plugins: [
replace({ 'process.env': 'import.meta.env' }),

View File

@ -0,0 +1,43 @@
import { dirname, join } from 'upath'
import { Plugin } from 'rollup'
const PREFIX = '\0virtual:'
export default function virtual (vfs: Record<string, string>): Plugin {
const extensions = ['', '.ts', '.vue', '.mjs', '.cjs', '.js', '.json']
const resolveWithExt = (id) => {
for (const ext of extensions) {
const rId = id + ext
if (rId in vfs) {
return rId
}
}
return null
}
return {
name: 'virtual',
resolveId (id, importer) {
const resolvedId = resolveWithExt(id)
if (resolvedId) { return PREFIX + resolvedId }
if (importer && id[0] !== '/') {
const importerNoPrefix = importer.startsWith(PREFIX) ? importer.slice(PREFIX.length) : importer
const importedDir = dirname(importerNoPrefix)
const resolved = resolveWithExt(join(importedDir, id))
if (resolved) { return PREFIX + resolved }
}
return null
},
load (id) {
if (!id.startsWith(PREFIX)) { return null }
const idNoPrefix = id.slice(PREFIX.length)
return {
code: vfs[idNoPrefix],
map: null
}
}
}
}

View File

@ -31,7 +31,6 @@ export async function buildServer (ctx: ViteBuildContext) {
outDir: resolve(ctx.nuxt.options.buildDir, 'dist/server'),
ssr: true,
rollupOptions: {
input: resolve(ctx.nuxt.options.buildDir, 'entry.mjs'),
output: {
entryFileNames: 'server.mjs',
preferConst: true,

View File

@ -7,7 +7,7 @@ export async function warmupViteServer (server: ViteDevServer, entries: string[]
if (warmedUrls.has(url)) { return undefined }
warmedUrls.add(url)
await server.transformRequest(url)
const deps = Array.from(server.moduleGraph.urlToModuleMap.get(url).importedModules)
const deps = Array.from(server.moduleGraph.urlToModuleMap.get(url)?.importedModules || [])
await Promise.all(deps.map(m => warmup(m.url)))
}

View File

@ -1,10 +1,12 @@
import * as vite from 'vite'
import { resolve } from 'upath'
import consola from 'consola'
import type { Nuxt } from '@nuxt/kit'
import type { InlineConfig, SSROptions } from 'vite'
import type { Options } from '@vitejs/plugin-vue'
import { buildClient } from './client'
import { buildServer } from './server'
import virtual from './plugins/virtual'
import { warmupViteServer } from './utils/warmup'
export interface ViteOptions extends InlineConfig {
@ -33,8 +35,8 @@ export async function bundle (nuxt: Nuxt) {
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
alias: {
...nuxt.options.alias,
'#build': nuxt.options.buildDir,
'#app': nuxt.options.appDir,
'/__app': nuxt.options.appDir,
'/__build': nuxt.options.buildDir,
'~': nuxt.options.srcDir,
'@': nuxt.options.srcDir,
@ -54,8 +56,14 @@ export async function bundle (nuxt: Nuxt) {
},
clearScreen: false,
build: {
emptyOutDir: false
emptyOutDir: false,
rollupOptions: {
input: resolve(nuxt.options.appDir, 'entry')
}
},
plugins: [
virtual(nuxt.vfs)
],
server: {
fs: {
strict: true,
@ -67,8 +75,7 @@ export async function bundle (nuxt: Nuxt) {
...nuxt.options.modulesDir
]
}
},
plugins: []
}
} as ViteOptions
)
}
@ -77,7 +84,7 @@ export async function bundle (nuxt: Nuxt) {
nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer) => {
const start = Date.now()
warmupViteServer(server, [`/@fs${nuxt.options.buildDir.replace(/\\/g, '/')}/entry.mjs`]).then(() => {
warmupViteServer(server, ['/__app/entry']).then(() => {
consola.info(`Vite warmed up in ${Date.now() - start}ms`)
}).catch(consola.error)
})

View File

@ -44,6 +44,7 @@
"webpack-bundle-analyzer": "^4.4.2",
"webpack-dev-middleware": "^5.0.0",
"webpack-hot-middleware": "^2.25.0",
"webpack-virtual-modules": "^0.4.3",
"webpackbar": "^5.0.0-3"
},
"devDependencies": {
@ -52,6 +53,7 @@
"@types/webpack-bundle-analyzer": "^4.4.1",
"@types/webpack-dev-middleware": "^5.0.1",
"@types/webpack-hot-middleware": "^2.25.5",
"@types/webpack-virtual-modules": "^0",
"unbuild": "^0.3.2"
}
}

View File

@ -23,7 +23,7 @@ function baseConfig (ctx: WebpackConfigContext) {
ctx.config = {
name: ctx.name,
entry: { app: [resolve(options.buildDir, 'entry')] },
entry: { app: [resolve(options.appDir, 'entry')] },
module: { rules: [] },
plugins: [],
externals: [],

View File

@ -5,6 +5,7 @@ import webpack from 'webpack'
import Glob from 'glob'
import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import VirtualModulesPlugin from 'webpack-virtual-modules'
import consola from 'consola'
import type { Compiler, Watching } from 'webpack'
@ -25,6 +26,7 @@ class WebpackBundler {
// TODO: change this when pify has better types https://github.com/sindresorhus/pify/pull/76
devMiddleware: Record<string, Function & { close?: () => Promise<void>, context?: WebpackDevMiddlewareContext }>
hotMiddleware: Record<string, Function>
virtualModules: VirtualModulesPlugin
mfs?: Compiler['outputFileSystem']
__closed?: boolean
@ -46,6 +48,20 @@ class WebpackBundler {
if (this.nuxt.options.dev) {
this.mfs = createMFS() as Compiler['outputFileSystem']
}
// Initialize virtual modules instance
this.virtualModules = new VirtualModulesPlugin(nuxt.vfs)
const writeFiles = () => {
for (const filePath in nuxt.vfs) {
this.virtualModules.writeModule(filePath, nuxt.vfs[filePath])
}
}
// Workaround to initialize virtual modules
nuxt.hook('build:compile', ({ compiler }) => {
if (compiler.name === 'server') { writeFiles() }
})
// Update virtual modules when templates are updated
nuxt.hook('app:templatesGenerated', writeFiles)
}
getWebpackConfig (name) {
@ -95,6 +111,10 @@ class WebpackBundler {
// Configure compilers
this.compilers = webpackConfigs.map((config) => {
// Support virtual modules (input)
config.plugins.push(this.virtualModules)
// Create compiler
const compiler = webpack(config)
// In dev, write files in memory FS

View File

@ -1473,6 +1473,7 @@ __metadata:
"@types/webpack-bundle-analyzer": ^4.4.1
"@types/webpack-dev-middleware": ^5.0.1
"@types/webpack-hot-middleware": ^2.25.5
"@types/webpack-virtual-modules": ^0
"@vue/babel-preset-jsx": ^1.2.4
"@vue/compiler-sfc": ^3.1.4
babel-loader: ^8.2.2
@ -1504,6 +1505,7 @@ __metadata:
webpack-bundle-analyzer: ^4.4.2
webpack-dev-middleware: ^5.0.0
webpack-hot-middleware: ^2.25.0
webpack-virtual-modules: ^0.4.3
webpackbar: ^5.0.0-3
languageName: unknown
linkType: soft
@ -2136,6 +2138,15 @@ __metadata:
languageName: node
linkType: hard
"@types/webpack-virtual-modules@npm:^0":
version: 0.1.1
resolution: "@types/webpack-virtual-modules@npm:0.1.1"
dependencies:
"@types/webpack": ^4
checksum: b84900c1c0638979773c9d8574fa67739e2c7bc0919a720c05eb3a4768e7447572463df9e2a68b2f7261ef6d4af6b4600301f81f05bb1ce24c8d34940f98a760
languageName: node
linkType: hard
"@types/webpack@npm:^4":
version: 4.41.30
resolution: "@types/webpack@npm:4.41.30"
@ -4727,9 +4738,9 @@ __metadata:
linkType: hard
"electron-to-chromium@npm:^1.3.723":
version: 1.3.775
resolution: "electron-to-chromium@npm:1.3.775"
checksum: 6e62b08d9a6d8a7304984cb1e6557346fad497079ba2719cf604125130ef7f0be33ea46046fbedb077cec83e185e1daddf9fde6dfe5ba189a8caafc195349ce8
version: 1.3.776
resolution: "electron-to-chromium@npm:1.3.776"
checksum: 9cdf721325f3c4cdecef157b12395c3749c9539c90320d63030d1fb860670c73e1433b90eddae4b3cf93e1628e5f5cbd9ea42ab615bc3fa6dc4d5215cac47579
languageName: node
linkType: hard
@ -10320,8 +10331,8 @@ fsevents@~2.3.2:
linkType: hard
"rollup@npm:^2.38.5, rollup@npm:^2.52.0, rollup@npm:^2.53.1":
version: 2.53.1
resolution: "rollup@npm:2.53.1"
version: 2.53.2
resolution: "rollup@npm:2.53.2"
dependencies:
fsevents: ~2.3.2
dependenciesMeta:
@ -10329,7 +10340,7 @@ fsevents@~2.3.2:
optional: true
bin:
rollup: dist/bin/rollup
checksum: 7c63a7251e87715795a4141371f4f7b31ce46c7dd8273a0f4b5240977eb7ecaddd1e8d0fdaf8eabfc7cf73075cc9d83e03c67b89e56f31a2e85cf1089e9fc34f
checksum: 41d32b1d44066f3e8eeb5239013397e3df74dbb37489aaa77f8357059b5998cb4af0108fb72b048d265ae5d88de933b0732007c07e5d5687f5ee3ae44fab9eee
languageName: node
linkType: hard
@ -12149,6 +12160,13 @@ fsevents@~2.3.2:
languageName: node
linkType: hard
"webpack-virtual-modules@npm:^0.4.3":
version: 0.4.3
resolution: "webpack-virtual-modules@npm:0.4.3"
checksum: 9d25285c2fcbfb54515e4fc0e12eb6261ad9565a6eb36e679e5277a0a4728425c59d75ae81f3d1536c73a6d446cb33338153b9e61b4f6c2c04c04394e57f06ff
languageName: node
linkType: hard
"webpack@npm:^5, webpack@npm:^5.1.0, webpack@npm:^5.38.1, webpack@npm:^5.44.0":
version: 5.44.0
resolution: "webpack@npm:5.44.0"