@platformatic/next 3.53.0 → 3.54.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
@@ -517,7 +517,7 @@ export interface PlatformaticNextJsConfig {
517
517
  /**
518
518
  * Enable the OpenTelemetry diagnostic logger. Diagnostic messages are forwarded to the Platformatic global logger using the current logger level.
519
519
  */
520
- diagLogger?: boolean;
520
+ diagLogger?: boolean | string;
521
521
  };
522
522
  verticalScaler?: {
523
523
  enabled?: boolean;
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.54.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/foundation": "3.54.0",
29
+ "@platformatic/basic": "3.54.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.54.0",
47
+ "@platformatic/service": "3.54.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.54.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Next.js Config",
5
5
  "type": "object",
@@ -1918,7 +1918,14 @@
1918
1918
  ]
1919
1919
  },
1920
1920
  "diagLogger": {
1921
- "type": "boolean",
1921
+ "anyOf": [
1922
+ {
1923
+ "type": "boolean"
1924
+ },
1925
+ {
1926
+ "type": "string"
1927
+ }
1928
+ ],
1922
1929
  "description": "Enable the OpenTelemetry diagnostic logger. Diagnostic messages are forwarded to the Platformatic global logger using the current logger level."
1923
1930
  }
1924
1931
  },