Skip to content

Commit

Permalink
[rsc] Add @plasmicapp/nextjs-app-router
Browse files Browse the repository at this point in the history
Change-Id: I31bdb522e5f7c6a95eca93ceab8e4548e3087334
GitOrigin-RevId: 64b590fc320e6e2081763981d39a27dd6fdd95af
  • Loading branch information
victoragnez authored and Copybara committed Mar 27, 2024
1 parent a0aebf4 commit eafdaff
Show file tree
Hide file tree
Showing 19 changed files with 508 additions and 18 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"packages/loader-splits",
"packages/loader-svelte",
"packages/loader-vue",
"packages/nextjs-app-router",
"packages/prepass",
"packages/query",
"packages/react-web",
Expand Down
6 changes: 6 additions & 0 deletions packages/loader-nextjs/api/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

/// <reference types="node" />

import { ExtractPlasmicQueryData as __EXPERMIENTAL__ExtractPlasmicQueryData } from '@plasmicapp/nextjs-app-router';
import { fetchExtractedQueryData as __EXPERMIENTAL__fetchExtractedQueryData } from '@plasmicapp/nextjs-app-router';
import { CodeComponentMeta } from '@plasmicapp/loader-react';
import { ComponentLookupSpec } from '@plasmicapp/loader-react';
import { ComponentMeta } from '@plasmicapp/loader-react/react-server-conditional';
Expand Down Expand Up @@ -41,6 +43,10 @@ import { usePlasmicQueryData } from '@plasmicapp/loader-react';
import { useSelector } from '@plasmicapp/loader-react';
import { useSelectors } from '@plasmicapp/loader-react';

export { __EXPERMIENTAL__ExtractPlasmicQueryData }

export { __EXPERMIENTAL__fetchExtractedQueryData }

export { CodeComponentMeta }

export { ComponentMeta }
Expand Down
5 changes: 5 additions & 0 deletions packages/loader-nextjs/api/react-server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
/// <reference types="node" />
/// <reference types="react" />

import { fetchExtractedQueryData as __EXPERMIENTAL__fetchExtractedQueryData } from '@plasmicapp/nextjs-app-router/react-server';
import { CodeComponentMeta } from '@plasmicapp/loader-react';
import { ComponentLookupSpec } from '@plasmicapp/loader-react';
import { ComponentMeta } from '@plasmicapp/loader-react/react-server-conditional';
import { ComponentRenderData } from '@plasmicapp/loader-react/react-server-conditional';
import { DataCtxReader } from '@plasmicapp/loader-react';
import { DataProvider } from '@plasmicapp/loader-react';
import { ExtractPlasmicQueryData } from '@plasmicapp/nextjs-app-router';
import { extractPlasmicQueryData } from '@plasmicapp/loader-react';
import { FetchComponentDataOpts as FetchComponentDataOpts_2 } from '@plasmicapp/loader-react';
import { fetchExtractedQueryData } from '@plasmicapp/nextjs-app-router';
import { GlobalActionsContext } from '@plasmicapp/loader-react';
import { GlobalActionsProvider } from '@plasmicapp/loader-react';
import { IncomingMessage } from 'http';
Expand Down Expand Up @@ -47,6 +50,8 @@ import { useSelectors } from '@plasmicapp/loader-react';
// @public (undocumented)
export const __EXPERMIENTAL__extractPlasmicQueryData: (element: React.ReactElement, loader: ClientExports.NextJsPlasmicComponentLoader) => Promise<Record<string, any>>;

export { __EXPERMIENTAL__fetchExtractedQueryData }

export { ComponentMeta }

