Skip to content

Commit 994be8e

Browse files
authored
feat: support nx integrated setups inside runtime (#251)
* feat: support nx integrated setups inside runtime * chore: update * chore: update * chore: fix last test
1 parent ece5542 commit 994be8e

29 files changed

+6180
-82
lines changed

src/build/content/server.ts

+11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ import { PluginContext } from '../plugin-context.js'
1212
* Copy App/Pages Router Javascript needed by the server handler
1313
*/
1414
export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
15+
// update the dist directory inside the required-server-files.json to work with
16+
// nx monorepos and other setups where the dist directory is modified
17+
const reqServerFilesPath = join(ctx.standaloneDir, '.next/required-server-files.json')
18+
const reqServerFiles = JSON.parse(await readFile(reqServerFilesPath, 'utf-8'))
19+
20+
// only override it if it was set before to a different value
21+
if (reqServerFiles.config.distDir) {
22+
reqServerFiles.config.distDir = '.next'
23+
await writeFile(reqServerFilesPath, JSON.stringify(reqServerFiles))
24+
}
25+
1526
const srcDir = join(ctx.standaloneDir, '.next')
1627
const destDir = join(ctx.serverHandlerDir, '.next')
1728

src/build/functions/server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const getHandlerFile = async (ctx: PluginContext): Promise<string> => {
5353
const template = await readFile(join(templatesDir, 'handler-monorepo.tmpl.js'), 'utf-8')
5454

5555
return template
56-
.replaceAll('{{cwd}}', join('/var/task', ctx.packagePath))
56+
.replaceAll('{{cwd}}', ctx.lambdaWorkingDirectory)
5757
.replace('{{nextServerHandler}}', ctx.nextServerHandler)
5858
}
5959

src/build/plugin-context.ts

+23-4
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,15 @@ export class PluginContext {
6565
/** Absolute path of the next runtime plugin directory */
6666
pluginDir = PLUGIN_DIR
6767

68+
get relPublishDir(): string {
69+
return this.constants.PUBLISH_DIR ?? '.next'
70+
}
71+
6872
/** Absolute path of the publish directory */
6973
get publishDir(): string {
7074
// Does not need to be resolved with the package path as it is always a repository absolute path
7175
// hence including already the `PACKAGE_PATH` therefore we don't use the `this.resolve`
72-
return resolve(this.constants.PUBLISH_DIR)
76+
return resolve(this.relPublishDir)
7377
}
7478

7579
/**
@@ -81,6 +85,13 @@ export class PluginContext {
8185
return this.constants.PACKAGE_PATH || ''
8286
}
8387

88+
/**
89+
* The working directory inside the lambda that is used for monorepos to execute the serverless function
90+
*/
91+
get lambdaWorkingDirectory(): string {
92+
return join('/var/task', this.relPublishDir.replace(/\.next$/, ''))
93+
}
94+
8495
/**
8596
* Retrieves the root of the `.next/standalone` directory
8697
*/
@@ -90,7 +101,12 @@ export class PluginContext {
90101

91102
/** Retrieves the `.next/standalone/` directory monorepo aware */
92103
get standaloneDir(): string {
93-
return join(this.standaloneRootDir, this.constants.PACKAGE_PATH || '')
104+
// the standalone directory mimics the structure of the publish directory
105+
// that said if the publish directory is `apps/my-app/.next` the standalone directory will be `.next/standalone/apps/my-app`
106+
// if the publish directory is .next the standalone directory will be `.next/standalone`
107+
// for nx workspaces where the publish directory is on the root of the repository
108+
// like `dist/apps/my-app/.next` the standalone directory will be `.next/standalone/dist/apps/my-app`
109+
return join(this.standaloneRootDir, this.relPublishDir.replace(/\.next$/, ''))
94110
}
95111

96112
/**
@@ -124,11 +140,14 @@ export class PluginContext {
124140
}
125141

126142
get serverHandlerDir(): string {
127-
return join(this.serverHandlerRootDir, this.constants.PACKAGE_PATH || '')
143+
return join(this.serverHandlerRootDir, this.relPublishDir.replace(/\.next$/, '') || '')
128144
}
129145

130146
get nextServerHandler(): string {
131-
return join(this.constants.PACKAGE_PATH || '', 'dist/run/handlers/server.js')
147+
if (this.packagePath.length !== 0) {
148+
return join(this.lambdaWorkingDirectory, 'dist/run/handlers/server.js')
149+
}
150+
return './dist/run/handlers/server.js'
132151
}
133152

134153
/**

src/build/templates/handler-monorepo.tmpl.js

+33-30
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import tracing, { trace } from '{{cwd}}/dist/run/handlers/tracing.js'
1+
import tracing, { trace } from '{{cwd}}dist/run/handlers/tracing.js'
22

33
process.chdir('{{cwd}}')
44

@@ -8,36 +8,39 @@ export default async function (req, context) {
88
tracing.start()
99
}
1010

11-
return trace
12-
.getTracer('Next.js Runtime')
13-
.startActiveSpan('Next.js Server Handler', async (span) => {
14-
try {
15-
span.setAttributes({
16-
'account.id': context.account.id,
17-
'deploy.id': context.deploy.id,
18-
'request.id': context.requestId,
19-
'site.id': context.site.id,
20-
'http.method': req.method,
21-
'http.target': req.url,
22-
monorepo: true,
23-
cwd: '{{cwd}}',
24-
})
25-
if (!cachedHandler) {
26-
const { default: handler } = await import('./{{nextServerHandler}}')
27-
cachedHandler = handler
28-
}
29-
const response = await cachedHandler(req, context)
30-
span.setAttributes({
31-
'http.status_code': response.status,
32-
})
33-
return response
34-
} catch (error) {
35-
span.recordException(error)
36-
throw error
37-
} finally {
38-
span.end()
11+
/** @type {import('@opentelemetry/api').Tracer} */
12+
const tracer = trace.getTracer('Next.js Runtime')
13+
return tracer.startActiveSpan('Next.js Server Handler', async (span) => {
14+
try {
15+
span.setAttributes({
16+
'account.id': context.account.id,
17+
'deploy.id': context.deploy.id,
18+
'request.id': context.requestId,
19+
'site.id': context.site.id,
20+
'http.method': req.method,
21+
'http.target': req.url,
22+
monorepo: true,
23+
cwd: '{{cwd}}',
24+
})
25+
if (!cachedHandler) {
26+
const { default: handler } = await import('{{nextServerHandler}}')
27+
cachedHandler = handler
3928
}
40-
})
29+
const response = await cachedHandler(req, context)
30+
span.setAttributes({
31+
'http.status_code': response.status,
32+
})
33+
return response
34+
} catch (error) {
35+
span.recordException(error)
36+
if (error instanceof Error) {
37+
span.addEvent({ name: error.name, message: error.message })
38+
}
39+
throw error
40+
} finally {
41+
span.end()
42+
}
43+
})
4144
}
4245

