Skip to content

Commit

Permalink
feat: qwik city route location user context (QwikDev#754)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdbradley authored Jul 13, 2022
1 parent 3a97635 commit 5d0a7ae
Show file tree
Hide file tree
Showing 17 changed files with 155 additions and 147 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "qwik-monorepo",
"version": "0.0.35",
"scripts": {
"build": "yarn node scripts --tsc --build --api --platform-binding-wasm-copy",
"build": "yarn node scripts --tsc --build --qwikcity --api --platform-binding-wasm-copy",
"build.full": "yarn node scripts --tsc --build --api --eslint --qwikcity --qwikreact --platform-binding --wasm",
"build.platform": "yarn node scripts --platform-binding",
"build.platform.copy": "yarn node scripts --platform-binding-wasm-copy",
Expand Down Expand Up @@ -74,7 +74,6 @@
"eslint-plugin-no-only-tests": "2.6.0",
"execa": "6.1.0",
"express": "4.18.1",
"headers-polyfill": "^3.0.9",
"jest": "27.5.1",
"mri": "1.2.0",
"node-fetch": "2.6.7",
Expand Down
28 changes: 9 additions & 19 deletions packages/qwik-city/buildtime/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
ScriptTarget,
SyntaxKind,
} from 'typescript';
import type { QwikViteDevResponse } from '../../../qwik/src/optimizer/src/plugins/vite';

/**
* @alpha
Expand Down Expand Up @@ -96,18 +97,7 @@ export function qwikCity(userOpts?: QwikCityVitePluginOptions) {
const match = route.pattern.exec(pathname);
if (match) {
const method: HttpMethod = req.method as any;
let body: ArrayBuffer | undefined = undefined;
if (!(req.method === 'GET' || req.method === 'HEAD')) {
const bytes: string[] = [];
await new Promise((resolve) => {
req.setEncoding('utf-8');
req.on('data', (bts) => bytes.push(bts));
req.on('end', resolve);
});
body = new TextEncoder().encode(bytes.join('')).buffer;
}
const headers = new Headers(Object.entries(req.headers as Record<string, any>));
const request = new Request(url.href, { method, headers, body });
const request = new Request(url.href, { method, headers: req.headers as any });
const params = getRouteParams(route.paramNames, match);

if (route.type !== 'endpoint') {
Expand All @@ -119,7 +109,9 @@ export function qwikCity(userOpts?: QwikCityVitePluginOptions) {
}
}

const endpointModule = await server.ssrLoadModule(route.filePath);
const endpointModule = await server.ssrLoadModule(route.filePath, {
fixStacktrace: true,
});

const endpointResponse = await getEndpointResponse(
request,
Expand Down Expand Up @@ -162,12 +154,10 @@ export function qwikCity(userOpts?: QwikCityVitePluginOptions) {
}
}

try {
const userContext = getQwikCityUserContext(endpointResponse);
res.setHeader('X-Qwik-Dev-User-Context', JSON.stringify(userContext));
} catch (e) {
console.error(e);
}
(res as QwikViteDevResponse)._qwikUserCtx = {
...(res as QwikViteDevResponse)._qwikUserCtx,
...getQwikCityUserContext(url, params, method, endpointResponse),
};
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik-city/middleware/request-handler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export async function requestHandler(
return endpointHandler(method, endpointResponse);
}

const pageResponse = await pageHandler(render, url, endpointResponse);
const pageResponse = await pageHandler(render, url, params, method, endpointResponse);
return pageResponse;
}
}
Expand Down
6 changes: 4 additions & 2 deletions packages/qwik-city/middleware/request-handler/page-handler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type { Render } from '@builder.io/qwik/server';
import type { EndpointResponse } from '../../runtime/src/library/types';
import type { EndpointResponse, HttpMethod, RouteParams } from '../../runtime/src/library/types';
import { createPageHeaders, getQwikCityUserContext, getStatus } from './utils';

export async function pageHandler(
render: Render,
url: URL,
params: RouteParams,
method: HttpMethod,
endpointResponse: EndpointResponse | null
) {
const status = getStatus(endpointResponse?.status, 200, 599, 200);
const headers = createPageHeaders(endpointResponse?.headers);

const result = await render({
url: url.href,
userContext: getQwikCityUserContext(endpointResponse),
userContext: getQwikCityUserContext(url, params, method, endpointResponse),
});

return new Response(result.html, {
Expand Down
32 changes: 28 additions & 4 deletions packages/qwik-city/middleware/request-handler/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { EndpointResponse } from '../../runtime/src/library/types';
import type {
EndpointResponse,
HttpMethod,
QwikCityUserContext,
RouteLocation,
RouteParams,
} from '../../runtime/src/library/types';

export function getStatus(input: any, min: number, max: number, fallback: number) {
if (typeof input === 'number' && input >= min && input <= max) {
Expand Down Expand Up @@ -60,10 +66,28 @@ export function checkEndpointRedirect(endpointResponse: EndpointResponse | null)
return null;
}

export function getQwikCityUserContext(endpointResponse: EndpointResponse | null) {
export function getQwikCityUserContext(
url: URL,
params: RouteParams,
method: HttpMethod,
endpointResponse: EndpointResponse | null
): QwikCityUserContext {
const qcRoute: RouteLocation = {
hash: url.hash,
hostname: url.hostname,
href: url.href,
params: { ...params },
pathname: url.pathname,
query: {},
search: url.search,
};
url.searchParams.forEach((value, key) => (qcRoute.query[key] = value));

return {
qwikCity: {
endpointResponse,
qcRoute,
qcRequest: {
method,
},
qcResponse: endpointResponse,
};
}
11 changes: 6 additions & 5 deletions packages/qwik-city/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,17 @@
},
"scripts": {
"start": "cd runtime && vite",
"dev.ssr": "cd runtime && vite --mode ssr",
"dev.ssr": "cd runtime && node --inspect ../../../node_modules/vite/bin/vite.js --mode ssr",
"build": "yarn build.client && yarn build.ssr",
"build.client": "cd runtime && vite build --config vite-app.config.ts",
"build.ssr": "cd runtime && vite build --config vite-app.config.ts --ssr src/entry.express.tsx",
"build.runtime": "cd runtime && vite build --mode lib",
"serve": "node runtime/server/entry.express.js",
"serve": "node --inspect runtime/server/entry.express.js",
"serve.debug": "node --inspect-brk runtime/server/entry.express.js",
"test": "yarn test.unit",
"test": "yarn test.unit && yarn test.e2e",
"test.unit": "node ../../node_modules/.bin/uvu -r tsm . \"(unit)\"",
"test.e2e": "node ../../node_modules/.bin/playwright test runtime/src/app/tests --config runtime/playwright.config.ts"
"test.e2e": "node ../../node_modules/.bin/playwright test runtime/src/app/tests --config runtime/playwright.config.ts",
"test.e2e.no-js": "DISABLE_JS=true node ../../node_modules/.bin/playwright test runtime/src/app/tests --config runtime/playwright.config.ts"
},
"dependencies": {
"@mdx-js/mdx": "2.1.2",
Expand All @@ -46,7 +47,7 @@
"vfile": "5.3.4"
},
"peerDependencies": {
"@builder.io/qwik": ">=0.0.34"
"@builder.io/qwik": "workspace:*"
},
"devDependencies": {
"@builder.io/qwik": "workspace:*",
Expand Down
5 changes: 3 additions & 2 deletions packages/qwik-city/runtime/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type { PlaywrightTestConfig } from '@playwright/test';

const javaScriptEnabled = process.env.DISABLE_JS !== 'true';

const config: PlaywrightTestConfig = {
use: {
viewport: {
width: 1200,
height: 800,
},
javaScriptEnabled,
},
timeout: 5000,
// workers: 1,
// retries: 3,
webServer: {
command: 'node server/entry.express.js',
port: 3000,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export default component$(
<a href="/sign-in">Sign In</a>
</li>
<li>
<a href="/">Home</a>
<a class="footer-home" href="/">
Home
</a>
</li>
</ul>
</Host>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable */
import { test, expect } from '@playwright/test';

test('ssr /', async ({ page }) => {
test('Qwik City', async ({ page, javaScriptEnabled }) => {
console.log('javaScriptEnabled', javaScriptEnabled);
page.on('pageerror', (err) => expect(err).toEqual(undefined));

const rsp = (await page.goto('/'))!;
Expand Down Expand Up @@ -33,6 +35,10 @@ test('ssr /', async ({ page }) => {
const logo = header.locator('.logo a');
expect(await logo.innerText()).toBe('Qwik City 🏙');

const footer = rootLayout.locator('footer');
const footerHomeLink = footer.locator('a.footer-home');
expect(await footerHomeLink.innerText()).toBe('Home');

const main = rootLayout.locator('main');
const h1 = main.locator('h1');
expect(await h1.innerText()).toBe('Welcome to Qwik City');
Expand Down
61 changes: 26 additions & 35 deletions packages/qwik-city/runtime/src/library/html.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import {
useWaitOn,
} from '@builder.io/qwik';
import type { HTMLAttributes } from '@builder.io/qwik';
import { loadRoute, matchRoute } from './routing';
import type { ContentState, PageModule, QwikCityRenderDocument, RouteLocation } from './types';
import { loadRoute } from './routing';
import type { ContentState, PageModule, QwikCityRenderDocument } from './types';
import {
ContentContext,
ContentMenusContext,
DocumentHeadContext,
RouteLocationContext,
} from './constants';
import { createDocumentHead, resolveHead } from './head';
import { loadEndpointResponse } from './use-endpoint';
import { getSsrEndpointResponse } from './use-endpoint';
import cityPlan from '@qwik-city-plan';

/**
Expand All @@ -33,31 +33,23 @@ export const Html = component$<HtmlProps>(
}
set(true);

const doc = useDocument() as QwikCityRenderDocument;

const routeLocation = useStore(() => {
const initRouteLocation = doc?._qwikUserCtx?.qcRoute;
if (!initRouteLocation) {
throw new Error(`Missing Qwik City User Context`);
}
return initRouteLocation;
});

const documentHead = useStore(createDocumentHead);
const content = useStore<ContentState>({
breadcrumbs: undefined,
headings: undefined,
modules: [],
});

const doc = useDocument() as QwikCityRenderDocument;
const documentHead = useStore(() => createDocumentHead());

const routeLocation = useStore<RouteLocation>(() => {
const docLocation = new URL(doc.defaultView!.location as any);
const matchedRoute = matchRoute(cityPlan.routes, docLocation.pathname);
const loc: RouteLocation = {
hash: docLocation.hash,
hostname: docLocation.hostname,
href: docLocation.href,
params: { ...matchedRoute?.params },
pathname: docLocation.pathname,
query: {},
search: docLocation.search,
};
docLocation.searchParams.forEach((value, key) => (loc.query[key] = value));
return loc;
});

useContextProvider(ContentContext, content);
useContextProvider(ContentMenusContext, cityPlan.menus || {});
useContextProvider(DocumentHeadContext, documentHead);
Expand All @@ -67,22 +59,21 @@ export const Html = component$<HtmlProps>(
loadRoute(cityPlan.routes, routeLocation.pathname)
.then((loadedRoute) => {
if (loadedRoute) {
loadEndpointResponse(doc, routeLocation.pathname).then((endpointResponse) => {
const contentModules = loadedRoute.modules;
const pageModule = contentModules[contentModules.length - 1] as PageModule;
const resolvedHead = resolveHead(endpointResponse, routeLocation, contentModules);
const contentModules = loadedRoute.modules;
const pageModule = contentModules[contentModules.length - 1] as PageModule;
const endpointResponse = getSsrEndpointResponse(doc);
const resolvedHead = resolveHead(endpointResponse, routeLocation, contentModules);

documentHead.links = resolvedHead.links;
documentHead.meta = resolvedHead.meta;
documentHead.styles = resolvedHead.styles;
documentHead.title = resolvedHead.title;
documentHead.links = resolvedHead.links;
documentHead.meta = resolvedHead.meta;
documentHead.styles = resolvedHead.styles;
documentHead.title = resolvedHead.title;

content.breadcrumbs = pageModule.breadcrumbs;
content.headings = pageModule.headings;
content.modules = noSerialize<any>(contentModules);
content.breadcrumbs = pageModule.breadcrumbs;
content.headings = pageModule.headings;
content.modules = noSerialize<any>(contentModules);

routeLocation.params = { ...loadedRoute.params };
});
routeLocation.params = { ...loadedRoute.params };
}
})
.catch((e) => console.error(e))
Expand Down
4 changes: 2 additions & 2 deletions packages/qwik-city/runtime/src/library/routing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MODULE_CACHE } from './constants';
import type { LoadedRoute, MatchedRoute, RouteData, RouteParams } from './types';
import type { ContentModule, LoadedRoute, MatchedRoute, RouteData, RouteParams } from './types';

export const matchRoute = (
routes: RouteData[] | undefined,
Expand Down Expand Up @@ -27,7 +27,7 @@ export const loadRoute = async (

if (matchedRoute) {
const moduleLoaders = matchedRoute.loaders;
const modules = new Array(moduleLoaders.length);
const modules: ContentModule[] = new Array(moduleLoaders.length);
const pendingLoads: Promise<any>[] = [];

moduleLoaders.forEach((moduleLoader, i) => {
Expand Down
12 changes: 8 additions & 4 deletions packages/qwik-city/runtime/src/library/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,13 @@ export interface EndpointResponse<BODY = unknown> {
}

export interface QwikCityRenderDocument extends RenderDocument {
__qwikUserCtx?: {
qwikCity?: {
endpointResponse?: EndpointResponse;
};
_qwikUserCtx?: QwikCityUserContext;
}

export interface QwikCityUserContext {
qcRoute: RouteLocation;
qcRequest: {
method: HttpMethod;
};
qcResponse: EndpointResponse | null;
}
Loading

0 comments on commit 5d0a7ae

Please sign in to comment.