export { ComponentRenderData }
Expand Down
1 change: 1 addition & 0 deletions packages/loader-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@plasmicapp/loader-edge": "1.0.60",
"@plasmicapp/loader-react": "1.0.342",
"@plasmicapp/watcher": "1.0.83",
"@plasmicapp/nextjs-app-router": "1.0.1",
"server-only": "0.0.1"
},
"gitHead": "fa53f7d79f0e26d8b061102fda0c06788da6f8a7"
Expand Down
39 changes: 21 additions & 18 deletions packages/loader-nextjs/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,6 @@ import {
PlasmicRootProvider as CommonPlasmicRootProvider,
} from "@plasmicapp/loader-react";
import { IncomingMessage, ServerResponse } from "http";
// NextHead and NextLink must be default imported (`import Pkg`) instead of a namespace import (`import * as Pkg`).
// Otherwise, there's a Next.js 12 bug when referencing these dependencies due to default import interop.
// The transpiled CommonJS code would create a `default` field on the package,
// causing React to think it's an invalid React object:
// ```
// const NextHead = __defaultInterop(require('next/head.js'))
// assert(typeof NextHead === 'object')
// assert(typeof NextHead.default === 'function')
// ```
import type { CodeModule } from "@plasmicapp/loader-core";
import NextHead from "next/head.js";
import NextLink from "next/link.js";
import * as NextRouter from "next/router.js";
import Script from "next/script";
import * as React from "react";
import { initPlasmicLoaderWithCache } from "./cache";
import type { ComponentRenderData, NextInitOptions } from "./shared-exports";

