@platformatic/next 2.72.0 → 3.0.0-alpha.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
@@ -5,7 +5,7 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
- export interface PlatformaticNextJsStackable {
8
+ export interface PlatformaticNextJsConfig {
9
9
  $schema?: string;
10
10
  logger?: {
11
11
  level: (
package/index.js CHANGED
@@ -1,361 +1,37 @@
1
- import {
2
- BaseStackable,
3
- transformConfig as basicTransformConfig,
4
- ChildManager,
5
- cleanBasePath,
6
- createChildProcessListener,
7
- createServerListener,
8
- errors,
9
- getServerUrl,
10
- importFile,
11
- resolvePackage,
12
- schemaOptions
13
- } from '@platformatic/basic'
14
- import { ConfigManager } from '@platformatic/config'
15
- import { ChildProcess } from 'node:child_process'
16
- import { once } from 'node:events'
17
- import { readFile } from 'node:fs/promises'
18
- import { dirname, resolve as pathResolve } from 'node:path'
19
- import { pathToFileURL } from 'node:url'
20
- import { parse, satisfies } from 'semver'
21
- import { packageJson, schema } from './lib/schema.js'
22
-
23
- const supportedVersions = ['^14.0.0', '^15.0.0']
24
-
25
- export * as cachingValkey from './lib/caching/valkey.js'
26
-
27
- export class NextStackable extends BaseStackable {
28
- #basePath
29
- #next
30
- #nextVersion
31
- #child
32
- #server
33
-
34
- constructor (options, root, configManager) {
35
- super('next', packageJson.version, options, root, configManager)
36
- }
37
-
38
- async init () {
39
- // This is needed to avoid Next.js to throw an error when the lockfile is not correct
40
- // and the user is using npm but has pnpm in its $PATH.
41
- //
42
- // See: https://github.com/platformatic/composer-next-node-fastify/pull/3
43
- //
44
- // PS by Paolo: Sob.
45
- process.env.NEXT_IGNORE_INCORRECT_LOCKFILE = 'true'
46
-
47
- this.#next = pathResolve(dirname(resolvePackage(this.root, 'next')), '../..')
48
- const nextPackage = JSON.parse(await readFile(pathResolve(this.#next, 'package.json'), 'utf-8'))
49
- this.#nextVersion = parse(nextPackage.version)
50
-
51
- if (this.#nextVersion.major < 15 || (this.#nextVersion.major <= 15 && this.#nextVersion.minor < 1)) {
52
- await import('./lib/create-context-patch.js')
53
- }
54
-
55
- /* c8 ignore next 3 */
56
- if (!supportedVersions.some(v => satisfies(nextPackage.version, v))) {
57
- throw new errors.UnsupportedVersion('next', nextPackage.version, supportedVersions)
58
- }
59
- }
60
-
61
- async start ({ listen }) {
62
- // Make this idempotent
63
- if (this.url) {
64
- return this.url
65
- }
66
-
67
- this.on('config', config => {
68
- this.#basePath = config.basePath
69
- })
70
-
71
- if (this.isProduction) {
72
- await this.#startProduction(listen)
73
- } else {
74
- await this.#startDevelopment(listen)
75
- }
76
-
77
- await this._collectMetrics()
78
- }
79
-
80
- async stop () {
81
- if (this.subprocess) {
82
- return this.stopCommand()
83
- }
84
-
85
- globalThis.platformatic.events.emit('plt:next:close')
86
-
87
- if (this.isProduction) {
88
- await new Promise((resolve, reject) => {
89
- this.#server.close(error => {
90
- /* c8 ignore next 3 */
91
- if (error) {
92
- return reject(error)
93
- }
94
-
95
- resolve()
96
- })
97
- })
98
-
99
- await this.childManager.close()
100
- } else {
101
- const exitPromise = once(this.#child, 'exit')
102
- await this.childManager.close()
103
- process.kill(this.#child.pid, 'SIGKILL')
104
- await exitPromise
105
- }
106
- }
107
-
108
- async build () {
109
- if (!this.#nextVersion) {
110
- await this.init()
111
- }
112
-
113
- const config = this.configManager.current
114
- const loader = new URL('./lib/loader.js', import.meta.url)
115
- this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
116
-
117
- let command = config.application.commands.build
118
-
119
- if (!command) {
120
- await this.init()
121
- command = ['node', pathResolve(this.#next, './dist/bin/next'), 'build', this.root]
122
- }
123
-
124
- return this.buildWithCommand(command, this.#basePath, { loader, scripts: this.#getChildManagerScripts() })
125
- }
126
-
127
- /* c8 ignore next 5 */
128
- async getWatchConfig () {
129
- return {
130
- enabled: false,
131
- path: this.root
132
- }
133
- }
134
-
135
- getMeta () {
136
- const composer = { prefix: this.basePath ?? this.#basePath, wantsAbsoluteUrls: true, needsRootTrailingSlash: false }
137
-
138
- if (this.url) {
139
- composer.tcp = true
140
- composer.url = this.url
141
- }
142
-
143
- return { composer }
144
- }
145
-
146
- async #startDevelopment () {
147
- const config = this.configManager.current
148
- const loaderUrl = new URL('./lib/loader.js', import.meta.url)
149
- const command = this.configManager.current.application.commands.development
150
-
151
- this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
152
-
153
- if (command) {
154
- return this.startWithCommand(command, loaderUrl, this.#getChildManagerScripts())
155
- }
156
-
157
- const { hostname, port } = this.serverConfig ?? {}
158
- const serverOptions = {
159
- host: hostname || '127.0.0.1',
160
- port: port || 0
161
- }
162
-
163
- const context = await this.getChildManagerContext(this.#basePath)
164
-
165
- this.childManager = new ChildManager({
166
- loader: loaderUrl,
167
- context: {
168
- ...context,
169
- port: false,
170
- wantsAbsoluteUrls: true
171
- },
172
- scripts: this.#getChildManagerScripts()
173
- })
174
-
175
- const promise = once(this.childManager, 'url')
176
- await this.#startDevelopmentNext(serverOptions)
177
- const [url, clientWs] = await promise
178
- this.url = url
179
- this.clientWs = clientWs
180
- }
181
-
182
- async #startDevelopmentNext (serverOptions) {
183
- const { nextDev } = await importFile(pathResolve(this.#next, './dist/cli/next-dev.js'))
184
-
185
- try {
186
- await this.childManager.inject()
187
- const childPromise = createChildProcessListener()
188
-
189
- this.#ensurePipeableStreamsInFork()
190
-
191
- if (this.#nextVersion.major === 14 && this.#nextVersion.minor < 2) {
192
- await nextDev({
193
- '--hostname': serverOptions.host,
194
- '--port': serverOptions.port,
195
- _: [this.root]
196
- })
197
- } else {
198
- await nextDev(serverOptions, 'default', this.root)
199
- }
200
-
201
- this.#child = await childPromise
202
- this.#child.stdout.setEncoding('utf8')
203
- this.#child.stderr.setEncoding('utf8')
204
-
205
- this.#child.stdout.pipe(process.stdout, { end: false })
206
- this.#child.stderr.pipe(process.stderr, { end: false })
207
- } finally {
208
- await this.childManager.eject()
209
- }
210
- }
211
-
212
- async #startProduction (listen) {
213
- const config = this.configManager.current
214
- const loaderUrl = new URL('./lib/loader.js', import.meta.url)
215
- const command = this.configManager.current.application.commands.production
216
-
217
- this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
218
-
219
- if (command) {
220
- const childManagerScripts = this.#getChildManagerScripts()
221
-
222
- if (this.#nextVersion.major < 15 || (this.#nextVersion.major <= 15 && this.#nextVersion.minor < 1)) {
223
- childManagerScripts.push(new URL('./lib/create-context-patch.js', import.meta.url))
224
- }
225
- return this.startWithCommand(command, loaderUrl, childManagerScripts)
226
- }
227
-
228
- this.childManager = new ChildManager({
229
- loader: loaderUrl,
230
- context: {
231
- config: this.configManager.current,
232
- serviceId: this.serviceId,
233
- workerId: this.workerId,
234
- // Always use URL to avoid serialization problem in Windows
235
- root: pathToFileURL(this.root).toString(),
236
- basePath: this.#basePath,
237
- logLevel: this.logger.level,
238
- isEntrypoint: this.isEntrypoint,
239
- runtimeBasePath: this.runtimeConfig.basePath,
240
- wantsAbsoluteUrls: true,
241
- telemetryConfig: this.telemetryConfig
242
- },
243
- scripts: this.#getChildManagerScripts()
244
- })
245
-
246
- this.verifyOutputDirectory(pathResolve(this.root, '.next'))
247
- await this.#startProductionNext()
248
- }
249
-
250
- async #startProductionNext () {
251
- try {
252
- globalThis.platformatic.config = this.configManager.current
253
- await this.childManager.inject()
254
- const { nextStart } = await importFile(pathResolve(this.#next, './dist/cli/next-start.js'))
255
-
256
- const { hostname, port } = this.serverConfig ?? {}
257
- const serverOptions = {
258
- hostname: hostname || '127.0.0.1',
259
- port: port || 0
260
- }
261
-
262
- this.childManager.register()
263
- const serverPromise = createServerListener(
264
- (this.isEntrypoint ? serverOptions?.port : undefined) ?? true,
265
- (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
266
- )
267
-
268
- if (this.#nextVersion.major === 14 && this.#nextVersion.minor < 2) {
269
- await nextStart({
270
- '--hostname': serverOptions.host,
271
- '--port': serverOptions.port,
272
- _: [this.root]
273
- })
274
- } else {
275
- await nextStart(serverOptions, this.root)
276
- }
277
-
278
- this.#server = await serverPromise
279
- this.url = getServerUrl(this.#server)
280
- } finally {
281
- await this.childManager.eject()
282
- }
283
- }
284
-
285
- #getChildManagerScripts () {
286
- const scripts = []
287
-
288
- if (this.#nextVersion.major === 15) {
289
- scripts.push(new URL('./lib/loader-next-15.cjs', import.meta.url))
290
- }
291
-
292
- return scripts
293
- }
294
-
295
- // In development mode, Next.js starts the dev server using child_process.fork with stdio set to 'inherit'.
296
- // In order to capture the output, we need to ensure that the streams are pipeable and thus we perform a one-time
297
- // monkey-patch of the ChildProcess.prototype.spawn method to override stdio[1] and stdio[2] to 'pipe'.
298
- #ensurePipeableStreamsInFork () {
299
- const originalSpawn = ChildProcess.prototype.spawn
300
-
301
- // IMPORTANT: If Next.js code changes this might not work anymore. When this gives error, dig into Next.js code
302
- // to evaluate the new path and/or if this is still necessary.
303
- const startServerPath = pathResolve(this.#next, './dist/server/lib/start-server.js')
304
-
305
- ChildProcess.prototype.spawn = function (options) {
306
- if (options.args?.[1] === startServerPath) {
307
- options.stdio[1] = 'pipe'
308
- options.stdio[2] = 'pipe'
309
-
310
- // Uninstall the patch
311
- ChildProcess.prototype.spawn = originalSpawn
312
- }
313
-
314
- return originalSpawn.call(this, options)
315
- }
316
- }
317
- }
1
+ import { transform as basicTransform, resolve, validationOptions } from '@platformatic/basic'
2
+ import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/utils'
3
+ import { schema } from './lib/schema.js'
4
+ import { NextStackable } from './lib/stackable.js'
318
5
 
319
6
  /* c8 ignore next 9 */
320
- function transformConfig () {
321
- if (this.current.watch === undefined) {
322
- this.current.watch = { enabled: false }
323
- }
7
+ export async function transform (config, schema, options) {
8
+ config = await basicTransform(config, schema, options)
9
+ config.watch = { enabled: false }
324
10
 
325
- if (typeof this.current.watch !== 'object') {
326
- this.current.watch = { enabled: this.current.watch || false }
11
+ if (config.cache?.adapter === 'redis') {
12
+ config.cache.adapter = 'valkey'
327
13
  }
328
14
 
329
- if (this.current.cache?.adapter === 'redis') {
330
- this.current.cache.adapter = 'valkey'
331
- }
332
-
333
- return basicTransformConfig.call(this)
15
+ return config
334
16
  }
335
17
 
336
- export async function buildStackable (opts) {
337
- const root = opts.context.directory
18
+ export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
19
+ const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'application')
338
20
 
339
- const configManager = new ConfigManager({
340
- schema,
341
- source: opts.config ?? {},
342
- schemaOptions,
343
- transformConfig,
344
- dirname: root,
345
- context: opts.context
21
+ return utilsLoadConfiguration(source, context?.schema ?? schema, {
22
+ validationOptions,
23
+ transform,
24
+ replaceEnv: true,
25
+ root,
26
+ ...context
346
27
  })
347
- await configManager.parseAndValidate()
348
-
349
- return new NextStackable(opts, root, configManager)
350
28
  }
351
29
 
352
- export default {
353
- configType: 'next',
354
- configManagerConfig: {
355
- schemaOptions,
356
- transformConfig
357
- },
358
- buildStackable,
359
- schema,
360
- version: packageJson.version
30
+ export async function create (configOrRoot, sourceOrConfig, context) {
31
+ const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
32
+ return new NextStackable(config[kMetadata].root, config, context)
361
33
  }
34
+
35
+ export * as cachingValkey from './lib/caching/valkey.js'
36
+ export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
37
+ export * from './lib/stackable.js'
package/lib/schema.js CHANGED
@@ -1,8 +1,10 @@
1
- import { schemaComponents } from '@platformatic/basic'
1
+ import { schemaComponents as basicSchemaComponents } from '@platformatic/basic'
2
2
  import { schemaComponents as utilsSchemaComponents } from '@platformatic/utils'
3
3
  import { readFileSync } from 'node:fs'
4
+ import { resolve } from 'node:path'
4
5
 
5
- export const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
6
+ export const packageJson = JSON.parse(readFileSync(resolve(import.meta.dirname, '../package.json'), 'utf8'))
7
+ export const version = packageJson.version
6
8
 
7
9
  export const cache = {
8
10
  type: 'object',
@@ -46,10 +48,12 @@ const next = {
46
48
  additionalProperties: false
47
49
  }
48
50
 
51
+ export const schemaComponents = { next }
52
+
49
53
  export const schema = {
50
54
  $id: `https://schemas.platformatic.dev/@platformatic/next/${packageJson.version}.json`,
51
55
  $schema: 'http://json-schema.org/draft-07/schema#',
52
- title: 'Platformatic Next.js Stackable',
56
+ title: 'Platformatic Next.js Config',
53
57
  type: 'object',
54
58
  properties: {
55
59
  $schema: {
@@ -57,8 +61,8 @@ export const schema = {
57
61
  },
58
62
  logger: utilsSchemaComponents.logger,
59
63
  server: utilsSchemaComponents.server,
60
- watch: schemaComponents.watch,
61
- application: schemaComponents.application,
64
+ watch: basicSchemaComponents.watch,
65
+ application: basicSchemaComponents.application,
62
66
  runtime: utilsSchemaComponents.wrappedRuntime,
63
67
  next,
64
68
  cache
@@ -0,0 +1,329 @@
1
+ import {
2
+ BaseStackable,
3
+ ChildManager,
4
+ cleanBasePath,
5
+ createChildProcessListener,
6
+ createServerListener,
7
+ errors,
8
+ getServerUrl,
9
+ importFile,
10
+ resolvePackage
11
+ } from '@platformatic/basic'
12
+ import { ChildProcess } from 'node:child_process'
13
+ import { once } from 'node:events'
14
+ import { readFile, writeFile } from 'node:fs/promises'
15
+ import { dirname, resolve as resolvePath } from 'node:path'
16
+ import { pathToFileURL } from 'node:url'
17
+ import { parse, satisfies } from 'semver'
18
+ import { version } from './schema.js'
19
+
20
+ const supportedVersions = ['^14.0.0', '^15.0.0']
21
+
22
+ export class NextStackable extends BaseStackable {
23
+ #basePath
24
+ #next
25
+ #nextVersion
26
+ #child
27
+ #server
28
+
29
+ constructor (root, config, context) {
30
+ super('next', version, root, config, context)
31
+ }
32
+
33
+ async init () {
34
+ await super.init()
35
+
36
+ // This is needed to avoid Next.js to throw an error when the lockfile is not correct
37
+ // and the user is using npm but has pnpm in its $PATH.
38
+ //
39
+ // See: https://github.com/platformatic/composer-next-node-fastify/pull/3
40
+ //
41
+ // PS by Paolo: Sob.
42
+ process.env.NEXT_IGNORE_INCORRECT_LOCKFILE = 'true'
43
+
44
+ this.#next = resolvePath(dirname(resolvePackage(this.root, 'next')), '../..')
45
+ const nextPackage = JSON.parse(await readFile(resolvePath(this.#next, 'package.json'), 'utf-8'))
46
+ this.#nextVersion = parse(nextPackage.version)
47
+
48
+ if (this.#nextVersion.major < 15 || (this.#nextVersion.major <= 15 && this.#nextVersion.minor < 1)) {
49
+ await import('./create-context-patch.js')
50
+ }
51
+
52
+ /* c8 ignore next 3 */
53
+ if (!supportedVersions.some(v => satisfies(nextPackage.version, v))) {
54
+ throw new errors.UnsupportedVersion('next', nextPackage.version, supportedVersions)
55
+ }
56
+ }
57
+
58
+ async start ({ listen }) {
59
+ // Make this idempotent
60
+ if (this.url) {
61
+ return this.url
62
+ }
63
+
64
+ this.on('config', config => {
65
+ this.#basePath = config.basePath
66
+ })
67
+
68
+ if (this.isProduction) {
69
+ await this.#startProduction(listen)
70
+ } else {
71
+ await this.#startDevelopment(listen)
72
+ }
73
+
74
+ await this._collectMetrics()
75
+ }
76
+
77
+ async stop () {
78
+ if (this.subprocess) {
79
+ return this.stopCommand()
80
+ }
81
+
82
+ globalThis.platformatic.events.emit('plt:next:close')
83
+
84
+ if (this.isProduction) {
85
+ await new Promise((resolve, reject) => {
86
+ this.#server.close(error => {
87
+ /* c8 ignore next 3 */
88
+ if (error) {
89
+ return reject(error)
90
+ }
91
+
92
+ resolve()
93
+ })
94
+ })
95
+
96
+ await this.childManager.close()
97
+ } else {
98
+ const exitPromise = once(this.#child, 'exit')
99
+ await this.childManager.close()
100
+ process.kill(this.#child.pid, 'SIGKILL')
101
+ await exitPromise
102
+ }
103
+ }
104
+
105
+ async build () {
106
+ if (!this.#nextVersion) {
107
+ await this.init()
108
+ }
109
+
110
+ const config = this.config
111
+ const loader = new URL('./loader.js', import.meta.url)
112
+ this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
113
+
114
+ let command = config.application.commands.build
115
+
116
+ if (!command) {
117
+ await this.init()
118
+ command = ['node', resolvePath(this.#next, './dist/bin/next'), 'build', this.root]
119
+ }
120
+
121
+ await this.buildWithCommand(command, this.#basePath, { loader, scripts: this.#getChildManagerScripts() })
122
+
123
+ // 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
124
+ if (
125
+ config.cache?.adapter &&
126
+ (this.#nextVersion.major > 15 || (this.#nextVersion.major === 15 && this.#nextVersion.minor >= 4))
127
+ ) {
128
+ const distDir = resolvePath(this.root, '.next')
129
+ const requiredServerFilesPath = resolvePath(distDir, 'required-server-files.json')
130
+ const requiredServerFiles = JSON.parse(await readFile(requiredServerFilesPath, 'utf-8'))
131
+
132
+ if (requiredServerFiles.config.cacheHandler) {
133
+ requiredServerFiles.config.cacheHandler = resolvePath(distDir, requiredServerFiles.config.cacheHandler)
134
+ await writeFile(requiredServerFilesPath, JSON.stringify(requiredServerFiles, null, 2))
135
+ }
136
+ }
137
+ }
138
+
139
+ /* c8 ignore next 5 */
140
+ async getWatchConfig () {
141
+ return {
142
+ enabled: false,
143
+ path: this.root
144
+ }
145
+ }
146
+
147
+ getMeta () {
148
+ const composer = { prefix: this.basePath ?? this.#basePath, wantsAbsoluteUrls: true, needsRootTrailingSlash: false }
149
+
150
+ if (this.url) {
151
+ composer.tcp = true
152
+ composer.url = this.url
153
+ }
154
+
155
+ return { composer }
156
+ }
157
+
158
+ async #startDevelopment () {
159
+ const config = this.config
160
+ const loaderUrl = new URL('./loader.js', import.meta.url)
161
+ const command = this.config.application.commands.development
162
+
163
+ this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
164
+
165
+ if (command) {
166
+ return this.startWithCommand(command, loaderUrl, this.#getChildManagerScripts())
167
+ }
168
+
169
+ const { hostname, port } = this.serverConfig ?? {}
170
+ const serverOptions = {
171
+ host: hostname || '127.0.0.1',
172
+ port: port || 0
173
+ }
174
+
175
+ const context = await this.getChildManagerContext(this.#basePath)
176
+
177
+ this.childManager = new ChildManager({
178
+ loader: loaderUrl,
179
+ context: {
180
+ ...context,
181
+ port: false,
182
+ wantsAbsoluteUrls: true
183
+ },
184
+ scripts: this.#getChildManagerScripts()
185
+ })
186
+
187
+ const promise = once(this.childManager, 'url')
188
+ await this.#startDevelopmentNext(serverOptions)
189
+ const [url, clientWs] = await promise
190
+ this.url = url
191
+ this.clientWs = clientWs
192
+ }
193
+
194
+ async #startDevelopmentNext (serverOptions) {
195
+ const { nextDev } = await importFile(resolvePath(this.#next, './dist/cli/next-dev.js'))
196
+
197
+ try {
198
+ await this.childManager.inject()
199
+ const childPromise = createChildProcessListener()
200
+
201
+ this.#ensurePipeableStreamsInFork()
202
+
203
+ if (this.#nextVersion.major === 14 && this.#nextVersion.minor < 2) {
204
+ await nextDev({
205
+ '--hostname': serverOptions.host,
206
+ '--port': serverOptions.port,
207
+ _: [this.root]
208
+ })
209
+ } else {
210
+ await nextDev(serverOptions, 'default', this.root)
211
+ }
212
+
213
+ this.#child = await childPromise
214
+ this.#child.stdout.setEncoding('utf8')
215
+ this.#child.stderr.setEncoding('utf8')
216
+
217
+ this.#child.stdout.pipe(process.stdout, { end: false })
218
+ this.#child.stderr.pipe(process.stderr, { end: false })
219
+ } finally {
220
+ await this.childManager.eject()
221
+ }
222
+ }
223
+
224
+ async #startProduction (listen) {
225
+ const config = this.config
226
+ const loaderUrl = new URL('./loader.js', import.meta.url)
227
+ const command = this.config.application.commands.production
228
+
229
+ this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
230
+
231
+ if (command) {
232
+ const childManagerScripts = this.#getChildManagerScripts()
233
+
234
+ if (this.#nextVersion.major < 15 || (this.#nextVersion.major <= 15 && this.#nextVersion.minor < 1)) {
235
+ childManagerScripts.push(new URL('./create-context-patch.js', import.meta.url))
236
+ }
237
+ return this.startWithCommand(command, loaderUrl, childManagerScripts)
238
+ }
239
+
240
+ this.childManager = new ChildManager({
241
+ loader: loaderUrl,
242
+ context: {
243
+ config: this.config,
244
+ serviceId: this.serviceId,
245
+ workerId: this.workerId,
246
+ // Always use URL to avoid serialization problem in Windows
247
+ root: pathToFileURL(this.root).toString(),
248
+ basePath: this.#basePath,
249
+ logLevel: this.logger.level,
250
+ isEntrypoint: this.isEntrypoint,
251
+ runtimeBasePath: this.runtimeConfig.basePath,
252
+ wantsAbsoluteUrls: true,
253
+ telemetryConfig: this.telemetryConfig
254
+ },
255
+ scripts: this.#getChildManagerScripts()
256
+ })
257
+
258
+ this.verifyOutputDirectory(resolvePath(this.root, '.next'))
259
+ await this.#startProductionNext()
260
+ }
261
+
262
+ async #startProductionNext () {
263
+ try {
264
+ globalThis.platformatic.config = this.config
265
+ await this.childManager.inject()
266
+ const { nextStart } = await importFile(resolvePath(this.#next, './dist/cli/next-start.js'))
267
+
268
+ const { hostname, port } = this.serverConfig ?? {}
269
+ const serverOptions = {
270
+ hostname: hostname || '127.0.0.1',
271
+ port: port || 0
272
+ }
273
+
274
+ this.childManager.register()
275
+ const serverPromise = createServerListener(
276
+ (this.isEntrypoint ? serverOptions?.port : undefined) ?? true,
277
+ (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
278
+ )
279
+
280
+ if (this.#nextVersion.major === 14 && this.#nextVersion.minor < 2) {
281
+ await nextStart({
282
+ '--hostname': serverOptions.host,
283
+ '--port': serverOptions.port,
284
+ _: [this.root]
285
+ })
286
+ } else {
287
+ await nextStart(serverOptions, this.root)
288
+ }
289
+
290
+ this.#server = await serverPromise
291
+ this.url = getServerUrl(this.#server)
292
+ } finally {
293
+ await this.childManager.eject()
294
+ }
295
+ }
296
+
297
+ #getChildManagerScripts () {
298
+ const scripts = []
299
+
300
+ if (this.#nextVersion.major === 15) {
301
+ scripts.push(new URL('./loader-next-15.cjs', import.meta.url))
302
+ }
303
+
304
+ return scripts
305
+ }
306
+
307
+ // In development mode, Next.js starts the dev server using child_process.fork with stdio set to 'inherit'.
308
+ // In order to capture the output, we need to ensure that the streams are pipeable and thus we perform a one-time
309
+ // monkey-patch of the ChildProcess.prototype.spawn method to override stdio[1] and stdio[2] to 'pipe'.
310
+ #ensurePipeableStreamsInFork () {
311
+ const originalSpawn = ChildProcess.prototype.spawn
312
+
313
+ // IMPORTANT: If Next.js code changes this might not work anymore. When this gives error, dig into Next.js code
314
+ // to evaluate the new path and/or if this is still necessary.
315
+ const startServerPath = resolvePath(this.#next, './dist/server/lib/start-server.js')
316
+
317
+ ChildProcess.prototype.spawn = function (options) {
318
+ if (options.args?.[1] === startServerPath) {
319
+ options.stdio[1] = 'pipe'
320
+ options.stdio[2] = 'pipe'
321
+
322
+ // Uninstall the patch
323
+ ChildProcess.prototype.spawn = originalSpawn
324
+ }
325
+
326
+ return originalSpawn.call(this, options)
327
+ }
328
+ }
329
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/next",
3
- "version": "2.72.0",
3
+ "version": "3.0.0-alpha.1",
4
4
  "description": "Platformatic Next.js Stackable",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -23,9 +23,8 @@
23
23
  "iovalkey": "^0.3.0",
24
24
  "msgpackr": "^1.11.2",
25
25
  "semver": "^7.6.3",
26
- "@platformatic/basic": "2.72.0",
27
- "@platformatic/config": "2.72.0",
28
- "@platformatic/utils": "2.72.0"
26
+ "@platformatic/basic": "3.0.0-alpha.1",
27
+ "@platformatic/utils": "3.0.0-alpha.1"
29
28
  },
30
29
  "devDependencies": {
31
30
  "@fastify/reply-from": "^12.0.0",
@@ -39,12 +38,12 @@
39
38
  "next": "^15.0.0",
40
39
  "typescript": "^5.5.4",
41
40
  "ws": "^8.18.0",
42
- "@platformatic/composer": "2.72.0",
43
- "@platformatic/service": "2.72.0"
41
+ "@platformatic/composer": "3.0.0-alpha.1",
42
+ "@platformatic/service": "3.0.0-alpha.1"
44
43
  },
45
44
  "scripts": {
46
- "test": "npm run lint && borp --concurrency=1 --no-timeout",
47
- "coverage": "npm run lint && borp -C -X test -X test/fixtures --concurrency=1 --no-timeout",
45
+ "test": "npm run lint && borp --concurrency=1 --timeout 1200000",
46
+ "coverage": "npm run lint && borp -C -X test -X test/fixtures --concurrency=1 --timeout 1200000",
48
47
  "gen-schema": "node lib/schema.js > schema.json",
49
48
  "gen-types": "json2ts > config.d.ts < schema.json",
50
49
  "build": "pnpm run gen-schema && pnpm run gen-types",
package/schema.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/next/2.72.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/next/3.0.0-alpha.1.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
- "title": "Platformatic Next.js Stackable",
4
+ "title": "Platformatic Next.js Config",
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "$schema": {