Skip to content

Commit

Permalink
feat: support modern.js data action (#4736)
Browse files Browse the repository at this point in the history
  • Loading branch information
yimingjfe authored Oct 8, 2023
1 parent d90d83a commit d54a7be
Show file tree
Hide file tree
Showing 42 changed files with 1,097 additions and 247 deletions.
42 changes: 42 additions & 0 deletions packages/cli/plugin-data-loader/src/cli/createRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,45 @@ export const createRequest = (routeId: string, method = 'get') => {
return res;
};
};

export const createActionRequest = (routeId: string) => {
return async ({
params,
request,
}: {
params: Record<string, string>;
request: Request;
}) => {
const url = getRequestUrl({ params, request, routeId });

const init: RequestInit = {
signal: request.signal,
};
if (request.method !== 'GET') {
init.method = request.method;

const contentType = request.headers.get('Content-Type');
if (contentType && /\bapplication\/json\b/.test(contentType)) {
init.headers = { 'Content-Type': contentType };
init.body = JSON.stringify(await request.json());
} else if (contentType && /\btext\/plain\b/.test(contentType)) {
init.headers = { 'Content-Type': contentType };
init.body = await request.text();
} else if (
contentType &&
/\bapplication\/x-www-form-urlencoded\b/.test(contentType)
) {
// eslint-disable-next-line node/prefer-global/url-search-params
init.body = new URLSearchParams(await request.text());
} else {
init.body = await request.formData();
}
}

const res: Response = await fetch(url, init);
if (!res.ok) {
throw res;
}
return res;
};
};
52 changes: 18 additions & 34 deletions packages/cli/plugin-data-loader/src/cli/generateClient.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,44 @@
import path from 'path';

