未验证 提交 74771bc5 编写于 作者: L Luis Fernando Alvarez D 提交者: GitHub

Add support for /public (#7213)

上级 9d936f3a
......@@ -13,6 +13,7 @@ export const BLOCKED_PAGES = [
'/_document',
'/_app',
]
export const CLIENT_PUBLIC_FILES_PATH = 'public'
export const CLIENT_STATIC_FILES_PATH = 'static'
export const CLIENT_STATIC_FILES_RUNTIME = 'runtime'
export const CLIENT_STATIC_FILES_RUNTIME_PATH = `${CLIENT_STATIC_FILES_PATH}/${CLIENT_STATIC_FILES_RUNTIME}`
......
import fs from 'fs'
import { join } from 'path'
/**
* Recursively read directory
* @param {string[]=[]} arr This doesn't have to be provided, it's used for the recursion
* @param {string=dir`} rootDir Used to replace the initial path, only the relative path is left, it's faster than path.relative.
* @returns Array holding all relative paths
*/
export function recursiveReadDirSync(dir: string, arr: string[] = [], rootDir = dir): string[] {
const result = fs.readdirSync(dir)
result.forEach((part: string) => {
const absolutePath = join(dir, part)
const pathStat = fs.statSync(absolutePath)
if (pathStat.isDirectory()) {
recursiveReadDirSync(absolutePath, arr, rootDir)
return
}
arr.push(absolutePath.replace(rootDir, ''))
})
return arr
}
......@@ -10,11 +10,15 @@ import { serveStatic } from './serve-static'
import Router, { route, Route } from './router'
import { isInternalUrl, isBlockedPage } from './utils'
import loadConfig from './config'
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
import {
PHASE_PRODUCTION_SERVER,
BUILD_ID_FILE,
CLIENT_PUBLIC_FILES_PATH,
CLIENT_STATIC_FILES_PATH,
CLIENT_STATIC_FILES_RUNTIME,
SERVER_DIRECTORY,
PAGES_MANIFEST,
} from '../lib/constants'
import * as envConfig from '../lib/runtime-config'
import { loadComponents } from './load-components'
......@@ -33,6 +37,7 @@ export default class Server {
quiet: boolean
nextConfig: NextConfig
distDir: string
publicDir: string
buildId: string
renderOpts: {
poweredByHeader: boolean
......@@ -56,6 +61,8 @@ export default class Server {
const phase = this.currentPhase()
this.nextConfig = loadConfig(phase, this.dir, conf)
this.distDir = join(this.dir, this.nextConfig.distDir)
// this.pagesDir = join(this.dir, 'pages')
this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
// Only serverRuntimeConfig needs the default
// publicRuntimeConfig gets it's default in client/index.js
......@@ -95,6 +102,7 @@ export default class Server {
const routes = this.generateRoutes()
this.router = new Router(routes)
this.setAssetPrefix(assetPrefix)
}
......@@ -193,6 +201,10 @@ export default class Server {
},
]
if (fs.existsSync(this.publicDir)) {
routes.push(...this.generatePublicRoutes())
}
if (this.nextConfig.useFileSystemPublicRoutes) {
// It's very important to keep this route's param optional.
// (but it should support as many params as needed, separated by '/')
......@@ -214,6 +226,29 @@ export default class Server {
return routes
}
private generatePublicRoutes(): Route[] {
const routes: Route[] = []
const publicFiles = recursiveReadDirSync(this.publicDir)
const serverBuildPath = join(this.distDir, SERVER_DIRECTORY)
const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))
publicFiles.forEach((path) => {
const unixPath = path.replace(/\\/g, '/')
// Only include public files that will not replace a page path
if (!pagesManifest[unixPath]) {
routes.push({
match: route(unixPath),
fn: async (req, res, _params, parsedUrl) => {
const p = join(this.publicDir, unixPath)
await this.serveStatic(req, res, p, parsedUrl)
},
})
}
})
return routes
}
private async run(
req: IncomingMessage,
res: ServerResponse,
......@@ -391,7 +426,8 @@ export default class Server {
const resolved = resolve(path)
if (
resolved.indexOf(join(this.distDir) + sep) !== 0 &&
resolved.indexOf(join(this.dir, 'static') + sep) !== 0
resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
resolved.indexOf(join(this.dir, 'public') + sep) !== 0
) {
// Seems like the user is trying to traverse the filesystem.
return false
......
......@@ -5,7 +5,7 @@ import mkdirpModule from 'mkdirp'
import { resolve, join } from 'path'
import { existsSync, readFileSync } from 'fs'
import loadConfig from 'next-server/next-config'
import { PHASE_EXPORT, SERVER_DIRECTORY, PAGES_MANIFEST, CONFIG_FILE, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH } from 'next-server/constants'
import { PHASE_EXPORT, SERVER_DIRECTORY, PAGES_MANIFEST, CONFIG_FILE, BUILD_ID_FILE, CLIENT_PUBLIC_FILES_PATH, CLIENT_STATIC_FILES_PATH } from 'next-server/constants'
import createProgress from 'tty-aware-progress'
import { promisify } from 'util'
import { recursiveDelete } from '../lib/recursive-delete'
......@@ -125,6 +125,23 @@ export default async function (dir, options, configuration) {
const ampValidations = {}
let hadValidationError = false
const publicDir = join(dir, CLIENT_PUBLIC_FILES_PATH)
// Copy public directory
if (existsSync(publicDir)) {
log(' copying "public" directory')
await cp(
publicDir,
outDir,
{
expand: true,
filter (path) {
// Exclude paths used by pages
return !exportPathMap['/' + path]
}
}
)
}
await Promise.all(
chunks.map(
chunk =>
......
......@@ -112,6 +112,11 @@ export default class DevServer extends Server {
return routes
}
// In development public files are not added to the router but handled as a fallback instead
generatePublicRoutes () {
return []
}
_filterAmpDevelopmentScript (html, event) {
if (event.code !== 'DISALLOWED_SCRIPT_TAG') {
return true
......@@ -145,8 +150,9 @@ export default class DevServer extends Server {
await this.hotReloader.ensurePage(pathname)
} catch (err) {
if (err.code === 'ENOENT') {
res.statusCode = 404
return this.renderErrorToHTML(null, req, res, pathname, query)
// Try to send a public file and let servePublic handle the request from here
await this.servePublic(req, res, pathname)
return null
}
if (!this.quiet) console.error(err)
}
......@@ -190,6 +196,11 @@ export default class DevServer extends Server {
res.setHeader('Cache-Control', 'no-store, must-revalidate')
}
servePublic (req, res, path) {
const p = join(this.publicDir, path)
return this.serveStatic(req, res, p)
}
async getCompilationError (page) {
const errors = await this.hotReloader.getCompilationErrors(page)
if (errors.length === 0) return
......
......@@ -17,6 +17,9 @@ module.exports = function (task) {
const output = transform(file.data, options)
const ext = extname(file.base)
// Include declaration files as they are
if (file.base.endsWith('.d.ts')) return
// Replace `.ts|.tsx` with `.js` in files with an extension
if (ext) {
const extRegex = new RegExp(ext.replace('.', '\\.') + '$', 'i')
......
export default () => (
<div className='about-page'>About Page</div>
)
about
\ No newline at end of file
data
\ No newline at end of file
test
\ No newline at end of file
......@@ -14,6 +14,7 @@ import errorRecovery from './error-recovery'
import dynamic from './dynamic'
import processEnv from './process-env'
import typescript from './typescript'
import publicFolder from './public-folder'
const context = {}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
......@@ -40,4 +41,5 @@ describe('Basic Features', () => {
errorRecovery(context, (p, q) => renderViaHTTP(context.appPort, p, q))
typescript(context, (p, q) => renderViaHTTP(context.appPort, p, q))
processEnv(context)
publicFolder(context)
})
/* eslint-env jest */
import { renderViaHTTP } from 'next-test-utils'
export default (context) => {
describe('Public folder', () => {
it('should allow access to public files', async () => {
const data = await renderViaHTTP(context.appPort, '/data/data.txt')
expect(data).toBe('data')
})
it('should prioritize pages over public files', async () => {
const html = await renderViaHTTP(context.appPort, '/about')
const data = await renderViaHTTP(context.appPort, '/file')
expect(html).toMatch(/About Page/)
expect(data).toBe('test')
})
})
}
query
\ No newline at end of file
......@@ -58,7 +58,7 @@ describe('Static Export', () => {
await nextExport(appDir, { outdir: outNoTrailSlash })
nextConfig.restore()
context.server = await startStaticServer(join(appDir, 'out'))
context.server = await startStaticServer(outdir)
context.port = context.server.address().port
context.serverNoTrailSlash = await startStaticServer(outNoTrailSlash)
......
......@@ -61,5 +61,20 @@ export default function (context) {
// contains "404", so need to be specific here
expect(html).not.toMatch(/404.*page.*not.*found/i)
})
it('Should serve static files', async () => {
const data = await renderViaHTTP(context.port, '/static/data/item.txt')
expect(data).toBe('item')
})
it('Should serve public files and prioritize pages', async () => {
const html = await renderViaHTTP(context.port, '/about')
const html2 = await renderViaHTTP(context.port, '/query')
const data = await renderViaHTTP(context.port, '/about/data.txt')
expect(html).toMatch(/This is the About page foobar/)
expect(html2).toMatch(/{"a":"blue"}/)
expect(data).toBe('data')
})
})
}
about
\ No newline at end of file
test
\ No newline at end of file
......@@ -268,6 +268,19 @@ describe('Production Usage', () => {
expect(data).toBe('item')
})
it('Should allow access to public files', async () => {
const data = await renderViaHTTP(appPort, '/data/data.txt')
expect(data).toBe('data')
})
it('Should prioritize pages over public files', async () => {
const html = await renderViaHTTP(appPort, '/about')
const data = await renderViaHTTP(appPort, '/file')
expect(html).toMatch(/About Page/)
expect(data).toBe('test')
})
it('should reload the page on page script error', async () => {
const browser = await webdriver(appPort, '/counter')
const counter = await browser
......
......@@ -30,7 +30,12 @@ module.exports = (context) => {
`/_next/:buildId/webpack/chunks/../../../info.json`,
`/_next/:buildId/webpack/../../../info.json`,
`/_next/../../../info.json`,
`/static/../../../info.json`
`/static/../../../info.json`,
`/static/../info.json`,
`/../../../info.json`,
`/../../info.json`,
`/../info.json`,
`/info.json`
]
for (const path of pathsToCheck) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册