feat: optional pages and refactor nuxt3 (#142)

This commit is contained in:
Pooya Parsa 2021-05-20 13:42:41 +02:00
parent d95e002d5b
commit 6b62d456d7
42 changed files with 355 additions and 570 deletions

View File

@ -0,0 +1 @@
export { default } from '<%= app.main %>'

View File

@ -1,28 +0,0 @@
<%= nuxtOptions.vite ? "import('vite/dynamic-import-polyfill')" : '' %>
import { createSSRApp, nextTick } from 'vue'
import { createNuxt, applyPlugins } from '@nuxt/app'
import plugins from './plugins'
import clientPlugins from './plugins.client'
import App from '<%= app.main %>'
async function initApp () {
const app = createSSRApp(App)
const nuxt = createNuxt({ app })
await applyPlugins(nuxt, plugins)
await applyPlugins(nuxt, clientPlugins)
await nuxt.hooks.callHook('app:created', app)
await nuxt.hooks.callHook('app:beforeMount', app)
app.mount('#__nuxt')
await nuxt.hooks.callHook('app:mounted', app)
await nextTick()
nuxt.isHydrating = false
}
initApp().catch((error) => {
console.error('Error while mounting app:', error) // eslint-disable-line no-console
})

View File

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

View File

@ -1,18 +0,0 @@
import { createApp } from 'vue'
import { createNuxt, applyPlugins } from '@nuxt/app'
import plugins from './plugins'
import serverPlugins from './plugins.server'
import App from '<%= app.main %>'
export default async function createNuxtAppServer (ssrContext = {}) {
const app = createApp(App)
const nuxt = createNuxt({ app, ssrContext })
await applyPlugins(nuxt, plugins)
await applyPlugins(nuxt, serverPlugins)
await nuxt.hooks.callHook('app:created', app)
return app
}

View File

@ -1,20 +0,0 @@
import { $fetch } from 'ohmyfetch'
import logs from '#app/plugins/logs.client.dev'
import progress from '#app/plugins/progress.client'
<% const plugins = app.plugins.filter(p => p.mode === 'client').map(p => p.src) %>
<%= nxt.importSources(plugins) %>
if (!globalThis.$fetch) {
globalThis.$fetch = $fetch
}
const plugins = [
progress,
<%= plugins.map(nxt.importName).join(',\n\t') %>
]
if (process.dev) {
plugins.push(logs)
}
export default plugins

View File

@ -1,15 +1,23 @@
import head from '#app/plugins/head' import head from '#app/plugins/head'
import router from '#app/plugins/router'
import vuex from '#app/plugins/vuex'
import legacy from '#app/plugins/legacy' import legacy from '#app/plugins/legacy'
import preload from '#app/plugins/preload.server'
<% const plugins = app.plugins.filter(p => p.mode === 'all').map(p => p.src) %> <%= utils.importSources(app.plugins.map(p => p.src)) %>
<%= nxt.importSources(plugins) %>
export default [ const commonPlugins = [
head, head,
router,
vuex,
legacy, legacy,
<%= plugins.map(nxt.importName).join(',\n\t') %> <%= app.plugins.filter(p => !p.mode || p.mode === 'all').map(p => utils.importName(p.src)).join(',\n ') %>
] ]
export const clientPluigns = [
...commonPlugins,<%= app.plugins.filter(p => p.mode === 'client').map(p => utils.importName(p.src)).join(',\n ') %>
]
export const serverPluigns = [
...commonPlugins,
preload,
<%= app.plugins.filter(p => p.mode === 'server').map(p => utils.importName(p.src)).join(',\n ') %>
]
export default process.client ? clientPluigns : serverPluigns

View File

@ -1,8 +0,0 @@
import preload from '#app/plugins/preload.server'
<% const plugins = app.plugins.filter(p => p.mode === 'server').map(p => p.src) %>
<%= nxt.importSources(plugins) %>
export default [
preload
<%= plugins.map(nxt.importName).join(',\n\t') %>
]

View File

@ -1,2 +0,0 @@
// TODO: Use webpack-virtual-modules
export default <%= nxt.serialize(app.routes.map(nxt.serializeRoute)) %>

View File

@ -6,8 +6,8 @@
<body {{ BODY_ATTRS }}> <body {{ BODY_ATTRS }}>
{{ BODY_SCRIPTS_PREPEND }} {{ BODY_SCRIPTS_PREPEND }}
<div id="__nuxt">{{ APP }}</div> <div id="__nuxt">{{ APP }}</div>
<% if (nuxtOptions.vite && nuxtOptions.dev) { %><script type="module" src="/@vite/client"></script> <% if (nuxt.options.vite && nuxt.options.dev) { %><script type="module" src="/@vite/client"></script>
<script type="module" src="/entry.client.mjs"></script><% } %> <script type="module" src="/entry.mjs"></script><% } %>
{{ BODY_SCRIPTS }} {{ BODY_SCRIPTS }}
</body> </body>
</html> </html>

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Server error</title>
<meta charset="utf-8">
<meta content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" name=viewport>
<style>
.__nuxt-error-page{padding: 1rem;background:#f7f8fb;color:#47494e;text-align:center;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;font-family:sans-serif;font-weight:100!important;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-font-smoothing:antialiased;position:absolute;top:0;left:0;right:0;bottom:0}.__nuxt-error-page .error{max-width:450px}.__nuxt-error-page .title{font-size:24px;font-size:1.5rem;margin-top:15px;color:#47494e;margin-bottom:8px}.__nuxt-error-page .description{color:#7f828b;line-height:21px;margin-bottom:10px}.__nuxt-error-page a{color:#7f828b!important;text-decoration:none}.__nuxt-error-page .logo{position:fixed;left:12px;bottom:12px}
</style>
</head>
<body>
<div class="__nuxt-error-page">
<div class="error">
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="#DBE1EC" viewBox="0 0 48 48"><path d="M22 30h4v4h-4zm0-16h4v12h-4zm1.99-10C12.94 4 4 12.95 4 24s8.94 20 19.99 20S44 35.05 44 24 35.04 4 23.99 4zM24 40c-8.84 0-16-7.16-16-16S15.16 8 24 8s16 7.16 16 16-7.16 16-16 16z"/></svg>
<div class="title">Server error</div>
<div class="description">{{ message }}</div>
</div>
<div class="logo">
<a href="https://nuxtjs.org" target="_blank" rel="noopener">Nuxt.js</a>
</div>
</div>
</body>
</html>

View File

@ -1,3 +0,0 @@
<template>
<router-view />
</template>

47
packages/app/src/entry.ts Normal file
View File

@ -0,0 +1,47 @@
import { createSSRApp, createApp, nextTick } from 'vue'
import { createNuxt, applyPlugins } from '@nuxt/app'
// @ts-ignore
import plugins from '#build/plugins'
// @ts-ignore
import App from '#build/app'
let entry: Function
if (process.server) {
entry = async function createNuxtAppServer (ssrContext = {}) {
const app = createApp(App)
const nuxt = createNuxt({ app, ssrContext })
await applyPlugins(nuxt, plugins)
await nuxt.hooks.callHook('app:created', app)
return app
}
}
if (process.client) {
entry = async function initApp () {
const app = createSSRApp(App)
const nuxt = createNuxt({ app })
await applyPlugins(nuxt, plugins)
await nuxt.hooks.callHook('app:created', app)
await nuxt.hooks.callHook('app:beforeMount', app)
app.mount('#__nuxt')
await nuxt.hooks.callHook('app:mounted', app)
await nextTick()
nuxt.isHydrating = false
}
entry().catch((error) => {
console.error('Error while mounting app:', error) // eslint-disable-line no-console
})
}
export default ctx => entry(ctx)

View File

@ -1 +0,0 @@
export * from './index.ts'

View File

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

View File

@ -1,4 +1,4 @@
import { Nuxt } from './nuxt' import { Nuxt, NuxtApp } from './nuxt'
import type { IncomingMessage, ServerResponse } from 'http' import type { IncomingMessage, ServerResponse } from 'http'
import type { Compiler, Configuration, Stats } from 'webpack' import type { Compiler, Configuration, Stats } from 'webpack'
import type { NuxtConfig, NuxtOptions } from '..' import type { NuxtConfig, NuxtOptions } from '..'
@ -23,6 +23,11 @@ export interface NuxtHooks {
// Don't break usage of untyped hooks // Don't break usage of untyped hooks
[key: string]: (...args: any[]) => HookResult [key: string]: (...args: any[]) => HookResult
// nuxt3
'app:resolve': (app: NuxtApp) => HookResult
'app:templates': (app: NuxtApp) => HookResult
'builder:generateApp': () => HookResult
// @nuxt/builder // @nuxt/builder
'build:before': 'build:before':
(builder: Builder, buildOptions: NuxtOptions['build']) => HookResult (builder: Builder, buildOptions: NuxtOptions['build']) => HookResult

View File

@ -22,3 +22,24 @@ export interface Nuxt {
/** The production or development server */ /** The production or development server */
server?: any server?: any
} }
export interface NuxtPlugin {
src: string
mode?: 'server' | 'client' | 'all',
options?: Record<string, any>
}
export interface NuxtTemplate {
path: string // Relative path of destination
src?: string // Absolute path to source file
compile?: (data: Record<string, any>) => string
data?: any
}
export interface NuxtApp {
main?: string
dir: string
extensions: string[]
plugins: NuxtPlugin[]
templates: NuxtTemplate[]
}

View File

@ -1,5 +1,6 @@
import { existsSync, lstatSync } from 'fs' import { existsSync, lstatSync } from 'fs'
import { resolve, join } from 'upath' import { resolve, join } from 'upath'
import globby from 'globby'
export interface ResolveOptions { export interface ResolveOptions {
/** /**
@ -93,3 +94,11 @@ export function tryResolvePath (path: string, opts: ResolveOptions = {}) {
} catch (e) { } catch (e) {
} }
} }
export async function resolveFiles (path: string, pattern: string) {
const files = await globby(pattern, {
cwd: path,
followSymbolicLinks: true
})
return files.map(p => resolve(path, p))
}

View File

@ -1,6 +1,6 @@
import consola from 'consola' import consola from 'consola'
const internalRegex = /^\.|\?|\.[mc]?js$|.ts$/ const internalRegex = /^\.|\?|\.[mc]?js$|.ts$|.json$/
export function autoMock () { export function autoMock () {
return { return {

View File

@ -20,6 +20,7 @@
"@nuxt/app": "^0.3.3", "@nuxt/app": "^0.3.3",
"@nuxt/kit": "^0.5.3", "@nuxt/kit": "^0.5.3",
"@nuxt/nitro": "^0.6.3", "@nuxt/nitro": "^0.6.3",
"@nuxt/pages": "^0.1.0",
"@nuxt/vite-builder": "^0.3.3", "@nuxt/vite-builder": "^0.3.3",
"@nuxt/webpack-builder": "^0.3.4", "@nuxt/webpack-builder": "^0.3.4",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",

View File

@ -1,79 +1,103 @@
import { resolve } from 'path' import { resolve, join, relative, dirname } from 'upath'
import globby from 'globby'
import lodashTemplate from 'lodash/template'
import defu from 'defu' import defu from 'defu'
import { tryResolvePath } from '@nuxt/kit' import { tryResolvePath, resolveFiles, Nuxt, NuxtApp, NuxtTemplate, NuxtPlugin } from '@nuxt/kit'
import { Builder } from './builder' import { mkdirp, writeFile, readFile } from 'fs-extra'
import { NuxtRoute, resolvePagesRoutes } from './pages' import * as templateUtils from './template.utils'
import { NuxtPlugin, resolvePlugins } from './plugins'
export interface NuxtApp { export function createApp (nuxt: Nuxt, options: Partial<NuxtApp> = {}): NuxtApp {
main?: string return defu(options, {
routes: NuxtRoute[]
dir: string
extensions: string[]
plugins: NuxtPlugin[]
templates: Record<string, string>
pages?: {
dir: string
}
}
// Scan project structure
export async function createApp (
builder: Builder,
options: Partial<NuxtApp> = {}
): Promise<NuxtApp> {
const { nuxt } = builder
// Create base app object
const app: NuxtApp = defu(options, {
dir: nuxt.options.srcDir, dir: nuxt.options.srcDir,
extensions: nuxt.options.extensions, extensions: nuxt.options.extensions,
routes: [],
plugins: [], plugins: [],
templates: {}, templates: {}
pages: {
dir: 'pages'
}
} as NuxtApp) } as NuxtApp)
}
// Resolve app.main export async function generateApp (nuxt: Nuxt, app: NuxtApp) {
// Resolve app
await resolveApp(nuxt, app)
// Scan 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))
// 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
})))
await nuxt.callHook('app:templatesGenerated', app)
}
export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
const resolveOptions = { const resolveOptions = {
base: nuxt.options.srcDir, base: nuxt.options.srcDir,
alias: nuxt.options.alias, alias: nuxt.options.alias,
extensions: nuxt.options.extensions extensions: nuxt.options.extensions
} }
// Resolve main (app.vue)
if (!app.main) { if (!app.main) {
app.main = tryResolvePath('~/App', resolveOptions) || app.main = tryResolvePath('~/App', resolveOptions) || tryResolvePath('~/app', resolveOptions)
tryResolvePath('~/app', resolveOptions)
} }
if (!app.main) {
// Resolve pages/
if (app.pages) {
app.routes.push(...(await resolvePagesRoutes(builder, app)))
}
if (app.routes.length) {
// Add 404 page is not added
const page404 = app.routes.find(route => route.name === '404')
if (!page404) {
app.routes.push({
name: '404',
path: '/:catchAll(.*)*',
file: resolve(nuxt.options.appDir, 'pages/404.vue'),
children: []
})
}
}
// Fallback app.main
if (!app.main && app.routes.length) {
app.main = resolve(nuxt.options.appDir, 'app.pages.vue')
} else if (!app.main) {
app.main = resolve(nuxt.options.appDir, 'app.tutorial.vue') app.main = resolve(nuxt.options.appDir, 'app.tutorial.vue')
} }
// Resolve plugins/ // Resolve plugins
app.plugins = await resolvePlugins(builder, app) app.plugins = [
...nuxt.options.plugins,
...await resolvePlugins(nuxt)
]
return app // Extend app
await nuxt.callHook('app:resolve', app)
}
async function generateTemplate (tmpl: NuxtTemplate, destDir: string, ctx) {
let compiledSrc: string = ''
const data = { ...ctx, ...tmpl.data }
if (tmpl.src) {
try {
const srcContents = await readFile(tmpl.src, 'utf-8')
compiledSrc = 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)
}
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']
} }

View File

@ -1,117 +1,48 @@
import { join, relative, resolve } from 'upath' import chokidar from 'chokidar'
import fsExtra from 'fs-extra'
import { debounce } from 'lodash'
import { Nuxt } from '@nuxt/kit' import { Nuxt } from '@nuxt/kit'
import { emptyDir } from 'fs-extra'
import { createApp, generateApp } from './app'
import { export async function build (nuxt: Nuxt) {
templateData, // Clear buildDir once
compileTemplates, await emptyDir(nuxt.options.buildDir)
scanTemplates,
NuxtTemplate
} from './template'
import { createWatcher, WatchCallback } from './watch'
import { createApp, NuxtApp } from './app'
import Ignore from './utils/ignore'
export class Builder { const app = createApp(nuxt)
nuxt: Nuxt await generateApp(nuxt, app)
globals: any
ignore: Ignore
templates: NuxtTemplate[]
app: NuxtApp
constructor (nuxt: Nuxt) {
this.nuxt = nuxt
this.ignore = new Ignore({
rootDir: nuxt.options.srcDir,
ignoreArray: nuxt.options.ignore.concat(
relative(nuxt.options.rootDir, nuxt.options.buildDir)
)
})
}
build () {
return _build(this)
}
close () {
// TODO: close watchers
}
}
// Extends VueRouter
async function _build (builder: Builder) {
const { nuxt } = builder
if (!nuxt.options.dev) {
await fsExtra.emptyDir(nuxt.options.buildDir)
}
await fsExtra.emptyDir(resolve(nuxt.options.buildDir, 'dist'))
await generate(builder)
if (nuxt.options.dev) { if (nuxt.options.dev) {
watch(builder) watch(nuxt)
nuxt.hook('builder:watch', async (event, path) => {
if (event !== 'change' && /app|plugins/i.test(path)) {
await generateApp(nuxt, app)
}
})
nuxt.hook('builder:generateApp', () => generateApp(nuxt, app))
} }
await bundle(builder) await bundle(nuxt)
await nuxt.callHook('build:done', { nuxt })
await nuxt.callHook('build:done', builder)
} }
function watch (builder: Builder) { function watch (nuxt: Nuxt) {
const { nuxt, ignore } = builder const watcher = chokidar.watch(nuxt.options.srcDir, {
...nuxt.options.watchers.chokidar,
// Watch internal templates cwd: nuxt.options.srcDir,
const options = nuxt.options.watchers.chokidar ignoreInitial: true,
const nuxtAppWatcher = createWatcher(nuxt.options.appDir, { ...options, cwd: nuxt.options.appDir }, ignore) ignored: [
nuxtAppWatcher.watchAll(debounce(() => compileTemplates(builder.templates, nuxt.options.buildDir), 100)) '.nuxt',
'.output',
// Watch user app 'node_modules'
// TODO: handle multiples app dirs ]
const appPattern = `${builder.app.dir}/**/*{${nuxt.options.extensions.join(',')}}` })
const appWatcher = createWatcher(appPattern, { ...options, cwd: builder.app.dir }, ignore) const watchHook = (event, path) => nuxt.callHook('builder:watch', event, path)
// appWatcher.debug('srcDir') watcher.on('all', watchHook)
const refreshTemplates = debounce(() => generate(builder), 100) nuxt.hook('close', () => watcher.close())
// Watch for App.vue creation return watcher
appWatcher.watch(/^(A|a)pp\.[a-z]{2,3}/, refreshTemplates, ['add', 'unlink'])
// Watch for page changes
appWatcher.watch(new RegExp(`^${nuxt.options.dir.pages}/`), refreshTemplates, ['add', 'unlink'])
// Watch for plugins changes
appWatcher.watch(/^plugins/, refreshTemplates, ['add', 'unlink'])
// Shared Watcher
const watchHook: WatchCallback = (event, path) => builder.nuxt.callHook('builder:watch', event, path)
const watchHookDebounced = debounce(watchHook, 100)
appWatcher.watchAll(watchHookDebounced)
nuxtAppWatcher.watchAll(watchHookDebounced)
} }
export async function generate (builder: Builder) { async function bundle (nuxt: Nuxt) {
const { nuxt } = builder
builder.app = await createApp(builder)
// Todo: Call app:created hook
const templatesDir = join(builder.nuxt.options.appDir, '_templates')
const appTemplates = await scanTemplates(templatesDir, templateData(builder))
// Todo: Call app:templates hook
builder.templates = [...appTemplates]
await compileTemplates(builder.templates, nuxt.options.buildDir)
}
async function bundle ({ nuxt }: Builder) {
// @ts-ignore
const useVite = !!nuxt.options.vite const useVite = !!nuxt.options.vite
const { bundle } = await (useVite ? import('@nuxt/vite-builder') : import('@nuxt/webpack-builder')) const { bundle } = await (useVite ? import('@nuxt/vite-builder') : import('@nuxt/webpack-builder'))
return bundle(nuxt) return bundle(nuxt)
} }
export function getBuilder (nuxt: Nuxt) {
return new Builder(nuxt)
}
export function build (nuxt: Nuxt) {
return getBuilder(nuxt).build()
}

View File

@ -49,6 +49,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
options.appDir = appDir options.appDir = appDir
options._majorVersion = 3 options._majorVersion = 3
options.alias.vue = require.resolve('vue/dist/vue.esm-bundler.js') options.alias.vue = require.resolve('vue/dist/vue.esm-bundler.js')
options.buildModules.push(require.resolve('@nuxt/pages/module'))
const nuxt = createNuxt(options) const nuxt = createNuxt(options)

View File

@ -1,26 +0,0 @@
import { NuxtApp } from './app'
import { Builder } from './builder'
import { resolveFiles } from './utils'
export interface NuxtPlugin {
src: string
mode: 'server' | 'client' | 'all'
}
const MODES_REGEX = /\.(server|client)(\.\w+)*$/
const getPluginMode = (src: string) => {
const [, mode = 'all'] = src.match(MODES_REGEX) || []
return mode as NuxtPlugin['mode']
}
export async function resolvePlugins (builder: Builder, app: NuxtApp) {
const plugins = await resolveFiles(builder, 'plugins/**/*.{js,ts}', app.dir)
return plugins.map(src => ({
src,
mode: getPluginMode(src)
})
)
}

View File

@ -1,58 +0,0 @@
import fsExtra from 'fs-extra'
import globby from 'globby'
import lodashTemplate from 'lodash/template'
import { join, relative, dirname } from 'upath'
import * as nxt from './utils/nxt'
import type { Builder } from './builder'
export interface NuxtTemplate {
src: string // Absolute path to source file
path: string // Relative path of destination
data?: any
}
export function templateData (builder: Builder) {
return {
globals: builder.globals,
app: builder.app,
nuxtOptions: builder.nuxt.options,
nxt
}
}
async function compileTemplate (tmpl: NuxtTemplate, destDir: string) {
const srcContents = await fsExtra.readFile(tmpl.src, 'utf-8')
let compiledSrc: string
try {
compiledSrc = lodashTemplate(srcContents, {})(tmpl.data)
} catch (err) {
console.error('Error compiling template: ', tmpl)
throw err
}
const dest = join(destDir, tmpl.path)
// consola.log('Compile template', dest)
await fsExtra.mkdirp(dirname(dest))
await fsExtra.writeFile(dest, compiledSrc)
}
export function compileTemplates (templates: NuxtTemplate[], destDir: string) {
return Promise.all(templates.map(t => compileTemplate(t, destDir)))
}
export async function scanTemplates (dir: string, data?: Record<string, any>) {
const templateFiles = (await globby(join(dir, '/**')))
return templateFiles.filter(src => !src.endsWith('.d.ts')).map(src => ({
src,
path: relative(dir, src),
data
}))
}
export function watchTemplate (template: NuxtTemplate, _watcher: any, _cb: () => any) {
template.data = new Proxy(template.data, {
// TODO: deep watch option changes
})
// TODO: Watch fs changes
}

View File

@ -1,8 +1,6 @@
import { basename, extname } from 'path' import { basename, extname } from 'path'
import hash from 'hash-sum' import hash from 'hash-sum'
import { camelCase } from 'scule' import { camelCase } from 'scule'
import { NuxtRoute } from '../pages'
// NXT is a set of utils for serializing JavaScript data to JS code
export const serialize = data => JSON.stringify(data, null, 2).replace(/"{(.+)}"/g, '$1') export const serialize = data => JSON.stringify(data, null, 2).replace(/"{(.+)}"/g, '$1')
@ -19,25 +17,3 @@ export const importSources = (sources: string | string[], { lazy = false } = {})
return `import ${importName(src)} from '${src}'` return `import ${importName(src)} from '${src}'`
}).join('\n') }).join('\n')
} }
interface SerializedRoute {
name?: string
path: string
children: SerializedRoute[]
/**
* @private
*/
__file: string
component: string
}
export const serializeRoute = (route: NuxtRoute): SerializedRoute => {
return {
name: route.name,
path: route.path,
children: route.children.map(serializeRoute),
// TODO: avoid exposing to prod, using process.env.NODE_ENV ?
__file: route.file,
component: `{() => import('${route.file}' /* webpackChunkName: '${route.name}' */)}`
}
}

View File

@ -1,79 +0,0 @@
import path from 'path'
import fs from 'fs-extra'
import ignore from 'ignore'
type IgnoreInstance = ReturnType<typeof ignore>
type IgnoreOptions = Parameters<typeof ignore>[0]
interface Options {
rootDir: string
ignore?: IgnoreInstance
ignoreArray?: Array<string | IgnoreInstance>
ignoreOptions?: IgnoreOptions
}
export default class Ignore {
rootDir: string
ignore?: IgnoreInstance
ignoreArray?: Array<string | IgnoreInstance>
ignoreFile?: string
ignoreOptions?: IgnoreOptions
constructor ({ ignoreArray, ignoreOptions, rootDir }: Options) {
this.rootDir = rootDir
this.ignoreOptions = ignoreOptions
this.ignoreArray = ignoreArray
this.addIgnoresRules()
}
static get IGNORE_FILENAME () {
return '.nuxtignore'
}
findIgnoreFile () {
if (!this.ignoreFile) {
const ignoreFile = path.resolve(this.rootDir, Ignore.IGNORE_FILENAME)
if (fs.existsSync(ignoreFile) && fs.statSync(ignoreFile).isFile()) {
this.ignoreFile = ignoreFile
this.ignore = ignore(this.ignoreOptions)
}
}
return this.ignoreFile
}
readIgnoreFile () {
if (this.findIgnoreFile() && this.ignoreFile) {
return fs.readFileSync(this.ignoreFile, 'utf8')
}
}
addIgnoresRules () {
const content = this.readIgnoreFile()
if (!this.ignore) {
this.ignore = ignore(this.ignoreOptions)
}
if (content) {
this.ignore.add(content)
}
if (this.ignoreArray && this.ignoreArray.length > 0) {
this.ignore.add(this.ignoreArray)
}
}
filter (paths: string[] = []) {
if (this.ignore) {
return this.ignore.filter(([] as string[]).concat(paths))
}
return paths
}
ignores (pathname: string) {
return this.ignore && this.ignore.ignores(pathname)
}
reload () {
delete this.ignore
delete this.ignoreFile
this.addIgnoresRules()
}
}

View File

@ -1,13 +0,0 @@
import { resolve } from 'path'
import globby from 'globby'
import { Builder } from '../builder'
// TODO: move to core resolver
export async function resolveFiles (builder: Builder, pattern: string, srcDir: string) {
const { nuxt } = builder
return builder.ignore.filter(await globby(pattern, {
cwd: srcDir,
followSymbolicLinks: nuxt.options.build.followSymlinks
})).map(p => resolve(srcDir, p))
}

View File

@ -1,51 +0,0 @@
import chokidar, { WatchOptions } from 'chokidar'
import defu from 'defu'
import consola from 'consola'
import Ignore from './utils/ignore'
export type WatchEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'
export type WatchCallback = (event: WatchEvent, path: string) => void
export type WatchFilter = (event: WatchEvent, path: string) => boolean | null
export function createWatcher (
pattern: string,
options?: WatchOptions,
ignore?: Ignore
) {
const opts = defu(options!, {
ignored: [],
ignoreInitial: true
})
const watcher = chokidar.watch(pattern, opts)
const watchAll = (cb: WatchCallback, filter?: WatchFilter) => {
watcher.on('all', (event, path: string) => {
if (ignore && ignore.ignores(path)) {
return
}
if (!filter || filter(event, path)) {
cb(event, path)
}
})
}
const watch = (pattern: string | RegExp, cb: WatchCallback, events?: WatchEvent[]) =>
watchAll(cb, (event, path) => path.match(pattern) && (!events || events.includes(event)))
const debug = (tag: string = '[Watcher]') => {
consola.log(tag, 'Watching ', pattern)
watchAll((event, path) => {
consola.log(tag, event, path)
})
}
return {
watchAll,
watch,
debug,
close: () => watcher.close()
}
}
export type Watcher = ReturnType<typeof createWatcher>

View File

@ -0,0 +1,12 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
declaration: true,
entries: [
'src/module',
{ input: 'src/runtime/', outDir: 'dist/runtime', format: 'esm' }
],
dependencies: [
'vue-router'
]
})

1
packages/pages/module.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./dist/module')

View File

@ -0,0 +1,24 @@
{
"name": "@nuxt/pages",
"version": "0.1.0",
"repository": "nuxt/framework",
"license": "MIT",
"types": "./dist/module.d.ts",
"files": [
"dist",
"module.js"
],
"scripts": {
"prepack": "unbuild"
},
"dependencies": {
"@nuxt/kit": "^0.5.3",
"globby": "^11.0.3",
"ufo": "^0.7.5",
"upath": "^2.0.1",
"vue-router": "^4.0.6"
},
"devDependencies": {
"unbuild": "^0.2.3"
}
}

View File

@ -0,0 +1,59 @@
import { existsSync } from 'fs'
import { defineNuxtModule } from '@nuxt/kit'
import { resolve } from 'upath'
import { 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')
nuxt.hook('builder:watch', async (event, path) => {
// Regenerate templates when adding or removing pages (plugin and routes)
if (event !== 'change' && path.startsWith('pages/')) {
await nuxt.callHook('builder:generateApp')
}
})
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
}
// Resolve routes
const routes = await resolvePagesRoutes(nuxt)
// Add 404 page is not added
const page404 = routes.find(route => route.name === '404')
if (!page404) {
routes.push({
name: '404',
path: '/:catchAll(.*)*',
file: resolve(runtimeDir, '404.vue'),
children: []
})
}
// 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')}`
}
})
})
}
})

