From cc212cedc88a7a5fb834ccb50266eba68260ed92 Mon Sep 17 00:00:00 2001 From: pooya parsa Date: Wed, 28 Jul 2021 13:35:24 +0200 Subject: [PATCH] refactor: improved template utils (#393) --- packages/cli/src/commands/dev.ts | 2 +- packages/components/src/module.ts | 10 +- packages/kit/src/module/container.ts | 53 ++++--- packages/kit/src/module/utils.ts | 200 +++++++++++++-------------- packages/kit/src/types/module.ts | 20 --- packages/kit/src/types/nuxt.ts | 28 ++-- packages/nuxt3/src/app.ts | 67 +++------ packages/pages/src/module.ts | 71 +++++----- 8 files changed, 219 insertions(+), 232 deletions(-) diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 6f3e87c159..3aab8dc535 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -59,7 +59,7 @@ export default defineNuxtCommand({ const dLoad = debounce(load, 250) const watcher = chokidar.watch([rootDir], { ignoreInitial: true, depth: 1 }) watcher.on('all', (_event, file) => { - if (file.includes('nuxt.config') || file.includes('modules')) { + if (file.includes('nuxt.config') || file.includes('modules') || file.includes('pages')) { dLoad(true) } }) diff --git a/packages/components/src/module.ts b/packages/components/src/module.ts index a0e88a75db..94232c1860 100644 --- a/packages/components/src/module.ts +++ b/packages/components/src/module.ts @@ -63,15 +63,17 @@ export default defineNuxtModule({ } app.templates.push({ - path: 'components.mjs', + filename: 'components.mjs', src: resolve(__dirname, 'runtime/components.tmpl.mjs'), - data: { components } + options: { components } }) + app.templates.push({ - path: 'components.d.ts', + filename: 'components.d.ts', src: resolve(__dirname, 'runtime/components.tmpl.d.ts'), - data: { components } + options: { components } }) + app.plugins.push({ src: '#build/components' }) }) diff --git a/packages/kit/src/module/container.ts b/packages/kit/src/module/container.ts index 9a8c0dddbd..ba2c029e82 100644 --- a/packages/kit/src/module/container.ts +++ b/packages/kit/src/module/container.ts @@ -1,13 +1,8 @@ -import type { Nuxt } from '../types/nuxt' -import { - addTemplate, - addErrorLayout, - addLayout, - addPlugin, - addServerMiddleware, - extendBuild, - extendRoutes -} from './utils' +import path from 'upath' +import consola from 'consola' +import type { Nuxt, NuxtPluginTemplate, NuxtTemplate } from '../types/nuxt' +import { chainFn } from '../utils/task' +import { addTemplate, addPluginTemplate, addServerMiddleware } from './utils' import { installModule } from './install' /** Legacy ModuleContainer for backwards compatibility with existing Nuxt 2 modules. */ @@ -39,7 +34,7 @@ export function createModuleContainer (nuxt: Nuxt) { addTemplate, /** - * Registers a plugin using `addTemplate` and prepends it to the plugins[] array. + * Registers a plugin template and prepends it to the plugins[] array. * * Note: You can use mode or .client and .server modifiers with fileName option * to use plugin only in client or server side. @@ -56,26 +51,52 @@ export function createModuleContainer (nuxt: Nuxt) { * }) * ``` */ - addPlugin, + addPlugin (pluginTemplate: NuxtPluginTemplate): NuxtPluginTemplate { + return addPluginTemplate(pluginTemplate) + }, /** Register a custom layout. If its name is 'error' it will override the default error layout. */ - addLayout, + addLayout (tmpl: NuxtTemplate, name: string) { + const { filename, src } = addTemplate(tmpl) + const layoutName = name || path.parse(src).name + const layout = nuxt.options.layouts[layoutName] + + if (layout) { + consola.warn(`Duplicate layout registration, "${layoutName}" has been registered as "${layout}"`) + } + + // Add to nuxt layouts + nuxt.options.layouts[layoutName] = `./${filename}` + + // If error layout, set ErrorPage + if (name === 'error') { + this.addErrorLayout(filename) + } + }, /** * Set the layout that will render Nuxt errors. It should already have been added via addLayout or addTemplate. * * @param dst - Path to layout file within the buildDir (`.nuxt/.vue`) */ - addErrorLayout, + addErrorLayout (dst: string) { + const relativeBuildDir = path.relative(nuxt.options.rootDir, nuxt.options.buildDir) + nuxt.options.ErrorPage = `~/${relativeBuildDir}/${dst}` + }, /** Adds a new server middleware to the end of the server middleware array. */ addServerMiddleware, /** Allows extending webpack build config by chaining `options.build.extend` function. */ - extendBuild, + extendBuild (fn) { + // @ts-ignore + nuxt.options.build.extend = chainFn(nuxt.options.build.extend, fn) + }, /** Allows extending routes by chaining `options.build.extendRoutes` function. */ - extendRoutes, + extendRoutes (fn) { + nuxt.options.router.extendRoutes = chainFn(nuxt.options.router.extendRoutes, fn) + }, /** `requireModule` is a shortcut for `addModule` */ requireModule: installModule, diff --git a/packages/kit/src/module/utils.ts b/packages/kit/src/module/utils.ts index c0fd824809..6f15b0948d 100644 --- a/packages/kit/src/module/utils.ts +++ b/packages/kit/src/module/utils.ts @@ -1,144 +1,144 @@ import fs from 'fs' -import path, { basename, parse } from 'upath' +import { basename, parse, resolve } from 'upath' import hash from 'hash-sum' -import consola from 'consola' import type { WebpackPluginInstance, Configuration as WebpackConfig } from 'webpack' import type { Plugin as VitePlugin, UserConfig as ViteConfig } from 'vite' import { useNuxt } from '../nuxt' -import { chainFn } from '../utils/task' -import type { TemplateOpts, PluginTemplateOpts } from '../types/module' +import type { NuxtTemplate, NuxtPlugin, NuxtPluginTemplate } from '../types/nuxt' /** - * Renders given template using lodash template during build into the project buildDir (`.nuxt`). - * - * If a fileName is not provided or the template is string, target file name defaults to - * [dirName].[fileName].[pathHash].[ext]. - * + * Renders given template using lodash template during build into the project buildDir */ -export function addTemplate (tmpl: TemplateOpts | string) { +export function addTemplate (_template: NuxtTemplate | string) { const nuxt = useNuxt() - if (!tmpl) { - throw new Error('Invalid tmpl: ' + JSON.stringify(tmpl)) - } + // Noprmalize template + const template = normalizeTemplate(_template) - // Validate & parse source - const src = typeof tmpl === 'string' ? tmpl : tmpl.src - const srcPath = parse(src) + // Remove any existing template with the same filename + nuxt.options.build.templates = nuxt.options.build.templates + .filter(p => normalizeTemplate(p).filename !== template.filename) - if (typeof src !== 'string' || !fs.existsSync(src)) { - throw new Error('tmpl src not found: ' + src) - } + // Add to templates array + nuxt.options.build.templates.push(template) - // Mostly for DX, some people prefer `filename` vs `fileName` - const fileName = typeof tmpl === 'string' ? '' : tmpl.fileName || tmpl.filename - // Generate unique and human readable dst filename if not provided - const dst = fileName || `${basename(srcPath.dir)}.${srcPath.name}.${hash(src)}${srcPath.ext}` - // Add to tmpls list - const tmplObj = { - src, - dst, - options: typeof tmpl === 'string' ? undefined : tmpl.options - } - - nuxt.options.build.templates.push(tmplObj) - - return tmplObj + return template } /** - * Registers a plugin using `addTemplate` and prepends it to the plugins[] array. + * Normalize a nuxt template object + */ +export function normalizeTemplate (template: NuxtTemplate | string): NuxtTemplate { + if (!template) { + throw new Error('Invalid template: ' + JSON.stringify(template)) + } + + // Normalize + if (typeof template === 'string') { + template = { src: template } + } + + // Use src if provided + if (template.src) { + if (!fs.existsSync(template.src)) { + throw new Error('Template not found: ' + template.src) + } + if (!template.filename) { + const srcPath = parse(template.src) + template.filename = template.fileName || + `${basename(srcPath.dir)}.${srcPath.name}.${hash(template.src)}${srcPath.ext}` + } + } + + if (!template.src && !template.getContents) { + throw new Error('Invalid template. Either getContents or src options should be provided: ' + JSON.stringify(template)) + } + + if (!template.filename) { + throw new Error('Invalid template. Either filename should be provided: ' + JSON.stringify(template)) + } + + // Resolve dst + if (!template.dst) { + const nuxt = useNuxt() + template.dst = resolve(nuxt.options.buildDir, template.filename) + } + + return template +} + +/** + * Normalize a nuxt plugin object + */ +export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin { + // Normalize src + if (typeof plugin === 'string') { + plugin = { src: plugin } + } + if (!plugin.src) { + throw new Error('Invalid plugin. src option is required: ' + JSON.stringify(plugin)) + } + + // Normalize mode + if (plugin.ssr) { + plugin.mode = 'server' + } + if (!plugin.mode) { + const [, mode = 'all'] = plugin.src.match(/\.(server|client)(\.\w+)*$/) || [] + plugin.mode = mode as 'all' | 'client' | 'server' + } + + return plugin +} + +/** + * Registers a nuxt plugin and to the plugins array. * * Note: You can use mode or .client and .server modifiers with fileName option * to use plugin only in client or server side. * - * If you choose to specify a fileName, you can configure a custom path for the - * fileName too, so you can choose the folder structure inside .nuxt folder in - * order to prevent name collisioning: + * Note: By default plugin is prepended to the plugins array. You can use second argument to append (push) instead. * * @example * ```js * addPlugin({ * src: path.resolve(__dirname, 'templates/foo.js'), - * fileName: 'foo.server.js' // [optional] only include in server bundle + * filename: 'foo.server.js' // [optional] only include in server bundle * }) * ``` */ -export function addPlugin (tmpl: PluginTemplateOpts) { +export interface AddPluginOptions { append?: Boolean } +export function addPlugin (_plugin: NuxtPlugin | string, opts: AddPluginOptions = {}) { const nuxt = useNuxt() - const { dst } = addTemplate(tmpl) + // Normalize plugin + const plugin = normalizePlugin(_plugin) - if (!tmpl.mode && typeof tmpl.ssr === 'boolean') { - tmpl.mode = tmpl.ssr ? 'server' : 'client' - } + // Remove any existing plugin with the same src + nuxt.options.plugins = nuxt.options.plugins.filter(p => normalizePlugin(p).src !== plugin.src) - // Add to nuxt plugins - nuxt.options.plugins.unshift({ - src: path.join(nuxt.options.buildDir, dst), - mode: tmpl.mode - }) -} + // Prepend to array by default to be before user provided plugins since is usually used by modules + nuxt.options.plugins[opts.append ? 'push' : 'unshift'](plugin) -/** Register a custom layout. If its name is 'error' it will override the default error layout. */ -export function addLayout (tmpl: TemplateOpts, name: string) { - const nuxt = useNuxt() - - const { dst, src } = addTemplate(tmpl) - const layoutName = name || path.parse(src).name - const layout = nuxt.options.layouts[layoutName] - - if (layout) { - consola.warn(`Duplicate layout registration, "${layoutName}" has been registered as "${layout}"`) - } - - // Add to nuxt layouts - nuxt.options.layouts[layoutName] = `./${dst}` - - // If error layout, set ErrorPage - if (name === 'error') { - addErrorLayout(dst) - } + return plugin } /** - * Set the layout that will render Nuxt errors. It should already have been added via addLayout or addTemplate. - * - * @param dst - Path to layout file within the buildDir (`.nuxt/.vue`) + * Adds a template and registers as a nuxt plugin. */ -export function addErrorLayout (dst: string) { - const nuxt = useNuxt() - - const relativeBuildDir = path.relative(nuxt.options.rootDir, nuxt.options.buildDir) - nuxt.options.ErrorPage = `~/${relativeBuildDir}/${dst}` +export function addPluginTemplate (plugin: NuxtPluginTemplate | string, opts: AddPluginOptions = {}): NuxtPluginTemplate { + if (typeof plugin === 'string') { + plugin = { src: plugin } + } + if (!plugin.src) { + plugin.src = addTemplate(plugin).dst + } + return addPlugin(plugin, opts) } /** Adds a new server middleware to the end of the server middleware array. */ export function addServerMiddleware (middleware) { - const nuxt = useNuxt() - - nuxt.options.serverMiddleware.push(middleware) -} - -/** - * Allows extending webpack build config by chaining `options.build.extend` function. - * - * @deprecated use extendWebpackConfig() instead - */ -export function extendBuild (fn) { - const nuxt = useNuxt() - - // @ts-ignore TODO - nuxt.options.build.extend = chainFn(nuxt.options.build.extend, fn) -} - -/** - * Allows extending routes by chaining `options.build.extendRoutes` function. - */ -export function extendRoutes (fn) { - const nuxt = useNuxt() - - nuxt.options.router.extendRoutes = chainFn(nuxt.options.router.extendRoutes, fn) + useNuxt().options.serverMiddleware.push(middleware) } export interface ExtendConfigOptions { diff --git a/packages/kit/src/types/module.ts b/packages/kit/src/types/module.ts index 945d21da3a..648c469c2e 100644 --- a/packages/kit/src/types/module.ts +++ b/packages/kit/src/types/module.ts @@ -42,23 +42,3 @@ export type ModuleInstallOptions = ModuleSrc | [ModuleSrc, ModuleOptions?] | Partial - -// -- Templates -- - -export interface TemplateOpts { - /** The target filename once the template is copied into the Nuxt buildDir */ - filename?: string - /** The target filename once the template is copied into the Nuxt buildDir */ - fileName?: string - /** An options object that will be accessible within the template via `<% options %>` */ - options?: Record - /** The resolved path to the source file to be templated */ - src: string -} - -export interface PluginTemplateOpts extends TemplateOpts { - /** @deprecated use mode */ - ssr?: boolean - /** Whether the plugin will be loaded on only server-side, only client-side or on both. */ - mode?: 'all' | 'server' | 'client' -} diff --git a/packages/kit/src/types/nuxt.ts b/packages/kit/src/types/nuxt.ts index 305a9e1743..2f871cc324 100644 --- a/packages/kit/src/types/nuxt.ts +++ b/packages/kit/src/types/nuxt.ts @@ -25,17 +25,26 @@ export interface Nuxt { vfs: Record } -export interface NuxtPlugin { - src: string - mode?: 'server' | 'client' | 'all', +export interface NuxtTemplate { + /** @deprecated filename */ + fileName?: string + /** resolved output file path (generated) */ + dst?: string + /** The target filename once the template is copied into the Nuxt buildDir */ + filename?: string + /** An options object that will be accessible within the template via `<% options %>` */ options?: Record + /** The resolved path to the source file to be template */ + src?: string + /** Provided compile option intead of src */ + getContents?: (data: Record) => string | Promise } -export interface NuxtTemplate { - path: string // Relative path of destination - src?: string // Absolute path to source file - compile?: (data: Record) => string - data?: any +export interface NuxtPlugin { + /** @deprecated use mode */ + ssr?: boolean + src: string + mode?: 'all' | 'server' | 'client' } export interface NuxtApp { @@ -45,3 +54,6 @@ export interface NuxtApp { plugins: NuxtPlugin[] templates: NuxtTemplate[] } + +type _TemplatePlugin = NuxtPlugin & NuxtTemplate +export interface NuxtPluginTemplate extends _TemplatePlugin {} diff --git a/packages/nuxt3/src/app.ts b/packages/nuxt3/src/app.ts index de1bfd651f..a32ce8e254 100644 --- a/packages/nuxt3/src/app.ts +++ b/packages/nuxt3/src/app.ts @@ -2,7 +2,7 @@ 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 { tryResolvePath, resolveFiles, Nuxt, NuxtApp, NuxtTemplate, normalizePlugin, normalizeTemplate } from '@nuxt/kit' import { readFile } from 'fs-extra' import * as templateUtils from './template.utils' @@ -11,7 +11,7 @@ export function createApp (nuxt: Nuxt, options: Partial = {}): NuxtApp dir: nuxt.options.srcDir, extensions: nuxt.options.extensions, plugins: [], - templates: {} + templates: [] } as NuxtApp) } @@ -19,39 +19,31 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp) { // Resolve app await resolveApp(nuxt, app) - // Scan templates + // Scan app templates const templatesDir = join(nuxt.options.appDir, '_templates') const templateFiles = await globby(join(templatesDir, '/**')) app.templates = templateFiles .filter(src => !src.endsWith('.d.ts')) - .map(src => ({ - src, - path: relative(templatesDir, src), - data: {} - } as NuxtTemplate)) + .map(src => ({ src, filename: relative(templatesDir, src) } as NuxtTemplate)) - // Custom templates (created with addTemplate) - const customTemplates = nuxt.options.build.templates.map(t => ({ - path: t.dst, - src: t.src, - data: { - options: t.options - } - })) - app.templates = app.templates.concat(customTemplates) + // User templates from options.build.templates + app.templates = app.templates.concat(nuxt.options.build.templates) - // Extend templates + // Extend templates with hook await nuxt.callHook('app:templates', app) + // Normalize templates + app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl)) + // 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) + const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename) nuxt.vfs[fullPath] = contents - const aliasPath = '#build/' + template.path.replace(/\.\w+$/, '') + const aliasPath = '#build/' + template.filename.replace(/\.\w+$/, '') nuxt.vfs[aliasPath] = contents })) @@ -76,41 +68,26 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) { // Resolve plugins app.plugins = [ ...nuxt.options.plugins, - ...await resolvePlugins(nuxt) - ] + ...await resolveFiles(nuxt.options.srcDir, 'plugins/**/*.{js,ts,mjs,cjs}') + ].map(plugin => normalizePlugin(plugin)) // Extend app await nuxt.callHook('app:resolve', app) } -async function compileTemplate (tmpl: NuxtTemplate, ctx: any) { - const data = { ...ctx, ...tmpl.data } - if (tmpl.src) { +async function compileTemplate (template: NuxtTemplate, ctx: any) { + const data = { ...ctx, ...template.options } + if (template.src) { try { - const srcContents = await readFile(tmpl.src, 'utf-8') + const srcContents = await readFile(template.src, 'utf-8') return lodashTemplate(srcContents, {})(data) } catch (err) { - console.error('Error compiling template: ', tmpl) + console.error('Error compiling template: ', template) throw err } } - if (tmpl.compile) { - return tmpl.compile(data) + if (template.getContents) { + return template.getContents(data) } - throw new Error('Invalid template:' + tmpl) -} - -async function resolvePlugins (nuxt: Nuxt) { - const plugins = await resolveFiles(nuxt.options.srcDir, 'plugins/**/*.{js,ts}') - - return plugins.map(src => ({ - src, - mode: getPluginMode(src) - }) - ) -} - -function getPluginMode (src: string) { - const [, mode = 'all'] = src.match(/\.(server|client)(\.\w+)*$/) || [] - return mode as NuxtPlugin['mode'] + throw new Error('Invalid template: ' + JSON.stringify(template)) } diff --git a/packages/pages/src/module.ts b/packages/pages/src/module.ts index 5bb7f518be..2be310778a 100644 --- a/packages/pages/src/module.ts +++ b/packages/pages/src/module.ts @@ -1,65 +1,60 @@ import { existsSync } from 'fs' -import { defineNuxtModule } from '@nuxt/kit' +import { defineNuxtModule, addTemplate, addPlugin } from '@nuxt/kit' import { resolve } from 'upath' import { resolveLayouts, resolvePagesRoutes } from './utils' export default defineNuxtModule({ name: 'router', setup (_options, nuxt) { - const runtimeDir = resolve(__dirname, 'runtime') const pagesDir = resolve(nuxt.options.srcDir, nuxt.options.dir.pages) - const routerPlugin = resolve(runtimeDir, 'router') + const runtimeDir = resolve(__dirname, 'runtime') + // Disable module if pages dir do not exists + if (!existsSync(pagesDir)) { + return + } + + // Regenerate templates when adding or removing pages nuxt.hook('builder:watch', async (event, path) => { - // Regenerate templates when adding or removing pages (plugin and routes) const pathPattern = new RegExp(`^(${nuxt.options.dir.pages}|${nuxt.options.dir.layouts})/`) if (event !== 'change' && path.match(pathPattern)) { await nuxt.callHook('builder:generateApp') } }) + // Add default layout for pages nuxt.hook('app:resolve', (app) => { - if (!existsSync(pagesDir)) { - return - } - app.plugins.push({ src: routerPlugin }) if (app.main.includes('app.tutorial')) { app.main = resolve(runtimeDir, 'app.vue') } }) - nuxt.hook('app:templates', async (app) => { - if (!existsSync(pagesDir)) { - return + // Add router plguin + addPlugin(resolve(runtimeDir, 'router')) + + // Add routes template + addTemplate({ + filename: 'routes.mjs', + async getContents () { + const routes = await resolvePagesRoutes(nuxt) + const serializedRoutes = routes.map(route => ({ ...route, component: `{() => import('${route.file}')}` })) + return `export default ${JSON.stringify(serializedRoutes, null, 2).replace(/"{(.+)}"/g, '$1')}` } + }) - // Resolve routes - const routes = await resolvePagesRoutes(nuxt) - - // Add routes.js - app.templates.push({ - path: 'routes.js', - compile: () => { - const serializedRoutes = routes.map(route => ({ ...route, component: `{() => import('${route.file}')}` })) - return `export default ${JSON.stringify(serializedRoutes, null, 2).replace(/"{(.+)}"/g, '$1')}` - } - }) - - const layouts = await resolveLayouts(nuxt) - - // Add routes.js - app.templates.push({ - path: 'layouts.js', - compile: () => { - const layoutsObject = Object.fromEntries(layouts.map(({ name, file }) => { - return [name, `{defineAsyncComponent({ suspensible: false, loader: () => import('${file}') })}`] - })) - return [ - 'import { defineAsyncComponent } from \'vue\'', - `export default ${JSON.stringify(layoutsObject, null, 2).replace(/"{(.+)}"/g, '$1')} - `].join('\n') - } - }) + // Add layouts template + addTemplate({ + filename: 'layouts.mjs', + async getContents () { + const layouts = await resolveLayouts(nuxt) + const layoutsObject = Object.fromEntries(layouts.map(({ name, file }) => { + return [name, `{defineAsyncComponent({ suspensible: false, loader: () => import('${file}') })}`] + })) + return [ + 'import { defineAsyncComponent } from \'vue\'', + `export default ${JSON.stringify(layoutsObject, null, 2).replace(/"{(.+)}"/g, '$1')}` + ].join('\n') + } }) } })