@platformatic/next 3.28.0-alpha.1 → 3.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.d.ts CHANGED
@@ -499,6 +499,7 @@ export interface PlatformaticNextJsConfig {
499
499
  [k: string]: string;
500
500
  };
501
501
  sourceMaps?: boolean;
502
+ nodeModulesSourceMaps?: string[];
502
503
  scheduler?: {
503
504
  enabled?: boolean | string;
504
505
  name: string;
@@ -553,6 +554,7 @@ export interface PlatformaticNextJsConfig {
553
554
  };
554
555
  envfile?: string;
555
556
  sourceMaps?: boolean;
557
+ nodeModulesSourceMaps?: string[];
556
558
  packageManager?: "npm" | "pnpm" | "yarn";
557
559
  preload?: string | string[];
558
560
  nodeOptions?: string;
@@ -584,6 +586,7 @@ export interface PlatformaticNextJsConfig {
584
586
  };
585
587
  next?: {
586
588
  trailingSlash?: boolean;
589
+ useExperimentalAdapter?: boolean;
587
590
  };
588
591
  cache?: {
589
592
  adapter: "redis" | "valkey";
package/index.js CHANGED
@@ -1,14 +1,9 @@
1
1
  import { transform as basicTransform, resolve, validationOptions } from '@platformatic/basic'
2
2
  import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
3
- import { sep } from 'node:path'
4
- import { fileURLToPath } from 'node:url'
5
- import { NextCapability } from './lib/capability.js'
3
+ import { resolve as resolvePath } from 'node:path'
4
+ import { getCacheHandlerPath, NextCapability } from './lib/capability.js'
6
5
  import { schema } from './lib/schema.js'
7
6
 
8
- function getCacheHandlerPath (name) {
9
- return fileURLToPath(new URL(`./lib/caching/${name}.js`, import.meta.url)).replaceAll(sep, '/')
10
- }
11
-
12
7
  /* c8 ignore next 9 */
13
8
  export async function transform (config, schema, options) {
14
9
  config = await basicTransform(config, schema, options)
@@ -21,6 +16,10 @@ export async function transform (config, schema, options) {
21
16
  return config
22
17
  }
23
18
 
19
+ export function getAdapterPath () {
20
+ return resolvePath(import.meta.dirname, 'lib', 'adapter.js')
21
+ }
22
+
24
23
  export async function enhanceNextConfig (nextConfig, ...args) {
25
24
  // This is to avoid https://github.com/vercel/next.js/issues/76981
26
25
  Headers.prototype[Symbol.for('nodejs.util.inspect.custom')] = undefined
@@ -35,20 +34,30 @@ export async function enhanceNextConfig (nextConfig, ...args) {
35
34
  nextConfig.basePath = basePath
36
35
  }
37
36
 
37
+ const modifications = []
38
+
38
39
  if (config.cache?.adapter) {
39
40
  if (nextVersion.major > 15 && config.cache?.cacheComponents && typeof nextConfig.cacheComponents === 'undefined') {
40
41
  nextConfig.cacheComponents = true
41
42
  nextConfig.cacheHandler = getCacheHandlerPath('null-isr')
42
43
  nextConfig.cacheHandlers = { default: getCacheHandlerPath(`${config.cache.adapter}-components`) }
43
44
  nextConfig.cacheMaxMemorySize = 0
45
+ modifications.push(['componentsCache', config.cache.adapter])
44
46
  } else if (typeof nextConfig.cacheHandler === 'undefined') {
45
47
  nextConfig.cacheHandler = getCacheHandlerPath(`${config.cache.adapter}-isr`)
46
48
  nextConfig.cacheMaxMemorySize = 0
49
+ modifications.push(['isrCache', config.cache.adapter])
47
50
  }
48
51
  }
49
52
 
50
53
  if (config.next?.trailingSlash && typeof nextConfig.trailingSlash === 'undefined') {
51
54
  nextConfig.trailingSlash = true
55
+ modifications.push(['trailingSlash', 'enabled'])
56
+ }
57
+
58
+ if (modifications.length > 0) {
59
+ nextConfig.env ??= {}
60
+ nextConfig.env.PLT_NEXT_MODIFICATIONS = JSON.stringify(Object.fromEntries(modifications))
52
61
  }
53
62
 
54
63
  globalThis.platformatic.notifyConfig(nextConfig)
@@ -74,4 +83,5 @@ export async function create (configOrRoot, sourceOrConfig, context) {
74
83
 
75
84
  export * as cachingValkey from './lib/caching/valkey-isr.js'
76
85
  export * from './lib/capability.js'
86
+ export * as errors from './lib/errors.js'
77
87
  export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
package/lib/adapter.js ADDED
@@ -0,0 +1,10 @@
1
+ import { enhanceNextConfig } from '../index.js'
2
+
3
+ export const adapter = {
4
+ name: '@platformatic/next/adapter',
5
+ async modifyConfig (config) {
6
+ return enhanceNextConfig(config)
7
+ }
8
+ }
9
+
10
+ export default adapter
package/lib/capability.js CHANGED
@@ -1,30 +1,37 @@
1
1
  import {
2
2
  BaseCapability,
3
+ errors as basicErrors,
3
4
  ChildManager,
4
5
  cleanBasePath,
5
6
  createChildProcessListener,
6
7
  createServerListener,
7
- errors,
8
8
  getServerUrl,
9
- importFile
9
+ importFile,
10
+ resolvePackageViaCJS
10
11
  } from '@platformatic/basic'
11
- import { resolvePackageViaCJS } from '@platformatic/basic/lib/utils.js'
12
12
  import { ChildProcess } from 'node:child_process'
13
13
  import { once } from 'node:events'
14
- import { readFile, writeFile } from 'node:fs/promises'
15
- import { dirname, resolve as resolvePath } from 'node:path'
14
+ import { existsSync } from 'node:fs'
15
+ import { glob, readFile, writeFile } from 'node:fs/promises'
16
+ import { dirname, resolve as resolvePath, sep } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
16
18
  import { parse, satisfies } from 'semver'
19
+ import * as errors from './errors.js'
17
20
  import { version } from './schema.js'
18
21
 
19
- const kITC = Symbol.for('plt.runtime.itc')
20
22
  const supportedVersions = ['^14.0.0', '^15.0.0', '^16.0.0']
21
23
 
24
+ export function getCacheHandlerPath (name) {
25
+ return fileURLToPath(new URL(`./caching/${name}.js`, import.meta.url)).replaceAll(sep, '/')
26
+ }
27
+
22
28
  export class NextCapability extends BaseCapability {
23
29
  #basePath
24
30
  #next
25
31
  #nextVersion
26
32
  #child
27
33
  #server
34
+ #configModified
28
35
 
29
36
  constructor (root, config, context) {
30
37
  super('next', version, root, config, context)
@@ -51,9 +58,13 @@ export class NextCapability extends BaseCapability {
51
58
  await import('./create-context-patch.js')
52
59
  }
53
60
 
61
+ if (this.#nextVersion.major < 16 && this.config.next?.useExperimentalAdapter === true) {
62
+ this.config.next.useExperimentalAdapter = false
63
+ }
64
+
54
65
  /* c8 ignore next 3 */
55
66
  if (!supportedVersions.some(v => satisfies(nextPackage.version, v))) {
56
- throw new errors.UnsupportedVersion('next', nextPackage.version, supportedVersions)
67
+ throw new basicErrors.UnsupportedVersion('next', nextPackage.version, supportedVersions)
57
68
  }
58
69
  }
59
70
 
@@ -66,6 +77,7 @@ export class NextCapability extends BaseCapability {
66
77
  await super._start({ listen })
67
78
 
68
79
  this.on('config', config => {
80
+ this.#configModified = true
69
81
  this.#basePath = config.basePath
70
82
  })
71
83
 
@@ -76,6 +88,18 @@ export class NextCapability extends BaseCapability {
76
88
  }
77
89
 
78
90
  await this._collectMetrics()
91
+
92
+ if (!this.#configModified && this.config.next?.useExperimentalAdapter) {
93
+ this.logger.warn(
94
+ 'The experimental Next.js adapterPath is enabled but the @platformatic/next adapter was not included.'
95
+ )
96
+ this.logger.warn(
97
+ 'Please ensure that your next.config.js is correctly set up to use the Platformatic Next.js adapter.'
98
+ )
99
+ this.logger.warn(
100
+ 'Refer to the documentation for more details: https://platformatic.dev/docs/reference/next/configuration#next.'
101
+ )
102
+ }
79
103
  }
80
104
 
81
105
  async stop () {
@@ -88,17 +112,7 @@ export class NextCapability extends BaseCapability {
88
112
  globalThis.platformatic.events.emit('plt:next:close')
89
113
 
90
114
  if (this.isProduction && this.#server) {
91
- await new Promise((resolve, reject) => {
92
- this.#server.close(error => {
93
- /* c8 ignore next 3 */
94
- if (error) {
95
- return reject(error)
96
- }
97
-
98
- resolve()
99
- })
100
- })
101
-
115
+ await this._closeServer(this.#server)
102
116
  await this.childManager.close()
103
117
  } else if (this.#child) {
104
118
  const exitPromise = once(this.#child, 'exit')
@@ -125,21 +139,7 @@ export class NextCapability extends BaseCapability {
125
139
  }
126
140
 
127
141
  await this.buildWithCommand(command, this.#basePath, { loader, scripts: this.#getChildManagerScripts() })
128
-
129
- // This is need to avoid Next.js 15.4+ to throw an error as process.cwd() is not the root of the Next.js application
130
- if (
131
- config.cache?.adapter &&
132
- (this.#nextVersion.major > 15 || (this.#nextVersion.major === 15 && this.#nextVersion.minor >= 4))
133
- ) {
134
- const distDir = resolvePath(this.root, '.next')
135
- const requiredServerFilesPath = resolvePath(distDir, 'required-server-files.json')
136
- const requiredServerFiles = JSON.parse(await readFile(requiredServerFilesPath, 'utf-8'))
137
-
138
- if (requiredServerFiles.config.cacheHandler) {
139
- requiredServerFiles.config.cacheHandler = resolvePath(distDir, requiredServerFiles.config.cacheHandler)
140
- await writeFile(requiredServerFilesPath, JSON.stringify(requiredServerFiles, null, 2))
141
- }
142
- }
142
+ await this.#fixRequiredServerFiles()
143
143
  }
144
144
 
145
145
  /* c8 ignore next 5 */
@@ -254,7 +254,12 @@ export class NextCapability extends BaseCapability {
254
254
  )
255
255
 
256
256
  this.verifyOutputDirectory(resolvePath(this.root, '.next'))
257
- await this.#startProductionNext()
257
+
258
+ if (existsSync(resolvePath(this.root, '.next/standalone'))) {
259
+ return this.#startProductionStandaloneNext()
260
+ } else {
261
+ return this.#startProductionNext()
262
+ }
258
263
  }
259
264
 
260
265
  async #startProductionNext () {
@@ -294,6 +299,96 @@ export class NextCapability extends BaseCapability {
294
299
  }
295
300
  }
296
301
 
302
+ async #startProductionStandaloneNext () {
303
+ const rootDir = resolvePath(this.root, '.next', 'standalone')
304
+
305
+ // If built in standalone mode, the generated standalone directory is not on the root of the project but somewhere
306
+ // inside .next/standalone due to turbopack limitations in determining the root of the project.
307
+ // In that case we search a server.js next to a .next folder inside the .next /standalone folder.
308
+ const serverEntrypoints = await Array.fromAsync(
309
+ glob(['**/server.js'], { cwd: rootDir, ignore: ['node_modules', '**/node_modules/**'] })
310
+ )
311
+
312
+ let serverEntrypoint
313
+ for (const entrypoint of serverEntrypoints) {
314
+ if (existsSync(resolvePath(rootDir, dirname(entrypoint), '.next'))) {
315
+ serverEntrypoint = resolvePath(rootDir, entrypoint)
316
+ break
317
+ }
318
+ }
319
+
320
+ if (!serverEntrypoint) {
321
+ throw new errors.StandaloneServerNotFound()
322
+ }
323
+
324
+ // The default Next.js standalone server uses chdir, which is not supported in worker threads.
325
+ // Therefore we need to reproduce the server.js logic here, which what we do in the rest of this method.
326
+
327
+ // Parse the server.js to extract the nextConfig.
328
+ // For now we use simple regex parsing, if it breaks, we can switch to proper AST parsing.
329
+ let nextConfig
330
+ try {
331
+ const serverJsContent = await readFile(serverEntrypoint, 'utf-8')
332
+ const nextConfigMatch = serverJsContent.match(/(?:const|let)\s*nextConfig\s*=\s*(\{.+)/)
333
+ nextConfig = JSON.parse(nextConfigMatch[1])
334
+ } catch (e) {
335
+ throw new errors.CannotParseStandaloneServer({ cause: e })
336
+ }
337
+
338
+ // Fix cache handlers path
339
+ if (nextConfig.env?.PLT_NEXT_MODIFICATIONS) {
340
+ const pltNextModifications = JSON.parse(nextConfig.env.PLT_NEXT_MODIFICATIONS)
341
+
342
+ if (pltNextModifications.isrCache) {
343
+ nextConfig.cacheHandler = getCacheHandlerPath(`${pltNextModifications.isrCache}-isr`)
344
+ } else if (pltNextModifications.componentsCache) {
345
+ nextConfig.cacheHandler = getCacheHandlerPath('null-isr')
346
+ nextConfig.cacheHandlers.default = getCacheHandlerPath(`${pltNextModifications.componentsCache}-components`)
347
+ }
348
+ }
349
+
350
+ try {
351
+ await this.childManager.inject()
352
+ await this.childManager.register()
353
+
354
+ const { hostname, port, backlog } = this.serverConfig ?? {}
355
+ const serverOptions = {
356
+ hostname: hostname || '127.0.0.1',
357
+ port: port || 0
358
+ }
359
+
360
+ const serverPromise = createServerListener(
361
+ (this.isEntrypoint ? serverOptions?.port : undefined) ?? true,
362
+ (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true,
363
+ typeof backlog === 'number' ? { backlog } : {}
364
+ )
365
+
366
+ let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
367
+ if (Number.isNaN(keepAliveTimeout) || !Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
368
+ keepAliveTimeout = undefined
369
+ }
370
+
371
+ // This is needed by Next.js standalone server to pick up the correct configuration
372
+ process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
373
+ const { startServer } = await importFile(resolvePath(this.#next, './dist/server/lib/start-server.js'))
374
+
375
+ await startServer({
376
+ dir: dirname(serverEntrypoint),
377
+ isDev: false,
378
+ config: nextConfig,
379
+ hostname: serverOptions.hostname,
380
+ port: serverOptions.port,
381
+ allowRetry: false,
382
+ keepAliveTimeout
383
+ })
384
+
385
+ this.#server = await serverPromise
386
+ this.url = getServerUrl(this.#server)
387
+ } finally {
388
+ await this.childManager.eject()
389
+ }
390
+ }
391
+
297
392
  #getChildManagerScripts () {
298
393
  const scripts = []
299
394
 
@@ -334,11 +429,27 @@ export class NextCapability extends BaseCapability {
334
429
  scripts
335
430
  })
336
431
 
337
- childManager.on('event', event => {
338
- globalThis[kITC].notify('event', event)
339
- this.emit('application:worker:event:' + event.event, event.payload)
340
- })
341
-
432
+ this.setupChildManagerEventsForwarding(childManager)
342
433
  return childManager
343
434
  }
435
+
436
+ async #fixRequiredServerFiles () {
437
+ const config = this.config
438
+ const distDir = resolvePath(this.root, '.next')
439
+
440
+ // This is need to avoid Next.js 15.4+ to throw an error as process.cwd() is not the root of the Next.js application
441
+ if (
442
+ config.cache?.adapter &&
443
+ (this.#nextVersion.major > 15 || (this.#nextVersion.major === 15 && this.#nextVersion.minor >= 4))
444
+ ) {
445
+ const requiredServerFilesPath = resolvePath(distDir, 'required-server-files.json')
446
+ const requiredServerFiles = JSON.parse(await readFile(requiredServerFilesPath, 'utf-8'))
447
+
448
+ if (requiredServerFiles.config.cacheHandler) {
449
+ requiredServerFiles.config.cacheHandler = resolvePath(distDir, requiredServerFiles.config.cacheHandler)
450
+ await writeFile(requiredServerFilesPath, JSON.stringify(requiredServerFiles, null, 2))
451
+ }
452
+ }
453
+ return distDir
454
+ }
344
455
  }
package/lib/errors.js ADDED
@@ -0,0 +1,13 @@
1
+ import createError from '@fastify/error'
2
+
3
+ export const ERROR_PREFIX = 'PLT_NEXT'
4
+
5
+ export const StandaloneServerNotFound = createError(
6
+ `${ERROR_PREFIX}_CANNOT_FIND_STANDALONE_SERVER`,
7
+ 'Cannot find server.js entrypoint in .next/standalone.'
8
+ )
9
+
10
+ export const CannotParseStandaloneServer = createError(
11
+ `${ERROR_PREFIX}_CANNOT_PARSE_STANDALONE_SERVER`,
12
+ 'Cannot parse nextConfig from standalone server.js.'
13
+ )
@@ -40,13 +40,18 @@ fsPromises.readFile = async function readAndPatchNextConfigTS (url, options) {
40
40
  return contents
41
41
  }
42
42
 
43
- const { code } = transformSync(contents.toString('utf-8'), { mode: 'strip-only' })
43
+ let transformed = contents
44
44
 
45
- const { transformESM, transformCJS } = await import('./loader.js')
46
- const transformer = detectFormat(code) === 'esm' ? transformESM : transformCJS
47
- const transformed = transformer(code)
45
+ if (!globalThis.platformatic.config.next?.useExperimentalAdapter) {
46
+ const { code } = transformSync(contents.toString('utf-8'), { mode: 'strip-only' })
47
+
48
+ const { transformESM, transformCJS } = await import('./loader.js')
49
+ const transformer = detectFormat(code) === 'esm' ? transformESM : transformCJS
50
+ transformed = transformer(code)
51
+
52
+ // Restore the original method
53
+ fsPromises.readFile = originalReadFile
54
+ }
48
55
 
49
- // Restore the original method
50
- fsPromises.readFile = originalReadFile
51
56
  return transformed
52
57
  }
package/lib/loader.js CHANGED
@@ -93,6 +93,10 @@ export async function initialize (data) {
93
93
  realRoot.pathname += '/'
94
94
  }
95
95
 
96
+ if (data.config.next?.useExperimentalAdapter === true) {
97
+ return
98
+ }
99
+
96
100
  candidates = candidates.map(c => new URL(c, realRoot).toString())
97
101
  }
98
102
 
package/lib/schema.js CHANGED
@@ -45,6 +45,10 @@ const next = {
45
45
  trailingSlash: {
46
46
  type: 'boolean',
47
47
  default: false
48
+ },
49
+ useExperimentalAdapter: {
50
+ type: 'boolean',
51
+ default: false
48
52
  }
49
53
  },
50
54
  default: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/next",
3
- "version": "3.28.0-alpha.1",
3
+ "version": "3.28.1",
4
4
  "description": "Platformatic Next.js Capability",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -23,8 +23,8 @@
23
23
  "iovalkey": "^0.3.0",
24
24
  "msgpackr": "^1.11.2",
25
25
  "semver": "^7.6.3",
26
- "@platformatic/basic": "3.28.0-alpha.1",
27
- "@platformatic/foundation": "3.28.0-alpha.1"
26
+ "@platformatic/basic": "3.28.1",
27
+ "@platformatic/foundation": "3.28.1"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@fastify/reply-from": "^12.0.0",
@@ -40,8 +40,8 @@
40
40
  "next": "^16.0.0",
41
41
  "typescript": "^5.5.4",
42
42
  "ws": "^8.18.0",
43
- "@platformatic/gateway": "3.28.0-alpha.1",
44
- "@platformatic/service": "3.28.0-alpha.1"
43
+ "@platformatic/service": "3.28.1",
44
+ "@platformatic/gateway": "3.28.1"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/next/3.28.0-alpha.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/next/3.28.1.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Next.js Config",
5
5
  "type": "object",
@@ -593,6 +593,12 @@
593
593
  "sourceMaps": {
594
594
  "type": "boolean"
595
595
  },
596
+ "nodeModulesSourceMaps": {
597
+ "type": "array",
598
+ "items": {
599
+ "type": "string"
600
+ }
601
+ },
596
602
  "packageManager": {
597
603
  "type": "string",
598
604
  "enum": [
@@ -1839,6 +1845,13 @@
1839
1845
  "type": "boolean",
1840
1846
  "default": false
1841
1847
  },
1848
+ "nodeModulesSourceMaps": {
1849
+ "type": "array",
1850
+ "items": {
1851
+ "type": "string"
1852
+ },
1853
+ "default": []
1854
+ },
1842
1855
  "scheduler": {
1843
1856
  "type": "array",
1844
1857
  "items": {
@@ -2092,6 +2105,12 @@
2092
2105
  "sourceMaps": {
2093
2106
  "type": "boolean"
2094
2107
  },
2108
+ "nodeModulesSourceMaps": {
2109
+ "type": "array",
2110
+ "items": {
2111
+ "type": "string"
2112
+ }
2113
+ },
2095
2114
  "packageManager": {
2096
2115
  "type": "string",
2097
2116
  "enum": [
@@ -2194,6 +2213,10 @@
2194
2213
  "trailingSlash": {
2195
2214
  "type": "boolean",
2196
2215
  "default": false
2216
+ },
2217
+ "useExperimentalAdapter": {
2218
+ "type": "boolean",
2219
+ "default": false
2197
2220
  }
2198
2221
  },
2199
2222
  "default": {},