diff --git a/.changeset/breezy-cheetahs-thank.md b/.changeset/breezy-cheetahs-thank.md new file mode 100644 index 00000000..7719304d --- /dev/null +++ b/.changeset/breezy-cheetahs-thank.md @@ -0,0 +1,9 @@ +--- +'@presta/adapter-cloudflare-workers': minor +'@presta/adapter-netlify': minor +'@presta/adapter-node': minor +'@presta/adapter-vercel': minor +'presta': minor +--- + +Output and consume manifest.json diff --git a/packages/adapter-cloudflare-workers/lib/index.ts b/packages/adapter-cloudflare-workers/lib/index.ts index c9998440..221ba148 100644 --- a/packages/adapter-cloudflare-workers/lib/index.ts +++ b/packages/adapter-cloudflare-workers/lib/index.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' import { mkdir } from 'mk-dirs/sync' -import { createPlugin, logger, HookPostBuildPayload, Config } from 'presta' +import { createPlugin, logger, HookPostBuildPayload, Config, getDynamicFilesFromManifest } from 'presta' import { build as esbuild } from 'esbuild' import { timer } from '@presta/utils/timer' import { requireSafe } from '@presta/utils/requireSafe' @@ -51,19 +51,20 @@ export function getWranglerConfig() { } export async function onPostBuild(props: HookPostBuildPayload) { - const { output, functionsManifest: fns } = props + const { output, manifest } = props const filepath = path.join(output, 'worker.js') mkdir(output) - const imports = Object.entries(fns) - .map(([route, filepath]) => { - return `import * as ${slugify(filepath)} from "${filepath}"` + const dynamicFiles = getDynamicFilesFromManifest(manifest) + const imports = dynamicFiles + .map((file) => { + return `import * as ${slugify(file.dest)} from "${file.dest}"` }) .join(';\n') - const runtime = Object.entries(fns) - .map(([route, filepath]) => { - return `routes.push({ route: "${route}", module: ${slugify(filepath)} })` + const runtime = dynamicFiles + .map((file) => { + return `routes.push({ route: "${file.route}", module: ${slugify(file.dest)} })` }) .join(';\n') diff --git a/packages/adapter-netlify/__tests__/index.ts b/packages/adapter-netlify/__tests__/index.ts index e07e4594..6d22d183 100644 --- a/packages/adapter-netlify/__tests__/index.ts +++ b/packages/adapter-netlify/__tests__/index.ts @@ -3,6 +3,7 @@ import path from 'path' import { suite } from 'uvu' import * as assert from 'uvu/assert' import { afix } from 'afix' +import { ManifestDynamicFile } from 'presta' import createPlugin, { toAbsolutePath, @@ -29,7 +30,13 @@ test('normalizeNetlifyRoute', async () => { }) test('prestaRoutesToNetlifyRedirects', async () => { - assert.equal(prestaRoutesToNetlifyRedirects([['*', 'Func']])[0], { + const dynamicFile: ManifestDynamicFile = { + type: 'dynamic', + src: 'src', + dest: 'Func', + route: '/*', + } + assert.equal(prestaRoutesToNetlifyRedirects([dynamicFile])[0], { from: '/*', to: '/.netlify/functions/Func', status: 200, @@ -171,7 +178,7 @@ test('onPostBuild - just static, netlify config matches presta config', async () output: path.join(fixtures.root, 'build'), staticOutput: path.join(fixtures.root, 'build/static'), functionsOutput: path.join(fixtures.root, 'build/functions'), - functionsManifest: {}, + manifest: { files: [] }, } await onPostBuild(config, props) @@ -196,7 +203,7 @@ test('onPostBuild - just static, netlify config does not match presta config', a output: path.join(fixtures.root, 'build'), staticOutput: path.join(fixtures.root, 'build/static'), functionsOutput: path.join(fixtures.root, 'build/functions'), - functionsManifest: {}, + manifest: { files: [] }, } await onPostBuild(config, props) @@ -221,7 +228,16 @@ test('onPostBuild - has functions, not configured', async () => { output: path.join(fixtures.root, 'build'), staticOutput: path.join(fixtures.root, 'build/static'), functionsOutput: path.join(fixtures.root, 'build/functions'), - functionsManifest: { '*': fixtures.files.lambda.path }, + manifest: { + files: [ + { + type: 'dynamic', + src: 'src', + dest: fixtures.files.lambda.path, + route: '*', + } as ManifestDynamicFile, + ], + }, } let plan = 0 @@ -256,7 +272,16 @@ test(`onPostBuild - has functions, paths match`, async () => { output: path.join(fixtures.root, 'build'), staticOutput: path.join(fixtures.root, 'build/static'), functionsOutput: path.join(fixtures.root, 'build/functions'), - functionsManifest: { '*': fixtures.files.lambda.path }, + manifest: { + files: [ + { + type: 'dynamic', + src: 'src', + dest: fixtures.files.lambda.path, + route: '*', + } as ManifestDynamicFile, + ], + }, } await onPostBuild(config, props) @@ -289,7 +314,16 @@ test(`onPostBuild - has functions, paths don't match`, async () => { output: path.join(fixtures.root, 'build'), staticOutput: path.join(fixtures.root, 'build/static'), functionsOutput: path.join(fixtures.root, 'build/functions'), - functionsManifest: { '*': fixtures.files.lambda.path }, + manifest: { + files: [ + { + type: 'dynamic', + src: 'src', + dest: fixtures.files.lambda.path, + route: '*', + } as ManifestDynamicFile, + ], + }, } await onPostBuild(config, props) diff --git a/packages/adapter-netlify/index.ts b/packages/adapter-netlify/index.ts index b9716bf8..56194aff 100644 --- a/packages/adapter-netlify/index.ts +++ b/packages/adapter-netlify/index.ts @@ -3,7 +3,7 @@ import fs from 'fs-extra' import path from 'path' import { premove } from 'premove/sync' import { mkdir } from 'mk-dirs/sync' -import { createPlugin, logger, HookPostBuildPayload } from 'presta' +import { createPlugin, logger, HookPostBuildPayload, ManifestDynamicFile, getDynamicFilesFromManifest } from 'presta' import { parse as toml } from 'toml' // @ts-ignore import { parseFileRedirects } from 'netlify-redirect-parser' @@ -62,10 +62,10 @@ export function normalizeNetlifyRoute(route: string) { return route } -export function prestaRoutesToNetlifyRedirects(routes: [string, string][]): NetlifyRedirect[] { - return routes.map(([route, filename]) => ({ - from: normalizeNetlifyRoute(route), - to: `/.netlify/functions/${path.basename(filename, '.js')}`, +export function prestaRoutesToNetlifyRedirects(files: ManifestDynamicFile[]): NetlifyRedirect[] { + return files.map((file) => ({ + from: normalizeNetlifyRoute(file.route), + to: `/.netlify/functions/${path.basename(file.dest, '.js')}`, status: 200, force: false, query: {}, @@ -91,7 +91,7 @@ export async function getUserConfiguredRedirects(dir: string) { } export async function onPostBuild(config: NetlifyConfig, props: HookPostBuildPayload) { - const { output, staticOutput, functionsOutput, functionsManifest } = props + const { output, staticOutput, functionsOutput, manifest } = props const hasFunctions = fs.existsSync(functionsOutput) const shouldCopyStaticFiles = config.build.publish !== staticOutput && fs.existsSync(staticOutput) const shouldCopyFunctions = config.build.functions !== functionsOutput && hasFunctions @@ -118,7 +118,7 @@ export async function onPostBuild(config: NetlifyConfig, props: HookPostBuildPay if (shouldCopyFunctions) fs.copySync(functionsOutput, config.build.functions as string) if (hasFunctions) { - const prestaRedirects = prestaRoutesToNetlifyRedirects(Object.entries(functionsManifest)) + const prestaRedirects = prestaRoutesToNetlifyRedirects(getDynamicFilesFromManifest(manifest)) const combinedRedirects = userConfiguredRedirects.concat(prestaRedirects) const redirectsFilepath = path.join(config.build.publish, '_redirects') diff --git a/packages/adapter-node/lib/__tests__/adapter.ts b/packages/adapter-node/lib/__tests__/adapter.ts index 9ac3df31..bd89e84a 100644 --- a/packages/adapter-node/lib/__tests__/adapter.ts +++ b/packages/adapter-node/lib/__tests__/adapter.ts @@ -50,8 +50,15 @@ test('adapter', async () => { adapter( { staticOutput: path.join(fixture.root, 'build/static'), - functionsManifest: { - [route]: fixture.files.fn.path, + manifest: { + files: [ + { + type: 'dynamic', + src: 'src', + dest: fixture.files.fn.path, + route, + }, + ], }, }, { port: 4000 } diff --git a/packages/adapter-node/lib/adapter.ts b/packages/adapter-node/lib/adapter.ts index 08622ca0..867e3cb7 100644 --- a/packages/adapter-node/lib/adapter.ts +++ b/packages/adapter-node/lib/adapter.ts @@ -8,18 +8,19 @@ import sirv from 'sirv' import { Handler } from 'lambda-types' import { requestToEvent } from '@presta/utils/requestToEvent' import { sendServerlessResponse } from '@presta/utils/sendServerlessResponse' -import { HookPostBuildPayload } from 'presta' +import { HookPostBuildPayload, getDynamicFilesFromManifest } from 'presta' import { Options } from './types' export function adapter(props: HookPostBuildPayload, options: Options) { const assets = sirv(path.resolve(__dirname, props.staticOutput)) const app = polka().use(assets) + const dynamicFiles = getDynamicFilesFromManifest(props.manifest) - for (const route in props.functionsManifest) { - app.all(route, async (req, res) => { + for (const file of dynamicFiles) { + app.all(file.route, async (req, res) => { const event = await requestToEvent(req) - const { handler } = require(props.functionsManifest[route]) as { handler: Handler } + const { handler } = require(file.dest) as { handler: Handler } // @ts-ignore const response = await handler(event, {}) // @ts-ignore diff --git a/packages/adapter-vercel/lib/__tests__/index.ts b/packages/adapter-vercel/lib/__tests__/index.ts index 8c7d904a..588f5de9 100644 --- a/packages/adapter-vercel/lib/__tests__/index.ts +++ b/packages/adapter-vercel/lib/__tests__/index.ts @@ -4,6 +4,7 @@ import { suite } from 'uvu' import * as assert from 'uvu/assert' import { afix } from 'afix' import { build as esbuild } from 'esbuild' +import { ManifestDynamicFile } from 'presta' import createPlugin, { vercelConfig, onPostBuild } from '../index' @@ -23,7 +24,7 @@ test('onPostBuild', async () => { output, staticOutput: path.join(output, 'static'), functionsOutput: path.join(output, 'functions'), - functionsManifest: {}, + manifest: { files: [] }, }) assert.ok(fs.existsSync(path.join(fixture.root, './.output/static/index.html'))) @@ -77,10 +78,20 @@ test('generateRoutes', async () => { process.chdir(fixture.root) const prestaOutput = path.join(fixture.root, 'build') - const prestaFunctionsManifest = { - '/': path.join(prestaOutput, 'functions/home.js'), - '*': path.join(prestaOutput, 'functions/page.js'), - } + const dynamicFiles: ManifestDynamicFile[] = [ + { + type: 'dynamic', + src: 'src', + dest: path.join(prestaOutput, 'functions/home.js'), + route: '/', + }, + { + type: 'dynamic', + src: 'src', + dest: path.join(prestaOutput, 'functions/page.js'), + route: '*', + }, + ] const { generateRoutes } = require('proxyquire')('../index', { esbuild: { @@ -93,7 +104,7 @@ test('generateRoutes', async () => { }, }) - await generateRoutes(prestaOutput, prestaFunctionsManifest) + await generateRoutes(prestaOutput, dynamicFiles) const routesManifestPath = path.join(process.cwd(), './.output/routes-manifest.json') const routesManifest = JSON.parse(fs.readFileSync(routesManifestPath, 'utf8')) diff --git a/packages/adapter-vercel/lib/adapter.ts b/packages/adapter-vercel/lib/adapter.ts index d98b1214..cc8b8f3b 100644 --- a/packages/adapter-vercel/lib/adapter.ts +++ b/packages/adapter-vercel/lib/adapter.ts @@ -5,7 +5,7 @@ import { parse as parseUrl } from 'url' import { NextApiRequest, NextApiResponse } from 'next' import { Response } from 'lambda-types' -import { Handler, Event, Context } from 'presta' +import type { Handler, Event, Context } from 'presta' import { normalizeHeaders } from '@presta/utils/normalizeHeaders' import { parseQueryStringParameters } from '@presta/utils/parseQueryStringParameters' import { sendServerlessResponse } from '@presta/utils/sendServerlessResponse' diff --git a/packages/adapter-vercel/lib/index.ts b/packages/adapter-vercel/lib/index.ts index 003df395..d52a11c4 100644 --- a/packages/adapter-vercel/lib/index.ts +++ b/packages/adapter-vercel/lib/index.ts @@ -2,7 +2,7 @@ import fs from 'fs-extra' import path from 'path' import merge from 'deep-extend' import toRegExp from 'regexparam' -import { createPlugin, logger, HookPostBuildPayload } from 'presta' +import { createPlugin, logger, HookPostBuildPayload, ManifestDynamicFile, getDynamicFilesFromManifest } from 'presta' import { build as esbuild } from 'esbuild' import { requireSafe } from '@presta/utils' @@ -31,20 +31,19 @@ export const routesManifest: { export async function generateRoutes( prestaOutput: HookPostBuildPayload['output'], - prestaFunctionsManifest: HookPostBuildPayload['functionsManifest'] + dynamicFiles: ManifestDynamicFile[] ) { const manifest = Object.assign({}, routesManifest) - for (const route of Object.keys(prestaFunctionsManifest)) { - const source = prestaFunctionsManifest[route] - const filename = /^\/$/.test(route) ? 'index' : path.basename(source, '.js') + for (const source of dynamicFiles) { + const filename = /^\/$/.test(source.route) ? 'index' : path.basename(source.dest, '.js') const tmpfile = path.join(prestaOutput, './.vercel', filename + '.js') - const { pattern } = toRegExp(route) + const { pattern } = toRegExp(source.route) fs.outputFileSync( tmpfile, `import { adapter } from '@presta/adapter-vercel/dist/adapter'; -import { handler } from '${source}'; +import { handler } from '${source.dest}'; export default adapter(handler);` ) @@ -78,10 +77,12 @@ export function mergeVercelConfig() { } export async function onPostBuild(props: HookPostBuildPayload) { - const { output: prestaOutput, staticOutput, functionsManifest } = props + const { output: prestaOutput, staticOutput, manifest } = props fs.copySync(staticOutput, path.join(process.cwd(), './.output/static')) - if (Object.keys(functionsManifest).length) await generateRoutes(prestaOutput, functionsManifest) + + const dynamicFiles = getDynamicFilesFromManifest(manifest) + if (dynamicFiles.length) await generateRoutes(prestaOutput, dynamicFiles) logger.info({ label: '@presta/adapter-vercel', diff --git a/packages/presta/lib/__tests__/config.ts b/packages/presta/lib/__tests__/config.ts index c414596e..8e43d364 100644 --- a/packages/presta/lib/__tests__/config.ts +++ b/packages/presta/lib/__tests__/config.ts @@ -104,7 +104,7 @@ test('create', async () => { plugins: [], staticOutputDir: path.join(output, 'static'), functionsOutputDir: path.join(output, 'functions'), - functionsManifest: path.join(output, 'routes.json'), + manifestFilepath: path.join(output, 'manifest.json'), } assert.equal(config, generated) diff --git a/packages/presta/lib/__tests__/outputLambdas.ts b/packages/presta/lib/__tests__/outputLambdas.ts index b6574eb9..761b3718 100644 --- a/packages/presta/lib/__tests__/outputLambdas.ts +++ b/packages/presta/lib/__tests__/outputLambdas.ts @@ -33,16 +33,11 @@ test('outputLambdas', async () => { config ) - assert.equal(slug[0], `/:slug`) - assert.ok(slug[1].includes(`slug.js`)) + assert.equal(slug.route, `/:slug`) + assert.ok(slug.dest.includes(`slug.js`)) - assert.equal(fallback[0], `/:slug?`) - assert.ok(fallback[1].includes(`fallback.js`)) - - const manifest = require(config.functionsManifest) - - assert.equal(manifest[slug[0]], slug[1]) - assert.equal(manifest[fallback[0]], fallback[1]) + assert.equal(fallback.route, `/:slug?`) + assert.ok(fallback.dest.includes(`fallback.js`)) }) test('outputLambdas - hashed in prod', async () => { @@ -62,9 +57,9 @@ test('outputLambdas - hashed in prod', async () => { const [slug] = outputLambdas([fixture.files.slug.path], config) const hash = hashContent(fixture.files.slug.content) - assert.equal(slug[0], `/:slug`) - assert.ok(slug[1].includes(`slug-${hash}.js`)) - assert.ok(fs.existsSync(slug[1])) + assert.equal(slug.route, `/:slug`) + assert.ok(slug.dest.includes(`slug-${hash}.js`)) + assert.ok(fs.existsSync(slug.dest)) }) test('slugify', async () => { diff --git a/packages/presta/lib/__tests__/serve.ts b/packages/presta/lib/__tests__/serve.ts index 5992a163..172e9807 100644 --- a/packages/presta/lib/__tests__/serve.ts +++ b/packages/presta/lib/__tests__/serve.ts @@ -6,10 +6,11 @@ import proxy from 'proxyquire' import { afix } from 'afix' import { create } from '../config' -import { createHttpError, getMimeType, loadLambdaFroManifest, processHandler } from '../serve' +import { createHttpError, getMimeType, loadLambdaFromManifest, processHandler } from '../serve' import { Event } from '../lambda' import { createEmitter, createHooks } from '../createEmitter' import { Env } from '../constants' +import { Manifest } from '../manifest' const test = suite('presta - serve') @@ -53,19 +54,31 @@ test('getMimeType', async () => { assert.equal(noHeaders, 'html') }) -test('loadLambdaFroManifest', async () => { +test('loadLambdaFromManifest', async () => { const fixture = afix({ lambda: ['lambda.js', `module.exports = { handler: true }`], }) - const manifest = { - '/page': fixture.files.lambda.path, - '/page/:slug': fixture.files.lambda.path, + const manifest: Manifest = { + files: [ + { + type: 'dynamic', + src: 'foo', + dest: fixture.files.lambda.path, + route: '/page', + }, + { + type: 'dynamic', + src: 'foo', + dest: fixture.files.lambda.path, + route: '/page/:slug', + }, + ], } - assert.equal(loadLambdaFroManifest('/page', manifest), { handler: true }) - assert.equal(loadLambdaFroManifest('/page/path', manifest), { handler: true }) - assert.equal(loadLambdaFroManifest('/page?query', manifest), { handler: true }) - assert.equal(loadLambdaFroManifest('/foo/bar', manifest), undefined) + assert.equal(loadLambdaFromManifest('/page', manifest), { handler: true }) + assert.equal(loadLambdaFromManifest('/page/path', manifest), { handler: true }) + assert.equal(loadLambdaFromManifest('/page?query', manifest), { handler: true }) + assert.equal(loadLambdaFromManifest('/foo/bar', manifest), undefined) fixture.cleanup() }) @@ -141,7 +154,14 @@ test('createRequestHandler', async () => { {} ) const manifest = { - '/': fixture.files.lambda.path, + files: [ + { + type: 'dynamic', + src: 'src', + dest: fixture.files.lambda.path, + route: '/', + }, + ], } const { createRequestHandler } = proxy('../serve', { '@presta/utils': { diff --git a/packages/presta/lib/build.ts b/packages/presta/lib/build.ts index 2530cb94..4c9cc283 100644 --- a/packages/presta/lib/build.ts +++ b/packages/presta/lib/build.ts @@ -9,6 +9,7 @@ import { buildStaticFiles } from './buildStaticFiles' import * as logger from './log' import { Config } from './config' import { Hooks } from './createEmitter' +import { ManifestStaticFile, ManifestDynamicFile, staticFilesMapToManifestFiles, writeManifest } from './manifest' export async function build(config: Config, hooks: Hooks) { const totalTime = timer() @@ -31,6 +32,8 @@ export async function build(config: Config, hooks: Hooks) { let staticFileAmount = 0 let dynamicTime = '' let copyTime = '' + let manifestStaticFiles: ManifestStaticFile[] = [] + let manifestDynamicFiles: ManifestDynamicFile[] = [] const tasks = await Promise.allSettled([ (async () => { @@ -38,6 +41,7 @@ export async function build(config: Config, hooks: Hooks) { const time = timer() const { staticFilesMap } = await buildStaticFiles(staticIds, config) + manifestStaticFiles = staticFilesMapToManifestFiles(staticFilesMap) staticTime = time() staticFileAmount = Object.keys(staticFilesMap).reduce((count, key) => { @@ -50,10 +54,10 @@ export async function build(config: Config, hooks: Hooks) { const time = timer() const pkg = requireSafe(path.join(process.cwd(), 'package.json')) - outputLambdas(dynamicIds, config) + manifestDynamicFiles = outputLambdas(dynamicIds, config) await esbuild({ - entryPoints: Object.values(require(config.functionsManifest)), + entryPoints: manifestDynamicFiles.map((f) => f.dest), outdir: config.functionsOutputDir, platform: 'node', target: ['node12'], @@ -101,6 +105,19 @@ export async function build(config: Config, hooks: Hooks) { throw new Error('presta build failed') } + const manifest = { + files: [...manifestStaticFiles, ...manifestDynamicFiles], + } + + writeManifest(manifest, config) + + hooks.emitPostBuild({ + output: config.output, + staticOutput: config.staticOutputDir, + functionsOutput: config.functionsOutputDir, + manifest, + }) + if (staticTime) { logger.info({ label: 'static', @@ -125,13 +142,6 @@ export async function build(config: Config, hooks: Hooks) { }) } - hooks.emitPostBuild({ - output: config.output, - staticOutput: config.staticOutputDir, - functionsOutput: config.functionsOutputDir, - functionsManifest: requireSafe(config.functionsManifest), - }) - if (staticTime || dynamicTime) { logger.info({ label: 'build', diff --git a/packages/presta/lib/config.ts b/packages/presta/lib/config.ts index d5acb664..73374b9e 100644 --- a/packages/presta/lib/config.ts +++ b/packages/presta/lib/config.ts @@ -18,7 +18,7 @@ export type Config = Options & { env: string staticOutputDir: string functionsOutputDir: string - functionsManifest: string + manifestFilepath: string } export const defaultConfigFilepath = 'presta.config.js' @@ -87,6 +87,6 @@ export function create( ...config, staticOutputDir: path.join(config.output, 'static'), functionsOutputDir: path.join(config.output, 'functions'), - functionsManifest: path.join(config.output, 'routes.json'), + manifestFilepath: path.join(config.output, 'manifest.json'), } } diff --git a/packages/presta/lib/createEmitter.ts b/packages/presta/lib/createEmitter.ts index a3d9104e..8f1012bd 100644 --- a/packages/presta/lib/createEmitter.ts +++ b/packages/presta/lib/createEmitter.ts @@ -1,3 +1,5 @@ +import { Manifest } from './manifest' + export enum Events { PostBuild = 'post-build', BuildFile = 'build-file', @@ -10,7 +12,7 @@ export type HookPostBuildPayload = { output: string staticOutput: string functionsOutput: string - functionsManifest: Record + manifest: Manifest } export type HookBuildFilePayload = { diff --git a/packages/presta/lib/index.ts b/packages/presta/lib/index.ts index 0b53ec39..0b20d66a 100644 --- a/packages/presta/lib/index.ts +++ b/packages/presta/lib/index.ts @@ -10,3 +10,10 @@ export * from './lambda' export { Config, Options } from './config' export { HookPostBuildPayload, HookBuildFilePayload } from './createEmitter' export { createPlugin, PluginInit, Plugin, PluginInterface } from './plugins' +export { + Manifest, + ManifestDynamicFile, + ManifestStaticFile, + getDynamicFilesFromManifest, + getStaticFilesFromManifest, +} from './manifest' diff --git a/packages/presta/lib/manifest.ts b/packages/presta/lib/manifest.ts new file mode 100644 index 00000000..62d9452f --- /dev/null +++ b/packages/presta/lib/manifest.ts @@ -0,0 +1,47 @@ +import fs from 'fs-extra' + +import { Config } from './config' +import { StaticFilesMap } from './buildStaticFiles' + +export type ManifestDynamicFile = { + type: 'dynamic' + src: string + dest: string + route: string +} + +export type ManifestStaticFile = { + type: 'static' + src: string + dest: string +} + +export type ManifestFile = ManifestDynamicFile | ManifestStaticFile + +export type Manifest = { + files: ManifestFile[] +} + +export function staticFilesMapToManifestFiles(staticFilesMap: StaticFilesMap): ManifestStaticFile[] { + return Object.keys(staticFilesMap) + .map((src) => { + return staticFilesMap[src].map((dest) => ({ + type: 'static', + src, + dest, + })) as ManifestStaticFile[] + }) + .flat() +} + +export function getDynamicFilesFromManifest(manifest: Manifest): ManifestDynamicFile[] { + return manifest.files.filter((file) => file.type === 'dynamic') as ManifestDynamicFile[] +} + +export function getStaticFilesFromManifest(manifest: Manifest): ManifestStaticFile[] { + return manifest.files.filter((file) => file.type === 'static') as ManifestStaticFile[] +} + +export function writeManifest(manifest: Manifest, config: Config): void { + fs.outputFileSync(config.manifestFilepath, JSON.stringify(manifest, null, ' '), 'utf8') +} diff --git a/packages/presta/lib/outputLambdas.ts b/packages/presta/lib/outputLambdas.ts index 37f14a0f..cd5c42fd 100644 --- a/packages/presta/lib/outputLambdas.ts +++ b/packages/presta/lib/outputLambdas.ts @@ -6,6 +6,7 @@ import { hashContent } from '@presta/utils' import * as logger from './log' import { Config } from './config' import { Env } from './constants' +import { ManifestDynamicFile } from './manifest' export function slugify(filename: string) { return filename @@ -50,7 +51,12 @@ export function outputLambdas(inputs: string[], config: Config) { export const handler = wrapHandler(file)` ) - return [route, output] + return { + type: 'dynamic', + src: input, + dest: output, + route, + } } catch (e) { logger.error({ label: 'error', @@ -58,20 +64,18 @@ export function outputLambdas(inputs: string[], config: Config) { }) } }) - .filter(Boolean) as [string, string][] + .filter(Boolean) as ManifestDynamicFile[] - const sorted = rsort(lambdas.map((l) => l[0])) - const manifest: { [route: string]: string } = {} + const sortedRoutes = rsort(lambdas.map((l) => l.route)) + const sortedFiles: ManifestDynamicFile[] = [] - for (const route of sorted) { - const match = lambdas.find((l) => l[0] === route) + for (const route of sortedRoutes) { + const match = lambdas.find((l) => l.route === route) if (match) { - manifest[route] = match[1] + sortedFiles.push(match) } } - fs.outputFileSync(config.functionsManifest, JSON.stringify(manifest)) - - return lambdas + return sortedFiles } diff --git a/packages/presta/lib/serve.ts b/packages/presta/lib/serve.ts index 77e2867f..d7d0ca9d 100644 --- a/packages/presta/lib/serve.ts +++ b/packages/presta/lib/serve.ts @@ -13,6 +13,7 @@ import { Handler, Event, Response, Context } from './lambda' import { Config } from './config' import { Hooks } from './createEmitter' import { normalizeResponse } from './normalizeResponse' +import { getDynamicFilesFromManifest, Manifest, ManifestDynamicFile } from './manifest' export interface HttpError extends Error { statusCode?: number @@ -31,19 +32,19 @@ export function getMimeType(response: Response) { return mime.extension(type as string) || 'html' } -export function loadLambdaFroManifest(url: string, manifest: { [route: string]: string }): { handler: Handler } { - const routes = Object.keys(manifest) - const lambdaFilepath = routes - .map((route) => ({ - matcher: toRegExp(route), - route, +export function loadLambdaFromManifest(url: string, manifest: Manifest): { handler: Handler } { + const dynamicFiles = getDynamicFilesFromManifest(manifest) + const lambda = dynamicFiles + .map((file) => ({ + matcher: toRegExp(file.route), + file, })) .filter(({ matcher }) => { return matcher.pattern.test(url.split('?')[0]) }) - .map(({ route }) => manifest[route])[0] + .map(({ file }) => file)[0] - return lambdaFilepath ? require(lambdaFilepath) : undefined + return lambda ? require(lambda.dest) : undefined } export async function processHandler(event: Event, lambda: { handler: Handler }) { @@ -82,8 +83,8 @@ export function createRequestHandler({ port, config }: { port: number; config: C return async function requestHandler(req: http.IncomingMessage, res: http.ServerResponse) { const time = timer() const event = await requestToEvent(req) // stock AWS Event shape - const manifest = requireFresh(config.functionsManifest) - const lambda = loadLambdaFroManifest(event.path, manifest) + const manifest = requireFresh(config.manifestFilepath) as Manifest + const lambda = loadLambdaFromManifest(event.path, manifest) const response = await processHandler(event, lambda) const redir = response.statusCode > 299 && response.statusCode < 399 const mime = getMimeType(response) diff --git a/packages/presta/lib/watch.ts b/packages/presta/lib/watch.ts index fec4d549..0432b994 100644 --- a/packages/presta/lib/watch.ts +++ b/packages/presta/lib/watch.ts @@ -3,7 +3,6 @@ import path from 'path' import { create } from 'watch-dependency-graph' import chokidar from 'chokidar' import match from 'picomatch' -import merge from 'deep-extend' import { timer } from '@presta/utils' import { outputLambdas } from './outputLambdas' @@ -12,6 +11,7 @@ import { getFiles, isStatic, isDynamic } from './getFiles' import { buildStaticFiles, removeBuiltStaticFile, StaticFilesMap } from './buildStaticFiles' import { Config } from './config' import { Hooks } from './createEmitter' +import { staticFilesMapToManifestFiles, writeManifest } from './manifest' /* * Wraps outputLambdas for logging @@ -20,7 +20,7 @@ function updateLambdas(inputs: string[], config: Config) { const time = timer() // always write this, even if inputs = [] - outputLambdas(inputs, config) + const lambdas = outputLambdas(inputs, config) // if user actually has routes configured, give feedback if (inputs.length) { @@ -30,6 +30,8 @@ function updateLambdas(inputs: string[], config: Config) { duration: time(), }) } + + return lambdas } export function isNewValidFile(file: string, globs: string[], existing: string[]) { @@ -38,32 +40,32 @@ export function isNewValidFile(file: string, globs: string[], existing: string[] export async function watch(config: Config, hooks: Hooks) { let staticFilesMap: StaticFilesMap = {} - const files = getFiles(config.files) + const allFiles = getFiles(config.files) - if (!files.length) { + if (!allFiles.length) { logger.warn({ label: 'paths', message: 'no files configured', }) } - async function buildFile(file: string, existing: string[], config: Config) { - delete require.cache[file] - - // render just file that changed - if (isStatic(file)) { - const result = await buildStaticFiles([file], config, staticFilesMap) - staticFilesMap = merge({}, staticFilesMap, result.staticFilesMap) - } - - // update dynamic entry with ALL dynamic files - updateLambdas(existing.filter(isDynamic), config) - } - async function buildFiles(files: string[], existing: string[], config: Config) { for (const file of files) { - await buildFile(file, existing, config) + delete require.cache[file] } + + const builtStaticFiles = await buildStaticFiles(files.filter(isStatic), config, staticFilesMap) + const builtLambdas = updateLambdas(existing.filter(isDynamic), config) + + // merge in new static files + staticFilesMap = Object.assign(staticFilesMap, builtStaticFiles.staticFilesMap) + + writeManifest( + { + files: [...staticFilesMapToManifestFiles(staticFilesMap), ...builtLambdas], + }, + config + ) } /** @@ -71,7 +73,7 @@ export async function watch(config: Config, hooks: Hooks) { * re-introduce "file priming" where we require all files and surface errors * on startup. */ - await buildFiles(files, files, config) + await buildFiles(allFiles, allFiles, config) hooks.emitBrowserRefresh() /* @@ -81,7 +83,7 @@ export async function watch(config: Config, hooks: Hooks) { const fileWatcher = create({ alias: { '@': process.cwd() } }) fileWatcher.onChange(async (changed) => { - await buildFiles(changed, files, config) + await buildFiles(changed, allFiles, config) hooks.emitBrowserRefresh() }) @@ -89,10 +91,10 @@ export async function watch(config: Config, hooks: Hooks) { logger.debug({ label: 'watch', message: `removed ${id}` }) // remove from local hash - files.splice(files.indexOf(id), 1) + allFiles.splice(allFiles.indexOf(id), 1) // update this regardless, not sure if [id] was dynamic or static - updateLambdas(files.filter(isDynamic), config) + updateLambdas(allFiles.filter(isDynamic), config) ;(staticFilesMap[id] || []).forEach((file) => removeBuiltStaticFile(path.join(config.staticOutputDir, file))) hooks.emitBrowserRefresh() @@ -105,7 +107,7 @@ export async function watch(config: Config, hooks: Hooks) { }) }) - await fileWatcher.add(files) + await fileWatcher.add(allFiles) /* * globalWatcher watches the raw file globs passed to the CLI or as `files` @@ -119,14 +121,14 @@ export async function watch(config: Config, hooks: Hooks) { globalWatcher.on('add', async (file) => { if (!fs.existsSync(file) || fs.lstatSync(file).isDirectory()) return - if (!isNewValidFile(file, config.files, files)) return + if (!isNewValidFile(file, config.files, allFiles)) return logger.debug({ label: 'watch', message: `add ${file}` }) - files.push(file) + allFiles.push(file) await fileWatcher.add(file) - await buildFile(file, files, config) + await buildFiles([file], allFiles, config) hooks.emitBrowserRefresh() }) @@ -135,7 +137,7 @@ export async function watch(config: Config, hooks: Hooks) { * Listens for events from plugins requesting a file to be built */ hooks.onBuildFile(async ({ file }) => { - await buildFile(file, files, config) + await buildFiles([file], allFiles, config) hooks.emitBrowserRefresh() })