aspect-build / aspect-build/rules_js

[FR]: Next.js Pages Router SSR fails with pre-compiled TypeScript - missing __N_SSP/__N_SSG markers

Open
#2,658 0 comments 0 reactions 0 assignees View on GitHub
enhancement
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.