aspect-build / aspect-build/rules_js
[FR]: Next.js Pages Router SSR fails with pre-compiled TypeScript - missing __N_SSP/__N_SSG markers
- Dominant language
- Starlark
- Stars
- 378
- Forks
- 183
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 32
Description
# Next.js SSR/SSG Fix for Pre-compiled TypeScript (ts_project)
## Actual behavior:
When using `ts_project` to pre-compile TypeScript before Next.js processes it, **client-side navigation to pages with `getServerSideProps` or `getStaticProps` fails silently**. The `pageProps` arrives as `{}` instead of the server-fetched data.
**Expected behavior:**
Client-side navigation should fetch data from /_next/data/... endpoints, and pageProps should contain the server-fetched data.
### Root Cause
Next.js uses internal markers (`__N_SSP` for `getServerSideProps`, `__N_SSG` for `getStaticProps`) to determine if a page needs server-side data fetching during client-side navigation. These markers are injected by Next.js's SWC transform during webpack compilation.
**The problem:** This transform only processes source files (`.tsx`, `.jsx`)—not pre-compiled `.js` files from Bazel's `ts_project`. Without these markers, the Next.js client router skips data fetching during navigation.
## The Solution
A custom webpack loader that injects the missing markers during Next.js's build:
### 1. Webpack Loader (`ssr-marker-loader.js`)
```javascript
/**
* Webpack loader to inject Next.js SSR/SSG markers into pre-compiled pages.
*/
module.exports = function ssrMarkerLoader(source) {
// Skip non-page files
if (
!this.resourcePath.includes('/pages/') ||
this.resourcePath.includes('node_modules') ||
this.resourcePath.includes('_app') ||
this.resourcePath.includes('_document')
) {
return source;
}
const hasGetServerSideProps =
/export\s+(async\s+)?function\s+getServerSideProps|exports\.getServerSideProps/.test(source);
const hasGetStaticProps =
/export\s+(async\s+)?function\s+getStaticProps|exports\.getStaticProps/.test(source);
let result = source;
if (hasGetServerSideProps && !source.includes('__N_SSP')) {
result += '\nexports.__N_SSP = true;';
}
if (hasGetStaticProps && !source.includes('__N_SSG')) {
result += '\nexports.__N_SSG = true;';
}
return result;
};
```
### 2. Next.js Configuration (`next.config.js`)
```javascript
const path = require('path');
module.exports = {
webpack: (config, { isServer }) => {
// Only apply to client-side bundles (where markers are checked)
if (!isServer) {
config.module.rules.push({
test: /\.js$/,
include: [path.resolve(__dirname, 'pages')],
use: [path.resolve(__dirname, 'ssr-marker-loader.js')],
});
}
return config;
},
};
```
### 3. Bazel BUILD Configuration
```starlark
next(
name = "next",
srcs = ["//pages"],
data = [
"next.config.js",
"ssr-marker-loader.js", # Include the loader
# ... other deps
],
)
```
## Why This Approach?
| Approach | Verdict | Reason |
|----------|---------|--------|
| Manual `export const __N_SSP = true` | ❌ | Requires changes to every page, error-prone |
| Custom SWC transpiler in Bazel | ❌ | Pages may be compiled by many separate BUILD files |
| SWC plugin | ❌ | Requires Rust/WASM, complex setup |
| **Webpack loader** | ✅ | Runs at correct point in pipeline, automatic, zero page changes |
## Key Points
1. **Bazel's TypeScript compilation happens before Next.js**—you can't rely on Next.js to add the markers
2. **Webpack loaders still run during Next.js build**—perfect injection point
3. **The `__N_SSP`/`__N_SSG` markers are undocumented**—discovered by reading Next.js router source (`next/dist/shared/lib/router/router.js`)
4. **This applies to any pre-compiled setup**—not just Bazel (esbuild, rollup, monorepo setups, etc.)
## Applicability
This solution works for:
- ✅ Bazel with `rules_js` / `rules_ts`
- ✅ Any monorepo that pre-compiles TypeScript before Next.js
- ✅ Custom bundler setups (esbuild, rollup, etc.)
- ✅ Next.js Pages Router with `getServerSideProps` or `getStaticProps`
## Working Example
A complete working example is available at: https://github.com/bazelbuild/examples/tree/main/frontend/next.js
### How to Test
To verify the fix works, create a page with `getServerSideProps`:
```tsx
// pages/ssr-test.tsx
import { GetServerSideProps } from 'next';
import Link from 'next/link';
export default function SSRTest({ timestamp }: { timestamp: string }) {
return (
SSR Test
Timestamp: {timestamp}
Home
);
}
export const getServerSideProps: GetServerSideProps = async () => {
return {
props: { timestamp: new Date().toISOString() },
};
};
```
Then test client-side navigation:
1. Run `bazel run //next.js:next_dev`
2. Navigate to the home page (`/`)
3. Open DevTools → Network tab
4. Click a link to navigate to `/ssr-test`
5. **Without the fix:** No `/_next/data/...` request, `timestamp` is empty
6. **With the fix:** `/_next/data/...` request appears, `timestamp` shows current time
Contributor guide
Research direction
Start by reading Next.js's router source mentioned in the issue, then inspect the proposed ssr-marker-loader.js, next.config.js, and Bazel BUILD configuration in the frontend/next.js example. Run bazel run //next.js:next_dev and test client-side navigation to a page using getServerSideProps. Done means a /_next/data/... request occurs and pageProps contains the server-fetched timestamp.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, next.js, typescript, webpack
- Domain
- build-system, frontend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100