export const generateClient = ({
mapFile,
loaderId,
inline,
action,
routeId,
}: {
mapFile: string;
loaderId?: string;
inline: boolean;
action?: boolean;
routeId: string;
}) => {
delete require.cache[mapFile];
const loadersMap: Record<
string,
{
routeId: string;
filePath: string;
inline: boolean;
}
> = require(mapFile);
let requestCode = ``;
let exportsCode = ``;
const requestCreatorPath = path
.join(__dirname, './createRequest')
.replace('/cjs/cli/', '/esm/cli/')
.replace(/\\/g, '/');

const importCode = `
import { createRequest } from '${requestCreatorPath}';
import { createRequest, createActionRequest } from '${requestCreatorPath}';
`;

if (!loaderId) {
requestCode = Object.keys(loadersMap)
.map(loaderId => {
const { routeId } = loadersMap[loaderId];
return `
const ${loaderId} = createRequest('${routeId}');
if (inline) {
if (action) {
requestCode = `
export const loader = createRequest('${routeId}');
export const action = createActionRequest('${routeId}')
`;
} else {
requestCode = `
export const loader = createRequest('${routeId}');
`;
})
.join('');

exportsCode = `export {`;
for (const loader of Object.keys(loadersMap)) {
exportsCode += `${loader},`;
}
exportsCode += '}';
} else {
const loader = loadersMap[loaderId];
requestCode = `
const loader = createRequest('${loader.routeId}');
`;

exportsCode = `export default loader;`;
export default createRequest('${routeId}');
`;
}

const generatedCode = `
${importCode}
${requestCode}
${exportsCode}
`;

return generatedCode;
Expand Down
27 changes: 14 additions & 13 deletions packages/cli/plugin-data-loader/src/cli/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { generateClient } from './generateClient';

type Context = {
mapFile: string;
loaderId?: string;
loaderId: string;
clientData?: boolean;
action: boolean;
inline: boolean;
routeId: string;
};

export default async function loader(
Expand All @@ -27,20 +30,16 @@ export default async function loader(
const options = resourceQuery
.slice(1)
.split('&')
.map(item => {
return item.split('=');
})
.reduce((pre, cur) => {
const [key, value] = cur;
if (!key || !value) {
return pre;
const [key, value] = cur.split('=');
if (key && value) {
// eslint-disable-next-line no-nested-ternary
pre[key] = value === 'true' ? true : value === 'false' ? false : value;
}
pre[key] = value;
return pre;
}, {} as Record<string, any>) as Context;
}, {} as Record<string, any>);

// if we can not parse mapFile from resourceQuery, it means the with no need for data-loader handle.
if (!options.mapFile) {
if (!options.loaderId) {
return source;
}

Expand All @@ -63,8 +62,10 @@ export default async function loader(
}

const code = generateClient({
mapFile: options.mapFile,
loaderId: options.loaderId,
inline: options.inline,
action: options.action,
routeId: options.routeId,
});

return code;
}
31 changes: 20 additions & 11 deletions packages/cli/plugin-data-loader/src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,28 @@ const createLoaderHeaders = (
return headers;
};

const createLoaderRequest = (context: ServerContext) => {
const createRequest = (context: ServerContext) => {
const origin = `${context.protocol}://${context.host}`;
// eslint-disable-next-line node/prefer-global/url
const url = new URL(context.url, origin);

const controller = new AbortController();

const init = {
const init: {
[key: string]: unknown;
} = {
method: context.method,
headers: createLoaderHeaders(context.headers),
signal: controller.signal,
};

return new Request(url.href, init);
if (!['GET', 'HEAD'].includes(context.method.toUpperCase())) {
init.body = context.req;
}

const request = new Request(url.href, init);

return request;
};

const sendLoaderResponse = async (
Expand Down Expand Up @@ -122,7 +130,7 @@ export const handleRequest = async ({
serverRoutes: ServerRoute[];
routes: NestedRoute[];
}) => {
const { method, query } = context;
const { query } = context;
const routeId = query[LOADER_ID_PARAM] as string;
const entry = matchEntry(context.path, serverRoutes);

Expand All @@ -131,18 +139,15 @@ export const handleRequest = async ({
return;
}

if (method.toLowerCase() !== 'get') {
throw new Error('CSR data loader request only support http GET method');
}

const basename = entry.urlPath;
const end = time();
const { res, logger, reporter } = context;
const routes = transformNestedRoutes(routesConfig, reporter);
const { queryRoute } = createStaticHandler(routes, {
basename,
});
const request = createLoaderRequest(context);

const request = createRequest(context);
const requestContext = createRequestContext();
// initial requestContext
// 1. inject reporter
Expand Down Expand Up @@ -187,7 +192,12 @@ export const handleRequest = async ({
}
} catch (error) {
const message = error instanceof ErrorResponse ? error.data : String(error);
logger?.error(message);
if (error instanceof Error) {
logger?.error(error);
} else {
logger?.error(message);
}

response = new NodeResponse(message, {
status: 500,
headers: {
Expand All @@ -198,6 +208,5 @@ export const handleRequest = async ({

const cost = end();
reporter.reportTiming(`${LOADER_REPORTER_NAME}-navigation`, cost);

await sendLoaderResponse(res, response);
};
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`data loader basic usage 1`] = `
"async function loader() {
return 'request profile page';
}
async function loader2() {
return 'request profile layout';
}
const loader3 = async () => {
return {
message: 'hello user',
};
};
// src/routes/layout.tsx
const loader4 = async () => {
return {
message: 'from server',
};
};
export {
loader as loader_0,
loader2 as loader_1,
loader3 as loader_2,
loader4 as loader_3,
};
"
import { createRequest } from '/packages/cli/plugin-data-loader/src/cli/createRequest';
const loader_0 = createRequest('main_user/profile/page');
const loader_1 = createRequest('main_user/profile/layout');
const loader_2 = createRequest('main_user/layout');
const loader_3 = createRequest('main_layout');
export {loader_0,loader_1,loader_2,loader_3,}
"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The `routes/[id]/page.tsx` file will be converted to the `/:id` route. Except fo

In the component, you can use [useParams](/apis/app/runtime/router/router#useparams) to obtain the corresponding named parameter.

When using the [loader](/guides/basic-features/data-fetch#the-loader-function) function to obtain data, `params` will be passed as an input parameter to the `loader` function, and the corresponding parameter can be obtained through the attribute of `params`.
When using the [loader](/guides/basic-features/data/data-fetch#the-loader-function) function to obtain data, `params` will be passed as an input parameter to the `loader` function, and the corresponding parameter can be obtained through the attribute of `params`.

## Layout Component

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ After the project is created, you can experience the project by running `pnpm ru
When using Rspack as the bundler, the following Features are temporarily unavailable as some of the capabilities are still under development and we will provide support in the future.

- Storybook Devtool
- The usage of [useLoader](/guides/basic-features/data-fetch.html) in Client Side Rendering
- The usage of [useLoader](/guides/basic-features/data/data-fetch.html) in Client Side Rendering

:::

Expand Down
Loading

0 comments on commit d54a7be

Please sign in to comment.