4346
export const config = {

src/build/templates/handler.tmpl.js

+29-23
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,36 @@ export default async function handler(req, context) {
55
if (process.env.NETLIFY_OTLP_TRACE_EXPORTER_URL) {
66
tracing.start()
77
}
8-
return trace
9-
.getTracer('Next.js Runtime')
10-
.startActiveSpan('Next.js Server Handler', async (span) => {
11-
try {
12-
span.setAttributes({
13-
'account.id': context.account.id,
14-
'deploy.id': context.deploy.id,
15-
'request.id': context.requestId,
16-
'site.id': context.site.id,
17-
'http.method': req.method,
18-
'http.target': req.url,
19-
})
20-
const response = await serverHandler(req, context)
21-
span.setAttributes({
22-
'http.status_code': response.status,
23-
})
24-
return response
25-
} catch (error) {
26-
span.recordException(error)
27-
throw error
28-
} finally {
29-
span.end()
8+
9+
/** @type {import('@opentelemetry/api').Tracer} */
10+
const tracer = trace.getTracer('Next.js Runtime')
11+
return tracer.startActiveSpan('Next.js Server Handler', async (span) => {
12+
try {
13+
span.setAttributes({
14+
'account.id': context.account.id,
15+
'deploy.id': context.deploy.id,
16+
'request.id': context.requestId,
17+
'site.id': context.site.id,
18+
'http.method': req.method,
19+
'http.target': req.url,
20+
monorepo: false,
21+
cwd: process.cwd(),
22+
})
23+
const response = await serverHandler(req, context)
24+
span.setAttributes({
25+
'http.status_code': response.status,
26+
})
27+
return response
28+
} catch (error) {
29+
span.recordException(error)
30+
if (error instanceof Error) {
31+
span.addEvent({ name: error.name, message: error.message })
3032
}
31-
})
33+
throw error
34+
} finally {
35+
span.end()
36+
}
37+
})
3238
}
3339

3440
export const config = {

src/run/handlers/server.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { toComputeResponse, toReqRes } from '@fastly/http-compute-js'
2-
import { trace } from '@opentelemetry/api'
2+
import { SpanStatusCode, trace } from '@opentelemetry/api'
33
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
44
import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
55

@@ -59,6 +59,13 @@ export default async (request: Request) => {
5959
logger.withError(error).error('next handler error')
6060
console.error(error)
6161
resProxy.statusCode = 500
62+
span.recordException(error)
63+
span.setAttribute('http.status_code', 500)
64+
span.setStatus({
65+
code: SpanStatusCode.ERROR,
66+
message: error instanceof Error ? error.message : String(error),
67+
})
68+
span.end()
6269
resProxy.end('Internal Server Error')
6370
})
6471

tests/e2e/nx-integrated.test.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { expect } from '@playwright/test'
2+
import { test } from '../utils/create-e2e-fixture.js'
3+
4+
test('Renders the Home page correctly', async ({ page, nxIntegrated }) => {
5+
await page.goto(nxIntegrated.url)
6+
7+
await expect(page).toHaveTitle('Welcome to next-app')
8+
9+
const h1 = page.locator('h1')
10+
await expect(h1).toHaveText('Hello there,\nWelcome next-app 👋')
11+
})
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# See http://help.github.com/ignore-files/ for more about ignoring files.
2+
3+
# compiled output
4+
dist
5+
tmp
6+
/out-tsc
7+
8+
# dependencies
9+
node_modules
10+
11+
# IDEs and editors
12+
/.idea
13+
.project
14+
.classpath
15+
.c9/
16+
*.launch
17+
.settings/
18+
*.sublime-workspace
19+
20+
# IDE - VSCode
21+
.vscode/*
22+
!.vscode/settings.json
23+
!.vscode/tasks.json
24+
!.vscode/launch.json
25+
!.vscode/extensions.json
26+
27+
# misc
28+
/.sass-cache
29+
/connect.lock
30+
/coverage
31+
/libpeerconnection.log
32+
npm-debug.log
33+
yarn-error.log
34+
testem.log
35+
/typings
36+
37+
# System Files
38+
.DS_Store
39+
Thumbs.db
40+
41+
.nx/cache
42+
43+
# Next.js
44+
.next
45+
46+
# Local Netlify folder
47+
.netlify

tests/fixtures/nx-integrated/.npmrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
strict-peer-dependencies=false
2+
auto-install-peers=true
3+
public-hoist-pattern[]=*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function GET(request: Request) {
2+
return new Response('Hello, from API!');
3+
}

0 commit comments

Comments
 (0)