Skip to content

Commit

Permalink
feat: 404 and catchall handling (#2175)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdbradley committed Nov 19, 2022
1 parent 55ec4fa commit 7f560eb
Show file tree
Hide file tree
Showing 61 changed files with 994 additions and 756 deletions.
1 change: 1 addition & 0 deletions packages/qwik-city/adaptors/cloudflare-pages/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function cloudflarePagesAdaptor(opts?: CloudflarePagesAdaptorOptions): an
export interface CloudflarePagesAdaptorOptions {
functionRoutes?: boolean;
staticGenerate?: StaticGenerateRenderOptions | true;
staticPaths?: string[];
}

export { StaticGenerateRenderOptions }
Expand Down
57 changes: 13 additions & 44 deletions packages/qwik-city/adaptors/cloudflare-pages/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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));
}
Expand All @@ -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 = <T>(v: T): v is NonNullable<T> => {
return v != null;
};
1 change: 1 addition & 0 deletions packages/qwik-city/adaptors/express/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/qwik-city/adaptors/netlify-edge/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function netifyEdgeAdaptor(opts?: NetlifyEdgeAdaptorOptions): any;
export interface NetlifyEdgeAdaptorOptions {
functionRoutes?: boolean;
staticGenerate?: StaticGenerateRenderOptions | true;
staticPaths?: string[];
}

export { StaticGenerateRenderOptions }
Expand Down
31 changes: 15 additions & 16 deletions packages/qwik-city/adaptors/netlify-edge/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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';
Expand All @@ -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,
};

Expand Down Expand Up @@ -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 };
87 changes: 70 additions & 17 deletions packages/qwik-city/adaptors/shared/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}`,
Expand All @@ -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,
};
}
},

Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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),
});
Expand Down Expand Up @@ -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<void>;
}

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`;

0 comments on commit 7f560eb

Please sign in to comment.