@platformatic/next 3.38.1 → 3.40.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
@@ -55,6 +55,10 @@ export interface PlatformaticNextJsConfig {
55
55
  customLevels?: {
56
56
  [k: string]: unknown;
57
57
  };
58
+ openTelemetryExporter?: {
59
+ protocol: "grpc" | "http";
60
+ url: string;
61
+ };
58
62
  [k: string]: unknown;
59
63
  };
60
64
  server?: {
@@ -186,6 +190,10 @@ export interface PlatformaticNextJsConfig {
186
190
  customLevels?: {
187
191
  [k: string]: unknown;
188
192
  };
193
+ openTelemetryExporter?: {
194
+ protocol: "grpc" | "http";
195
+ url: string;
196
+ };
189
197
  [k: string]: unknown;
190
198
  };
191
199
  server?: {
@@ -324,6 +332,12 @@ export interface PlatformaticNextJsConfig {
324
332
  */
325
333
  socket?: string;
326
334
  };
335
+ management?:
336
+ | boolean
337
+ | {
338
+ enabled?: boolean;
339
+ operations?: string[];
340
+ };
327
341
  metrics?:
328
342
  | boolean
329
343
  | {
@@ -648,6 +662,32 @@ export interface PlatformaticNextJsConfig {
648
662
  standalone?: boolean;
649
663
  trailingSlash?: boolean;
650
664
  useExperimentalAdapter?: boolean;
665
+ https?: {
666
+ enabled?: boolean | string;
667
+ key?: string;
668
+ cert?: string;
669
+ ca?: string;
670
+ };
671
+ imageOptimizer?: {
672
+ enabled: boolean;
673
+ fallback: string;
674
+ storage?:
675
+ | {
676
+ type: "memory";
677
+ }
678
+ | {
679
+ type: "filesystem";
680
+ path?: string;
681
+ }
682
+ | {
683
+ type: "redis" | "valkey";
684
+ url: string;
685
+ prefix?: string;
686
+ db?: number | string;
687
+ };
688
+ timeout?: number | string;
689
+ maxAttempts?: number | string;
690
+ };
651
691
  };
652
692
  cache?: {
653
693
  enabled?: boolean | string;
package/index.js CHANGED
@@ -2,6 +2,7 @@ import { transform as basicTransform, resolve, validationOptions } from '@platfo
2
2
  import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
3
3
  import { resolve as resolvePath } from 'node:path'
4
4
  import { getCacheHandlerPath, NextCapability } from './lib/capability.js'
5
+ import { NextImageOptimizerCapability } from './lib/image-optimizer.js'
5
6
  import { schema } from './lib/schema.js'
6
7
 
7
8
  /* c8 ignore next 9 */
@@ -103,10 +104,12 @@ export async function loadConfiguration (configOrRoot, sourceOrConfig, context)
103
104
 
104
105
  export async function create (configOrRoot, sourceOrConfig, context) {
105
106
  const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
106
- return new NextCapability(config[kMetadata].root, config, context)
107
+
108
+ const Capability = config.next?.imageOptimizer?.enabled ? NextImageOptimizerCapability : NextCapability
109
+ return new Capability(config[kMetadata].root, config, context)
107
110
  }
108
111
 
109
- export * as cachingValkey from './lib/caching/valkey-isr.js'
110
112
  export * from './lib/capability.js'
111
113
  export * as errors from './lib/errors.js'
114
+ export * from './lib/image-optimizer.js'
112
115
  export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
@@ -1,12 +1,17 @@
1
1
  import { buildPinoFormatters, buildPinoTimestamp } from '@platformatic/foundation'
2
- import { Redis } from 'iovalkey'
3
- import { pack, unpack } from 'msgpackr'
4
2
  import { existsSync, readFileSync } from 'node:fs'
3
+ import { createRequire } from 'node:module'
5
4
  import { hostname } from 'node:os'
6
5
  import { resolve } from 'node:path'
7
6
  import { fileURLToPath } from 'node:url'
8
7
  import { pino } from 'pino'
9
8
 
9
+ // commonjs require is superior because it allows lazy loading
10
+ const require = createRequire(import.meta.url)
11
+
12
+ let Redis
13
+ let msgpackr
14
+
10
15
  globalThis.platformatic ??= {}
11
16
  globalThis.platformatic.valkeyClients = new Map()
12
17
 
@@ -30,6 +35,18 @@ export function keyFor (prefix, subprefix, section, key) {
30
35
  return result
31
36
  }
32
37
 
38
+ export function ensureRedis () {
39
+ if (!Redis) {
40
+ Redis = require('iovalkey').Redis
41
+ }
42
+ }
43
+
44
+ export function ensureMsgpackr () {
45
+ if (!msgpackr) {
46
+ msgpackr = require('msgpackr')
47
+ }
48
+ }
49
+
33
50
  export function getConnection (url) {
34
51
  let client = globalThis.platformatic.valkeyClients.get(url)
35
52
 
@@ -93,9 +110,9 @@ export function getPlatformaticMeta () {
93
110
  }
94
111
 
95
112
  export function serialize (data) {
96
- return pack(data).toString('base64url')
113
+ return msgpackr.pack(data).toString('base64url')
97
114
  }
98
115
 
99
116
  export function deserialize (data) {
100
- return unpack(Buffer.from(data, 'base64url'))
117
+ return msgpackr.unpack(Buffer.from(data, 'base64url'))
101
118
  }
@@ -3,6 +3,8 @@ import { ReadableStream } from 'node:stream/web'
3
3
  import {
4
4
  createPlatformaticLogger,
5
5
  deserialize,
6
+ ensureMsgpackr,
7
+ ensureRedis,
6
8
  getConnection,
7
9
  getPlatformaticMeta,
8
10
  getPlatformaticSubprefix,
@@ -37,6 +39,9 @@ export class CacheHandler {
37
39
  #cacheMissMetric
38
40
 
39
41
  constructor () {
42
+ ensureRedis()
43
+ ensureMsgpackr()
44
+
40
45
  this.#config ??= globalThis.platformatic.config.cache
41
46
  this.#logger ??= createPlatformaticLogger()
42
47
  this.#store ??= getConnection(this.#config.url)
@@ -2,6 +2,8 @@ import { ensureLoggableError } from '@platformatic/foundation'
2
2
  import {
3
3
  createPlatformaticLogger,
4
4
  deserialize,
5
+ ensureMsgpackr,
6
+ ensureRedis,
5
7
  getConnection,
6
8
  getPlatformaticMeta,
7
9
  getPlatformaticSubprefix,
@@ -33,6 +35,9 @@ export class CacheHandler {
33
35
  constructor (options) {
34
36
  options ??= {}
35
37
 
38
+ ensureRedis()
39
+ ensureMsgpackr()
40
+
36
41
  this.#standalone = options.standalone
37
42
  this.#config = options.config
38
43
  this.#logger = options.logger
package/lib/capability.js CHANGED
@@ -20,7 +20,7 @@ import { parse, satisfies } from 'semver'
20
20
  import * as errors from './errors.js'
21
21
  import { version } from './schema.js'
22
22
 
23
- const supportedVersions = ['^14.0.0', '^15.0.0', '^16.0.0']
23
+ export const supportedVersions = ['^14.0.0', '^15.0.0', '^16.0.0']
24
24
 
25
25
  export function getCacheHandlerPath (name) {
26
26
  return fileURLToPath(new URL(`./caching/${name}.js`, import.meta.url)).replaceAll(sep, '/')
@@ -239,21 +239,71 @@ export class NextCapability extends BaseCapability {
239
239
  this.#ensurePipeableStreamsInFork()
240
240
 
241
241
  if (this.#nextVersion.major === 14 && this.#nextVersion.minor < 2) {
242
- await nextDev({
242
+ const devOptions = {
243
243
  '--hostname': serverOptions.host,
244
244
  '--port': serverOptions.port,
245
245
  _: [this.root]
246
- })
246
+ }
247
+
248
+ const httpsOptions = this.config.next?.https ?? {}
249
+
250
+ if (httpsOptions.enabled) {
251
+ devOptions['--experimental-https-key'] = true
252
+
253
+ if (httpsOptions.key) {
254
+ devOptions['--experimental-https-key'] = resolvePath(this.root, httpsOptions.key)
255
+ }
256
+
257
+ if (httpsOptions.cert) {
258
+ devOptions['--experimental-https-cert'] = resolvePath(this.root, httpsOptions.cert)
259
+ }
260
+
261
+ if (httpsOptions.ca) {
262
+ devOptions['--experimental-https-ca'] = resolvePath(this.root, httpsOptions.ca)
263
+ }
264
+ }
265
+
266
+ await nextDev(devOptions)
247
267
  } else {
268
+ const nextConfig = this.config.next ?? {}
269
+ const httpsOptions = nextConfig.https ?? {}
270
+
271
+ if (httpsOptions.enabled) {
272
+ serverOptions.experimentalHttps = true
273
+
274
+ if (httpsOptions.key) {
275
+ serverOptions.experimentalHttpsKey = resolvePath(this.root, httpsOptions.key)
276
+ }
277
+
278
+ if (httpsOptions.cert) {
279
+ serverOptions.experimentalHttpsCert = resolvePath(this.root, httpsOptions.cert)
280
+ }
281
+
282
+ if (httpsOptions.ca) {
283
+ serverOptions.experimentalHttpsCa = resolvePath(this.root, httpsOptions.ca)
284
+ }
285
+ }
286
+
248
287
  await nextDev(serverOptions, 'default', this.root)
249
288
  }
250
289
 
251
290
  this.#child = await childPromise
252
- this.#child.stdout.setEncoding('utf8')
253
- this.#child.stderr.setEncoding('utf8')
254
291
 
255
- this.#child.stdout.pipe(process.stdout, { end: false })
256
- this.#child.stderr.pipe(process.stderr, { end: false })
292
+ // Paolo: I couldn't really reproduce this, but in some environments our child_process patch
293
+ // might not work and thus the stdio streams are not pipeable.
294
+ // Rathen than throwing an error in that case, we log a warning and continue without redirecting the output,
295
+ // as the worst thing is that the user will lose our formatted output.
296
+ //
297
+ // This should be fixable once https://github.com/nodejs/node/pull/61836 lands and it is broadly available.
298
+ if (typeof this.#child.stdout?.pipe === 'function' && typeof this.#child.stderr?.pipe === 'function') {
299
+ this.#child.stdout.setEncoding('utf8')
300
+ this.#child.stderr.setEncoding('utf8')
301
+
302
+ this.#child.stdout.pipe(process.stdout, { end: false })
303
+ this.#child.stderr.pipe(process.stderr, { end: false })
304
+ } else {
305
+ this.logger.warn('Unable to redirect Next.js development server output to the main process')
306
+ }
257
307
  } finally {
258
308
  await this.childManager.eject()
259
309
  }
@@ -493,7 +543,8 @@ export class NextCapability extends BaseCapability {
493
543
 
494
544
  try {
495
545
  serverModule = createRequire(serverEntrypoint)('next/dist/server/lib/start-server.js')
496
- } catch (e) { // Fallback to bundled capability
546
+ } catch (e) {
547
+ // Fallback to bundled capability
497
548
  serverModule = createRequire(import.meta.file)('next/dist/server/lib/start-server.js')
498
549
  }
499
550
 
@@ -0,0 +1,283 @@
1
+ import {
2
+ BaseCapability,
3
+ errors as basicErrors,
4
+ createServerListener,
5
+ getServerUrl,
6
+ importFile,
7
+ resolvePackageViaCJS
8
+ } from '@platformatic/basic'
9
+ import { cleanBasePath } from '@platformatic/basic/lib/utils.js'
10
+ import { ensureLoggableError } from '@platformatic/foundation/lib/errors.js'
11
+ import { createQueue } from '@platformatic/image-optimizer'
12
+ import { FileStorage, MemoryStorage, RedisStorage } from '@platformatic/job-queue'
13
+ import inject from 'light-my-request'
14
+ import { readFile } from 'node:fs/promises'
15
+ import { createServer } from 'node:http'
16
+ import { dirname, resolve as resolvePath } from 'node:path'
17
+ import { satisfies } from 'semver'
18
+ import { supportedVersions } from './capability.js'
19
+ import { version } from './schema.js'
20
+
21
+ export class NextImageOptimizerCapability extends BaseCapability {
22
+ #basePath
23
+ #fallbackDomain
24
+ #next
25
+ #nextConfig
26
+ #validateParams
27
+ #app
28
+ #server
29
+ #dispatcher
30
+ #queue
31
+ #fetchTimeout
32
+
33
+ constructor (root, config, context) {
34
+ super('next-image', version, root, config, context)
35
+ }
36
+
37
+ async init () {
38
+ await super.init()
39
+
40
+ // This is needed to avoid Next.js to throw an error when the lockfile is not correct
41
+ // and the user is using npm but has pnpm in its $PATH.
42
+ //
43
+ // See: https://github.com/platformatic/composer-next-node-fastify/pull/3
44
+ //
45
+ // PS by Paolo: Sob.
46
+ process.env.NEXT_IGNORE_INCORRECT_LOCKFILE = 'true'
47
+
48
+ this.#next = resolvePath(dirname(await resolvePackageViaCJS(this.root, 'next')), '../..')
49
+ const nextPackage = JSON.parse(await readFile(resolvePath(this.#next, 'package.json'), 'utf-8'))
50
+
51
+ /* c8 ignore next 3 */
52
+ if (!this.isProduction && !supportedVersions.some(v => satisfies(nextPackage.version, v))) {
53
+ throw new basicErrors.UnsupportedVersion('next', nextPackage.version, supportedVersions)
54
+ }
55
+
56
+ const imageOptimizerConfig = this.config.next?.imageOptimizer ?? {}
57
+
58
+ this.#fetchTimeout = imageOptimizerConfig?.timeout ?? 30000
59
+ this.#fallbackDomain = this.config.next?.imageOptimizer?.fallback
60
+
61
+ // If it's not a full URL, it's a local service
62
+ if (!this.#fallbackDomain.startsWith('http://') && !this.#fallbackDomain.startsWith('https://')) {
63
+ this.#fallbackDomain = `http://${this.#fallbackDomain}.plt.local`
64
+ }
65
+
66
+ if (this.#fallbackDomain.endsWith('/')) {
67
+ this.#fallbackDomain = this.#fallbackDomain.slice(0, -1)
68
+ }
69
+
70
+ this.#queue = await this.#createQueue(imageOptimizerConfig)
71
+ }
72
+
73
+ async start ({ listen }) {
74
+ // Make this idempotent
75
+ if (this.url) {
76
+ return this.url
77
+ }
78
+
79
+ const config = this.config
80
+ this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
81
+ await super._start({ listen })
82
+
83
+ if (this.#app && listen) {
84
+ const serverOptions = this.serverConfig
85
+ const listenOptions = { host: serverOptions?.hostname || '127.0.0.1', port: serverOptions?.port || 0 }
86
+
87
+ if (typeof serverOptions?.backlog === 'number') {
88
+ createServerListener(false, false, { backlog: serverOptions.backlog })
89
+ listenOptions.backlog = serverOptions.backlog
90
+ }
91
+
92
+ this.#server = await new Promise((resolve, reject) => {
93
+ return this.#app
94
+ .listen(listenOptions, function () {
95
+ resolve(this)
96
+ })
97
+ .on('error', reject)
98
+ })
99
+
100
+ this.url = getServerUrl(this.#server)
101
+
102
+ return this.url
103
+ }
104
+
105
+ const { ImageOptimizerCache } = await importFile(resolvePath(this.#next, './dist/server/image-optimizer.js'))
106
+ this.#validateParams = ImageOptimizerCache.validateParams.bind(ImageOptimizerCache)
107
+
108
+ try {
109
+ const { default: loadConfigAPI } = await importFile(resolvePath(this.#next, './dist/server/config.js'))
110
+ this.#nextConfig = await loadConfigAPI.default('production', this.root)
111
+ } catch (error) {
112
+ this.logger.error({ err: ensureLoggableError(error) }, 'Error loading Next.js configuration.')
113
+ throw new Error('Failed to load Next.js configuration.', { cause: error })
114
+ }
115
+
116
+ this.#app = createServer(this.#handleRequest.bind(this))
117
+ this.#dispatcher = this.#app.listeners('request')[0]
118
+ await this.#queue.start()
119
+ }
120
+
121
+ async stop () {
122
+ await super.stop()
123
+ await this.#queue?.stop()
124
+
125
+ globalThis.platformatic.events.emit('plt:next:close')
126
+
127
+ if (!this.#app || !this.#server?.listening) {
128
+ return
129
+ }
130
+ const { promise, resolve, reject } = Promise.withResolvers()
131
+
132
+ this.#server.close(error => {
133
+ if (error) {
134
+ return reject(error)
135
+ }
136
+
137
+ resolve()
138
+ })
139
+
140
+ return promise
141
+ }
142
+
143
+ /* c8 ignore next 5 */
144
+ async getWatchConfig () {
145
+ return {
146
+ enabled: false,
147
+ path: this.root
148
+ }
149
+ }
150
+
151
+ getMeta () {
152
+ const gateway = { prefix: this.basePath ?? this.#basePath, wantsAbsoluteUrls: true, needsRootTrailingSlash: false }
153
+
154
+ if (this.url) {
155
+ gateway.tcp = true
156
+ gateway.url = this.url
157
+ }
158
+
159
+ return { gateway }
160
+ }
161
+
162
+ async inject (injectParams, onInject) {
163
+ this.logger.trace({ injectParams }, 'injecting via light-my-request')
164
+ const res = inject(this.#dispatcher ?? this.#app, injectParams, onInject)
165
+
166
+ /* c8 ignore next 3 */
167
+ if (onInject) {
168
+ return
169
+ }
170
+
171
+ // Since inject might be called from the main thread directly via ITC, let's clean it up
172
+ const { statusCode, headers, body, payload, rawPayload } = res
173
+
174
+ return { statusCode, headers, body, payload, rawPayload }
175
+ }
176
+
177
+ async #createQueue (imageOptimizerConfig) {
178
+ const queueOptions = {
179
+ visibilityTimeout: imageOptimizerConfig.timeout,
180
+ maxRetries: imageOptimizerConfig.maxAttempts,
181
+ logger: this.logger
182
+ }
183
+
184
+ if (imageOptimizerConfig.storage.type === 'memory') {
185
+ queueOptions.storage = new MemoryStorage()
186
+ } else if (imageOptimizerConfig.storage.type === 'filesystem') {
187
+ queueOptions.storage = new FileStorage({
188
+ basePath: imageOptimizerConfig.storage.path ?? resolvePath(this.root, '.next/cache/image-optimizer')
189
+ })
190
+ } else {
191
+ const redisStorageOptions = {
192
+ url: this.#buildRedisStorageUrl(imageOptimizerConfig.storage.url, imageOptimizerConfig.storage.db)
193
+ }
194
+
195
+ if (typeof imageOptimizerConfig.storage.prefix === 'string') {
196
+ redisStorageOptions.keyPrefix = imageOptimizerConfig.storage.prefix
197
+ }
198
+
199
+ queueOptions.storage = new RedisStorage(redisStorageOptions)
200
+ }
201
+
202
+ return createQueue(queueOptions)
203
+ }
204
+
205
+ #buildRedisStorageUrl (url, db) {
206
+ if (typeof db === 'undefined') {
207
+ return url
208
+ }
209
+
210
+ const parsedUrl = new URL(url)
211
+ parsedUrl.pathname = `/${db}`
212
+ return parsedUrl.toString()
213
+ }
214
+
215
+ async #handleRequest (request, response) {
216
+ const { pathname, searchParams } = new URL(request.url, 'http://localhost')
217
+ const imagePath = `${this.#basePath}/_next/image`
218
+
219
+ if (request.method !== 'GET' || pathname !== imagePath) {
220
+ response.statusCode = 404
221
+ response.end('Not Found')
222
+ return
223
+ }
224
+
225
+ const query = {}
226
+
227
+ for (const [key, value] of searchParams.entries()) {
228
+ if (typeof query[key] === 'undefined') {
229
+ query[key] = value
230
+ } else if (Array.isArray(query[key])) {
231
+ query[key].push(value)
232
+ } else {
233
+ query[key] = [query[key], value]
234
+ }
235
+ }
236
+
237
+ try {
238
+ // Extract and validate the parameters
239
+ const params = this.#validateParams(request, query, this.#nextConfig, false)
240
+
241
+ if (params.errorMessage) {
242
+ const error = new Error('Invalid optimization parameters.')
243
+ error.reason = params.errorMessage
244
+ throw error
245
+ }
246
+
247
+ let url
248
+ if (params.isAbsolute) {
249
+ url = params.href
250
+ } else {
251
+ url = `${this.#fallbackDomain}${params.href.startsWith('/') ? '' : '/'}${params.href}`
252
+ }
253
+
254
+ const result = await this.#queue.fetchAndOptimize(
255
+ url,
256
+ params.width,
257
+ params.quality,
258
+ this.#nextConfig.images.dangerouslyAllowSVG,
259
+ { timeout: this.#fetchTimeout }
260
+ )
261
+ const buffer = Buffer.from(result.buffer, 'base64')
262
+
263
+ response.statusCode = 200
264
+ response.setHeader('Content-Type', result.contentType)
265
+ response.setHeader('Cache-Control', result.cacheControl)
266
+ response.end(buffer)
267
+ } catch (error) {
268
+ response.statusCode = 502
269
+ response.setHeader('Content-Type', 'application/json; charset=utf-8')
270
+ response.end(
271
+ JSON.stringify({
272
+ error: 'Bad Gateway',
273
+ message: 'An error occurred while optimizing the image.',
274
+ statusCode: 502,
275
+ cause: {
276
+ ...ensureLoggableError(error.originalError ? JSON.parse(error.originalError) : error),
277
+ stack: undefined
278
+ }
279
+ })
280
+ )
281
+ }
282
+ }
283
+ }
package/lib/schema.js CHANGED
@@ -63,7 +63,7 @@ const next = {
63
63
  type: 'object',
64
64
  properties: {
65
65
  standalone: {
66
- type: 'boolean',
66
+ type: 'boolean'
67
67
  },
68
68
  trailingSlash: {
69
69
  type: 'boolean',
@@ -72,6 +72,132 @@ const next = {
72
72
  useExperimentalAdapter: {
73
73
  type: 'boolean',
74
74
  default: false
75
+ },
76
+ https: {
77
+ type: 'object',
78
+ properties: {
79
+ enabled: {
80
+ anyOf: [
81
+ {
82
+ type: 'boolean'
83
+ },
84
+ {
85
+ type: 'string'
86
+ }
87
+ ]
88
+ },
89
+ key: {
90
+ type: 'string'
91
+ },
92
+ cert: {
93
+ type: 'string'
94
+ },
95
+ ca: {
96
+ type: 'string'
97
+ }
98
+ },
99
+ additionalProperties: false
100
+ },
101
+ imageOptimizer: {
102
+ type: 'object',
103
+ properties: {
104
+ enabled: {
105
+ type: 'boolean',
106
+ default: false
107
+ },
108
+ fallback: {
109
+ type: 'string'
110
+ },
111
+ storage: {
112
+ oneOf: [
113
+ {
114
+ type: 'object',
115
+ properties: {
116
+ type: {
117
+ const: 'memory'
118
+ }
119
+ },
120
+ required: ['type'],
121
+ additionalProperties: false
122
+ },
123
+ {
124
+ type: 'object',
125
+ properties: {
126
+ type: {
127
+ const: 'filesystem'
128
+ },
129
+ path: {
130
+ type: 'string',
131
+ resolvePath: true
132
+ }
133
+ },
134
+ required: ['type'],
135
+ additionalProperties: false
136
+ },
137
+ {
138
+ type: 'object',
139
+ properties: {
140
+ type: {
141
+ type: 'string',
142
+ enum: ['redis', 'valkey']
143
+ },
144
+ url: {
145
+ type: 'string'
146
+ },
147
+ prefix: {
148
+ type: 'string'
149
+ },
150
+ db: {
151
+ anyOf: [
152
+ {
153
+ type: 'number',
154
+ minimum: 0,
155
+ default: 0
156
+ },
157
+ {
158
+ type: 'string',
159
+ pattern: '^[0-9]+$'
160
+ }
161
+ ]
162
+ }
163
+ },
164
+ required: ['type', 'url'],
165
+ additionalProperties: false
166
+ }
167
+ ],
168
+ default: {
169
+ type: 'memory'
170
+ }
171
+ },
172
+ timeout: {
173
+ default: 30000,
174
+ anyOf: [
175
+ {
176
+ type: 'number',
177
+ minimum: 0
178
+ },
179
+ {
180
+ type: 'string',
181
+ pattern: '^[0-9]+$'
182
+ }
183
+ ]
184
+ },
185
+ maxAttempts: {
186
+ default: 3,
187
+ anyOf: [
188
+ {
189
+ type: 'number',
190
+ minimum: 1
191
+ },
192
+ {
193
+ type: 'string',
194
+ pattern: '^[1-9][0-9]*$'
195
+ }
196
+ ]
197
+ }
198
+ },
199
+ required: ['enabled', 'fallback'],
200
+ additionalProperties: false
75
201
  }
76
202
  },
77
203
  default: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/next",
3
- "version": "3.38.1",
3
+ "version": "3.40.0",
4
4
  "description": "Platformatic Next.js Capability",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -19,12 +19,14 @@
19
19
  "@babel/parser": "^7.25.3",
20
20
  "@babel/traverse": "^7.25.3",
21
21
  "@babel/types": "^7.25.2",
22
+ "@platformatic/image-optimizer": "^0.2.0",
22
23
  "amaro": "^0.3.0",
23
24
  "iovalkey": "^0.3.0",
25
+ "light-my-request": "^6.0.0",
24
26
  "msgpackr": "^1.11.2",
25
27
  "semver": "^7.6.3",
26
- "@platformatic/basic": "3.38.1",
27
- "@platformatic/foundation": "3.38.1"
28
+ "@platformatic/basic": "3.40.0",
29
+ "@platformatic/foundation": "3.40.0"
28
30
  },
29
31
  "devDependencies": {
30
32
  "@fastify/reply-from": "^12.0.0",
@@ -34,14 +36,14 @@
34
36
  "cleaner-spec-reporter": "^0.5.0",
35
37
  "eslint": "9",
36
38
  "execa": "^9.5.1",
37
- "fastify": "^5.7.0",
39
+ "fastify": "^5.7.3",
38
40
  "json-schema-to-typescript": "^15.0.1",
39
41
  "neostandard": "^0.12.0",
40
- "next": "^16.0.0",
42
+ "next": "^16.1.0",
41
43
  "typescript": "^5.5.4",
42
44
  "ws": "^8.18.0",
43
- "@platformatic/service": "3.38.1",
44
- "@platformatic/gateway": "3.38.1"
45
+ "@platformatic/gateway": "3.40.0",
46
+ "@platformatic/service": "3.40.0"
45
47
  },
46
48
  "engines": {
47
49
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/next/3.38.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/next/3.40.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Next.js Config",
5
5
  "type": "object",
@@ -152,6 +152,26 @@
152
152
  "customLevels": {
153
153
  "type": "object",
154
154
  "additionalProperties": true
155
+ },
156
+ "openTelemetryExporter": {
157
+ "type": "object",
158
+ "properties": {
159
+ "protocol": {
160
+ "type": "string",
161
+ "enum": [
162
+ "grpc",
163
+ "http"
164
+ ]
165
+ },
166
+ "url": {
167
+ "type": "string"
168
+ }
169
+ },
170
+ "required": [
171
+ "protocol",
172
+ "url"
173
+ ],
174
+ "additionalProperties": false
155
175
  }
156
176
  },
157
177
  "default": {},
@@ -716,6 +736,29 @@
716
736
  "additionalProperties": false
717
737
  }
718
738
  ]
739
+ },
740
+ "management": {
741
+ "anyOf": [
742
+ {
743
+ "type": "boolean"
744
+ },
745
+ {
746
+ "type": "object",
747
+ "properties": {
748
+ "enabled": {
749
+ "type": "boolean",
750
+ "default": true
751
+ },
752
+ "operations": {
753
+ "type": "array",
754
+ "items": {
755
+ "type": "string"
756
+ }
757
+ }
758
+ },
759
+ "additionalProperties": false
760
+ }
761
+ ]
719
762
  }
720
763
  }
721
764
  }
@@ -935,6 +978,26 @@
935
978
  "customLevels": {
936
979
  "type": "object",
937
980
  "additionalProperties": true
981
+ },
982
+ "openTelemetryExporter": {
983
+ "type": "object",
984
+ "properties": {
985
+ "protocol": {
986
+ "type": "string",
987
+ "enum": [
988
+ "grpc",
989
+ "http"
990
+ ]
991
+ },
992
+ "url": {
993
+ "type": "string"
994
+ }
995
+ },
996
+ "required": [
997
+ "protocol",
998
+ "url"
999
+ ],
1000
+ "additionalProperties": false
938
1001
  }
939
1002
  },
940
1003
  "default": {},
@@ -1435,6 +1498,29 @@
1435
1498
  ],
1436
1499
  "default": true
1437
1500
  },
1501
+ "management": {
1502
+ "anyOf": [
1503
+ {
1504
+ "type": "boolean"
1505
+ },
1506
+ {
1507
+ "type": "object",
1508
+ "properties": {
1509
+ "enabled": {
1510
+ "type": "boolean",
1511
+ "default": true
1512
+ },
1513
+ "operations": {
1514
+ "type": "array",
1515
+ "items": {
1516
+ "type": "string"
1517
+ }
1518
+ }
1519
+ },
1520
+ "additionalProperties": false
1521
+ }
1522
+ ]
1523
+ },
1438
1524
  "metrics": {
1439
1525
  "anyOf": [
1440
1526
  {
@@ -2372,6 +2458,145 @@
2372
2458
  "useExperimentalAdapter": {
2373
2459
  "type": "boolean",
2374
2460
  "default": false
2461
+ },
2462
+ "https": {
2463
+ "type": "object",
2464
+ "properties": {
2465
+ "enabled": {
2466
+ "anyOf": [
2467
+ {
2468
+ "type": "boolean"
2469
+ },
2470
+ {
2471
+ "type": "string"
2472
+ }
2473
+ ]
2474
+ },
2475
+ "key": {
2476
+ "type": "string"
2477
+ },
2478
+ "cert": {
2479
+ "type": "string"
2480
+ },
2481
+ "ca": {
2482
+ "type": "string"
2483
+ }
2484
+ },
2485
+ "additionalProperties": false
2486
+ },
2487
+ "imageOptimizer": {
2488
+ "type": "object",
2489
+ "properties": {
2490
+ "enabled": {
2491
+ "type": "boolean",
2492
+ "default": false
2493
+ },
2494
+ "fallback": {
2495
+ "type": "string"
2496
+ },
2497
+ "storage": {
2498
+ "oneOf": [
2499
+ {
2500
+ "type": "object",
2501
+ "properties": {
2502
+ "type": {
2503
+ "const": "memory"
2504
+ }
2505
+ },
2506
+ "required": [
2507
+ "type"
2508
+ ],
2509
+ "additionalProperties": false
2510
+ },
2511
+ {
2512
+ "type": "object",
2513
+ "properties": {
2514
+ "type": {
2515
+ "const": "filesystem"
2516
+ },
2517
+ "path": {
2518
+ "type": "string",
2519
+ "resolvePath": true
2520
+ }
2521
+ },
2522
+ "required": [
2523
+ "type"
2524
+ ],
2525
+ "additionalProperties": false
2526
+ },
2527
+ {
2528
+ "type": "object",
2529
+ "properties": {
2530
+ "type": {
2531
+ "type": "string",
2532
+ "enum": [
2533
+ "redis",
2534
+ "valkey"
2535
+ ]
2536
+ },
2537
+ "url": {
2538
+ "type": "string"
2539
+ },
2540
+ "prefix": {
2541
+ "type": "string"
2542
+ },
2543
+ "db": {
2544
+ "anyOf": [
2545
+ {
2546
+ "type": "number",
2547
+ "minimum": 0,
2548
+ "default": 0
2549
+ },
2550
+ {
2551
+ "type": "string",
2552
+ "pattern": "^[0-9]+$"
2553
+ }
2554
+ ]
2555
+ }
2556
+ },
2557
+ "required": [
2558
+ "type",
2559
+ "url"
2560
+ ],
2561
+ "additionalProperties": false
2562
+ }
2563
+ ],
2564
+ "default": {
2565
+ "type": "memory"
2566
+ }
2567
+ },
2568
+ "timeout": {
2569
+ "default": 30000,
2570
+ "anyOf": [
2571
+ {
2572
+ "type": "number",
2573
+ "minimum": 0
2574
+ },
2575
+ {
2576
+ "type": "string",
2577
+ "pattern": "^[0-9]+$"
2578
+ }
2579
+ ]
2580
+ },
2581
+ "maxAttempts": {
2582
+ "default": 3,
2583
+ "anyOf": [
2584
+ {
2585
+ "type": "number",
2586
+ "minimum": 1
2587
+ },
2588
+ {
2589
+ "type": "string",
2590
+ "pattern": "^[1-9][0-9]*$"
2591
+ }
2592
+ ]
2593
+ }
2594
+ },
2595
+ "required": [
2596
+ "enabled",
2597
+ "fallback"
2598
+ ],
2599
+ "additionalProperties": false
2375
2600
  }
2376
2601
  },
2377
2602
  "default": {},