@platformatic/next 3.53.0 → 3.55.0

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
@@ -259,6 +259,8 @@ export interface PlatformaticNextJsConfig {
259
259
  maxHeapTotal?: number | string;
260
260
  maxYoungGeneration?: number | string;
261
261
  codeRangeSize?: number | string;
262
+ bufferPoolSize?: number | string;
263
+ defaultHighWaterMark?: number | string;
262
264
  };
263
265
  undici?: {
264
266
  agentOptions?: {
@@ -361,6 +363,10 @@ export interface PlatformaticNextJsConfig {
361
363
  * The label name to use for the application identifier in metrics (e.g., applicationId, serviceId)
362
364
  */
363
365
  applicationLabel?: string;
366
+ /**
367
+ * Enable outgoing HTTP client request duration metrics
368
+ */
369
+ httpClientMetrics?: boolean | string;
364
370
  readiness?:
365
371
  | boolean
366
372
  | {
@@ -517,7 +523,7 @@ export interface PlatformaticNextJsConfig {
517
523
  /**
518
524
  * Enable the OpenTelemetry diagnostic logger. Diagnostic messages are forwarded to the Platformatic global logger using the current logger level.
519
525
  */
520
- diagLogger?: boolean;
526
+ diagLogger?: boolean | string;
521
527
  };
522
528
  verticalScaler?: {
523
529
  enabled?: boolean;
@@ -619,6 +625,8 @@ export interface PlatformaticNextJsConfig {
619
625
  maxHeapTotal?: number | string;
620
626
  maxYoungGeneration?: number | string;
621
627
  codeRangeSize?: number | string;
628
+ bufferPoolSize?: number | string;
629
+ defaultHighWaterMark?: number | string;
622
630
  };
623
631
  arguments?: string[];
624
632
  env?: {
package/index.js CHANGED
@@ -1,21 +1,8 @@
1
- import { transform as basicTransform, resolve, validationOptions } from '@platformatic/basic'
2
- import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
1
+ import { kMetadata } from '@platformatic/foundation'
3
2
  import { resolve as resolvePath } from 'node:path'
4
3
  import { getCacheHandlerPath, NextCapability } from './lib/capability.js'
4
+ import { loadConfiguration } from './lib/config.js'
5
5
  import { NextImageOptimizerCapability } from './lib/image-optimizer.js'
6
- import { schema } from './lib/schema.js'
7
-
8
- /* c8 ignore next 9 */
9
- export async function transform (config, schema, options) {
10
- config = await basicTransform(config, schema, options)
11
- config.watch = { enabled: false }
12
-
13
- if (config.cache?.adapter === 'redis') {
14
- config.cache.adapter = 'valkey'
15
- }
16
-
17
- return config
18
- }
19
6
 
20
7
  export function getAdapterPath () {
21
8
  return resolvePath(import.meta.dirname, 'lib', 'adapter.js')
@@ -99,18 +86,6 @@ export async function enhanceNextConfig (nextConfig, ...args) {
99
86
  return nextConfig
100
87
  }
101
88
 
102
- export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
103
- const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'application')
104
-
105
- return utilsLoadConfiguration(source, context?.schema ?? schema, {
106
- validationOptions,
107
- transform,
108
- replaceEnv: true,
109
- root,
110
- ...context
111
- })
112
- }
113
-
114
89
  export async function create (configOrRoot, sourceOrConfig, context) {
115
90
  const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
116
91
 
@@ -119,6 +94,8 @@ export async function create (configOrRoot, sourceOrConfig, context) {
119
94
  }
120
95
 
121
96
  export * from './lib/capability.js'
97
+ export * from './lib/commands/index.js'
98
+ export * from './lib/config.js'
122
99
  export * as errors from './lib/errors.js'
123
100
  export * from './lib/image-optimizer.js'
124
101
  export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
package/lib/capability.js CHANGED
@@ -11,14 +11,12 @@ import {
11
11
  } from '@platformatic/basic'
12
12
  import { ChildProcess } from 'node:child_process'
13
13
  import { once } from 'node:events'
14
- import { existsSync } from 'node:fs'
15
- import { glob, readFile, writeFile } from 'node:fs/promises'
16
- import { createRequire } from 'node:module'
14
+ import { readFile, writeFile } from 'node:fs/promises'
17
15
  import { dirname, resolve as resolvePath, sep } from 'node:path'
18
16
  import { fileURLToPath } from 'node:url'
19
17
  import { parse, satisfies } from 'semver'
20
- import * as errors from './errors.js'
21
18
  import { version } from './schema.js'
19
+ import { parseStandaloneNextConfig, requireStandaloneStartServer, resolveStandaloneEntrypoint } from './standalone.js'
22
20
 
23
21
  export const supportedVersions = ['^14.0.0', '^15.0.0', '^16.0.0']
24
22
 
@@ -53,7 +51,7 @@ export class NextCapability extends BaseCapability {
53
51
  process.env.NEXT_IGNORE_INCORRECT_LOCKFILE = 'true'
54
52
 
55
53
  if (!building && this.isProduction && this.config.next?.standalone) {
56
- this.#standaloneEntrypoint = await this.#resolveStandaloneEntrypoint()
54
+ this.#standaloneEntrypoint = await resolveStandaloneEntrypoint(this.root)
57
55
  this.#next = resolvePath(dirname(await resolvePackageViaCJS(this.#standaloneEntrypoint, 'next')), '../..')
58
56
  } else {
59
57
  this.#next = resolvePath(dirname(await resolvePackageViaCJS(this.root, 'next')), '../..')
@@ -382,14 +380,7 @@ export class NextCapability extends BaseCapability {
382
380
 
383
381
  // Parse the server.js to extract the nextConfig.
384
382
  // For now we use simple regex parsing, if it breaks, we can switch to proper AST parsing.
385
- let nextConfig
386
- try {
387
- const serverJsContent = await readFile(this.#standaloneEntrypoint, 'utf-8')
388
- const nextConfigMatch = serverJsContent.match(/(?:const|let)\s*nextConfig\s*=\s*(\{.+)/)
389
- nextConfig = JSON.parse(nextConfigMatch[1])
390
- } catch (e) {
391
- throw new errors.CannotParseStandaloneServer({ cause: e })
392
- }
383
+ const nextConfig = await parseStandaloneNextConfig(this.#standaloneEntrypoint)
393
384
 
394
385
  // Fix cache handlers path
395
386
  if (nextConfig.env?.PLT_NEXT_MODIFICATIONS) {
@@ -430,7 +421,7 @@ export class NextCapability extends BaseCapability {
430
421
 
431
422
  // This is needed by Next.js standalone server to pick up the correct configuration
432
423
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
433
- const { startServer } = this.#requireStandaloneEntrypoint(this.#standaloneEntrypoint)
424
+ const { startServer } = requireStandaloneStartServer(this.#standaloneEntrypoint)
434
425
 
435
426
  await startServer({
436
427
  dir: dirname(this.#standaloneEntrypoint),
@@ -529,45 +520,4 @@ export class NextCapability extends BaseCapability {
529
520
 
530
521
  return distDir
531
522
  }
532
-
533
- async #resolveStandaloneEntrypoint () {
534
- // If built in standalone mode, the generated standalone directory is not on the root of the project but somewhere
535
- // inside .next/standalone due to turbopack limitations in determining the root of the project.
536
- // In that case we search a server.js next to a .next folder inside the .next /standalone folder.
537
- const serverEntrypoints = await Array.fromAsync(
538
- glob(['**/server.js'], { cwd: this.root, ignore: ['node_modules', '**/node_modules/**'] })
539
- )
540
-
541
- let serverEntrypoint
542
- for (const entrypoint of serverEntrypoints) {
543
- if (existsSync(resolvePath(this.root, dirname(entrypoint), '.next'))) {
544
- const candidate = resolvePath(this.root, entrypoint)
545
- const contents = await readFile(candidate, 'utf-8')
546
-
547
- if (contents.includes('process.env.__NEXT_PRIVATE_STANDALONE_CONFIG =')) {
548
- serverEntrypoint = candidate
549
- break
550
- }
551
- }
552
- }
553
-
554
- if (!serverEntrypoint) {
555
- throw new errors.StandaloneServerNotFound()
556
- }
557
-
558
- return serverEntrypoint
559
- }
560
-
561
- #requireStandaloneEntrypoint (serverEntrypoint) {
562
- let serverModule
563
-
564
- try {
565
- serverModule = createRequire(serverEntrypoint)('next/dist/server/lib/start-server.js')
566
- } catch (e) {
567
- // Fallback to bundled capability
568
- serverModule = createRequire(import.meta.file)('next/dist/server/lib/start-server.js')
569
- }
570
-
571
- return serverModule.default ?? serverModule
572
- }
573
523
  }
@@ -0,0 +1,26 @@
1
+ import { packCommand, helpFooter as packHelpFooter } from './pack.js'
2
+
3
+ export function createCommands (id) {
4
+ return {
5
+ commands: {
6
+ [`${id}:pack`]: packCommand
7
+ },
8
+ help: {
9
+ [`${id}:pack`]: {
10
+ usage: `${id}:pack`,
11
+ description: 'Create a self-contained standalone bundle for a Next.js application',
12
+ footer: packHelpFooter,
13
+ options: [
14
+ {
15
+ usage: '-o, --output <path>',
16
+ description: 'Output directory for the packed bundle'
17
+ },
18
+ {
19
+ usage: '--no-build',
20
+ description: 'Fail if the standalone build output is missing instead of building it first'
21
+ }
22
+ ]
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,121 @@
1
+ import { ensureLoggableError, findRuntimeConfigurationFile, kMetadata, logFatalError } from '@platformatic/foundation'
2
+ import { dirname } from 'node:path'
3
+ import {
4
+ copyStandaloneFiles,
5
+ ensurePortableNodeModules,
6
+ materializeRuntimeDependencies,
7
+ prepareBundleDirectory,
8
+ resolveOutputDirectory,
9
+ writeBundleConfiguration,
10
+ writeBundleMetadata
11
+ } from '../pack.js'
12
+ import { NextCapability } from '../capability.js'
13
+ import { loadConfiguration } from '../config.js'
14
+ import { resolveStandaloneEntrypoint } from '../standalone.js'
15
+
16
+ export async function packCommand (logger, configFile, args, context) {
17
+ const {
18
+ values: { output, 'no-build': noBuild },
19
+ positionals
20
+ } = context.parseArgs(
21
+ args,
22
+ {
23
+ output: {
24
+ type: 'string',
25
+ short: 'o'
26
+ },
27
+ 'no-build': {
28
+ type: 'boolean'
29
+ }
30
+ },
31
+ false
32
+ )
33
+
34
+ if (positionals.length > 0) {
35
+ return logFatalError(logger, `Unexpected positional arguments: ${positionals.join(' ')}`)
36
+ }
37
+
38
+ let config
39
+ try {
40
+ config = await loadConfiguration(configFile)
41
+ } catch (error) {
42
+ return logFatalError(
43
+ logger,
44
+ { err: ensureLoggableError(error) },
45
+ `Cannot load the Next.js application configuration: ${error.message}`
46
+ )
47
+ }
48
+
49
+ if (config.next?.standalone !== true) {
50
+ return logFatalError(logger, 'The Next.js pack command requires next.standalone to be set to true.')
51
+ }
52
+
53
+ const applicationRoot = config[kMetadata].root
54
+ const applicationId = context.application?.id ?? 'main'
55
+ const invocationCwd = context.cwd ?? process.cwd()
56
+ const bundleRoot = resolveOutputDirectory(output, `.platformatic/${applicationId}-bundle`, invocationCwd)
57
+
58
+ let standaloneEntrypoint
59
+ try {
60
+ standaloneEntrypoint = await resolveStandaloneEntrypoint(applicationRoot)
61
+ } catch (error) {
62
+ if (noBuild) {
63
+ return logFatalError(logger, 'Cannot find a Next standalone build. Run the build first or omit --no-build.')
64
+ }
65
+
66
+ const capability = new NextCapability(applicationRoot, config, {
67
+ applicationId,
68
+ isProduction: true
69
+ })
70
+
71
+ try {
72
+ logger.info(`Building application ${applicationId} before packing ...`)
73
+ await capability.build()
74
+ standaloneEntrypoint = await resolveStandaloneEntrypoint(applicationRoot)
75
+ } catch (buildError) {
76
+ return logFatalError(
77
+ logger,
78
+ { err: ensureLoggableError(buildError) },
79
+ `Cannot build the Next.js application for packing: ${buildError.message}`
80
+ )
81
+ }
82
+ }
83
+
84
+ const standaloneRoot = dirname(standaloneEntrypoint)
85
+
86
+ try {
87
+ const runtimeConfig = await findRuntimeConfigurationFile(logger, invocationCwd, null, false, false, false)
88
+
89
+ await prepareBundleDirectory(bundleRoot)
90
+ await copyStandaloneFiles({ applicationRoot, standaloneRoot, bundleRoot })
91
+ await writeBundleConfiguration(config, bundleRoot)
92
+ const packages = await materializeRuntimeDependencies({ bundleRoot, logger })
93
+ await writeBundleMetadata({
94
+ applicationId,
95
+ applicationConfig: configFile,
96
+ runtimeConfig,
97
+ bundleRoot,
98
+ packages
99
+ })
100
+ await ensurePortableNodeModules(bundleRoot)
101
+ } catch (error) {
102
+ return logFatalError(
103
+ logger,
104
+ { err: ensureLoggableError(error) },
105
+ `Cannot pack the Next.js application: ${error.message}`
106
+ )
107
+ }
108
+
109
+ logger.done(`Packed application ${applicationId} into ${bundleRoot}.`)
110
+ }
111
+
112
+ export const helpFooter = `
113
+ This command only supports Next.js applications configured with:
114
+
115
+ - Next.js standalone output enabled in next.config.js
116
+ - next.standalone = true in the Platformatic application config
117
+
118
+ The packed output is intended to be started with:
119
+
120
+ - ./node_modules/.bin/wattpm start
121
+ `
package/lib/config.js ADDED
@@ -0,0 +1,26 @@
1
+ import { transform as basicTransform, resolve, validationOptions } from '@platformatic/basic'
2
+ import { loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
3
+ import { schema } from './schema.js'
4
+
5
+ export async function transform (config, schema, options) {
6
+ config = await basicTransform(config, schema, options)
7
+ config.watch = { enabled: false }
8
+
9
+ if (config.cache?.adapter === 'redis') {
10
+ config.cache.adapter = 'valkey'
11
+ }
12
+
13
+ return config
14
+ }
15
+
16
+ export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
17
+ const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'application')
18
+
19
+ return utilsLoadConfiguration(source, context?.schema ?? schema, {
20
+ validationOptions,
21
+ transform,
22
+ replaceEnv: true,
23
+ root,
24
+ ...context
25
+ })
26
+ }
package/lib/pack.js ADDED
@@ -0,0 +1,467 @@
1
+ import { saveConfigurationFile } from '@platformatic/foundation'
2
+ import { spawn } from 'node:child_process'
3
+ import { existsSync } from 'node:fs'
4
+ import {
5
+ chmod,
6
+ cp,
7
+ lstat,
8
+ mkdir,
9
+ mkdtemp,
10
+ readFile,
11
+ readdir,
12
+ readlink,
13
+ realpath,
14
+ rm,
15
+ symlink,
16
+ writeFile
17
+ } from 'node:fs/promises'
18
+ import { createRequire } from 'node:module'
19
+ import { tmpdir } from 'node:os'
20
+ import { dirname, isAbsolute, relative, resolve as resolvePath } from 'node:path'
21
+ import { version as platformaticVersion } from './schema.js'
22
+
23
+ const runtimePackages = ['wattpm', '@platformatic/next']
24
+
25
+ export function resolveOutputDirectory (output, fallback, cwd) {
26
+ const target = output ?? fallback
27
+ return isAbsolute(target) ? target : resolvePath(cwd, target)
28
+ }
29
+
30
+ export async function prepareBundleDirectory (directory) {
31
+ await rm(directory, { recursive: true, force: true })
32
+ await mkdir(directory, { recursive: true })
33
+ }
34
+
35
+ export async function copyStandaloneFiles ({ applicationRoot, standaloneRoot, bundleRoot }) {
36
+ await cp(standaloneRoot, bundleRoot, {
37
+ recursive: true,
38
+ dereference: true,
39
+ preserveTimestamps: true
40
+ })
41
+
42
+ try {
43
+ await cp(resolvePath(applicationRoot, '.next', 'static'), resolvePath(bundleRoot, '.next', 'static'), {
44
+ recursive: true,
45
+ force: true,
46
+ errorOnExist: false,
47
+ preserveTimestamps: true
48
+ })
49
+ } catch (error) {
50
+ if (error.code !== 'ENOENT') {
51
+ throw error
52
+ }
53
+ }
54
+
55
+ try {
56
+ await cp(resolvePath(applicationRoot, 'public'), resolvePath(bundleRoot, 'public'), {
57
+ recursive: true,
58
+ force: true,
59
+ errorOnExist: false,
60
+ preserveTimestamps: true
61
+ })
62
+ } catch (error) {
63
+ if (error.code !== 'ENOENT') {
64
+ throw error
65
+ }
66
+ }
67
+ }
68
+
69
+ export async function writeBundleConfiguration (config, bundleRoot) {
70
+ const bundleConfig = JSON.parse(JSON.stringify(config))
71
+ bundleConfig.next ??= {}
72
+ bundleConfig.next.standalone = true
73
+ await saveConfigurationFile(resolvePath(bundleRoot, 'watt.json'), bundleConfig)
74
+ }
75
+
76
+ export async function materializeRuntimeDependencies ({ bundleRoot, logger }) {
77
+ const bundleNodeModules = resolvePath(bundleRoot, 'node_modules')
78
+ const stagingRoot = await mkdtemp(resolvePath(tmpdir(), 'platformatic-next-pack-'))
79
+
80
+ try {
81
+ await mkdir(bundleNodeModules, { recursive: true })
82
+
83
+ const workspacePackages = await collectWorkspacePackages(runtimePackages)
84
+ const tarballDirectory = resolvePath(stagingRoot, 'tarballs')
85
+ const dependencies = {}
86
+ const packages = new Map()
87
+
88
+ await mkdir(tarballDirectory, { recursive: true })
89
+
90
+ for (const name of runtimePackages) {
91
+ const pkg = workspacePackages.get(name) ?? (await loadInstalledPackage(name, import.meta.url))
92
+ packages.set(pkg.name, pkg)
93
+ }
94
+
95
+ for (const pkg of workspacePackages.values()) {
96
+ const tarball = await packWorkspacePackage(pkg, tarballDirectory)
97
+ dependencies[pkg.name] = `file:${relative(stagingRoot, tarball)}`
98
+ }
99
+
100
+ for (const pkg of packages.values()) {
101
+ dependencies[pkg.name] ??= pkg.version
102
+ }
103
+
104
+ await writeFile(
105
+ resolvePath(stagingRoot, 'package.json'),
106
+ JSON.stringify({ name: 'platformatic-next-pack', private: true, dependencies }, null, 2),
107
+ 'utf-8'
108
+ )
109
+
110
+ await runCommand('npm', ['install', '--omit=dev', '--ignore-scripts', '--no-fund', '--no-audit'], { cwd: stagingRoot })
111
+ await mergeDirectory(resolvePath(stagingRoot, 'node_modules'), bundleNodeModules)
112
+ await cleanupBrokenSymlinks(resolvePath(bundleNodeModules, '.bin'))
113
+ await writePackageBins(packages, bundleNodeModules)
114
+
115
+ logger?.debug?.({ packages: [...packages.keys()] }, 'Materialized runtime packages for packed bundle.')
116
+
117
+ return packages
118
+ } finally {
119
+ await rm(stagingRoot, { recursive: true, force: true })
120
+ }
121
+ }
122
+
123
+ async function collectWorkspacePackages (rootNames) {
124
+ const packages = new Map()
125
+ const queue = [...rootNames]
126
+
127
+ while (queue.length) {
128
+ const name = queue.shift()
129
+ if (packages.has(name)) {
130
+ continue
131
+ }
132
+
133
+ const root = resolveWorkspacePackageRoot(name)
134
+ if (!root) {
135
+ continue
136
+ }
137
+
138
+ const pkg = await loadPackageFromRoot(root, name)
139
+ packages.set(pkg.name, pkg)
140
+
141
+ for (const dependencyName of listWorkspaceDependencies(pkg.packageJson)) {
142
+ queue.push(dependencyName)
143
+ }
144
+ }
145
+
146
+ return packages
147
+ }
148
+
149
+ function listWorkspaceDependencies (packageJson) {
150
+ return Object.keys({
151
+ ...(packageJson.dependencies ?? {}),
152
+ ...(packageJson.optionalDependencies ?? {})
153
+ }).filter(name => !!resolveWorkspacePackageRoot(name))
154
+ }
155
+
156
+ async function loadInstalledPackage (name, from) {
157
+ const root = await realpath(await resolvePackageRoot(name, from))
158
+ return loadPackageFromRoot(root, name)
159
+ }
160
+
161
+ async function loadPackageFromRoot (root, fallbackName) {
162
+ const packageJson = JSON.parse(await readFile(resolvePath(root, 'package.json'), 'utf-8'))
163
+ return {
164
+ name: packageJson.name ?? fallbackName,
165
+ version: packageJson.version,
166
+ root,
167
+ packageJson
168
+ }
169
+ }
170
+
171
+ async function resolvePackageRoot (name, from) {
172
+ const require = createRequire(from)
173
+
174
+ try {
175
+ const packageJsonPath = require.resolve(`${name}/package.json`)
176
+ return dirname(packageJsonPath)
177
+ } catch (error) {
178
+ const packageJsonPath = resolvePackageJsonFromSearchPaths(require, name)
179
+ if (packageJsonPath) {
180
+ return dirname(packageJsonPath)
181
+ }
182
+
183
+ try {
184
+ const resolvedEntry = require.resolve(name)
185
+ return await findPackageRoot(dirname(resolvedEntry))
186
+ } catch (resolveError) {
187
+ const workspacePackage = resolveWorkspacePackageRoot(name)
188
+ if (workspacePackage) {
189
+ return workspacePackage
190
+ }
191
+
192
+ throw resolveError
193
+ }
194
+ }
195
+ }
196
+
197
+ function resolvePackageJsonFromSearchPaths (require, name) {
198
+ const searchPaths = require.resolve.paths(name) ?? []
199
+ const packageSegments = name.split('/')
200
+
201
+ for (const searchPath of searchPaths) {
202
+ const candidate = resolvePath(searchPath, ...packageSegments, 'package.json')
203
+ if (existsSync(candidate)) {
204
+ return candidate
205
+ }
206
+ }
207
+
208
+ return null
209
+ }
210
+
211
+ async function findPackageRoot (directory) {
212
+ let current = directory
213
+
214
+ while (true) {
215
+ const candidate = resolvePath(current, 'package.json')
216
+ if (existsSync(candidate)) {
217
+ return current
218
+ }
219
+
220
+ const parent = resolvePath(current, '..')
221
+ if (parent === current) {
222
+ throw new Error(`Cannot determine package root for ${directory}.`)
223
+ }
224
+
225
+ current = parent
226
+ }
227
+ }
228
+
229
+ function resolveWorkspacePackageRoot (name) {
230
+ if (name !== 'wattpm' && !name.startsWith('@platformatic/')) {
231
+ return null
232
+ }
233
+
234
+ const packageDirectory = name === 'wattpm' ? 'wattpm' : name.replace('@platformatic/', '')
235
+ const candidate = resolvePath(import.meta.dirname, '..', '..', packageDirectory)
236
+
237
+ if (existsSync(resolvePath(candidate, 'package.json'))) {
238
+ return candidate
239
+ }
240
+
241
+ return null
242
+ }
243
+
244
+ async function packWorkspacePackage (pkg, tarballDirectory) {
245
+ const { stdout } = await runCommand('pnpm', ['pack', '--pack-destination', tarballDirectory], { cwd: pkg.root })
246
+ const tarball = stdout
247
+ .split('\n')
248
+ .map(line => line.trim())
249
+ .filter(line => line.endsWith('.tgz'))
250
+ .at(-1)
251
+
252
+ if (!tarball) {
253
+ throw new Error(`Cannot determine the tarball path for ${pkg.name}.`)
254
+ }
255
+
256
+ return tarball
257
+ }
258
+
259
+ async function runCommand (command, args, options) {
260
+ return await new Promise((resolve, reject) => {
261
+ const child = spawn(command, args, {
262
+ cwd: options.cwd,
263
+ env: process.env,
264
+ stdio: ['ignore', 'pipe', 'pipe']
265
+ })
266
+
267
+ let stdout = ''
268
+ let stderr = ''
269
+
270
+ child.stdout.on('data', chunk => {
271
+ stdout += chunk.toString()
272
+ })
273
+
274
+ child.stderr.on('data', chunk => {
275
+ stderr += chunk.toString()
276
+ })
277
+
278
+ child.on('error', reject)
279
+ child.on('close', code => {
280
+ if (code === 0) {
281
+ resolve({ stdout, stderr })
282
+ } else {
283
+ reject(new Error(`Command failed: ${command} ${args.join(' ')}\n${stderr || stdout}`))
284
+ }
285
+ })
286
+ })
287
+ }
288
+
289
+ async function mergeDirectory (source, destination) {
290
+ await mkdir(destination, { recursive: true })
291
+ const entries = await readdir(source, { withFileTypes: true })
292
+
293
+ for (const entry of entries) {
294
+ const sourcePath = resolvePath(source, entry.name)
295
+ const destinationPath = resolvePath(destination, entry.name)
296
+ const stats = await lstat(sourcePath)
297
+
298
+ if (stats.isSymbolicLink()) {
299
+ await copySymlink(sourcePath, destinationPath)
300
+ continue
301
+ }
302
+
303
+ if (entry.isDirectory()) {
304
+ if (!existsSync(destinationPath)) {
305
+ await cp(sourcePath, destinationPath, {
306
+ recursive: true,
307
+ dereference: false,
308
+ preserveTimestamps: true
309
+ })
310
+ } else {
311
+ await mergeDirectory(sourcePath, destinationPath)
312
+ }
313
+ continue
314
+ }
315
+
316
+ if (!existsSync(destinationPath)) {
317
+ await cp(sourcePath, destinationPath, { dereference: false, preserveTimestamps: true })
318
+ }
319
+ }
320
+ }
321
+
322
+ async function copySymlink (source, destination) {
323
+ if (existsSync(destination)) {
324
+ return
325
+ }
326
+
327
+ await mkdir(dirname(destination), { recursive: true })
328
+ await symlink(await readlink(source), destination)
329
+ }
330
+
331
+ async function cleanupBrokenSymlinks (directory) {
332
+ if (!existsSync(directory)) {
333
+ return
334
+ }
335
+
336
+ const entries = await readdir(directory, { withFileTypes: true })
337
+
338
+ for (const entry of entries) {
339
+ const path = resolvePath(directory, entry.name)
340
+ const stats = await lstat(path)
341
+
342
+ if (!stats.isSymbolicLink()) {
343
+ continue
344
+ }
345
+
346
+ try {
347
+ await realpath(path)
348
+ } catch (error) {
349
+ if (error.code === 'ENOENT') {
350
+ await rm(path, { force: true })
351
+ continue
352
+ }
353
+
354
+ throw error
355
+ }
356
+ }
357
+ }
358
+
359
+ async function writePackageBins (packages, bundleNodeModules) {
360
+ const binDirectory = resolvePath(bundleNodeModules, '.bin')
361
+ await mkdir(binDirectory, { recursive: true })
362
+
363
+ for (const pkg of packages.values()) {
364
+ const bin = normalizeBinField(pkg.packageJson.bin, pkg.name)
365
+ for (const [name, target] of Object.entries(bin)) {
366
+ await writeNodeShim(resolvePath(binDirectory, name), pkg.name, target)
367
+ }
368
+ }
369
+ }
370
+
371
+ function normalizeBinField (bin, pkgName) {
372
+ if (!bin) {
373
+ return {}
374
+ }
375
+
376
+ if (typeof bin === 'string') {
377
+ return { [pkgName]: bin }
378
+ }
379
+
380
+ return bin
381
+ }
382
+
383
+ async function writeNodeShim (destination, packageName, target) {
384
+ const normalizedTarget = target.replaceAll('\\', '/')
385
+ const unixScript = `#!/usr/bin/env node\nimport '../${packageName}/${normalizedTarget}'\n`
386
+
387
+ await rm(destination, { force: true })
388
+ await rm(`${destination}.cmd`, { force: true })
389
+ await writeFile(destination, unixScript, 'utf-8')
390
+ await chmod(destination, 0o755)
391
+ await writeFile(
392
+ `${destination}.cmd`,
393
+ `@ECHO off\r\nnode "%~dp0\\..\\${packageName.replaceAll('/', '\\')}\\${normalizedTarget.replaceAll('/', '\\')}" %*\r\n`,
394
+ 'utf-8'
395
+ )
396
+ }
397
+
398
+ export async function writeBundleMetadata ({
399
+ applicationId,
400
+ applicationConfig,
401
+ runtimeConfig,
402
+ bundleRoot,
403
+ packages
404
+ }) {
405
+ const metadataRoot = resolvePath(bundleRoot, '.platformatic')
406
+ await mkdir(metadataRoot, { recursive: true })
407
+
408
+ await writeFile(
409
+ resolvePath(metadataRoot, 'manifest.json'),
410
+ JSON.stringify(
411
+ {
412
+ bundleVersion: 1,
413
+ applicationId,
414
+ packedAt: new Date().toISOString(),
415
+ source: {
416
+ applicationConfig,
417
+ runtimeConfig: runtimeConfig ?? null
418
+ },
419
+ platformaticVersion,
420
+ packages: Object.fromEntries(
421
+ [...packages.values()].map(pkg => [pkg.name, { version: pkg.version, root: pkg.root }])
422
+ )
423
+ },
424
+ null,
425
+ 2
426
+ ) + '\n',
427
+ 'utf-8'
428
+ )
429
+
430
+ await writeFile(
431
+ resolvePath(metadataRoot, 'NOTICE'),
432
+ 'This directory was generated by the Next standalone pack command.\n',
433
+ 'utf-8'
434
+ )
435
+ }
436
+
437
+ export async function ensurePortableNodeModules (bundleRoot) {
438
+ const nodeModules = resolvePath(bundleRoot, 'node_modules')
439
+ await assertNoExternalSymlinks(nodeModules, bundleRoot)
440
+ }
441
+
442
+ async function assertNoExternalSymlinks (directory, bundleRoot) {
443
+ const entries = await readdir(directory, { withFileTypes: true })
444
+
445
+ for (const entry of entries) {
446
+ const path = resolvePath(directory, entry.name)
447
+ const stats = await lstat(path)
448
+
449
+ if (stats.isSymbolicLink()) {
450
+ let target
451
+ try {
452
+ target = await realpath(path)
453
+ } catch (error) {
454
+ if (error.code === 'ENOENT') {
455
+ continue
456
+ }
457
+ throw error
458
+ }
459
+
460
+ if (!target.startsWith(bundleRoot)) {
461
+ throw new Error(`Packed bundle contains a symlink outside the bundle: ${relative(bundleRoot, path)} -> ${target}`)
462
+ }
463
+ } else if (entry.isDirectory()) {
464
+ await assertNoExternalSymlinks(path, bundleRoot)
465
+ }
466
+ }
467
+ }
@@ -0,0 +1,51 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { glob, readFile } from 'node:fs/promises'
3
+ import { createRequire } from 'node:module'
4
+ import { dirname, resolve as resolvePath } from 'node:path'
5
+ import * as errors from './errors.js'
6
+
7
+ export async function resolveStandaloneEntrypoint (root) {
8
+ const serverEntrypoints = await Array.fromAsync(
9
+ glob(['server.js', '.next/standalone/**/server.js', '**/server.js'], {
10
+ cwd: root,
11
+ exclude: ['node_modules', '**/node_modules/**']
12
+ })
13
+ )
14
+
15
+ for (const entrypoint of serverEntrypoints) {
16
+ if (entrypoint !== 'server.js' && !existsSync(resolvePath(root, dirname(entrypoint), '.next'))) {
17
+ continue
18
+ }
19
+
20
+ const candidate = resolvePath(root, entrypoint)
21
+ const contents = await readFile(candidate, 'utf-8')
22
+
23
+ if (contents.includes('process.env.__NEXT_PRIVATE_STANDALONE_CONFIG =')) {
24
+ return candidate
25
+ }
26
+ }
27
+
28
+ throw new errors.StandaloneServerNotFound()
29
+ }
30
+
31
+ export async function parseStandaloneNextConfig (serverEntrypoint) {
32
+ try {
33
+ const serverJsContent = await readFile(serverEntrypoint, 'utf-8')
34
+ const nextConfigMatch = serverJsContent.match(/(?:const|let)\s*nextConfig\s*=\s*(\{.+)/)
35
+ return JSON.parse(nextConfigMatch[1])
36
+ } catch (e) {
37
+ throw new errors.CannotParseStandaloneServer({ cause: e })
38
+ }
39
+ }
40
+
41
+ export function requireStandaloneStartServer (serverEntrypoint) {
42
+ let serverModule
43
+
44
+ try {
45
+ serverModule = createRequire(serverEntrypoint)('next/dist/server/lib/start-server.js')
46
+ } catch (e) {
47
+ serverModule = createRequire(import.meta.url)('next/dist/server/lib/start-server.js')
48
+ }
49
+
50
+ return serverModule.default ?? serverModule
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/next",
3
- "version": "3.53.0",
3
+ "version": "3.55.0",
4
4
  "description": "Platformatic Next.js Capability",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -25,8 +25,8 @@
25
25
  "light-my-request": "^6.0.0",
26
26
  "msgpackr": "^1.11.2",
27
27
  "semver": "^7.6.3",
28
- "@platformatic/basic": "3.53.0",
29
- "@platformatic/foundation": "3.53.0"
28
+ "@platformatic/basic": "3.55.0",
29
+ "@platformatic/foundation": "3.55.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@fastify/reply-from": "^12.0.0",
@@ -43,8 +43,8 @@
43
43
  "next": "^16.1.0",
44
44
  "typescript": "^5.5.4",
45
45
  "ws": "^8.18.0",
46
- "@platformatic/gateway": "3.53.0",
47
- "@platformatic/service": "3.53.0"
46
+ "@platformatic/gateway": "3.55.0",
47
+ "@platformatic/service": "3.55.0"
48
48
  },
49
49
  "engines": {
50
50
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/next/3.53.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/next/3.55.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Next.js Config",
5
5
  "type": "object",
@@ -606,6 +606,28 @@
606
606
  "type": "string"
607
607
  }
608
608
  ]
609
+ },
610
+ "bufferPoolSize": {
611
+ "anyOf": [
612
+ {
613
+ "type": "number",
614
+ "minimum": 0
615
+ },
616
+ {
617
+ "type": "string"
618
+ }
619
+ ]
620
+ },
621
+ "defaultHighWaterMark": {
622
+ "anyOf": [
623
+ {
624
+ "type": "number",
625
+ "minimum": 0
626
+ },
627
+ {
628
+ "type": "string"
629
+ }
630
+ ]
609
631
  }
610
632
  },
611
633
  "additionalProperties": false
@@ -1315,6 +1337,30 @@
1315
1337
  }
1316
1338
  ],
1317
1339
  "default": 268435456
1340
+ },
1341
+ "bufferPoolSize": {
1342
+ "anyOf": [
1343
+ {
1344
+ "type": "number",
1345
+ "minimum": 0
1346
+ },
1347
+ {
1348
+ "type": "string"
1349
+ }
1350
+ ],
1351
+ "default": 262144
1352
+ },
1353
+ "defaultHighWaterMark": {
1354
+ "anyOf": [
1355
+ {
1356
+ "type": "number",
1357
+ "minimum": 0
1358
+ },
1359
+ {
1360
+ "type": "string"
1361
+ }
1362
+ ],
1363
+ "default": 262144
1318
1364
  }
1319
1365
  },
1320
1366
  "additionalProperties": false
@@ -1597,6 +1643,18 @@
1597
1643
  "default": "applicationId",
1598
1644
  "description": "The label name to use for the application identifier in metrics (e.g., applicationId, serviceId)"
1599
1645
  },
1646
+ "httpClientMetrics": {
1647
+ "anyOf": [
1648
+ {
1649
+ "type": "boolean"
1650
+ },
1651
+ {
1652
+ "type": "string"
1653
+ }
1654
+ ],
1655
+ "default": false,
1656
+ "description": "Enable outgoing HTTP client request duration metrics"
1657
+ },
1600
1658
  "readiness": {
1601
1659
  "anyOf": [
1602
1660
  {
@@ -1918,7 +1976,14 @@
1918
1976
  ]
1919
1977
  },
1920
1978
  "diagLogger": {
1921
- "type": "boolean",
1979
+ "anyOf": [
1980
+ {
1981
+ "type": "boolean"
1982
+ },
1983
+ {
1984
+ "type": "string"
1985
+ }
1986
+ ],
1922
1987
  "description": "Enable the OpenTelemetry diagnostic logger. Diagnostic messages are forwarded to the Platformatic global logger using the current logger level."
1923
1988
  }
1924
1989
  },
@@ -2321,6 +2386,28 @@
2321
2386
  "type": "string"
2322
2387
  }
2323
2388
  ]
2389
+ },
2390
+ "bufferPoolSize": {
2391
+ "anyOf": [
2392
+ {
2393
+ "type": "number",
2394
+ "minimum": 0
2395
+ },
2396
+ {
2397
+ "type": "string"
2398
+ }
2399
+ ]
2400
+ },
2401
+ "defaultHighWaterMark": {
2402
+ "anyOf": [
2403
+ {
2404
+ "type": "number",
2405
+ "minimum": 0
2406
+ },
2407
+ {
2408
+ "type": "string"
2409
+ }
2410
+ ]
2324
2411
  }
2325
2412
  },
2326
2413
  "additionalProperties": false