From 233aa698e823db55bb8e283449df4c204b130aa7 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 17 Nov 2022 18:15:16 -0600 Subject: [PATCH 1/5] feat: 404 and catchall handling --- .../adaptors/cloudflare-pages/api.md | 1 + .../adaptors/cloudflare-pages/vite/index.ts | 57 +-- .../qwik-city/adaptors/express/vite/index.ts | 3 + .../qwik-city/adaptors/netlify-edge/api.md | 1 + .../adaptors/netlify-edge/vite/index.ts | 31 +- .../qwik-city/adaptors/shared/vite/index.ts | 87 +++- .../adaptors/shared/vite/post-build.ts | 131 ++++++ .../qwik-city/adaptors/vercel-edge/api.md | 1 + .../adaptors/vercel-edge/vite/index.ts | 27 +- .../qwik-city/buildtime/build-menus.unit.ts | 4 +- .../qwik-city/buildtime/build-pages.unit.ts | 24 +- packages/qwik-city/buildtime/build.ts | 1 - packages/qwik-city/buildtime/context.ts | 4 +- .../buildtime/markdown/markdown-url.unit.ts | 114 ++--- .../qwik-city/buildtime/markdown/menu.unit.ts | 4 +- .../buildtime/routing/resolve-source-file.ts | 23 +- .../buildtime/routing/source-file.ts | 8 +- .../buildtime/routing/source-file.unit.ts | 388 +++++++++--------- packages/qwik-city/buildtime/types.ts | 6 +- .../qwik-city/buildtime/vite/dev-server.ts | 6 +- .../middleware/cloudflare-pages/api.md | 6 +- .../middleware/cloudflare-pages/index.ts | 48 +-- .../qwik-city/middleware/netlify-edge/api.md | 4 - .../middleware/netlify-edge/index.ts | 44 +- packages/qwik-city/middleware/node/http.ts | 11 +- packages/qwik-city/middleware/node/index.ts | 16 +- .../request-handler/error-handler.ts | 11 +- .../middleware/request-handler/index.ts | 2 +- .../request-handler/not-found-paths.ts | 11 + .../request-handler/page-handler.ts | 8 +- .../request-handler/request-handler.ts | 3 - .../request-handler/static-paths.ts | 11 + .../middleware/request-handler/test-utils.ts | 10 +- .../middleware/request-handler/types.ts | 2 + .../request-handler/user-response.ts | 2 +- .../qwik-city/middleware/vercel-edge/index.ts | 29 +- packages/qwik-city/runtime/src/types.ts | 4 - packages/qwik-city/static/api.md | 1 + packages/qwik-city/static/main-thread.ts | 129 +++--- packages/qwik-city/static/node/node-system.ts | 23 +- packages/qwik-city/static/not-found.ts | 29 ++ packages/qwik-city/static/types.ts | 8 +- packages/qwik-city/static/worker-thread.ts | 9 +- packages/qwik-city/utils/fs.ts | 18 +- packages/qwik-city/utils/fs.unit.ts | 132 +++--- scripts/qwik-city.ts | 15 +- tsconfig.json | 4 + 47 files changed, 881 insertions(+), 630 deletions(-) create mode 100644 packages/qwik-city/adaptors/shared/vite/post-build.ts create mode 100644 packages/qwik-city/middleware/request-handler/not-found-paths.ts create mode 100644 packages/qwik-city/middleware/request-handler/static-paths.ts create mode 100644 packages/qwik-city/static/not-found.ts diff --git a/packages/qwik-city/adaptors/cloudflare-pages/api.md b/packages/qwik-city/adaptors/cloudflare-pages/api.md index 1f37d28588c..84b16d3e242 100644 --- a/packages/qwik-city/adaptors/cloudflare-pages/api.md +++ b/packages/qwik-city/adaptors/cloudflare-pages/api.md @@ -13,6 +13,7 @@ export function cloudflarePagesAdaptor(opts?: CloudflarePagesAdaptorOptions): an export interface CloudflarePagesAdaptorOptions { functionRoutes?: boolean; staticGenerate?: StaticGenerateRenderOptions | true; + staticPaths?: string[]; } export { StaticGenerateRenderOptions } diff --git a/packages/qwik-city/adaptors/cloudflare-pages/vite/index.ts b/packages/qwik-city/adaptors/cloudflare-pages/vite/index.ts index 303be3b461d..8c65c3c323a 100644 --- a/packages/qwik-city/adaptors/cloudflare-pages/vite/index.ts +++ b/packages/qwik-city/adaptors/cloudflare-pages/vite/index.ts @@ -11,6 +11,8 @@ export function cloudflarePagesAdaptor(opts: CloudflarePagesAdaptorOptions = {}) name: 'cloudflare-pages', origin: process?.env?.CF_PAGES_URL || 'https://your.cloudflare.pages.dev', staticGenerate: opts.staticGenerate, + staticPaths: opts.staticPaths, + cleanStaticGenerated: true, config() { return { @@ -31,49 +33,14 @@ export function cloudflarePagesAdaptor(opts: CloudflarePagesAdaptorOptions = {}) }; }, - async generateRoutes({ clientOutDir, staticPaths, warn }) { - const clientFiles = await fs.promises.readdir(clientOutDir, { withFileTypes: true }); - const exclude = clientFiles - .map((f) => { - if (f.name.startsWith('.')) { - return null; - } - if (f.isDirectory()) { - return `/${f.name}/*`; - } else if (f.isFile()) { - return `/${f.name}`; - } - return null; - }) - .filter(isNotNullable); - const include: string[] = ['/*']; - - const hasRoutesJson = exclude.includes('/_routes.json'); + async generate({ clientOutDir, basePathname }) { + const routesJsonPath = join(clientOutDir, '_routes.json'); + const hasRoutesJson = fs.existsSync(routesJsonPath); if (!hasRoutesJson && opts.functionRoutes !== false) { - staticPaths.sort(); - staticPaths.sort((a, b) => a.length - b.length); - exclude.push(...staticPaths); - - const routesJsonPath = join(clientOutDir, '_routes.json'); - const total = include.length + exclude.length; - const maxRules = 100; - if (total > maxRules) { - const toRemove = total - maxRules; - const removed = exclude.splice(-toRemove, toRemove); - warn( - `Cloudflare Pages does not support more than 100 static rules. Qwik SSG generated ${total}, the following rules were excluded: ${JSON.stringify( - removed, - undefined, - 2 - )}` - ); - warn('Please manually create a routes config in the "public/_routes.json" directory.'); - } - const routesJson = { version: 1, - include, - exclude, + include: [basePathname + '*'], + exclude: [basePathname + 'build/*', basePathname + 'assets/*'], }; await fs.promises.writeFile(routesJsonPath, JSON.stringify(routesJson, undefined, 2)); } @@ -97,10 +64,12 @@ export interface CloudflarePagesAdaptorOptions { * Determines if the adaptor should also run Static Site Generation (SSG). */ staticGenerate?: StaticGenerateRenderOptions | true; + /** + * Manually add pathnames that should be treated as static paths and not SSR. + * For example, when these pathnames are requested, their response should + * come from a static file, rather than a server-side rendered response. + */ + staticPaths?: string[]; } export type { StaticGenerateRenderOptions }; - -const isNotNullable = (v: T): v is NonNullable => { - return v != null; -}; diff --git a/packages/qwik-city/adaptors/express/vite/index.ts b/packages/qwik-city/adaptors/express/vite/index.ts index 092bd6d6077..56f853cbebb 100644 --- a/packages/qwik-city/adaptors/express/vite/index.ts +++ b/packages/qwik-city/adaptors/express/vite/index.ts @@ -9,6 +9,7 @@ export function expressAdaptor(opts: ExpressAdaptorOptions = {}): any { name: 'express', origin: process?.env?.URL || 'https://yoursitename.qwik.builder.io', staticGenerate: opts.staticGenerate, + cleanStaticGenerated: true, config() { return { @@ -18,6 +19,8 @@ export function expressAdaptor(opts: ExpressAdaptorOptions = {}): any { publicDir: false, }; }, + + async generate() {}, }); } diff --git a/packages/qwik-city/adaptors/netlify-edge/api.md b/packages/qwik-city/adaptors/netlify-edge/api.md index 9891dac14bf..2fd1990b9b1 100644 --- a/packages/qwik-city/adaptors/netlify-edge/api.md +++ b/packages/qwik-city/adaptors/netlify-edge/api.md @@ -13,6 +13,7 @@ export function netifyEdgeAdaptor(opts?: NetlifyEdgeAdaptorOptions): any; export interface NetlifyEdgeAdaptorOptions { functionRoutes?: boolean; staticGenerate?: StaticGenerateRenderOptions | true; + staticPaths?: string[]; } export { StaticGenerateRenderOptions } diff --git a/packages/qwik-city/adaptors/netlify-edge/vite/index.ts b/packages/qwik-city/adaptors/netlify-edge/vite/index.ts index 5ebcd04176a..4a5792c52d0 100644 --- a/packages/qwik-city/adaptors/netlify-edge/vite/index.ts +++ b/packages/qwik-city/adaptors/netlify-edge/vite/index.ts @@ -2,6 +2,7 @@ import type { StaticGenerateRenderOptions } from '@builder.io/qwik-city/static'; import { getParentDir, viteAdaptor } from '../../shared/vite'; import fs from 'node:fs'; import { join } from 'node:path'; +import { basePathname } from '@qwik-city-plan'; /** * @alpha @@ -11,6 +12,8 @@ export function netifyEdgeAdaptor(opts: NetlifyEdgeAdaptorOptions = {}): any { name: 'netlify-edge', origin: process?.env?.URL || 'https://yoursitename.netlify.app', staticGenerate: opts.staticGenerate, + staticPaths: opts.staticPaths, + cleanStaticGenerated: true, config(config) { const outDir = config.build?.outDir || '.netlify/edge-functions/entry.netlify-edge'; @@ -33,26 +36,16 @@ export function netifyEdgeAdaptor(opts: NetlifyEdgeAdaptorOptions = {}): any { }; }, - async generateRoutes({ serverOutDir, routes, staticPaths }) { + async generate({ serverOutDir }) { if (opts.functionRoutes !== false) { - const ssrRoutes = routes.filter((r) => !staticPaths.includes(r.pathname)); - // https://docs.netlify.com/edge-functions/create-integration/#generate-declarations const netlifyEdgeManifest = { - functions: ssrRoutes.map((r) => { - if (r.paramNames.length > 0) { - return { - // Replace opening and closing "/" if present - pattern: r.pattern.toString().replace(/^\//, '').replace(/\/$/, ''), - function: 'entry.netlify-edge', - }; - } - - return { - path: r.pathname, + functions: [ + { + path: basePathname + '*', function: 'entry.netlify-edge', - }; - }), + }, + ], version: 1, }; @@ -82,6 +75,12 @@ export interface NetlifyEdgeAdaptorOptions { * Determines if the adaptor should also run Static Site Generation (SSG). */ staticGenerate?: StaticGenerateRenderOptions | true; + /** + * Manually add pathnames that should be treated as static paths and not SSR. + * For example, when these pathnames are requested, their response should + * come from a static file, rather than a server-side rendered response. + */ + staticPaths?: string[]; } export type { StaticGenerateRenderOptions }; diff --git a/packages/qwik-city/adaptors/shared/vite/index.ts b/packages/qwik-city/adaptors/shared/vite/index.ts index 2245d100754..d7569c88306 100644 --- a/packages/qwik-city/adaptors/shared/vite/index.ts +++ b/packages/qwik-city/adaptors/shared/vite/index.ts @@ -6,9 +6,10 @@ import type { StaticGenerateRenderOptions, StaticGenerateResult, } from '@builder.io/qwik-city/static'; +import type { BuildRoute } from '../../../buildtime/types'; import fs from 'node:fs'; import { basename, dirname, join, resolve } from 'node:path'; -import type { BuildRoute } from 'packages/qwik-city/buildtime/types'; +import { postBuild } from './post-build'; export function viteAdaptor(opts: ViteAdaptorPluginOptions) { let qwikCityPlugin: QwikCityPlugin | null = null; @@ -17,6 +18,7 @@ export function viteAdaptor(opts: ViteAdaptorPluginOptions) { let renderModulePath: string | null = null; let qwikCityPlanModulePath: string | null = null; let isSsrBuild = false; + let format = 'esm'; const plugin: Plugin = { name: `vite-plugin-qwik-city-${opts.name}`, @@ -29,31 +31,54 @@ export function viteAdaptor(opts: ViteAdaptorPluginOptions) { } }, - configResolved({ build, plugins }) { - isSsrBuild = !!build.ssr; + configResolved(config) { + isSsrBuild = !!config.build.ssr; if (isSsrBuild) { - qwikCityPlugin = plugins.find((p) => p.name === 'vite-plugin-qwik-city') as QwikCityPlugin; + qwikCityPlugin = config.plugins.find( + (p) => p.name === 'vite-plugin-qwik-city' + ) as QwikCityPlugin; if (!qwikCityPlugin) { throw new Error('Missing vite-plugin-qwik-city'); } - qwikVitePlugin = plugins.find((p) => p.name === 'vite-plugin-qwik') as QwikVitePlugin; + qwikVitePlugin = config.plugins.find( + (p) => p.name === 'vite-plugin-qwik' + ) as QwikVitePlugin; if (!qwikVitePlugin) { throw new Error('Missing vite-plugin-qwik'); } - serverOutDir = build.outDir; + serverOutDir = config.build.outDir; - if (build?.ssr !== true) { + if (config.build?.ssr !== true) { throw new Error( `"build.ssr" must be set to "true" in order to use the "${opts.name}" adaptor.` ); } - if (!build?.rollupOptions?.input) { + if (!config.build?.rollupOptions?.input) { throw new Error( `"build.rollupOptions.input" must be set in order to use the "${opts.name}" adaptor.` ); } + + if (config.ssr?.format === 'cjs') { + format = 'cjs'; + } + } + }, + + resolveId(id) { + if (id === STATIC_PATHS_ID) { + return { + id: './' + RESOLVED_STATIC_PATHS_ID, + external: true, + }; + } + if (id === NOT_FOUND_PATHS_ID) { + return { + id: './' + RESOLVED_NOT_FOUND_PATHS_ID, + external: true, + }; } }, @@ -91,6 +116,11 @@ export function viteAdaptor(opts: ViteAdaptorPluginOptions) { await fs.promises.mkdir(serverOutDir, { recursive: true }); await fs.promises.writeFile(serverPackageJsonPath, serverPackageJsonCode); + const staticPaths: string[] = opts.staticPaths || []; + const routes = qwikCityPlugin.api.getRoutes(); + const basePathname = qwikCityPlugin.api.getBasePathname(); + const clientOutDir = qwikVitePlugin.api.getClientOutDir()!; + let staticGenerateResult: StaticGenerateResult | null = null; if (opts.staticGenerate && renderModulePath && qwikCityPlanModulePath) { let origin = opts.origin; @@ -107,8 +137,8 @@ export function viteAdaptor(opts: ViteAdaptorPluginOptions) { const staticGenerate = await import('../../../static'); let generateOpts: StaticGenerateOptions = { - basePathname: qwikCityPlugin.api.getBasePathname(), - outDir: qwikVitePlugin.api.getClientOutDir()!, + basePathname, + outDir: clientOutDir, origin, renderModulePath, qwikCityPlanModulePath, @@ -127,14 +157,29 @@ export function viteAdaptor(opts: ViteAdaptorPluginOptions) { `Error while runnning SSG from "${opts.name}" adaptor. At least one path failed to render.` ); } + + staticPaths.push(...staticGenerateResult.staticPaths); } - if (typeof opts.generateRoutes === 'function') { - await opts.generateRoutes({ + const { staticPathsCode, notFoundPathsCode } = await postBuild( + clientOutDir, + basePathname, + staticPaths, + format, + !!opts.cleanStaticGenerated + ); + + await Promise.all([ + fs.promises.writeFile(join(serverOutDir, RESOLVED_STATIC_PATHS_ID), staticPathsCode), + fs.promises.writeFile(join(serverOutDir, RESOLVED_NOT_FOUND_PATHS_ID), notFoundPathsCode), + ]); + + if (typeof opts.generate === 'function') { + await opts.generate({ serverOutDir, - clientOutDir: qwikVitePlugin.api.getClientOutDir()!, - routes: qwikCityPlugin.api.getRoutes(), - staticPaths: staticGenerateResult?.staticPaths ?? [], + clientOutDir, + basePathname, + routes, warn: (message) => this.warn(message), error: (message) => this.error(message), }); @@ -164,14 +209,22 @@ export function getParentDir(startDir: string, dirName: string) { interface ViteAdaptorPluginOptions { name: string; origin: string; + staticPaths?: string[]; staticGenerate: true | StaticGenerateRenderOptions | undefined; + cleanStaticGenerated?: boolean; config?: (config: UserConfig) => UserConfig; - generateRoutes?: (generateOpts: { + generate?: (generateOpts: { clientOutDir: string; serverOutDir: string; + basePathname: string; routes: BuildRoute[]; - staticPaths: string[]; warn: (message: string) => void; error: (message: string) => void; }) => Promise; } + +const STATIC_PATHS_ID = '@qwik-city-static-paths'; +const RESOLVED_STATIC_PATHS_ID = `${STATIC_PATHS_ID}.js`; + +const NOT_FOUND_PATHS_ID = '@qwik-city-not-found-paths'; +const RESOLVED_NOT_FOUND_PATHS_ID = `${NOT_FOUND_PATHS_ID}.js`; diff --git a/packages/qwik-city/adaptors/shared/vite/post-build.ts b/packages/qwik-city/adaptors/shared/vite/post-build.ts new file mode 100644 index 00000000000..e98199b2277 --- /dev/null +++ b/packages/qwik-city/adaptors/shared/vite/post-build.ts @@ -0,0 +1,131 @@ +import fs from 'node:fs'; +import { join } from 'node:path'; +import { getErrorHtml } from '../../../middleware/request-handler'; + +export async function postBuild( + clientOutDir: string, + basePathname: string, + userStaticPaths: string[], + format: string, + cleanStatic: boolean +) { + const ingorePathnames = new Set([basePathname + 'build/', basePathname + 'assets/']); + + const staticPaths = new Set(userStaticPaths); + const notFounds: string[][] = []; + + const loadItem = async (fsDir: string, fsName: string, pathname: string) => { + if (ingorePathnames.has(pathname)) { + return; + } + + const fsPath = join(fsDir, fsName); + + if (fsName === 'index.html' || fsName === 'q-data.json') { + // static index.html file + if (!staticPaths.has(pathname) && cleanStatic) { + await fs.promises.unlink(fsPath); + } + return; + } + + if (fsName === '404.html') { + // static 404.html file + const notFoundHtml = await fs.promises.readFile(fsPath, 'utf-8'); + notFounds.push([pathname, notFoundHtml]); + return; + } + + const stat = await fs.promises.stat(fsPath); + if (stat.isDirectory()) { + await loadDir(fsPath, pathname + fsName + '/'); + } else if (stat.isFile()) { + staticPaths.add(pathname + fsName); + } + }; + + const loadDir = async (fsDir: string, pathname: string) => { + const itemNames = await fs.promises.readdir(fsDir); + await Promise.all(itemNames.map((i) => loadItem(fsDir, i, pathname))); + }; + + await loadDir(clientOutDir, basePathname); + + const staticPathsCode = createStaticPathsModule(basePathname, staticPaths, format); + const notFoundPathsCode = createNotFoundPathsModule(basePathname, notFounds, format); + + return { + staticPathsCode, + notFoundPathsCode, + }; +} + +function createStaticPathsModule(basePathname: string, staticPaths: Set, format: string) { + const assetsPath = basePathname + 'assets/'; + const baseBuildPath = basePathname + 'build/'; + + const c: string[] = []; + + c.push( + `const staticPaths = new Set(${JSON.stringify( + Array.from(new Set(staticPaths)).sort() + )});` + ); + + c.push(`function isStaticPath(p) {`); + c.push(` if (p.startsWith(${JSON.stringify(baseBuildPath)})) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` if (p.startsWith(${JSON.stringify(assetsPath)})) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` if (staticPaths.has(p)) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` return false;`); + c.push(`}`); + + if (format === 'cjs') { + c.push('module.exports = { isStaticPath: isStaticPath };'); + } else { + c.push('export default { isStaticPath };'); + } + + return c.join('\n'); +} + +function createNotFoundPathsModule(basePathname: string, notFounds: string[][], format: string) { + notFounds.sort((a, b) => { + if (a[0].length > b[0].length) return -1; + if (a[0].length < b[0].length) return 1; + if (a[0] < b[0]) return -1; + if (a[0] > b[0]) return 1; + return 0; + }); + + if (!notFounds.some((r) => r[0] === basePathname)) { + const html = getErrorHtml(404, 'Resource Not Found'); + notFounds.push([basePathname, html]); + } + + const c: string[] = []; + + c.push(`const notFounds = ${JSON.stringify(notFounds, null, 2)};`); + + c.push(`function getNotFound(p) {`); + c.push(` for (const r of notFounds) {`); + c.push(` if (p.startsWith(r[0])) {`); + c.push(` return r[1];`); + c.push(` }`); + c.push(` }`); + c.push(` return "Resource Not Found";`); + c.push(`}`); + + if (format === 'cjs') { + c.push('module.exports = { getNotFound: getNotFound };'); + } else { + c.push('export default { getNotFound };'); + } + + return c.join('\n'); +} diff --git a/packages/qwik-city/adaptors/vercel-edge/api.md b/packages/qwik-city/adaptors/vercel-edge/api.md index 8ef44419ff2..f62f42a14e9 100644 --- a/packages/qwik-city/adaptors/vercel-edge/api.md +++ b/packages/qwik-city/adaptors/vercel-edge/api.md @@ -15,6 +15,7 @@ export function vercelEdgeAdaptor(opts?: VercelEdgeAdaptorOptions): any; export interface VercelEdgeAdaptorOptions { outputConfig?: boolean; staticGenerate?: StaticGenerateRenderOptions | true; + staticPaths?: string[]; vcConfigEntryPoint?: string; vcConfigEnvVarsInUse?: string[]; } diff --git a/packages/qwik-city/adaptors/vercel-edge/vite/index.ts b/packages/qwik-city/adaptors/vercel-edge/vite/index.ts index d45302d2a25..233ef3a5307 100644 --- a/packages/qwik-city/adaptors/vercel-edge/vite/index.ts +++ b/packages/qwik-city/adaptors/vercel-edge/vite/index.ts @@ -11,6 +11,8 @@ export function vercelEdgeAdaptor(opts: VercelEdgeAdaptorOptions = {}): any { name: 'vercel-edge', origin: process?.env?.VERCEL_URL || 'https://yoursitename.vercel.app', staticGenerate: opts.staticGenerate, + staticPaths: opts.staticPaths, + cleanStaticGenerated: true, config(config) { const outDir = config.build?.outDir || '.vercel/output/functions/_qwik-city.func'; @@ -33,24 +35,19 @@ export function vercelEdgeAdaptor(opts: VercelEdgeAdaptorOptions = {}): any { }; }, - async generateRoutes({ clientOutDir, serverOutDir, routes, staticPaths }) { + async generate({ clientOutDir, serverOutDir, basePathname }) { const vercelOutputDir = getParentDir(serverOutDir, 'output'); if (opts.outputConfig !== false) { - const ssrRoutes = routes.filter((r) => !staticPaths.includes(r.pathname)); - // https://vercel.com/docs/build-output-api/v3#features/edge-middleware const vercelOutputConfig = { - routes: ssrRoutes.map((r) => { - let src = r.pattern.toString().slice(1, -2).replace(/\\\//g, '/'); - if (src === '^/') { - src = '^/?'; - } - return { - src, + routes: [ + { handle: 'filesystem' }, + { + src: basePathname + '(.*)', middlewarePath: '_qwik-city', - }; - }), + }, + ], version: 3, }; @@ -107,6 +104,12 @@ export interface VercelEdgeAdaptorOptions { * Determines if the adaptor should also run Static Site Generation (SSG). */ staticGenerate?: StaticGenerateRenderOptions | true; + /** + * Manually add pathnames that should be treated as static paths and not SSR. + * For example, when these pathnames are requested, their response should + * come from a static file, rather than a server-side rendered response. + */ + staticPaths?: string[]; } export type { StaticGenerateRenderOptions }; diff --git a/packages/qwik-city/buildtime/build-menus.unit.ts b/packages/qwik-city/buildtime/build-menus.unit.ts index d452667bfbf..b8371cf0ca8 100644 --- a/packages/qwik-city/buildtime/build-menus.unit.ts +++ b/packages/qwik-city/buildtime/build-menus.unit.ts @@ -8,9 +8,9 @@ test('menus found', ({ menus }) => { }); test('docs menu', ({ menus }) => { - const docsMenu = menus.find((r) => r.pathname === '/docs')!; + const docsMenu = menus.find((r) => r.pathname === '/docs/')!; ok(docsMenu, 'found docs menu'); - equal(docsMenu.pathname, '/docs'); + equal(docsMenu.pathname, '/docs/'); }); test.run(); diff --git a/packages/qwik-city/buildtime/build-pages.unit.ts b/packages/qwik-city/buildtime/build-pages.unit.ts index a7f433a8af5..e2ea815830d 100644 --- a/packages/qwik-city/buildtime/build-pages.unit.ts +++ b/packages/qwik-city/buildtime/build-pages.unit.ts @@ -4,7 +4,7 @@ import { testAppSuite } from '../utils/test-suite'; const test = testAppSuite('Build Pages'); test('layoutStop file', ({ assertRoute }) => { - const r = assertRoute('/mit'); + const r = assertRoute('/mit/'); assert.equal(r.id, 'Mit'); assert.equal(r.pattern, /^\/mit\/?$/); assert.equal(r.paramNames.length, 0); @@ -12,7 +12,7 @@ test('layoutStop file', ({ assertRoute }) => { }); test('pathless directory', ({ assertRoute }) => { - const r = assertRoute('/sign-in'); + const r = assertRoute('/sign-in/'); assert.equal(r.id, 'AuthSignin'); assert.equal(r.pattern, /^\/sign-in\/?$/); assert.equal(r.paramNames.length, 0); @@ -24,7 +24,7 @@ test('pathless directory', ({ assertRoute }) => { test('index file w/ nested named layout, in directory w/ nested named layout', ({ assertRoute, }) => { - const r = assertRoute('/api'); + const r = assertRoute('/api/'); assert.equal(r.id, 'ApiIndexapi'); assert.equal(r.pattern, /^\/api\/?$/); assert.equal(r.paramNames.length, 0); @@ -38,7 +38,7 @@ test('index file w/ nested named layout, in directory w/ nested named layout', ( }); test('index file w/out named layout, in directory w/ named layout', ({ assertRoute }) => { - const r = assertRoute('/dashboard'); + const r = assertRoute('/dashboard/'); assert.equal(r.id, 'Dashboard'); assert.equal(r.pattern, /^\/dashboard\/?$/); assert.equal(r.paramNames.length, 0); @@ -48,7 +48,7 @@ test('index file w/out named layout, in directory w/ named layout', ({ assertRou }); test('index file in directory w/ nested named layout file', ({ assertRoute }) => { - const r = assertRoute('/dashboard/profile'); + const r = assertRoute('/dashboard/profile/'); assert.equal(r.id, 'DashboardProfile'); assert.equal(r.pattern, /^\/dashboard\/profile\/?$/); assert.equal(r.paramNames.length, 0); @@ -58,7 +58,7 @@ test('index file in directory w/ nested named layout file', ({ assertRoute }) => }); test('index file in directory w/ top named layout file', ({ assertRoute }) => { - const r = assertRoute('/dashboard/settings'); + const r = assertRoute('/dashboard/settings/'); assert.equal(r.id, 'DashboardSettings'); assert.equal(r.pattern, /^\/dashboard\/settings\/?$/); assert.equal(r.paramNames.length, 0); @@ -70,7 +70,7 @@ test('index file in directory w/ top named layout file', ({ assertRoute }) => { test('params route, index file w/out named layout, in directory w/ top layout directory', ({ assertRoute, }) => { - const r = assertRoute('/docs/[category]/[id]'); + const r = assertRoute('/docs/[category]/[id]/'); assert.equal(r.id, 'DocsCategoryId'); assert.equal(r.pattern, /^\/docs\/([^/]+?)\/([^/]+?)\/?$/); assert.equal(r.paramNames.length, 2); @@ -83,7 +83,7 @@ test('params route, index file w/out named layout, in directory w/ top layout di test('markdown index file w/out named layout, in directory w/ top layout directory', ({ assertRoute, }) => { - const r = assertRoute('/docs/overview'); + const r = assertRoute('/docs/overview/'); assert.equal(r.id, 'DocsOverview'); assert.equal(r.pattern, /^\/docs\/overview\/?$/); assert.equal(r.paramNames.length, 0); @@ -94,7 +94,7 @@ test('markdown index file w/out named layout, in directory w/ top layout directo test('markdown file w/out named layout, in directory w/ top layout directory', ({ assertRoute, }) => { - const r = assertRoute('/docs/getting-started'); + const r = assertRoute('/docs/getting-started/'); assert.equal(r.id, 'DocsGettingstarted'); assert.equal(r.pattern, /^\/docs\/getting-started\/?$/); assert.equal(r.paramNames.length, 0); @@ -103,7 +103,7 @@ test('markdown file w/out named layout, in directory w/ top layout directory', ( }); test('index file w/out named layout, in directory w/ top layout directory', ({ assertRoute }) => { - const r = assertRoute('/docs'); + const r = assertRoute('/docs/'); assert.equal(r.id, 'Docs'); assert.equal(r.pattern, /^\/docs\/?$/); assert.equal(r.paramNames.length, 0); @@ -114,7 +114,7 @@ test('index file w/out named layout, in directory w/ top layout directory', ({ a }); test('named file w/out named layout, in directory w/ layout directory', ({ assertRoute }) => { - const r = assertRoute('/about-us'); + const r = assertRoute('/about-us/'); assert.equal(r.id, 'Aboutus'); assert.equal(r.pattern, /^\/about-us\/?$/); assert.equal(r.paramNames.length, 0); @@ -124,7 +124,7 @@ test('named file w/out named layout, in directory w/ layout directory', ({ asser }); test('named tsx file', ({ assertRoute }) => { - const r = assertRoute('/about-us'); + const r = assertRoute('/about-us/'); assert.equal(r.id, 'Aboutus'); assert.equal(r.pattern, /^\/about-us\/?$/); assert.equal(r.paramNames.length, 0); diff --git a/packages/qwik-city/buildtime/build.ts b/packages/qwik-city/buildtime/build.ts index 2860f8253e0..72ae378a0ed 100644 --- a/packages/qwik-city/buildtime/build.ts +++ b/packages/qwik-city/buildtime/build.ts @@ -28,7 +28,6 @@ export async function updateBuildContext(ctx: BuildContext) { const resolved = resolveSourceFiles(ctx.opts, sourceFiles); ctx.layouts = resolved.layouts; ctx.routes = resolved.routes; - ctx.errors = resolved.errors; ctx.entries = resolved.entries; ctx.serviceWorkers = resolved.serviceWorkers; ctx.menus = resolved.menus; diff --git a/packages/qwik-city/buildtime/context.ts b/packages/qwik-city/buildtime/context.ts index 676a085fd4f..fb84a50ec78 100644 --- a/packages/qwik-city/buildtime/context.ts +++ b/packages/qwik-city/buildtime/context.ts @@ -11,7 +11,6 @@ export function createBuildContext( rootDir: normalizePath(rootDir), opts: normalizeOptions(rootDir, userOpts), routes: [], - errors: [], layouts: [], entries: [], serviceWorkers: [], @@ -30,7 +29,6 @@ export function createBuildContext( export function resetBuildContext(ctx: BuildContext | null) { if (ctx) { ctx.routes.length = 0; - ctx.errors.length = 0; ctx.layouts.length = 0; ctx.entries.length = 0; ctx.menus.length = 0; @@ -66,7 +64,7 @@ function normalizeOptions(rootDir: string, userOpts: PluginOptions | undefined) } if (typeof opts.trailingSlash !== 'boolean') { - opts.trailingSlash = false; + opts.trailingSlash = true; } opts.mdx = opts.mdx || {}; diff --git a/packages/qwik-city/buildtime/markdown/markdown-url.unit.ts b/packages/qwik-city/buildtime/markdown/markdown-url.unit.ts index 1887e424245..715ffabd651 100644 --- a/packages/qwik-city/buildtime/markdown/markdown-url.unit.ts +++ b/packages/qwik-city/buildtime/markdown/markdown-url.unit.ts @@ -5,58 +5,70 @@ import type { NormalizedPluginOptions } from '../types'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -test('getMarkdownRelativeUrl', () => { - const routesDir = tmpdir(); - const menuFilePath = join(routesDir, 'docs', 'menu.md'); - - const t = [ - { - href: './getting-started/index.mdx', - expect: '/docs/getting-started', - }, - { - href: './getting-started/index.mdx?intro', - expect: '/docs/getting-started?intro', - }, - { - href: './getting-started/index.mdx#intro', - expect: '/docs/getting-started#intro', - }, - { - href: './getting-started/index.mdx#intro', - trailingSlash: true, - expect: '/docs/getting-started/#intro', - }, - { - href: '/link', - expect: '/link', - }, - { - href: '/link/index.mdx', - expect: '/link', - }, - { - href: 'http://builder.io/', - expect: 'http://builder.io/', - }, - { - href: '#hash', - expect: '#hash', - }, - { - href: '', - expect: '', - }, - { - href: './getting-started.txt', - expect: './getting-started.txt', - }, - ]; - - t.forEach((c) => { +const routesDir = tmpdir(); +const menuFilePath = join(routesDir, 'docs', 'menu.md'); +[ + { + href: './getting-started/index.mdx', + trailingSlash: false, + expect: '/docs/getting-started', + }, + { + href: './getting-started/index.mdx?intro', + trailingSlash: false, + expect: '/docs/getting-started?intro', + }, + { + href: './getting-started/index.mdx#intro', + trailingSlash: false, + expect: '/docs/getting-started#intro', + }, + { + href: './getting-started/index.mdx#intro', + trailingSlash: true, + expect: '/docs/getting-started/#intro', + }, + { + href: '/link', + trailingSlash: false, + expect: '/link', + }, + { + href: '/link/', + trailingSlash: true, + expect: '/link/', + }, + { + href: '/link/index.mdx', + trailingSlash: false, + expect: '/link', + }, + { + href: '/link/index.mdx', + trailingSlash: true, + expect: '/link/', + }, + { + href: 'http://builder.io/', + expect: 'http://builder.io/', + }, + { + href: '#hash', + expect: '#hash', + }, + { + href: '', + expect: '', + }, + { + href: './getting-started.txt', + expect: './getting-started.txt', + }, +].forEach((t) => { + test(`getMarkdownRelativeUrl ${t.href}`, () => { const opts: NormalizedPluginOptions = { basePathname: '/', - trailingSlash: !!c.trailingSlash, + trailingSlash: !!t.trailingSlash, routesDir: routesDir, mdxPlugins: { remarkGfm: true, @@ -66,7 +78,7 @@ test('getMarkdownRelativeUrl', () => { mdx: {}, baseUrl: '/', }; - equal(getMarkdownRelativeUrl(opts, menuFilePath, c.href), c.expect); + equal(getMarkdownRelativeUrl(opts, menuFilePath, t.href), t.expect); }); }); diff --git a/packages/qwik-city/buildtime/markdown/menu.unit.ts b/packages/qwik-city/buildtime/markdown/menu.unit.ts index 8af7c5756fd..889beb5cf60 100644 --- a/packages/qwik-city/buildtime/markdown/menu.unit.ts +++ b/packages/qwik-city/buildtime/markdown/menu.unit.ts @@ -24,7 +24,7 @@ test('parse menu.md menu', ({ opts }) => { `; const menu = createMenu(opts, filePath); - assert.is(menu.pathname, '/guide'); + assert.is(menu.pathname, '/guide/'); const i = parseMenu(opts, filePath, content, false); assert.is(i.text, 'Heading'); @@ -38,7 +38,7 @@ test('parse menu.md menu', ({ opts }) => { assert.is(i.items![1].text, 'Section B'); assert.is(i.items![1].items?.length, 2); assert.is(i.items![1].items![0].text, 'Link B1'); - assert.is(i.items![1].items![0].href, '/guide/link-b1'); + assert.is(i.items![1].items![0].href, '/guide/link-b1/'); assert.is(i.items![1].items![1].text, 'Text B1'); assert.is(i.items![2].text, 'Section C'); diff --git a/packages/qwik-city/buildtime/routing/resolve-source-file.ts b/packages/qwik-city/buildtime/routing/resolve-source-file.ts index a98c02d47d6..d778a626cf0 100644 --- a/packages/qwik-city/buildtime/routing/resolve-source-file.ts +++ b/packages/qwik-city/buildtime/routing/resolve-source-file.ts @@ -29,12 +29,6 @@ export function resolveSourceFiles(opts: NormalizedPluginOptions, sourceFiles: R .map((s) => resolveRoute(opts, layouts, s)) .sort(routeSortCompare); - const errors = sourceFiles - .filter((s) => s.type === 'error') - .map((s) => resolveError(opts, layouts, s)) - .filter((s) => s) - .sort(routeSortCompare); - const entries = sourceFiles .filter((s) => s.type === 'entry') .map((s) => resolveEntry(opts, s)) @@ -71,11 +65,10 @@ export function resolveSourceFiles(opts: NormalizedPluginOptions, sourceFiles: R uniqueIds(layouts); uniqueIds(routes); - uniqueIds(errors); uniqueIds(entries); uniqueIds(serviceWorkers); - return { layouts, routes, errors, entries, menus, serviceWorkers }; + return { layouts, routes, entries, menus, serviceWorkers }; } export function resolveLayout(opts: NormalizedPluginOptions, layoutSourceFile: RouteSourceFile) { @@ -123,7 +116,11 @@ export function resolveRoute( const layouts: BuildLayout[] = []; const routesDir = opts.routesDir; const { layoutName, layoutStop } = parseRouteIndexName(sourceFile.extlessName); - const pathname = getPathnameFromDirPath(opts, sourceFile.dirPath); + let pathname = getPathnameFromDirPath(opts, sourceFile.dirPath); + + if (sourceFile.extlessName === '404') { + pathname += sourceFile.extlessName + '.html'; + } if (!layoutStop) { let currentDir = normalizePath(dirname(filePath)); @@ -169,14 +166,6 @@ export function resolveRoute( return buildRoute; } -export function resolveError( - opts: NormalizedPluginOptions, - appLayouts: BuildLayout[], - sourceFile: RouteSourceFile -) { - return resolveRoute(opts, appLayouts, sourceFile); -} - function resolveEntry(opts: NormalizedPluginOptions, sourceFile: RouteSourceFile) { const pathname = getPathnameFromDirPath(opts, sourceFile.dirPath); const chunkFileName = pathname.slice(1); diff --git a/packages/qwik-city/buildtime/routing/source-file.ts b/packages/qwik-city/buildtime/routing/source-file.ts index 578491f8ef6..a1a73c1cbc9 100644 --- a/packages/qwik-city/buildtime/routing/source-file.ts +++ b/packages/qwik-city/buildtime/routing/source-file.ts @@ -19,7 +19,10 @@ export function getSourceFile(fileName: string) { const isMarkdown = isMarkdownExt(ext); let type: RouteSourceType | null = null; - if (extlessName.startsWith('index') && (isPageModule || isModule || isMarkdown)) { + if ( + (extlessName.startsWith('index') || isErrorName(extlessName)) && + (isPageModule || isModule || isMarkdown) + ) { // route page or endpoint // index@layoutname or index! - ts|tsx|js|jsx|md|mdx type = 'route'; @@ -29,9 +32,6 @@ export function getSourceFile(fileName: string) { } else if (isEntryName(extlessName) && isModule) { // entry module - ts|js type = 'entry'; - } else if (isErrorName(extlessName) && (isPageModule || isMarkdown)) { - // 404 or 500 - ts|tsx|js|jsx|md|mdx - type = 'error'; } else if (isMenuFileName(fileName)) { // menu.md type = 'menu'; diff --git a/packages/qwik-city/buildtime/routing/source-file.unit.ts b/packages/qwik-city/buildtime/routing/source-file.unit.ts index 42560f2581f..91c386dec23 100644 --- a/packages/qwik-city/buildtime/routing/source-file.unit.ts +++ b/packages/qwik-city/buildtime/routing/source-file.unit.ts @@ -2,202 +2,200 @@ import { test } from 'uvu'; import { equal } from 'uvu/assert'; import { getSourceFile } from './source-file'; -test(`getSourceFile`, () => { - const t = [ - { - fileName: '404.md', - expect: { - type: 'error', - extlessName: '404', - ext: '.md', - }, - }, - { - fileName: '404.tsx', - expect: { - type: 'error', - extlessName: '404', - ext: '.tsx', - }, - }, - { - fileName: '500.tsx', - expect: { - type: 'error', - extlessName: '500', - ext: '.tsx', - }, - }, - { - fileName: 'entry.md', - expect: null, - }, - { - fileName: 'entry.ts', - expect: { - type: 'entry', - extlessName: 'entry', - ext: '.ts', - }, - }, - { - fileName: 'service-worker.ts', - expect: { - type: 'service-worker', - extlessName: 'service-worker', - ext: '.ts', - }, - }, - { - fileName: 'service-worker.js', - expect: { - type: 'service-worker', - extlessName: 'service-worker', - ext: '.js', - }, - }, - { - fileName: 'service-worker.tsx', - expect: null, - }, - { - fileName: 'menu.md', - expect: { - type: 'menu', - extlessName: 'menu', - ext: '.md', - }, - }, - { - fileName: 'menu.mdx', - expect: null, - }, - { - fileName: 'menu.tsx', - expect: null, - }, - { - fileName: 'layout-name!.jsx', - expect: { - type: 'layout', - extlessName: 'layout-name!', - ext: '.jsx', - }, - }, - { - fileName: 'layout-name.jsx', - expect: { - type: 'layout', - extlessName: 'layout-name', - ext: '.jsx', - }, - }, - { - fileName: 'layout.jsx', - expect: { - type: 'layout', - extlessName: 'layout', - ext: '.jsx', - }, - }, - { - fileName: 'layout!.js', - expect: { - type: 'layout', - extlessName: 'layout!', - ext: '.js', - }, - }, - { - fileName: 'layout.js', - expect: { - type: 'layout', - extlessName: 'layout', - ext: '.js', - }, - }, - { - fileName: 'layou.css', - expect: null, - }, - { - fileName: 'index.css', - expect: null, - }, - { - fileName: 'index.tsx.json', - expect: null, - }, - { - fileName: 'index.mdx', - expect: { - type: 'route', - extlessName: 'index', - ext: '.mdx', - }, - }, - { - fileName: 'index.md', - expect: { - type: 'route', - extlessName: 'index', - ext: '.md', - }, - }, - { - fileName: 'index.ts', - expect: { - type: 'route', - extlessName: 'index', - ext: '.ts', - }, - }, - { - fileName: 'index@layoutname!.tsx', - expect: { - type: 'route', - extlessName: 'index@layoutname!', - ext: '.tsx', - }, - }, - { - fileName: 'index@layoutname.tsx', - expect: { - type: 'route', - extlessName: 'index@layoutname', - ext: '.tsx', - }, - }, - { - fileName: 'index!.tsx', - expect: { - type: 'route', - extlessName: 'index!', - ext: '.tsx', - }, - }, - { - fileName: 'index.tsx', - expect: { - type: 'route', - extlessName: 'index', - ext: '.tsx', - }, - }, - { - fileName: 'index.d.ts', - expect: null, - }, - ]; - - t.forEach((c) => { - const s = getSourceFile(c.fileName); - if (s == null || c.expect == null) { - equal(s, c.expect, c.fileName); +[ + { + fileName: '404.md', + expect: { + type: 'route', + extlessName: '404', + ext: '.md', + }, + }, + { + fileName: '404.tsx', + expect: { + type: 'route', + extlessName: '404', + ext: '.tsx', + }, + }, + { + fileName: '500.tsx', + expect: { + type: 'route', + extlessName: '500', + ext: '.tsx', + }, + }, + { + fileName: 'entry.md', + expect: null, + }, + { + fileName: 'entry.ts', + expect: { + type: 'entry', + extlessName: 'entry', + ext: '.ts', + }, + }, + { + fileName: 'service-worker.ts', + expect: { + type: 'service-worker', + extlessName: 'service-worker', + ext: '.ts', + }, + }, + { + fileName: 'service-worker.js', + expect: { + type: 'service-worker', + extlessName: 'service-worker', + ext: '.js', + }, + }, + { + fileName: 'service-worker.tsx', + expect: null, + }, + { + fileName: 'menu.md', + expect: { + type: 'menu', + extlessName: 'menu', + ext: '.md', + }, + }, + { + fileName: 'menu.mdx', + expect: null, + }, + { + fileName: 'menu.tsx', + expect: null, + }, + { + fileName: 'layout-name!.jsx', + expect: { + type: 'layout', + extlessName: 'layout-name!', + ext: '.jsx', + }, + }, + { + fileName: 'layout-name.jsx', + expect: { + type: 'layout', + extlessName: 'layout-name', + ext: '.jsx', + }, + }, + { + fileName: 'layout.jsx', + expect: { + type: 'layout', + extlessName: 'layout', + ext: '.jsx', + }, + }, + { + fileName: 'layout!.js', + expect: { + type: 'layout', + extlessName: 'layout!', + ext: '.js', + }, + }, + { + fileName: 'layout.js', + expect: { + type: 'layout', + extlessName: 'layout', + ext: '.js', + }, + }, + { + fileName: 'layou.css', + expect: null, + }, + { + fileName: 'index.css', + expect: null, + }, + { + fileName: 'index.tsx.json', + expect: null, + }, + { + fileName: 'index.mdx', + expect: { + type: 'route', + extlessName: 'index', + ext: '.mdx', + }, + }, + { + fileName: 'index.md', + expect: { + type: 'route', + extlessName: 'index', + ext: '.md', + }, + }, + { + fileName: 'index.ts', + expect: { + type: 'route', + extlessName: 'index', + ext: '.ts', + }, + }, + { + fileName: 'index@layoutname!.tsx', + expect: { + type: 'route', + extlessName: 'index@layoutname!', + ext: '.tsx', + }, + }, + { + fileName: 'index@layoutname.tsx', + expect: { + type: 'route', + extlessName: 'index@layoutname', + ext: '.tsx', + }, + }, + { + fileName: 'index!.tsx', + expect: { + type: 'route', + extlessName: 'index!', + ext: '.tsx', + }, + }, + { + fileName: 'index.tsx', + expect: { + type: 'route', + extlessName: 'index', + ext: '.tsx', + }, + }, + { + fileName: 'index.d.ts', + expect: null, + }, +].forEach((t) => { + test(`getSourceFile ${t.fileName}`, () => { + const s = getSourceFile(t.fileName); + if (s == null || t.expect == null) { + equal(s, t.expect, t.fileName); } else { - equal(s.type, c.expect.type, c.fileName); - equal(s.extlessName, c.expect.extlessName, c.fileName); - equal(s.ext, c.expect.ext, c.fileName); + equal(s.type, t.expect.type, t.fileName); + equal(s.extlessName, t.expect.extlessName, t.fileName); + equal(s.ext, t.expect.ext, t.fileName); } }); }); diff --git a/packages/qwik-city/buildtime/types.ts b/packages/qwik-city/buildtime/types.ts index 29195e33e0b..7f08f58051a 100644 --- a/packages/qwik-city/buildtime/types.ts +++ b/packages/qwik-city/buildtime/types.ts @@ -2,7 +2,6 @@ export interface BuildContext { rootDir: string; opts: NormalizedPluginOptions; routes: BuildRoute[]; - errors: BuildRoute[]; layouts: BuildLayout[]; entries: BuildEntry[]; serviceWorkers: BuildEntry[]; @@ -46,7 +45,7 @@ export interface RouteSourceFileName { ext: string; } -export type RouteSourceType = 'route' | 'layout' | 'entry' | 'menu' | 'error' | 'service-worker'; +export type RouteSourceType = 'route' | 'layout' | 'entry' | 'menu' | 'service-worker'; export interface BuildRoute extends ParsedPathname { /** @@ -119,7 +118,8 @@ export interface PluginOptions { */ basePathname?: string; /** - * Ensure a trailing slash ends page urls. Defaults to `false`. + * Ensure a trailing slash ends page urls. Defaults to `true`. + * (Note: Previous versions defaulted to `false`). */ trailingSlash?: boolean; /** diff --git a/packages/qwik-city/buildtime/vite/dev-server.ts b/packages/qwik-city/buildtime/vite/dev-server.ts index 278371327c7..6a5fdb6178d 100644 --- a/packages/qwik-city/buildtime/vite/dev-server.ts +++ b/packages/qwik-city/buildtime/vite/dev-server.ts @@ -61,7 +61,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { return; } - const requestCtx = fromNodeHttp(url, req, res); + const requestCtx = fromNodeHttp(url, req, res, 'dev'); updateRequestCtx(requestCtx, ctx.opts.trailingSlash); await updateBuildContext(ctx); @@ -103,7 +103,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { if (userResponse.type === 'pagedata') { // dev server endpoint handler - await pageHandler('dev', requestCtx, userResponse, noopDevRender); + await pageHandler(requestCtx, userResponse, noopDevRender); return; } @@ -199,7 +199,7 @@ export function dev404Middleware() { return async (req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => { try { const url = new URL(req.originalUrl!, `http://${req.headers.host}`); - const requestCtx = fromNodeHttp(url, req, res); + const requestCtx = fromNodeHttp(url, req, res, 'dev'); await notFoundHandler(requestCtx); } catch (e) { next(e); diff --git a/packages/qwik-city/middleware/cloudflare-pages/api.md b/packages/qwik-city/middleware/cloudflare-pages/api.md index 9cf6784e27a..5d731371232 100644 --- a/packages/qwik-city/middleware/cloudflare-pages/api.md +++ b/packages/qwik-city/middleware/cloudflare-pages/api.md @@ -6,11 +6,10 @@ import type { Render } from '@builder.io/qwik/server'; import type { RenderOptions } from '@builder.io/qwik/server'; -import type { RenderOptions as RenderOptions_2 } from '@builder.io/qwik'; import type { RequestHandler as RequestHandler_2 } from '@builder.io/qwik-city'; // @alpha (undocumented) -export function createQwikCity(opts: QwikCityCloudflarePagesOptions): ({ request, env, waitUntil }: EventPluginContext) => Promise; +export function createQwikCity(opts: QwikCityCloudflarePagesOptions): ({ request, env, waitUntil, next }: EventPluginContext) => Promise; // @alpha (undocumented) export interface EventPluginContext { @@ -24,9 +23,6 @@ export interface EventPluginContext { waitUntil: (promise: Promise) => void; } -// @alpha @deprecated (undocumented) -export function qwikCity(render: Render, opts?: RenderOptions_2): ({ request, env, waitUntil }: EventPluginContext) => Promise; - // Warning: (ae-forgotten-export) The symbol "QwikCityHandlerOptions" needs to be exported by the entry point index.d.ts // // @alpha (undocumented) diff --git a/packages/qwik-city/middleware/cloudflare-pages/index.ts b/packages/qwik-city/middleware/cloudflare-pages/index.ts index bb0affab0ae..84d67cd204b 100644 --- a/packages/qwik-city/middleware/cloudflare-pages/index.ts +++ b/packages/qwik-city/middleware/cloudflare-pages/index.ts @@ -1,10 +1,9 @@ import type { QwikCityHandlerOptions, QwikCityRequestContext } from '../request-handler/types'; -import { notFoundHandler, requestHandler } from '../request-handler'; -import type { RenderOptions } from '@builder.io/qwik'; -import type { Render } from '@builder.io/qwik/server'; import type { RequestHandler } from '@builder.io/qwik-city'; -import qwikCityPlan from '@qwik-city-plan'; +import { requestHandler } from '../request-handler'; import { mergeHeadersCookies } from '../request-handler/cookie'; +import qwikCityStaticPaths from '@qwik-city-static-paths'; +import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; // @builder.io/qwik-city/middleware/cloudflare-pages @@ -12,10 +11,17 @@ import { mergeHeadersCookies } from '../request-handler/cookie'; * @alpha */ export function createQwikCity(opts: QwikCityCloudflarePagesOptions) { - async function onRequest({ request, env, waitUntil }: EventPluginContext) { + const { isStaticPath } = qwikCityStaticPaths; + + async function onRequest({ request, env, waitUntil, next }: EventPluginContext) { try { const url = new URL(request.url); + if (isStaticPath(url.pathname)) { + // known static path, let cloudflare handle it + return next(); + } + // https://developers.cloudflare.com/workers/runtime-apis/cache/ const useCache = url.hostname !== '127.0.0.1' && @@ -32,6 +38,7 @@ export function createQwikCity(opts: QwikCityCloudflarePagesOptions) { } const requestCtx: QwikCityRequestContext = { + mode: 'server', locale: undefined, url, request, @@ -78,20 +85,23 @@ export function createQwikCity(opts: QwikCityCloudflarePagesOptions) { }; // send request to qwik city request handler - const handledResponse = await requestHandler('server', requestCtx, opts); + const handledResponse = await requestHandler(requestCtx, opts); if (handledResponse) { return handledResponse; } // qwik city did not have a route for this request - // respond with qwik city's 404 handler - const notFoundResponse = await notFoundHandler(requestCtx); - return notFoundResponse; + // response with 404 for this pathname + const notFoundHtml = qwikCityNotFoundPaths.getNotFound(url.pathname); + return new Response(notFoundHtml, { + status: 404, + headers: { 'Content-Type': 'text/html; charset=utf-8', 'X-Not-Found': url.pathname }, + }); } catch (e: any) { console.error(e); return new Response(String(e || 'Error'), { status: 500, - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Error': 'cloudflare-pages' }, }); } } @@ -114,24 +124,6 @@ export interface EventPluginContext { env: Record; } -/** - * @alpha - * @deprecated Please use `createQwikCity()` instead. - * - * Example: - * - * ```ts - * import { createQwikCity } from '@builder.io/qwik-city/middleware/cloudflare-pages'; - * import qwikCityPlan from '@qwik-city-plan'; - * import render from './entry.ssr'; - * - * export const onRequest = createQwikCity({ render, qwikCityPlan }); - * ``` - */ -export function qwikCity(render: Render, opts?: RenderOptions) { - return createQwikCity({ render, qwikCityPlan, ...opts }); -} - /** * @alpha */ diff --git a/packages/qwik-city/middleware/netlify-edge/api.md b/packages/qwik-city/middleware/netlify-edge/api.md index c90f97179c8..8b286fff52e 100644 --- a/packages/qwik-city/middleware/netlify-edge/api.md +++ b/packages/qwik-city/middleware/netlify-edge/api.md @@ -7,7 +7,6 @@ import type { Context } from '@netlify/edge-functions'; import type { Render } from '@builder.io/qwik/server'; import type { RenderOptions } from '@builder.io/qwik/server'; -import type { RenderOptions as RenderOptions_2 } from '@builder.io/qwik'; import type { RequestHandler as RequestHandler_2 } from '@builder.io/qwik-city'; // @alpha (undocumented) @@ -17,9 +16,6 @@ export function createQwikCity(opts: QwikCityNetlifyOptions): (request: Request, export interface EventPluginContext extends Context { } -// @alpha @deprecated (undocumented) -export function qwikCity(render: Render, opts?: RenderOptions_2): (request: Request, context: Context) => Promise; - // Warning: (ae-forgotten-export) The symbol "QwikCityHandlerOptions" needs to be exported by the entry point index.d.ts // // @alpha (undocumented) diff --git a/packages/qwik-city/middleware/netlify-edge/index.ts b/packages/qwik-city/middleware/netlify-edge/index.ts index 32bc8576046..ad458e6a692 100644 --- a/packages/qwik-city/middleware/netlify-edge/index.ts +++ b/packages/qwik-city/middleware/netlify-edge/index.ts @@ -1,11 +1,10 @@ import type { Context } from '@netlify/edge-functions'; import type { QwikCityHandlerOptions, QwikCityRequestContext } from '../request-handler/types'; -import { notFoundHandler, requestHandler } from '../request-handler'; -import type { Render } from '@builder.io/qwik/server'; -import type { RenderOptions } from '@builder.io/qwik'; import type { RequestHandler } from '@builder.io/qwik-city'; -import qwikCityPlan from '@qwik-city-plan'; +import { requestHandler } from '../request-handler'; import { mergeHeadersCookies } from '../request-handler/cookie'; +import qwikCityStaticPaths from '@qwik-city-static-paths'; +import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; // @builder.io/qwik-city/middleware/netlify-edge @@ -13,15 +12,19 @@ import { mergeHeadersCookies } from '../request-handler/cookie'; * @alpha */ export function createQwikCity(opts: QwikCityNetlifyOptions) { + const { isStaticPath } = qwikCityStaticPaths; + async function onRequest(request: Request, context: Context) { try { const url = new URL(request.url); - if (url.pathname.startsWith('/.netlify')) { + if (isStaticPath(url.pathname) || url.pathname.startsWith('/.netlify')) { + // known static path, let netlify handle it return context.next(); } const requestCtx: QwikCityRequestContext = { + mode: 'server', locale: undefined, url, request, @@ -61,20 +64,23 @@ export function createQwikCity(opts: QwikCityNetlifyOptions) { }; // send request to qwik city request handler - const handledResponse = await requestHandler('server', requestCtx, opts); + const handledResponse = await requestHandler(requestCtx, opts); if (handledResponse) { return handledResponse; } // qwik city did not have a route for this request - // respond with qwik city's 404 handler - const notFoundResponse = await notFoundHandler(requestCtx); - return notFoundResponse; + // response with 404 for this pathname + const notFoundHtml = qwikCityNotFoundPaths.getNotFound(url.pathname); + return new Response(notFoundHtml, { + status: 404, + headers: { 'Content-Type': 'text/html; charset=utf-8', 'X-Not-Found': url.pathname }, + }); } catch (e: any) { console.error(e); return new Response(String(e || 'Error'), { status: 500, - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Error': 'netlify-edge' }, }); } } @@ -92,24 +98,6 @@ export interface QwikCityNetlifyOptions extends QwikCityHandlerOptions {} */ export interface EventPluginContext extends Context {} -/** - * @alpha - * @deprecated Please use `createQwikCity()` instead. - * - * Example: - * - * ```ts - * import { createQwikCity } from '@builder.io/qwik-city/middleware/netlify-edge'; - * import qwikCityPlan from '@qwik-city-plan'; - * import render from './entry.ssr'; - * - * export default createQwikCity({ render, qwikCityPlan }); - * ``` - */ -export function qwikCity(render: Render, opts?: RenderOptions) { - return createQwikCity({ render, qwikCityPlan, ...opts }); -} - /** * @alpha */ diff --git a/packages/qwik-city/middleware/node/http.ts b/packages/qwik-city/middleware/node/http.ts index 554bb6ffcd5..dbc922a8d03 100644 --- a/packages/qwik-city/middleware/node/http.ts +++ b/packages/qwik-city/middleware/node/http.ts @@ -1,6 +1,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; -import { createHeaders } from '../request-handler/headers'; +import type { QwikCityMode } from '../../runtime/src/types'; import type { QwikCityRequestContext } from '../request-handler/types'; +import { createHeaders } from '../request-handler/headers'; export function getUrl(req: IncomingMessage) { const protocol = @@ -8,7 +9,12 @@ export function getUrl(req: IncomingMessage) { return new URL(req.url || '/', `${protocol}://${req.headers.host}`); } -export function fromNodeHttp(url: URL, req: IncomingMessage, res: ServerResponse) { +export function fromNodeHttp( + url: URL, + req: IncomingMessage, + res: ServerResponse, + mode: QwikCityMode +) { const requestHeaders = createHeaders(); const nodeRequestHeaders = req.headers; for (const key in nodeRequestHeaders) { @@ -31,6 +37,7 @@ export function fromNodeHttp(url: URL, req: IncomingMessage, res: ServerResponse }; const requestCtx: QwikCityRequestContext = { + mode, request: { headers: requestHeaders, formData: async () => { diff --git a/packages/qwik-city/middleware/node/index.ts b/packages/qwik-city/middleware/node/index.ts index 6a6aec4cf03..20504717120 100644 --- a/packages/qwik-city/middleware/node/index.ts +++ b/packages/qwik-city/middleware/node/index.ts @@ -1,11 +1,12 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; import type { QwikCityHandlerOptions } from '../request-handler/types'; -import { errorHandler, notFoundHandler, requestHandler } from '../request-handler'; +import { errorHandler, requestHandler } from '../request-handler'; import { fromNodeHttp, getUrl } from './http'; import { patchGlobalFetch } from './node-fetch'; import type { Render } from '@builder.io/qwik/server'; import type { RenderOptions } from '@builder.io/qwik'; import qwikCityPlan from '@qwik-city-plan'; +import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; // @builder.io/qwik-city/middleware/node @@ -21,9 +22,9 @@ export function createQwikCity(opts: QwikCityNodeRequestOptions) { next: NodeRequestNextFunction ) => { try { - const requestCtx = fromNodeHttp(getUrl(req), req, res); + const requestCtx = fromNodeHttp(getUrl(req), req, res, 'server'); try { - const rsp = await requestHandler('server', requestCtx, opts); + const rsp = await requestHandler(requestCtx, opts); if (!rsp) { next(); } @@ -38,8 +39,13 @@ export function createQwikCity(opts: QwikCityNodeRequestOptions) { const notFound = async (req: IncomingMessage, res: ServerResponse, next: (e: any) => void) => { try { - const requestCtx = fromNodeHttp(getUrl(req), req, res); - await notFoundHandler(requestCtx); + const url = getUrl(req); + const notFoundHtml = qwikCityNotFoundPaths.getNotFound(url.pathname); + res.writeHead(404, { + 'Content-Type': 'text/html; charset=utf-8', + 'X-Not-Found': url.pathname, + }); + res.end(notFoundHtml); } catch (e) { console.error(e); next(e); diff --git a/packages/qwik-city/middleware/request-handler/error-handler.ts b/packages/qwik-city/middleware/request-handler/error-handler.ts index bc284067ff4..f24288ce25b 100644 --- a/packages/qwik-city/middleware/request-handler/error-handler.ts +++ b/packages/qwik-city/middleware/request-handler/error-handler.ts @@ -94,14 +94,11 @@ function minimalHtmlResponse(status: number, message?: string, stack?: string) { -

- ${status} - ${message} -

- ${stack ? `
${stack}
` : ``} +

${status} ${message}

${ + stack ? `\n
${stack}
` : `` + } - -`; +`; } const COLOR_400 = '#006ce9'; diff --git a/packages/qwik-city/middleware/request-handler/index.ts b/packages/qwik-city/middleware/request-handler/index.ts index 15cfbe30fce..81f4eb32480 100644 --- a/packages/qwik-city/middleware/request-handler/index.ts +++ b/packages/qwik-city/middleware/request-handler/index.ts @@ -1,2 +1,2 @@ -export { errorHandler, notFoundHandler } from './error-handler'; +export { getErrorHtml, errorHandler, notFoundHandler } from './error-handler'; export { requestHandler } from './request-handler'; diff --git a/packages/qwik-city/middleware/request-handler/not-found-paths.ts b/packages/qwik-city/middleware/request-handler/not-found-paths.ts new file mode 100644 index 00000000000..b6be6b2b80d --- /dev/null +++ b/packages/qwik-city/middleware/request-handler/not-found-paths.ts @@ -0,0 +1,11 @@ +/** + * Generated at build time + */ + +function getNotFound(_pathname: string) { + return 'Resource Not Found'; +} + +export default { + getNotFound, +}; diff --git a/packages/qwik-city/middleware/request-handler/page-handler.ts b/packages/qwik-city/middleware/request-handler/page-handler.ts index 5abac8bc2e7..97dbd190874 100644 --- a/packages/qwik-city/middleware/request-handler/page-handler.ts +++ b/packages/qwik-city/middleware/request-handler/page-handler.ts @@ -12,7 +12,6 @@ import { HttpStatus } from './http-status-codes'; import type { QwikCityRequestContext, UserResponseContext } from './types'; export function pageHandler( - mode: QwikCityMode, requestCtx: QwikCityRequestContext, userResponse: UserResponseContext, render: Render, @@ -38,7 +37,12 @@ export function pageHandler( try { const result = await render({ stream: isPageData ? noopStream : stream, - envData: getQwikCityEnvData(requestHeaders, userResponse, requestCtx.locale, mode), + envData: getQwikCityEnvData( + requestHeaders, + userResponse, + requestCtx.locale, + requestCtx.mode + ), ...opts, }); diff --git a/packages/qwik-city/middleware/request-handler/request-handler.ts b/packages/qwik-city/middleware/request-handler/request-handler.ts index bbd5ab211a2..0a2f40836e6 100644 --- a/packages/qwik-city/middleware/request-handler/request-handler.ts +++ b/packages/qwik-city/middleware/request-handler/request-handler.ts @@ -1,5 +1,4 @@ import { loadRoute } from '../../runtime/src/routing'; -import type { QwikCityMode } from '../../runtime/src/types'; import { endpointHandler } from './endpoint-handler'; import { errorHandler, ErrorResponse, errorResponse } from './error-handler'; import { pageHandler } from './page-handler'; @@ -11,7 +10,6 @@ import { loadUserResponse, updateRequestCtx } from './user-response'; * @alpha */ export async function requestHandler( - mode: QwikCityMode, requestCtx: QwikCityRequestContext, opts: QwikCityHandlerOptions ): Promise { @@ -45,7 +43,6 @@ export async function requestHandler( } const pageResult = await pageHandler( - mode, requestCtx, userResponse, render, diff --git a/packages/qwik-city/middleware/request-handler/static-paths.ts b/packages/qwik-city/middleware/request-handler/static-paths.ts new file mode 100644 index 00000000000..66a5b88f567 --- /dev/null +++ b/packages/qwik-city/middleware/request-handler/static-paths.ts @@ -0,0 +1,11 @@ +/** + * Generated at build time + */ + +function isStaticPath(_: string) { + return false; +} + +export default { + isStaticPath, +}; diff --git a/packages/qwik-city/middleware/request-handler/test-utils.ts b/packages/qwik-city/middleware/request-handler/test-utils.ts index 8bb9be6aa53..b8e181306ed 100644 --- a/packages/qwik-city/middleware/request-handler/test-utils.ts +++ b/packages/qwik-city/middleware/request-handler/test-utils.ts @@ -39,7 +39,15 @@ export function mockRequestContext(opts?: { }); }; - return { url, request, response, responseData, platform: { testing: true }, locale: undefined }; + return { + url, + request, + response, + responseData, + platform: { testing: true }, + locale: undefined, + mode: 'dev', + }; } export interface TestQwikCityRequestContext extends QwikCityRequestContext { diff --git a/packages/qwik-city/middleware/request-handler/types.ts b/packages/qwik-city/middleware/request-handler/types.ts index 106a17ed7b9..fc92899b49b 100644 --- a/packages/qwik-city/middleware/request-handler/types.ts +++ b/packages/qwik-city/middleware/request-handler/types.ts @@ -2,6 +2,7 @@ import type { StreamWriter } from '@builder.io/qwik'; import type { Render, RenderOptions } from '@builder.io/qwik/server'; import type { ClientPageData, + QwikCityMode, QwikCityPlan, RequestContext, RouteParams, @@ -13,6 +14,7 @@ export interface QwikCityRequestContext { url: URL; platform: Record; locale: string | undefined; + mode: QwikCityMode; } export interface QwikCityDevRequestContext extends QwikCityRequestContext { diff --git a/packages/qwik-city/middleware/request-handler/user-response.ts b/packages/qwik-city/middleware/request-handler/user-response.ts index 3087e6af897..ee2079a28d1 100644 --- a/packages/qwik-city/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/middleware/request-handler/user-response.ts @@ -44,7 +44,7 @@ export async function loadUserResponse( let hasRequestMethodHandler = false; - if (isPageModule && pathname !== basePathname) { + if (isPageModule && pathname !== basePathname && !pathname.endsWith('.html')) { // only check for slash redirect on pages if (trailingSlash) { // must have a trailing slash diff --git a/packages/qwik-city/middleware/vercel-edge/index.ts b/packages/qwik-city/middleware/vercel-edge/index.ts index d73d428c4e0..9b952777373 100644 --- a/packages/qwik-city/middleware/vercel-edge/index.ts +++ b/packages/qwik-city/middleware/vercel-edge/index.ts @@ -1,6 +1,8 @@ import type { QwikCityHandlerOptions, QwikCityRequestContext } from '../request-handler/types'; -import { notFoundHandler, requestHandler } from '../request-handler'; +import { requestHandler } from '../request-handler'; import { mergeHeadersCookies } from '../request-handler/cookie'; +import qwikCityStaticPaths from '@qwik-city-static-paths'; +import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; // @builder.io/qwik-city/middleware/vercel-edge @@ -8,11 +10,23 @@ import { mergeHeadersCookies } from '../request-handler/cookie'; * @alpha */ export function createQwikCity(opts: QwikCityVercelEdgeOptions) { + const { isStaticPath } = qwikCityStaticPaths; + async function onRequest(request: Request) { try { const url = new URL(request.url); + if (isStaticPath(url.pathname)) { + // known static path, let vercel handle it + return new Response(null, { + headers: { + 'x-middleware-next': '1', + }, + }); + } + const requestCtx: QwikCityRequestContext = { + mode: 'server', locale: undefined, url, request, @@ -53,20 +67,23 @@ export function createQwikCity(opts: QwikCityVercelEdgeOptions) { }; // send request to qwik city request handler - const handledResponse = await requestHandler('server', requestCtx, opts); + const handledResponse = await requestHandler(requestCtx, opts); if (handledResponse) { return handledResponse; } // qwik city did not have a route for this request - // respond with qwik city's 404 handler - const notFoundResponse = await notFoundHandler(requestCtx); - return notFoundResponse; + // response with 404 for this pathname + const notFoundHtml = qwikCityNotFoundPaths.getNotFound(url.pathname); + return new Response(notFoundHtml, { + status: 404, + headers: { 'Content-Type': 'text/html; charset=utf-8', 'X-Not-Found': url.pathname }, + }); } catch (e: any) { console.error(e); return new Response(String(e || 'Error'), { status: 500, - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Error': 'vercel-edge' }, }); } } diff --git a/packages/qwik-city/runtime/src/types.ts b/packages/qwik-city/runtime/src/types.ts index 6cc74691f9b..281471d816c 100644 --- a/packages/qwik-city/runtime/src/types.ts +++ b/packages/qwik-city/runtime/src/types.ts @@ -189,10 +189,6 @@ export type RouteData = routeBundleNames: string[] ]; -export type FallbackRouteData = - | [pattern: RegExp, loaders: ModuleLoader[]] - | [pattern: RegExp, loaders: ModuleLoader[], paramNames: string[]]; - export type MenuData = [pathname: string, menuLoader: MenuModuleLoader]; /** diff --git a/packages/qwik-city/static/api.md b/packages/qwik-city/static/api.md index 0e0d905d7e6..cbe455bd423 100644 --- a/packages/qwik-city/static/api.md +++ b/packages/qwik-city/static/api.md @@ -18,6 +18,7 @@ export interface StaticGenerateOptions extends StaticGenerateRenderOptions { // @alpha (undocumented) export interface StaticGenerateRenderOptions extends RenderOptions { + emit404Pages?: boolean; emitData?: boolean; emitHtml?: boolean; log?: 'debug'; diff --git a/packages/qwik-city/static/main-thread.ts b/packages/qwik-city/static/main-thread.ts index c438a1d6c25..f0bf2f8b03f 100644 --- a/packages/qwik-city/static/main-thread.ts +++ b/packages/qwik-city/static/main-thread.ts @@ -1,6 +1,7 @@ -import type { PageModule, QwikCityPlan, RouteParams } from '../runtime/src/types'; +import type { PageModule, QwikCityPlan, RouteData, RouteParams } from '../runtime/src/types'; import type { StaticGenerateOptions, StaticGenerateResult, StaticRoute, System } from './types'; import { msToString } from '../utils/format'; +import { generateNotFoundPages } from './not-found'; import { getPathnameForDynamicRoute } from '../utils/pathname'; import { pathToFileURL } from 'node:url'; @@ -31,6 +32,42 @@ export async function mainThread(sys: System) { let isCompleted = false; let isRoutesLoaded = false; + const completed = async () => { + const closePromise = main.close(); + + await generateNotFoundPages(sys, opts, routes); + + generatorResult.duration = timer(); + + log.info('\nSSG results'); + if (generatorResult.rendered > 0) { + log.info( + `- Generated: ${generatorResult.rendered} page${ + generatorResult.rendered === 1 ? '' : 's' + }` + ); + } + + if (generatorResult.errors > 0) { + log.info(`- Errors: ${generatorResult.errors}`); + } + + log.info(`- Duration: ${msToString(generatorResult.duration)}`); + + const total = generatorResult.rendered + generatorResult.errors; + if (total > 0) { + log.info(`- Average: ${msToString(generatorResult.duration / total)} per page`); + } + + log.info(``); + + closePromise + .then(() => { + setTimeout(() => resolve(generatorResult)); + }) + .catch(reject); + }; + const next = () => { while (!isCompleted && main.hasAvailableWorker() && queue.length > 0) { const staticRoute = queue.shift(); @@ -41,37 +78,7 @@ export async function mainThread(sys: System) { if (!isCompleted && isRoutesLoaded && queue.length === 0 && active.size === 0) { isCompleted = true; - - generatorResult.duration = timer(); - - log.info('\nSSG results'); - if (generatorResult.rendered > 0) { - log.info( - `- Generated: ${generatorResult.rendered} page${ - generatorResult.rendered === 1 ? '' : 's' - }` - ); - } - - if (generatorResult.errors > 0) { - log.info(`- Errors: ${generatorResult.errors}`); - } - - log.info(`- Duration: ${msToString(generatorResult.duration)}`); - - const total = generatorResult.rendered + generatorResult.errors; - if (total > 0) { - log.info(`- Average: ${msToString(generatorResult.duration / total)} per page`); - } - - log.info(``); - - main - .close() - .then(() => { - setTimeout(() => resolve(generatorResult)); - }) - .catch(reject); + completed(); } }; @@ -146,41 +153,39 @@ export async function mainThread(sys: System) { } }; - const loadStaticRoutes = async () => { - await Promise.all( - routes.map(async (route) => { - const [_, loaders, paramNames, originalPathname] = route; - const modules = await Promise.all(loaders.map((loader) => loader())); - const pageModule: PageModule = modules[modules.length - 1] as any; - - if (pageModule.default) { - // page module (not an endpoint) - - if (Array.isArray(paramNames) && paramNames.length > 0) { - if (typeof pageModule.onStaticGenerate === 'function' && paramNames.length > 0) { - // dynamic route page module - const staticGenerate = await pageModule.onStaticGenerate(); - if (Array.isArray(staticGenerate.params)) { - for (const params of staticGenerate.params) { - const pathname = getPathnameForDynamicRoute( - originalPathname!, - paramNames, - params - ); - addToQueue(pathname, params); - } - } + const loadStaticRoute = async (route: RouteData) => { + const [_, loaders, paramNames, originalPathname] = route; + const modules = await Promise.all(loaders.map((loader) => loader())); + const pageModule: PageModule = modules[modules.length - 1] as any; + + if (pageModule.default) { + // page module (not an endpoint) + + if (Array.isArray(paramNames) && paramNames.length > 0) { + if (typeof pageModule.onStaticGenerate === 'function' && paramNames.length > 0) { + // dynamic route page module + const staticGenerate = await pageModule.onStaticGenerate(); + if (Array.isArray(staticGenerate.params)) { + for (const params of staticGenerate.params) { + const pathname = getPathnameForDynamicRoute( + originalPathname!, + paramNames, + params + ); + addToQueue(pathname, params); } - } else { - // static route page module - addToQueue(originalPathname, undefined); } } - }) - ); + } else { + // static route page module + addToQueue(originalPathname, undefined); + } + } + }; + const loadStaticRoutes = async () => { + await Promise.all(routes.map(loadStaticRoute)); isRoutesLoaded = true; - flushQueue(); }; diff --git a/packages/qwik-city/static/node/node-system.ts b/packages/qwik-city/static/node/node-system.ts index e25af2ae09a..4fbebc15706 100644 --- a/packages/qwik-city/static/node/node-system.ts +++ b/packages/qwik-city/static/node/node-system.ts @@ -52,13 +52,20 @@ export async function createSystem(opts: StaticGenerateOptions) { }; const getPageFilePath = (pathname: string) => { - pathname = getFsDir(pathname) + 'index.html'; + if (pathname.endsWith('.html')) { + pathname = pathname.slice(basenameLen); + } else { + pathname = getFsDir(pathname) + 'index.html'; + } return join(outDir, pathname); }; const getDataFilePath = (pathname: string) => { - pathname = getFsDir(pathname) + 'q-data.json'; - return join(outDir, pathname); + if (!pathname.endsWith('.html')) { + pathname = getFsDir(pathname) + 'q-data.json'; + return join(outDir, pathname); + } + return null; }; const sys: System = { @@ -69,6 +76,7 @@ export async function createSystem(opts: StaticGenerateOptions) { ensureDir, createWriteStream, createTimer, + access, getPageFilePath, getDataFilePath, platform: { @@ -81,9 +89,14 @@ export async function createSystem(opts: StaticGenerateOptions) { } export const ensureDir = async (filePath: string) => { + await fs.promises.mkdir(dirname(filePath), { recursive: true }); +}; + +export const access = async (path: string) => { try { - await fs.promises.mkdir(dirname(filePath), { recursive: true }); + await fs.promises.access(path); + return true; } catch (e) { - // + return false; } }; diff --git a/packages/qwik-city/static/not-found.ts b/packages/qwik-city/static/not-found.ts new file mode 100644 index 00000000000..ec39530e1bf --- /dev/null +++ b/packages/qwik-city/static/not-found.ts @@ -0,0 +1,29 @@ +import { getErrorHtml } from '../middleware/request-handler/error-handler'; +import type { RouteData } from '../runtime/src/types'; +import type { StaticGenerateOptions, System } from './types'; + +export async function generateNotFoundPages( + sys: System, + opts: StaticGenerateOptions, + routes: RouteData[] +) { + if (opts.emit404Pages !== false) { + const basePathname = opts.basePathname || '/'; + const rootNotFoundPathname = basePathname + '404.html'; + + const hasRootNotFound = routes.some((r) => r[3] === rootNotFoundPathname); + if (!hasRootNotFound) { + const filePath = sys.getPageFilePath(rootNotFoundPathname); + + const html = getErrorHtml(404, 'Resource Not Found'); + + await sys.ensureDir(filePath); + + return new Promise((resolve) => { + const writer = sys.createWriteStream(filePath); + writer.write(html); + writer.close(resolve); + }); + } + } +} diff --git a/packages/qwik-city/static/types.ts b/packages/qwik-city/static/types.ts index 295db7cc23e..1bf8d3d708a 100644 --- a/packages/qwik-city/static/types.ts +++ b/packages/qwik-city/static/types.ts @@ -11,10 +11,11 @@ export interface System { createLogger: () => Promise; getOptions: () => StaticGenerateOptions; ensureDir: (filePath: string) => Promise; + access: (path: string) => Promise; createWriteStream: (filePath: string) => StaticStreamWriter; createTimer: () => () => number; getPageFilePath: (pathname: string) => string; - getDataFilePath: (pathname: string) => string; + getDataFilePath: (pathname: string) => string | null; platform: { [key: string]: any }; } @@ -87,6 +88,11 @@ export interface StaticGenerateRenderOptions extends RenderOptions { * Defaults to `true`. */ emitData?: boolean; + /** + * Set to `false` if the static build should not write custom or default `404.html` pages. + * Defaults to `true`. + */ + emit404Pages?: boolean; } /** diff --git a/packages/qwik-city/static/worker-thread.ts b/packages/qwik-city/static/worker-thread.ts index 9451f1f1fa1..2795053fe90 100644 --- a/packages/qwik-city/static/worker-thread.ts +++ b/packages/qwik-city/static/worker-thread.ts @@ -60,6 +60,7 @@ async function workerRender( const request = new SsgRequestContext(url); const requestCtx: QwikCityRequestContext = { + mode: 'static', locale: undefined, url, request, @@ -80,12 +81,12 @@ async function workerRender( } if (result.ok) { - const writeHtmlEnabled = opts.emitHtml !== false; - const writeDataEnabled = opts.emitData !== false; - const htmlFilePath = sys.getPageFilePath(staticRoute.pathname); const dataFilePath = sys.getDataFilePath(staticRoute.pathname); + const writeHtmlEnabled = opts.emitHtml !== false; + const writeDataEnabled = opts.emitData !== false && !!dataFilePath; + if (writeHtmlEnabled || writeDataEnabled) { await sys.ensureDir(htmlFilePath); } @@ -128,7 +129,7 @@ async function workerRender( platform: sys.platform, }; - const promise = requestHandler('static', requestCtx, opts) + const promise = requestHandler(requestCtx, opts) .then((rsp) => { if (rsp == null) { callback(result); diff --git a/packages/qwik-city/utils/fs.ts b/packages/qwik-city/utils/fs.ts index b0cc7795a40..6e03dc9c577 100644 --- a/packages/qwik-city/utils/fs.ts +++ b/packages/qwik-city/utils/fs.ts @@ -26,15 +26,15 @@ export function getPathnameFromDirPath(opts: NormalizedPluginOptions, dirPath: s const relFilePath = relative(opts.routesDir, dirPath); // ensure file system path uses / (POSIX) instead of \\ (windows) - const pathname = normalizePath(relFilePath); - - return ( - normalizePathname(pathname, opts.basePathname, opts.trailingSlash)! - .split('/') - // remove grouped layout segments - .filter((segment) => !isGroupedLayoutName(segment)) - .join('/') - ); + let pathname = normalizePath(relFilePath); + + pathname = normalizePathname(pathname, opts.basePathname, opts.trailingSlash)! + .split('/') + // remove grouped layout segments + .filter((segment) => !isGroupedLayoutName(segment)) + .join('/'); + + return pathname; } export function getMenuPathname(opts: NormalizedPluginOptions, filePath: string) { diff --git a/packages/qwik-city/utils/fs.unit.ts b/packages/qwik-city/utils/fs.unit.ts index 2b1bfde0a36..9daf2cec839 100644 --- a/packages/qwik-city/utils/fs.unit.ts +++ b/packages/qwik-city/utils/fs.unit.ts @@ -1,5 +1,5 @@ import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { basename, join } from 'node:path'; import { test } from 'uvu'; import { equal } from 'uvu/assert'; import type { NormalizedPluginOptions } from '../buildtime/types'; @@ -186,81 +186,79 @@ test('createFileId, Layout', () => { equal(p, 'DashboardSettingsLayout'); }); -test('getPathnameFromDirPath', () => { - const routesDir = tmpdir(); - - const t = [ - { - dirPath: join(routesDir, '(a)', 'about', '(b)', 'info', '(c)'), - basePathname: '/', - trailingSlash: true, - expect: '/about/info/', - }, - { - dirPath: join(routesDir, 'about'), - basePathname: '/app/', - trailingSlash: true, - expect: '/app/about/', - }, - { - dirPath: join(routesDir, 'about'), - basePathname: '/app/', - trailingSlash: false, - expect: '/app/about', - }, - { - dirPath: join(routesDir, 'about'), - basePathname: '/', - trailingSlash: true, - expect: '/about/', - }, - { - dirPath: join(routesDir, 'about'), - basePathname: '/', - trailingSlash: false, - expect: '/about', - }, - { - dirPath: routesDir, - basePathname: '/', - trailingSlash: false, - expect: '/', - }, - { - dirPath: routesDir, - basePathname: '/', - trailingSlash: true, - expect: '/', - }, - { - dirPath: routesDir, - basePathname: '/app/', - trailingSlash: false, - expect: '/app/', - }, - { - dirPath: routesDir, - basePathname: '/app/', - trailingSlash: true, - expect: '/app/', - }, - ]; - - t.forEach((c) => { +[ + { + dirPath: join(routesDir, '(a)', 'about', '(b)', 'info', '(c)'), + basePathname: '/', + trailingSlash: true, + expect: '/about/info/', + }, + { + dirPath: join(routesDir, 'about'), + basePathname: '/app/', + trailingSlash: true, + expect: '/app/about/', + }, + { + dirPath: join(routesDir, 'about'), + basePathname: '/app/', + trailingSlash: false, + expect: '/app/about', + }, + { + dirPath: join(routesDir, 'about'), + basePathname: '/', + trailingSlash: true, + expect: '/about/', + }, + { + dirPath: join(routesDir, 'about'), + basePathname: '/', + trailingSlash: false, + expect: '/about', + }, + { + dirPath: routesDir, + basePathname: '/', + trailingSlash: false, + expect: '/', + }, + { + dirPath: routesDir, + basePathname: '/', + trailingSlash: true, + expect: '/', + }, + { + dirPath: routesDir, + basePathname: '/app/', + trailingSlash: false, + expect: '/app/', + }, + { + dirPath: routesDir, + basePathname: '/app/', + trailingSlash: true, + expect: '/app/', + }, +].forEach((t) => { + test(`getPathnameFromDirPath, dirPath: ${basename(t.dirPath)}, basePathname: ${ + t.basePathname + }`, () => { const opts: NormalizedPluginOptions = { routesDir: routesDir, - basePathname: c.basePathname, - trailingSlash: c.trailingSlash, + basePathname: t.basePathname, + trailingSlash: t.trailingSlash, mdxPlugins: { remarkGfm: true, rehypeSyntaxHighlight: true, rehypeAutolinkHeadings: true, }, mdx: {}, - baseUrl: c.basePathname, + baseUrl: t.basePathname, }; - const pathname = getPathnameFromDirPath(opts, c.dirPath); - equal(pathname, c.expect, c.dirPath); + const pathname = getPathnameFromDirPath(opts, t.dirPath); + equal(pathname, t.expect, t.dirPath); }); }); diff --git a/scripts/qwik-city.ts b/scripts/qwik-city.ts index 2f592fe8d76..e2391f122db 100644 --- a/scripts/qwik-city.ts +++ b/scripts/qwik-city.ts @@ -409,7 +409,7 @@ async function buildMiddlewareCloudflarePages( ) { const entryPoints = [join(inputDir, 'middleware', 'cloudflare-pages', 'index.ts')]; - const external = ['@qwik-city-plan']; + const external = ['@qwik-city-plan', '@qwik-city-static-paths', '@qwik-city-not-found-paths']; await build({ entryPoints, @@ -430,7 +430,7 @@ async function buildMiddlewareNetlifyEdge( ) { const entryPoints = [join(inputDir, 'middleware', 'netlify-edge', 'index.ts')]; - const external = ['@qwik-city-plan']; + const external = ['@qwik-city-plan', '@qwik-city-static-paths', '@qwik-city-not-found-paths']; await build({ entryPoints, @@ -447,7 +447,13 @@ async function buildMiddlewareNetlifyEdge( async function buildMiddlewareNode(config: BuildConfig, inputDir: string, outputDir: string) { const entryPoints = [join(inputDir, 'middleware', 'node', 'index.ts')]; - const external = ['node-fetch', 'path', '@qwik-city-plan']; + const external = [ + 'node-fetch', + 'path', + '@qwik-city-plan', + '@qwik-city-static-paths', + '@qwik-city-not-found-paths', + ]; await build({ entryPoints, @@ -475,6 +481,8 @@ async function buildMiddlewareNode(config: BuildConfig, inputDir: string, output async function buildMiddlewareVercelEdge(config: BuildConfig, inputDir: string, outputDir: string) { const entryPoints = [join(inputDir, 'middleware', 'vercel-edge', 'index.ts')]; + const external = ['@qwik-city-plan', '@qwik-city-static-paths', '@qwik-city-not-found-paths']; + await build({ entryPoints, outfile: join(outputDir, 'middleware', 'vercel-edge', 'index.mjs'), @@ -482,6 +490,7 @@ async function buildMiddlewareVercelEdge(config: BuildConfig, inputDir: string, platform: 'node', target: nodeTarget, format: 'esm', + external, watch: watcher(config), }); } diff --git a/tsconfig.json b/tsconfig.json index 4840f032cc6..be10f0152d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -56,6 +56,10 @@ "packages/qwik-city/buildtime/runtime-generation/sw-register-build.ts" ], "@qwik-city-sw-register": ["packages/qwik-city/runtime/src/sw-register-runtime.ts"], + "@qwik-city-not-found-paths": [ + "packages/qwik-city/middleware/request-handler/not-found-paths.ts" + ], + "@qwik-city-static-paths": ["packages/qwik-city/middleware/request-handler/static-paths.ts"], "create-qwik": ["packages/create-qwik/api/index.ts"] }, "types": ["node", "vite/client"], From eeffd45681b0eeca2901384874892c42a034dba1 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Fri, 18 Nov 2022 11:12:05 -0600 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8D=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adaptors/shared/vite/post-build.ts | 76 +++++++++---------- .../middleware/cloudflare-pages/index.ts | 8 +- .../middleware/netlify-edge/index.ts | 8 +- packages/qwik-city/middleware/node/index.ts | 4 +- .../generated/not-found-paths.ts | 3 + .../request-handler/generated/static-paths.ts | 3 + .../request-handler/not-found-paths.ts | 11 --- .../request-handler/static-paths.ts | 11 --- .../qwik-city/middleware/vercel-edge/index.ts | 8 +- scripts/qwik-city.ts | 66 ++++++++-------- .../src/components/footer/footer.tsx | 12 +-- .../src/components/header/header.tsx | 26 +++---- starters/dev-server.ts | 11 +++ tsconfig.json | 9 ++- 14 files changed, 121 insertions(+), 135 deletions(-) create mode 100644 packages/qwik-city/middleware/request-handler/generated/not-found-paths.ts create mode 100644 packages/qwik-city/middleware/request-handler/generated/static-paths.ts delete mode 100644 packages/qwik-city/middleware/request-handler/not-found-paths.ts delete mode 100644 packages/qwik-city/middleware/request-handler/static-paths.ts diff --git a/packages/qwik-city/adaptors/shared/vite/post-build.ts b/packages/qwik-city/adaptors/shared/vite/post-build.ts index e98199b2277..f94e09d2c77 100644 --- a/packages/qwik-city/adaptors/shared/vite/post-build.ts +++ b/packages/qwik-city/adaptors/shared/vite/post-build.ts @@ -51,49 +51,15 @@ export async function postBuild( await loadDir(clientOutDir, basePathname); - const staticPathsCode = createStaticPathsModule(basePathname, staticPaths, format); const notFoundPathsCode = createNotFoundPathsModule(basePathname, notFounds, format); + const staticPathsCode = createStaticPathsModule(basePathname, staticPaths, format); return { - staticPathsCode, notFoundPathsCode, + staticPathsCode, }; } -function createStaticPathsModule(basePathname: string, staticPaths: Set, format: string) { - const assetsPath = basePathname + 'assets/'; - const baseBuildPath = basePathname + 'build/'; - - const c: string[] = []; - - c.push( - `const staticPaths = new Set(${JSON.stringify( - Array.from(new Set(staticPaths)).sort() - )});` - ); - - c.push(`function isStaticPath(p) {`); - c.push(` if (p.startsWith(${JSON.stringify(baseBuildPath)})) {`); - c.push(` return true;`); - c.push(` }`); - c.push(` if (p.startsWith(${JSON.stringify(assetsPath)})) {`); - c.push(` return true;`); - c.push(` }`); - c.push(` if (staticPaths.has(p)) {`); - c.push(` return true;`); - c.push(` }`); - c.push(` return false;`); - c.push(`}`); - - if (format === 'cjs') { - c.push('module.exports = { isStaticPath: isStaticPath };'); - } else { - c.push('export default { isStaticPath };'); - } - - return c.join('\n'); -} - function createNotFoundPathsModule(basePathname: string, notFounds: string[][], format: string) { notFounds.sort((a, b) => { if (a[0].length > b[0].length) return -1; @@ -122,9 +88,43 @@ function createNotFoundPathsModule(basePathname: string, notFounds: string[][], c.push(`}`); if (format === 'cjs') { - c.push('module.exports = { getNotFound: getNotFound };'); + c.push('exports.getNotFound = getNotFound;'); + } else { + c.push('export { getNotFound };'); + } + + return c.join('\n'); +} + +function createStaticPathsModule(basePathname: string, staticPaths: Set, format: string) { + const assetsPath = basePathname + 'assets/'; + const baseBuildPath = basePathname + 'build/'; + + const c: string[] = []; + + c.push( + `const staticPaths = new Set(${JSON.stringify( + Array.from(new Set(staticPaths)).sort() + )});` + ); + + c.push(`function isStaticPath(p) {`); + c.push(` if (p.startsWith(${JSON.stringify(baseBuildPath)})) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` if (p.startsWith(${JSON.stringify(assetsPath)})) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` if (staticPaths.has(p)) {`); + c.push(` return true;`); + c.push(` }`); + c.push(` return false;`); + c.push(`}`); + + if (format === 'cjs') { + c.push('exports.isStaticPath = isStaticPath;'); } else { - c.push('export default { getNotFound };'); + c.push('export { isStaticPath };'); } return c.join('\n'); diff --git a/packages/qwik-city/middleware/cloudflare-pages/index.ts b/packages/qwik-city/middleware/cloudflare-pages/index.ts index 84d67cd204b..7b3d138383d 100644 --- a/packages/qwik-city/middleware/cloudflare-pages/index.ts +++ b/packages/qwik-city/middleware/cloudflare-pages/index.ts @@ -2,8 +2,8 @@ import type { QwikCityHandlerOptions, QwikCityRequestContext } from '../request- import type { RequestHandler } from '@builder.io/qwik-city'; import { requestHandler } from '../request-handler'; import { mergeHeadersCookies } from '../request-handler/cookie'; -import qwikCityStaticPaths from '@qwik-city-static-paths'; -import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; +import { getNotFound } from '@qwik-city-not-found-paths'; +import { isStaticPath } from '@qwik-city-static-paths'; // @builder.io/qwik-city/middleware/cloudflare-pages @@ -11,8 +11,6 @@ import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; * @alpha */ export function createQwikCity(opts: QwikCityCloudflarePagesOptions) { - const { isStaticPath } = qwikCityStaticPaths; - async function onRequest({ request, env, waitUntil, next }: EventPluginContext) { try { const url = new URL(request.url); @@ -92,7 +90,7 @@ export function createQwikCity(opts: QwikCityCloudflarePagesOptions) { // qwik city did not have a route for this request // response with 404 for this pathname - const notFoundHtml = qwikCityNotFoundPaths.getNotFound(url.pathname); + const notFoundHtml = getNotFound(url.pathname); return new Response(notFoundHtml, { status: 404, headers: { 'Content-Type': 'text/html; charset=utf-8', 'X-Not-Found': url.pathname }, diff --git a/packages/qwik-city/middleware/netlify-edge/index.ts b/packages/qwik-city/middleware/netlify-edge/index.ts index ad458e6a692..3dfd8fed276 100644 --- a/packages/qwik-city/middleware/netlify-edge/index.ts +++ b/packages/qwik-city/middleware/netlify-edge/index.ts @@ -3,8 +3,8 @@ import type { QwikCityHandlerOptions, QwikCityRequestContext } from '../request- import type { RequestHandler } from '@builder.io/qwik-city'; import { requestHandler } from '../request-handler'; import { mergeHeadersCookies } from '../request-handler/cookie'; -import qwikCityStaticPaths from '@qwik-city-static-paths'; -import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; +import { getNotFound } from '@qwik-city-not-found-paths'; +import { isStaticPath } from '@qwik-city-static-paths'; // @builder.io/qwik-city/middleware/netlify-edge @@ -12,8 +12,6 @@ import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; * @alpha */ export function createQwikCity(opts: QwikCityNetlifyOptions) { - const { isStaticPath } = qwikCityStaticPaths; - async function onRequest(request: Request, context: Context) { try { const url = new URL(request.url); @@ -71,7 +69,7 @@ export function createQwikCity(opts: QwikCityNetlifyOptions) { // qwik city did not have a route for this request // response with 404 for this pathname - const notFoundHtml = qwikCityNotFoundPaths.getNotFound(url.pathname); + const notFoundHtml = getNotFound(url.pathname); return new Response(notFoundHtml, { status: 404, headers: { 'Content-Type': 'text/html; charset=utf-8', 'X-Not-Found': url.pathname }, diff --git a/packages/qwik-city/middleware/node/index.ts b/packages/qwik-city/middleware/node/index.ts index 20504717120..3a7b90136af 100644 --- a/packages/qwik-city/middleware/node/index.ts +++ b/packages/qwik-city/middleware/node/index.ts @@ -5,8 +5,8 @@ import { fromNodeHttp, getUrl } from './http'; import { patchGlobalFetch } from './node-fetch'; import type { Render } from '@builder.io/qwik/server'; import type { RenderOptions } from '@builder.io/qwik'; +import { getNotFound } from '@qwik-city-not-found-paths'; import qwikCityPlan from '@qwik-city-plan'; -import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; // @builder.io/qwik-city/middleware/node @@ -40,7 +40,7 @@ export function createQwikCity(opts: QwikCityNodeRequestOptions) { const notFound = async (req: IncomingMessage, res: ServerResponse, next: (e: any) => void) => { try { const url = getUrl(req); - const notFoundHtml = qwikCityNotFoundPaths.getNotFound(url.pathname); + const notFoundHtml = getNotFound(url.pathname); res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8', 'X-Not-Found': url.pathname, diff --git a/packages/qwik-city/middleware/request-handler/generated/not-found-paths.ts b/packages/qwik-city/middleware/request-handler/generated/not-found-paths.ts new file mode 100644 index 00000000000..8a59dcff065 --- /dev/null +++ b/packages/qwik-city/middleware/request-handler/generated/not-found-paths.ts @@ -0,0 +1,3 @@ +export function getNotFound(_pathname: string) { + return 'Resource Not Found'; +} diff --git a/packages/qwik-city/middleware/request-handler/generated/static-paths.ts b/packages/qwik-city/middleware/request-handler/generated/static-paths.ts new file mode 100644 index 00000000000..f95f06c276a --- /dev/null +++ b/packages/qwik-city/middleware/request-handler/generated/static-paths.ts @@ -0,0 +1,3 @@ +export function isStaticPath(pathname: string) { + return /\.(jpg|jpeg|png|webp|avif|gif|svg)$/.test(pathname); +} diff --git a/packages/qwik-city/middleware/request-handler/not-found-paths.ts b/packages/qwik-city/middleware/request-handler/not-found-paths.ts deleted file mode 100644 index b6be6b2b80d..00000000000 --- a/packages/qwik-city/middleware/request-handler/not-found-paths.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated at build time - */ - -function getNotFound(_pathname: string) { - return 'Resource Not Found'; -} - -export default { - getNotFound, -}; diff --git a/packages/qwik-city/middleware/request-handler/static-paths.ts b/packages/qwik-city/middleware/request-handler/static-paths.ts deleted file mode 100644 index 66a5b88f567..00000000000 --- a/packages/qwik-city/middleware/request-handler/static-paths.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated at build time - */ - -function isStaticPath(_: string) { - return false; -} - -export default { - isStaticPath, -}; diff --git a/packages/qwik-city/middleware/vercel-edge/index.ts b/packages/qwik-city/middleware/vercel-edge/index.ts index 9b952777373..0a861a572c4 100644 --- a/packages/qwik-city/middleware/vercel-edge/index.ts +++ b/packages/qwik-city/middleware/vercel-edge/index.ts @@ -1,8 +1,8 @@ import type { QwikCityHandlerOptions, QwikCityRequestContext } from '../request-handler/types'; import { requestHandler } from '../request-handler'; import { mergeHeadersCookies } from '../request-handler/cookie'; -import qwikCityStaticPaths from '@qwik-city-static-paths'; -import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; +import { getNotFound } from '@qwik-city-not-found-paths'; +import { isStaticPath } from '@qwik-city-static-paths'; // @builder.io/qwik-city/middleware/vercel-edge @@ -10,8 +10,6 @@ import qwikCityNotFoundPaths from '@qwik-city-not-found-paths'; * @alpha */ export function createQwikCity(opts: QwikCityVercelEdgeOptions) { - const { isStaticPath } = qwikCityStaticPaths; - async function onRequest(request: Request) { try { const url = new URL(request.url); @@ -74,7 +72,7 @@ export function createQwikCity(opts: QwikCityVercelEdgeOptions) { // qwik city did not have a route for this request // response with 404 for this pathname - const notFoundHtml = qwikCityNotFoundPaths.getNotFound(url.pathname); + const notFoundHtml = getNotFound(url.pathname); return new Response(notFoundHtml, { status: 404, headers: { 'Content-Type': 'text/html; charset=utf-8', 'X-Not-Found': url.pathname }, diff --git a/scripts/qwik-city.ts b/scripts/qwik-city.ts index e2391f122db..0d0c9117dd5 100644 --- a/scripts/qwik-city.ts +++ b/scripts/qwik-city.ts @@ -90,6 +90,14 @@ export async function buildQwikCity(config: BuildConfig) { import: './middleware/node/index.mjs', require: './middleware/node/index.cjs', }, + './middleware/request-handler/utils/not-found-paths': { + import: './middleware/request-handler/utils/not-found-paths.mjs', + require: './middleware/request-handler/utils/not-found-paths.cjs', + }, + './middleware/request-handler/utils/static-paths': { + import: './middleware/request-handler/utils/static-paths.mjs', + require: './middleware/request-handler/utils/static-paths.cjs', + }, './middleware/vercel-edge': { types: './middleware/vercel-edge/index.d.ts', import: './middleware/vercel-edge/index.mjs', @@ -247,8 +255,6 @@ async function buildAdaptorCloudflarePagesVite( ) { const entryPoints = [join(inputDir, 'adaptors', 'cloudflare-pages', 'vite', 'index.ts')]; - const external = ['vite', 'fs', 'path', '@builder.io/qwik-city/static']; - await build({ entryPoints, outfile: join(outputDir, 'adaptors', 'cloudflare-pages', 'vite', 'index.mjs'), @@ -257,7 +263,7 @@ async function buildAdaptorCloudflarePagesVite( target: nodeTarget, format: 'esm', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.mjs')], }); @@ -269,7 +275,7 @@ async function buildAdaptorCloudflarePagesVite( target: nodeTarget, format: 'cjs', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.cjs')], }); } @@ -277,8 +283,6 @@ async function buildAdaptorCloudflarePagesVite( async function buildAdaptorExpressVite(config: BuildConfig, inputDir: string, outputDir: string) { const entryPoints = [join(inputDir, 'adaptors', 'express', 'vite', 'index.ts')]; - const external = ['vite', 'fs', 'path', '@builder.io/qwik-city/static']; - await build({ entryPoints, outfile: join(outputDir, 'adaptors', 'express', 'vite', 'index.mjs'), @@ -287,7 +291,7 @@ async function buildAdaptorExpressVite(config: BuildConfig, inputDir: string, ou target: nodeTarget, format: 'esm', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.mjs')], }); @@ -299,7 +303,7 @@ async function buildAdaptorExpressVite(config: BuildConfig, inputDir: string, ou target: nodeTarget, format: 'cjs', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.cjs')], }); } @@ -311,8 +315,6 @@ async function buildAdaptorNetlifyEdgeVite( ) { const entryPoints = [join(inputDir, 'adaptors', 'netlify-edge', 'vite', 'index.ts')]; - const external = ['vite', 'fs', 'path', '@builder.io/qwik-city/static']; - await build({ entryPoints, outfile: join(outputDir, 'adaptors', 'netlify-edge', 'vite', 'index.mjs'), @@ -321,7 +323,7 @@ async function buildAdaptorNetlifyEdgeVite( target: nodeTarget, format: 'esm', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.mjs')], }); @@ -333,7 +335,7 @@ async function buildAdaptorNetlifyEdgeVite( target: nodeTarget, format: 'cjs', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.cjs')], }); } @@ -341,8 +343,6 @@ async function buildAdaptorNetlifyEdgeVite( async function buildAdaptorStaticVite(config: BuildConfig, inputDir: string, outputDir: string) { const entryPoints = [join(inputDir, 'adaptors', 'static', 'vite', 'index.ts')]; - const external = ['vite', 'fs', 'path', '@builder.io/qwik-city/static']; - await build({ entryPoints, outfile: join(outputDir, 'adaptors', 'static', 'vite', 'index.mjs'), @@ -351,7 +351,7 @@ async function buildAdaptorStaticVite(config: BuildConfig, inputDir: string, out target: nodeTarget, format: 'esm', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.mjs')], }); @@ -363,7 +363,7 @@ async function buildAdaptorStaticVite(config: BuildConfig, inputDir: string, out target: nodeTarget, format: 'cjs', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.cjs')], }); } @@ -375,8 +375,6 @@ async function buildAdaptorVercelEdgeVite( ) { const entryPoints = [join(inputDir, 'adaptors', 'vercel-edge', 'vite', 'index.ts')]; - const external = ['vite', 'fs', 'path', '@builder.io/qwik-city/static']; - await build({ entryPoints, outfile: join(outputDir, 'adaptors', 'vercel-edge', 'vite', 'index.mjs'), @@ -385,7 +383,7 @@ async function buildAdaptorVercelEdgeVite( target: nodeTarget, format: 'esm', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.mjs')], }); @@ -397,7 +395,7 @@ async function buildAdaptorVercelEdgeVite( target: nodeTarget, format: 'cjs', watch: watcher(config), - external, + external: ADAPTOR_EXTERNALS, plugins: [importPath(/static$/, '../../../static/index.cjs')], }); } @@ -409,8 +407,6 @@ async function buildMiddlewareCloudflarePages( ) { const entryPoints = [join(inputDir, 'middleware', 'cloudflare-pages', 'index.ts')]; - const external = ['@qwik-city-plan', '@qwik-city-static-paths', '@qwik-city-not-found-paths']; - await build({ entryPoints, outfile: join(outputDir, 'middleware', 'cloudflare-pages', 'index.mjs'), @@ -419,7 +415,7 @@ async function buildMiddlewareCloudflarePages( target: nodeTarget, format: 'esm', watch: watcher(config), - external, + external: MIDDLEWARE_EXTERNALS, }); } @@ -430,8 +426,6 @@ async function buildMiddlewareNetlifyEdge( ) { const entryPoints = [join(inputDir, 'middleware', 'netlify-edge', 'index.ts')]; - const external = ['@qwik-city-plan', '@qwik-city-static-paths', '@qwik-city-not-found-paths']; - await build({ entryPoints, outfile: join(outputDir, 'middleware', 'netlify-edge', 'index.mjs'), @@ -440,20 +434,14 @@ async function buildMiddlewareNetlifyEdge( target: nodeTarget, format: 'esm', watch: watcher(config), - external, + external: MIDDLEWARE_EXTERNALS, }); } async function buildMiddlewareNode(config: BuildConfig, inputDir: string, outputDir: string) { const entryPoints = [join(inputDir, 'middleware', 'node', 'index.ts')]; - const external = [ - 'node-fetch', - 'path', - '@qwik-city-plan', - '@qwik-city-static-paths', - '@qwik-city-not-found-paths', - ]; + const external = ['node-fetch', 'path', ...MIDDLEWARE_EXTERNALS]; await build({ entryPoints, @@ -481,8 +469,6 @@ async function buildMiddlewareNode(config: BuildConfig, inputDir: string, output async function buildMiddlewareVercelEdge(config: BuildConfig, inputDir: string, outputDir: string) { const entryPoints = [join(inputDir, 'middleware', 'vercel-edge', 'index.ts')]; - const external = ['@qwik-city-plan', '@qwik-city-static-paths', '@qwik-city-not-found-paths']; - await build({ entryPoints, outfile: join(outputDir, 'middleware', 'vercel-edge', 'index.mjs'), @@ -490,7 +476,7 @@ async function buildMiddlewareVercelEdge(config: BuildConfig, inputDir: string, platform: 'node', target: nodeTarget, format: 'esm', - external, + external: MIDDLEWARE_EXTERNALS, watch: watcher(config), }); } @@ -606,3 +592,11 @@ export async function releaseQwikCity() { const npmPublishArgs = ['publish', '--tag', distTag, '--access', 'public']; await run('npm', npmPublishArgs, false, false, { cwd: pkgRootDir }); } + +const ADAPTOR_EXTERNALS = ['vite', 'fs', 'path', '@builder.io/qwik-city/static']; + +const MIDDLEWARE_EXTERNALS = [ + '@qwik-city-plan', + '@qwik-city-not-found-paths', + '@qwik-city-static-paths', +]; diff --git a/starters/apps/qwikcity-test/src/components/footer/footer.tsx b/starters/apps/qwikcity-test/src/components/footer/footer.tsx index 2ec2e592a1e..03d72637927 100644 --- a/starters/apps/qwikcity-test/src/components/footer/footer.tsx +++ b/starters/apps/qwikcity-test/src/components/footer/footer.tsx @@ -9,25 +9,25 @@ export default component$(() => {