@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 +40 -0
- package/index.js +5 -2
- package/lib/caching/valkey-common.js +21 -4
- package/lib/caching/valkey-components.js +5 -0
- package/lib/caching/valkey-isr.js +5 -0
- package/lib/capability.js +59 -8
- package/lib/image-optimizer.js +283 -0
- package/lib/schema.js +127 -1
- package/package.json +9 -7
- package/schema.json +226 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
256
|
-
|
|
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) {
|
|
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.
|
|
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.
|
|
27
|
-
"@platformatic/foundation": "3.
|
|
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.
|
|
39
|
+
"fastify": "^5.7.3",
|
|
38
40
|
"json-schema-to-typescript": "^15.0.1",
|
|
39
41
|
"neostandard": "^0.12.0",
|
|
40
|
-
"next": "^16.
|
|
42
|
+
"next": "^16.1.0",
|
|
41
43
|
"typescript": "^5.5.4",
|
|
42
44
|
"ws": "^8.18.0",
|
|
43
|
-
"@platformatic/
|
|
44
|
-
"@platformatic/
|
|
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.
|
|
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": {},
|