View File

@ -1,6 +1,5 @@
<template> <template>
<div> <div>
<!-- TODO: Move this page to @nuxt/nice -->
404 | Page Not Found 404 | Page Not Found
</div> </div>
</template> </template>

View File

@ -1,3 +1,3 @@
<template> <template>
<Nuxt /> <RouterView />
</template> </template>

View File

@ -5,11 +5,11 @@ import {
createMemoryHistory, createMemoryHistory,
RouterLink RouterLink
} from 'vue-router' } from 'vue-router'
import NuxtPage from './NuxtPage.vue' // @ts-ignore
import NuxtPage from './page.vue'
import type { Plugin } from '@nuxt/app' import type { Plugin } from '@nuxt/app'
// @ts-ignore // @ts-ignore
import routes from '#build/routes' import routes from '#build/routes'
// @ts-ignore
export default <Plugin> function router (nuxt) { export default <Plugin> function router (nuxt) {
const { app } = nuxt const { app } = nuxt

View File

@ -1,10 +1,7 @@
import { resolve, extname, relative } from 'path' import { extname, relative, resolve } from 'upath'
import { encodePath } from 'ufo' import { encodePath } from 'ufo'
import { NuxtApp } from './app' import { Nuxt, resolveFiles } from '@nuxt/kit'
import { Builder } from './builder'
import { resolveFiles } from './utils'
// Check if name has [slug]
export interface NuxtRoute { export interface NuxtRoute {
name?: string name?: string
path: string path: string
@ -12,14 +9,12 @@ export interface NuxtRoute {
children: NuxtRoute[] children: NuxtRoute[]
} }
// TODO: should be const
enum SegmentParserState { enum SegmentParserState {
initial, initial,
static, static,
dynamic, dynamic,
} }
// TODO: should be const
enum SegmentTokenType { enum SegmentTokenType {
static, static,
dynamic, dynamic,
@ -30,19 +25,17 @@ interface SegmentToken {
value: string value: string
} }
export async function resolvePagesRoutes (builder: Builder, app: NuxtApp) { export async function resolvePagesRoutes (nuxt: Nuxt) {
const pagesDir = resolve(app.dir, app.pages!.dir) const pagesDir = resolve(nuxt.options.srcDir, nuxt.options.dir.pages)
const pagesPattern = `${app.pages!.dir}/**/*{${app.extensions.join(',')}}` const files = await resolveFiles(pagesDir, `**/*{${nuxt.options.extensions.join(',')}}`)
const files = await resolveFiles(builder, pagesPattern, app.dir)
// Sort to make sure parent are listed first // Sort to make sure parent are listed first
return generateRoutesFromFiles(files.sort(), pagesDir) files.sort()
return generateRoutesFromFiles(files, pagesDir)
} }
export function generateRoutesFromFiles ( export function generateRoutesFromFiles (files: string[], pagesDir: string): NuxtRoute[] {
files: string[],
pagesDir: string
): NuxtRoute[] {
const routes: NuxtRoute[] = [] const routes: NuxtRoute[] = []
for (const file of files) { for (const file of files) {
@ -57,7 +50,7 @@ export function generateRoutesFromFiles (
children: [] children: []
} }
// array where routes should be added, useful when adding child routes // Array where routes should be added, useful when adding child routes
let parent = routes let parent = routes
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {

View File

@ -1,7 +1,6 @@
import { resolve } from 'path' import { resolve } from 'path'
import * as vite from 'vite' import * as vite from 'vite'
import vuePlugin from '@vitejs/plugin-vue' import vuePlugin from '@vitejs/plugin-vue'
import { watch } from 'chokidar'
import { mkdirp, writeFile } from 'fs-extra' import { mkdirp, writeFile } from 'fs-extra'
import debounce from 'debounce' import debounce from 'debounce'
import consola from 'consola' import consola from 'consola'
@ -29,7 +28,7 @@ export async function buildServer (ctx: ViteBuildContext) {
outDir: 'dist/server', outDir: 'dist/server',
ssr: true, ssr: true,
rollupOptions: { rollupOptions: {
input: resolve(ctx.nuxt.options.buildDir, 'entry.server.mjs'), input: resolve(ctx.nuxt.options.buildDir, 'entry.mjs'),
onwarn (warning, rollupWarn) { onwarn (warning, rollupWarn) {
if (!['UNUSED_EXTERNAL_IMPORT'].includes(warning.code)) { if (!['UNUSED_EXTERNAL_IMPORT'].includes(warning.code)) {
rollupWarn(warning) rollupWarn(warning)
@ -48,7 +47,7 @@ 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.server")', 'utf8') 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')
const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs) const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)
@ -67,19 +66,6 @@ export async function buildServer (ctx: ViteBuildContext) {
await build() await build()
const watcher = watch([ ctx.nuxt.hook('builder:watch', () => build())
ctx.nuxt.options.buildDir, ctx.nuxt.hook('app:templatesGenerated', () => build())
ctx.nuxt.options.srcDir,
ctx.nuxt.options.rootDir
], {
ignored: [
'**/dist/server/**'
]
})
watcher.on('change', () => build())
ctx.nuxt.hook('close', async () => {
await watcher.close()
})
} }

View File

@ -64,7 +64,7 @@ export async function bundle (nuxt: Nuxt) {
nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer) => { nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer) => {
const start = Date.now() const start = Date.now()
warmupViteServer(server, ['/entry.client.mjs']).then(() => { warmupViteServer(server, ['/entry.mjs']).then(() => {
consola.info(`Vite warmed up in ${Date.now() - start}ms`) consola.info(`Vite warmed up in ${Date.now() - start}ms`)
}).catch(consola.error) }).catch(consola.error)
}) })

View File

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

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
... Hello
</div> </div>
</template> </template>

View File

@ -1741,6 +1741,19 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@nuxt/pages@^0.1.0, @nuxt/pages@workspace:packages/pages":
version: 0.0.0-use.local
resolution: "@nuxt/pages@workspace:packages/pages"
dependencies:
"@nuxt/kit": ^0.5.3
globby: ^11.0.3
ufo: ^0.7.5
unbuild: ^0.2.3
upath: ^2.0.1
vue-router: ^4.0.6
languageName: unknown
linkType: soft
"@nuxt/vite-builder@^0.3.3, @nuxt/vite-builder@workspace:packages/vite": "@nuxt/vite-builder@^0.3.3, @nuxt/vite-builder@workspace:packages/vite":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@nuxt/vite-builder@workspace:packages/vite" resolution: "@nuxt/vite-builder@workspace:packages/vite"
@ -10663,6 +10676,7 @@ __metadata:
"@nuxt/app": ^0.3.3 "@nuxt/app": ^0.3.3
"@nuxt/kit": ^0.5.3 "@nuxt/kit": ^0.5.3
"@nuxt/nitro": ^0.6.3 "@nuxt/nitro": ^0.6.3
"@nuxt/pages": ^0.1.0
"@nuxt/vite-builder": ^0.3.3 "@nuxt/vite-builder": ^0.3.3
"@nuxt/webpack-builder": ^0.3.4 "@nuxt/webpack-builder": ^0.3.4
"@types/fs-extra": ^9.0.11 "@types/fs-extra": ^9.0.11
@ -14864,7 +14878,7 @@ typescript@^4.2.4:
languageName: node languageName: node
linkType: hard linkType: hard
"vue-router@npm:^4.0.8": "vue-router@npm:^4.0.6, vue-router@npm:^4.0.8":
version: 4.0.8 version: 4.0.8
resolution: "vue-router@npm:4.0.8" resolution: "vue-router@npm:4.0.8"
dependencies: dependencies: