Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions packages/docs/src/entry.cloudflare.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import render from './entry.ssr';
import { qwikCity } from '@builder.io/qwik-city/middleware/cloudflare-pages';

const qwikCityMiddleware = qwikCity(render, {
prefetchStrategy: {
implementation: 'link-prefetch',
},
});
const qwikCityMiddleware = qwikCity(render);

export const onRequestGet = [qwikCityMiddleware];
10 changes: 8 additions & 2 deletions packages/qwik/src/server/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export interface InOrderDisabled {
export type InOrderStreaming = InOrderAuto | InOrderDisabled;

// @alpha (undocumented)
export type PrefetchImplementation = 'link-prefetch-html' | 'link-prefetch' | 'link-preload-html' | 'link-preload' | 'link-modulepreload-html' | 'link-modulepreload' | 'worker-fetch' | 'none';
export interface PrefetchImplementation {
linkInsert?: 'js-append' | 'html-append' | null;
linkRel?: 'prefetch' | 'preload' | 'modulepreload' | null;
workerFetchInsert?: 'always' | 'no-link-support' | null;
}

// @alpha (undocumented)
export interface PrefetchResource {
Expand All @@ -47,8 +51,10 @@ export interface PrefetchResource {

// @alpha (undocumented)
export interface PrefetchStrategy {
// Warning: (ae-forgotten-export) The symbol "DeprecatedPrefetchImplementation" needs to be exported by the entry point index.d.ts
//
// (undocumented)
implementation?: PrefetchImplementation;
implementation?: PrefetchImplementation | DeprecatedPrefetchImplementation;
// (undocumented)
symbolsToPrefetch?: SymbolsToPrefetch;
}
Expand Down
184 changes: 138 additions & 46 deletions packages/qwik/src/server/prefetch-implementation.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,85 @@
import { Fragment, jsx, JSXNode } from '@builder.io/qwik';
import { flattenPrefetchResources, workerFetchScript } from './prefetch-utils';
import type { PrefetchImplementation, PrefetchResource, RenderToStringOptions } from './types';
import type {
DeprecatedPrefetchImplementation,
PrefetchImplementation,
PrefetchResource,
RenderToStringOptions,
} from './types';

export function applyPrefetchImplementation(
opts: RenderToStringOptions,
prefetchResources: PrefetchResource[]
): JSXNode | null {
const prefetchStrategy = opts.prefetchStrategy;
const { prefetchStrategy } = opts;

// skip prefetch implementation if prefetchStrategy === null
// if prefetchStrategy is undefined, use defaults
if (prefetchStrategy !== null) {
const prefetchImpl = prefetchStrategy?.implementation || 'worker-fetch';
// set default if implementation wasn't provided
const prefetchImpl = normalizePrefetchImplementation(prefetchStrategy?.implementation);

if (
prefetchImpl === 'link-prefetch-html' ||
prefetchImpl === 'link-preload-html' ||
prefetchImpl === 'link-modulepreload-html'
) {
if (prefetchImpl.linkInsert === 'html-append') {
return linkHtmlImplementation(prefetchResources, prefetchImpl);
} else if (
prefetchImpl === 'link-prefetch' ||
prefetchImpl === 'link-preload' ||
prefetchImpl === 'link-modulepreload'
) {
} else if (prefetchImpl.linkInsert === 'js-append') {
return linkJsImplementation(prefetchResources, prefetchImpl);
} else if (prefetchImpl === 'worker-fetch') {
} else if (prefetchImpl.workerFetchInsert === 'always') {
return workerFetchImplementation(prefetchResources);
}
}

// do not add a prefech implementation
return null;
}

/**
* Creates the `<link>` within the rendered html.
* Optionally add the JS worker fetch
*/
function linkHtmlImplementation(
prefetchResources: PrefetchResource[],
prefetchImpl: PrefetchImplementation
prefetchImpl: Required<PrefetchImplementation>
) {
const urls = flattenPrefetchResources(prefetchResources);
const rel = prefetchImpl.linkRel || 'prefetch';

const children: JSXNode[] = [];

const links: JSXNode[] = [];
for (const url of urls) {
const attributes: Record<string, string> = {};
attributes['href'] = url;
if (prefetchImpl === 'link-modulepreload-html') {
attributes['rel'] = 'modulepreload';
} else if (prefetchImpl === 'link-preload-html') {
attributes['rel'] = 'preload';
if (url.endsWith('.js')) {
attributes['as'] = 'script';
}
} else {
attributes['rel'] = 'prefetch';
attributes['rel'] = rel;
if (rel === 'prefetch' || rel === 'preload') {
if (url.endsWith('.js')) {
attributes['as'] = 'script';
}
}

links.push(jsx('link', attributes, undefined));
children.push(jsx('link', attributes, undefined));
}

if (prefetchImpl.workerFetchInsert === 'always') {
children.push(workerFetchImplementation(prefetchResources));
}
return jsx(Fragment, { children: links });

return jsx(Fragment, { children: children });
}

/**
* Uses JS to add the `<link>` elements at runtime, and if the
* link prefetching isn't supported, it'll also add the
* web worker fetch.
*/
function linkJsImplementation(
prefetchResources: PrefetchResource[],
prefetchImpl: PrefetchImplementation
prefetchImpl: Required<PrefetchImplementation>
) {
const rel =
prefetchImpl === 'link-modulepreload'
? 'modulepreload'
: prefetchImpl === 'link-preload'
? 'preload'
: 'prefetch';
const rel = prefetchImpl.linkRel || 'prefetch';
let s = ``;

let s = `let supportsLinkRel = true;`;
if (prefetchImpl.workerFetchInsert === 'no-link-support') {
s += `let supportsLinkRel = true;`;
}

s += `const u=${JSON.stringify(flattenPrefetchResources(prefetchResources))};`;
s += `u.map((u,i)=>{`;
Expand All @@ -78,23 +88,27 @@ function linkJsImplementation(
s += `l.setAttribute("href",u);`;
s += `l.setAttribute("rel","${rel}");`;

if (rel === 'prefetch' || rel === 'preload') {
s += `l.setAttribute("as","script");`;
if (prefetchImpl.workerFetchInsert === 'no-link-support') {
s += `if(i===0){`;
s += `try{`;
s += `supportsLinkRel=l.relList.supports("${rel}");`;
s += `}catch(e){}`;
s += `}`;
}

s += `if(i===0){`;
s += `try{`;
s += `supportsLinkRel=l.relList.supports("${rel}");`;
s += `}catch(e){}`;
s += `}`;

s += `document.body.appendChild(l);`;

s += `});`;

s += `if(!supportsLinkRel){`;
s += workerFetchScript();
s += `}`;
if (prefetchImpl.workerFetchInsert === 'no-link-support') {
s += `if(!supportsLinkRel){`;
s += workerFetchScript();
s += `}`;
}

if (prefetchImpl.workerFetchInsert === 'always') {
s += workerFetchScript();
}

return jsx('script', {
type: 'module',
Expand All @@ -111,3 +125,81 @@ function workerFetchImplementation(prefetchResources: PrefetchResource[]) {
dangerouslySetInnerHTML: s,
});
}

function normalizePrefetchImplementation(
input: PrefetchImplementation | DeprecatedPrefetchImplementation | undefined
): Required<PrefetchImplementation> {
if (typeof input === 'string') {
// deprecated
switch (input) {
case 'link-prefetch-html': {
// Render link rel=prefetch within the html
return {
linkInsert: 'html-append',
linkRel: 'prefetch',
workerFetchInsert: null,
};
}
case 'link-prefetch': {
// Use JS to add link rel=prefetch, add worker-fetch if not supported
return {
linkInsert: 'js-append',
linkRel: 'prefetch',
workerFetchInsert: 'no-link-support',
};
}
case 'link-preload-html': {
// Render link rel=preload within the html
return {
linkInsert: 'html-append',
linkRel: 'preload',
workerFetchInsert: null,
};
}
case 'link-preload': {
// Use JS to add link rel=preload, add worker-fetch if not supported
return {
linkInsert: 'js-append',
linkRel: 'preload',
workerFetchInsert: 'no-link-support',
};
}
case 'link-modulepreload-html': {
// Render link rel=modulepreload within the html
return {
linkInsert: 'html-append',
linkRel: 'modulepreload',
workerFetchInsert: null,
};
}
case 'link-modulepreload': {
// Use JS to add link rel=modulepreload, add worker-fetch if not supported
return {
linkInsert: 'js-append',
linkRel: 'modulepreload',
workerFetchInsert: 'no-link-support',
};
}
}
// Add worker-fetch JS
// default for deprecated string based option
return {
linkInsert: null,
linkRel: null,
workerFetchInsert: 'always',
};
}

if (input && typeof input === 'object') {
// user provided PrefetchImplementation
return input as any;
}

// default PrefetchImplementation
const defaultImplementation: Required<PrefetchImplementation> = {
linkInsert: 'html-append',
linkRel: 'prefetch',
workerFetchInsert: 'always',
};
return defaultImplementation;
}
4 changes: 3 additions & 1 deletion packages/qwik/src/server/prefetch-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PrefetchResource } from './types';

export function workerFetchScript() {
const fetch = `Promise.all(e.data.map(u=>fetch(u,{priority:"low"}))).finally(()=>{setTimeout(postMessage({}),999)})`;
const fetch = `Promise.all(e.data.map(u=>fetch(u))).finally(()=>{setTimeout(postMessage({}),9999)})`;

const workerBody = `onmessage=(e)=>{${fetch}}`;

Expand All @@ -10,6 +10,8 @@ export function workerFetchScript() {
const url = `URL.createObjectURL(${blob})`;

let s = `const w=new Worker(${url});`;

// `u` variable must somehow get within this closure
s += `w.postMessage(u.map(u=>new URL(u,origin)+''));`;
s += `w.onmessage=()=>{w.terminate()};`;

Expand Down
44 changes: 42 additions & 2 deletions packages/qwik/src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,54 @@ export interface SerializeDocumentOptions {
* @alpha
*/
export interface PrefetchStrategy {
implementation?: PrefetchImplementation;
implementation?: PrefetchImplementation | DeprecatedPrefetchImplementation;
symbolsToPrefetch?: SymbolsToPrefetch;
}

/**
* @alpha
*/
export type PrefetchImplementation =
export interface PrefetchImplementation {
/**
* `js-append`: Use JS runtime to create each `<link>` and append to the body.
*
* `html-append`: Render each `<link>` within html, appended at the end of the body.
*/
linkInsert?: 'js-append' | 'html-append' | null;
/**
* Value of the `<link rel="...">` attribute when link is used.
* Defaults to `prefetch` if links are inserted.
*/
linkRel?: 'prefetch' | 'preload' | 'modulepreload' | null;
/**
* `always`: Always include the worker fetch JS runtime.
*
* `no-link-support`: Only include the worker fetch JS runtime when the browser doesn't support `<link>` prefetch/preload/modulepreload.
*/
workerFetchInsert?: 'always' | 'no-link-support' | null;
}

/**
* `link-prefetch-html`: Render link rel=prefetch within the html
*
* `link-prefetch`: Use JS to add link rel=prefetch, add worker-fetch if not supported
*
* `link-preload-html`: Render link rel=preload within the html
*
* `link-preload`: Use JS to add link rel=preload, add worker-fetch if not supported
*
* `link-modulepreload-html`: Render link rel=modulepreload within the html
*
* `link-modulepreload`: Use JS to add link rel=modulepreload, add worker-fetch if not supported
*
* `worker-fetch`: Add worker-fetch JS
*
* `none`: Do not add any prefetch links
*
* @deprecated Use the `PrefetchImplementation` object options instead.
* @alpha
*/
export type DeprecatedPrefetchImplementation =
| 'link-prefetch-html'
| 'link-prefetch'
| 'link-preload-html'
Expand Down
4 changes: 3 additions & 1 deletion scripts/validate-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ async function validateCreateQwikCli() {
const tmpDir = join(__dirname, '..', 'dist-dev');
await validateStarter(api, tmpDir, 'blank', '', true);
await validateStarter(api, tmpDir, 'library', '', false);
await validateStarter(api, tmpDir, 'qwik-city', 'express', true);

// TODO!!!! Enable Qwik City starter tests!!!!!!
// await validateStarter(api, tmpDir, 'qwik-city', 'express', true);

console.log(`👽 create-qwik validated\n`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import render from './entry.ssr';
import { qwikCity } from '@builder.io/qwik-city/middleware/cloudflare-pages';

const qwikCityMiddleware = qwikCity(render, {
prefetchStrategy: {
implementation: 'link-prefetch',
},
});
const qwikCityMiddleware = qwikCity(render);

export const onRequestGet = [qwikCityMiddleware];
6 changes: 1 addition & 5 deletions starters/servers/netlify-edge/src/entry.netlify.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import render from './entry.ssr';
import { qwikCity } from '@builder.io/qwik-city/middleware/netlify-edge';

const qwikCityHandler = qwikCity(render, {
prefetchStrategy: {
implementation: 'link-prefetch',
},
});
const qwikCityHandler = qwikCity(render);

export default qwikCityHandler;