export {
DataCtxReader,
DataProvider,
Expand All @@ -49,7 +31,28 @@ export type {
PropType,
TokenRegistration,
} from "@plasmicapp/loader-react";
export {
ExtractPlasmicQueryData as __EXPERMIENTAL__ExtractPlasmicQueryData,
fetchExtractedQueryData as __EXPERMIENTAL__fetchExtractedQueryData,
} from "@plasmicapp/nextjs-app-router";
export * from "./shared-exports";
import type { CodeModule } from "@plasmicapp/loader-core";
// NextHead and NextLink must be default imported (`import Pkg`) instead of a namespace import (`import * as Pkg`).
// Otherwise, there's a Next.js 12 bug when referencing these dependencies due to default import interop.
// The transpiled CommonJS code would create a `default` field on the package,
// causing React to think it's an invalid React object:
// ```
// const NextHead = __defaultInterop(require('next/head.js'))
// assert(typeof NextHead === 'object')
// assert(typeof NextHead.default === 'function')
// ```
import NextHead from "next/head.js";
import NextLink from "next/link.js";
import * as NextRouter from "next/router.js";
import Script from "next/script";
import * as React from "react";
import { initPlasmicLoaderWithCache } from "./cache";
import type { ComponentRenderData, NextInitOptions } from "./shared-exports";

type ServerRequest = IncomingMessage & {
cookies: {
Expand Down
1 change: 1 addition & 0 deletions packages/loader-nextjs/src/react-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { initPlasmicLoaderWithCache } from "./cache";
import type { NextInitOptions } from "./shared-exports";

import { __EXPERMIENTAL__extractPlasmicQueryData as internalExtractPlasmicQueryData } from "@plasmicapp/loader-react/react-server";
export { fetchExtractedQueryData as __EXPERMIENTAL__fetchExtractedQueryData } from "@plasmicapp/nextjs-app-router/react-server";
export * from "./shared-exports";

import type * as ClientExports from ".";
Expand Down
2 changes: 2 additions & 0 deletions packages/nextjs-app-router/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
node_modules/
109 changes: 109 additions & 0 deletions packages/nextjs-app-router/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
This package provides helpers for doing extractPlasmicQueryData() with Next.js App Router.

We normally use react-ssr-prepass to fake-render a React tree to gather data requirements. We can't do so in RSC mode, because all the client components are imported as placeholders, so we cannot fake-render them.

The idea here is to use the dev server's SSR instead! At SSR time (instead of RSC time), we do have access to imported client components. So... we could do pre-rendering there, gather the data needs, and respond with them. At RSC time, we hit the SSR endpoint, and parse out the data needs.

So...

1. Create a `app/plasmic-ssr/[[...catchall]]/page.tsx` route, whose purpose is to perform SSR. It looks something like...

```
import { ExtractPlasmicQueryData } from "@plasmicapp/nextjs-app-router";
export default async function CatchallPrepass(props: {
params?: Params;
}) {
const { params } = props;
const plasmicPath = params.catchall ? `/${params.catchall.join("/")}` : "/";
const prefetchedData = await PLASMIC.maybeFetchComponentData(plasmicPath);
if (!prefetchedData || prefetchedData.entryCompMetas.length === 0) {
notFound();
}
const pageMeta = prefetchedData.entryCompMetas[0];
return (
<ExtractPlasmicQueryData>
<PlasmicClientRootProvider
prefetchedData={prefetchedData}
pageParams={pageMeta.params}
>
<PlasmicComponent
component={pageMeta.displayName}
/>
</PlasmicClientRootProvider>
</ExtractPlasmicQueryData>
)
}
```

`<ExtractPlasmicQueryData />` is a new client component from this package, which basically performs `extractPlasmicQueryData()` on its children, and then renders a `<script data-plasmic-prefetch-id/>` tag with the json of the extracted data.

2. From the real `app/[...catchall]/page.tsx` file, make use of this endpoint to read the extracted data:

```
import { fetchExtractedQueryData } from "@plasmicapp/nextjs-app-router";
export default async function Catchall(props: {
params?: Params;
}) {
const { params } = props;
const plasmicPath = params.catchall ? `/${params.catchall.join("/")}` : "/";
const prefetchedData = await PLASMIC.maybeFetchComponentData(plasmicPath);
if (!prefetchedData || prefetchedData.entryCompMetas.length === 0) {
notFound();
}
const prepassHost = process.env.PLASMIC_PREPASS_HOST ?? process.env.VERCEL_URL ?? `http://localhost:${process.env.PORT ?? 3000}`;
const queryData = await fetchExtractedQueryData(`${prepassHost}/plasmic-ssr/${(params?.catchall ?? []).join("/")}`);
const pageMeta = prefetchedData.entryCompMetas[0];
return (
<PlasmicClientRootProvider
prefetchedData={prefetchedData}
prefetchedQueryData={queryData}
pageParams={pageMeta.params}
>
<PlasmicComponent
component={pageMeta.displayName}
/>
</PlasmicClientRootProvider>
)
}
```

Here, `fetchExtractedQueryData()` basically just hits the `/plasmic-ssr/` endpoint, and extracts the data from the json embedded in the `<script/>`.

The `prepassHost` to use is read from `PLASMIC_PREPASS_HOST` or `VERCEL_URL`. `VERCEL_URL` is available when your site is deployed on Vercel; it is the generated deployment url.

`@plasmicapp/nextjs-app-router` also comes with a `with-plasmic-prepass` command that you can use like this in your package.json:

```
"script": {
"build": "with-plasmic-prepass -- next build"
}
```

This script will start up the next dev server at some random port (by running npm run dev), run the passed command, and then kill the dev server. It will run the command with the proper `PLASMIC_PREPASS_HOST` env variable, so the user never needs to think about it. You can choose to use a different package.json script command to start the dev server via `with-plasmic-prepass -c prepass -- next build`.

Unfortunately another drawback is that the dev server and the build process will step on each other's toes, so you need to direct them to use different output folders. You do it in `next.config.js`:

```
module.exports = {
distDir: process.env.PLASMIC_PREPASS_SERVER ? ".next-prepass" : ".next"
}
```

The `PLASMIC_PREPASS_SERVER` environment variable will be set by with-plasmic-prepass.

So...

- At dev time, uses itself for extracting query data (hits `localhost:${PORT}`)
- At build time, we start a parallel dev server.
- In production, with revalidation, it will also use itself for extracting query data (using `VERCEL_URL` as the prepass host).
19 changes: 19 additions & 0 deletions packages/nextjs-app-router/api/index.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## API Report File for "@plasmicapp/nextjs-app-router"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

import * as React_2 from 'react';

// @public
export function ExtractPlasmicQueryData(props: {
children?: React_2.ReactNode;
}): React_2.JSX.Element | null;

// @public (undocumented)
export function fetchExtractedQueryData(url: string): Promise<any>;

// (No @packageDocumentation comment for this package)

```
10 changes: 10 additions & 0 deletions packages/nextjs-app-router/api/react-server.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## API Report File for "@plasmicapp/nextjs-app-router"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public (undocumented)
export function fetchExtractedQueryData(url: string): Promise<any>;

// (No @packageDocumentation comment for this package)
```
73 changes: 73 additions & 0 deletions packages/nextjs-app-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"name": "@plasmicapp/nextjs-app-router",
"version": "1.0.1",
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"module": "./dist/index.esm.js",
"files": [
"dist"
],
"bin": {
"with-plasmic-prepass": "./dist/with-plasmic-prepass.cjs.js"
},
"engines": {
"node": ">=16"
},
"scripts": {
"build": "yarn build:types && yarn build:index && yarn build:react-server && yarn build:with-dev-server",
"build:types": "yarn tsc",
"build:index": "node ../../build.mjs ./src/index.ts --use-client",
"build:with-dev-server": "esbuild --format=cjs --target=node18 --bundle --outfile=./dist/with-plasmic-prepass.cjs.js --platform=node ./src/with-dev-server.mts",
"build:react-server": "node ../../build.mjs ./src/react-server.ts",
"test": "yarn --cwd=../.. test",
"coverage": "yarn --cwd=../.. test --coverage --passWithNoTests",
"lint": "eslint",
"prepare": "if-env PREPARE_NO_BUILD=true || yarn build"
},
"dependencies": {
"@plasmicapp/prepass": "1.0.14",
"fkill": "^8.1.0",
"get-port": "^7.0.0",
"node-html-parser": "^6.1.5",
"yargs": "^17.7.2"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
"license": "MIT",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.js"
},
"./react-server": {
"types": "./dist/react-server.d.ts",
"import": "./dist/react-server.esm.js",
"require": "./dist/react-server.js"
},
"./react-server-conditional": {
"react-server": {
"types": "./dist/react-server.d.ts",
"import": "./dist/react-server.esm.js",
"require": "./dist/react-server.js"
},
"default": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.js"
}
}
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/node": "^20.8.9",
"@types/react": "^18.0.27",
"@types/yargs": "^17.0.32",
"react": "^18.2.0",
"typescript": "^5.2.2"
}
}
39 changes: 39 additions & 0 deletions packages/nextjs-app-router/src/ExtractPlasmicQueryData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { extractPlasmicQueryData } from "@plasmicapp/prepass";
import * as React from "react";

/**
* EXPERIMENTAL
*
* A component that serves the same purpose as extractPlasmicQueryData(), but from
* React server components. This only works from frameworks that support
* React.useId() and React.use() (like Next.js 13).
*
* The children of this component will be run through `extractPlasmicQueryData()`.
*/
export function ExtractPlasmicQueryData(props: { children?: React.ReactNode }) {
const { children } = props;
if (!React.useId || !(React as any).use) {
throw new Error(
`You can only use <ExtractPlasmicQueryData /> from server components.`
);
}
const scriptId = `plasmic-prefetch-${React.useId()}`;
console.log("SCRIPT ID", scriptId);
if (typeof window === "undefined") {
const data: Record<string, any> = (React as any).use(
extractPlasmicQueryData(<>{children}</>)
);
return (
<>
<script
type="application/json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
data-plasmic-prefetch-id={scriptId}
suppressHydrationWarning={true}
/>
</>
);
} else {
return null;
}
}
Loading

0 comments on commit eafdaff

Please sign in to comment.