@platformatic/next 3.16.0 → 3.18.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 +1 -0
- package/index.js +41 -1
- package/lib/caching/null-isr.js +12 -0
- package/lib/caching/valkey-common.js +101 -0
- package/lib/caching/valkey-components.js +231 -0
- package/lib/caching/{valkey.js → valkey-isr.js} +21 -99
- package/lib/capability.js +9 -22
- package/lib/loader-next-15.cjs +1 -2
- package/lib/loader.js +11 -79
- package/lib/schema.js +3 -0
- package/package.json +11 -7
- package/schema.json +5 -3
package/config.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { transform as basicTransform, resolve, validationOptions } from '@platformatic/basic'
|
|
2
2
|
import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
|
|
3
|
+
import { sep } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
3
5
|
import { NextCapability } from './lib/capability.js'
|
|
4
6
|
import { schema } from './lib/schema.js'
|
|
5
7
|
|
|
8
|
+
function getCacheHandlerPath (name) {
|
|
9
|
+
return fileURLToPath(new URL(`./lib/caching/${name}.js`, import.meta.url)).replaceAll(sep, '/')
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
/* c8 ignore next 9 */
|
|
7
13
|
export async function transform (config, schema, options) {
|
|
8
14
|
config = await basicTransform(config, schema, options)
|
|
@@ -15,6 +21,40 @@ export async function transform (config, schema, options) {
|
|
|
15
21
|
return config
|
|
16
22
|
}
|
|
17
23
|
|
|
24
|
+
export async function enhanceNextConfig (nextConfig, ...args) {
|
|
25
|
+
// This is to avoid https://github.com/vercel/next.js/issues/76981
|
|
26
|
+
Headers.prototype[Symbol.for('nodejs.util.inspect.custom')] = undefined
|
|
27
|
+
|
|
28
|
+
if (typeof nextConfig === 'function') {
|
|
29
|
+
nextConfig = await nextConfig(...args)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { basePath, config, nextVersion } = globalThis.platformatic
|
|
33
|
+
|
|
34
|
+
if (typeof nextConfig.basePath === 'undefined') {
|
|
35
|
+
nextConfig.basePath = basePath
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (config.cache?.adapter) {
|
|
39
|
+
if (nextVersion.major > 15 && config.cache?.cacheComponents && typeof nextConfig.cacheComponents === 'undefined') {
|
|
40
|
+
nextConfig.cacheComponents = true
|
|
41
|
+
nextConfig.cacheHandler = getCacheHandlerPath('null-isr')
|
|
42
|
+
nextConfig.cacheHandlers = { default: getCacheHandlerPath(`${config.cache.adapter}-components`) }
|
|
43
|
+
nextConfig.cacheMaxMemorySize = 0
|
|
44
|
+
} else if (typeof nextConfig.cacheHandler === 'undefined') {
|
|
45
|
+
nextConfig.cacheHandler = getCacheHandlerPath(`${config.cache.adapter}-isr`)
|
|
46
|
+
nextConfig.cacheMaxMemorySize = 0
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (config.next?.trailingSlash && typeof nextConfig.trailingSlash === 'undefined') {
|
|
51
|
+
nextConfig.trailingSlash = true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
globalThis.platformatic.notifyConfig(nextConfig)
|
|
55
|
+
return nextConfig
|
|
56
|
+
}
|
|
57
|
+
|
|
18
58
|
export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
|
|
19
59
|
const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'application')
|
|
20
60
|
|
|
@@ -32,6 +72,6 @@ export async function create (configOrRoot, sourceOrConfig, context) {
|
|
|
32
72
|
return new NextCapability(config[kMetadata].root, config, context)
|
|
33
73
|
}
|
|
34
74
|
|
|
35
|
-
export * as cachingValkey from './lib/caching/valkey.js'
|
|
75
|
+
export * as cachingValkey from './lib/caching/valkey-isr.js'
|
|
36
76
|
export * from './lib/capability.js'
|
|
37
77
|
export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { buildPinoFormatters, buildPinoTimestamp } from '@platformatic/foundation'
|
|
2
|
+
import { Redis } from 'iovalkey'
|
|
3
|
+
import { pack, unpack } from 'msgpackr'
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { hostname } from 'node:os'
|
|
6
|
+
import { resolve } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { pino } from 'pino'
|
|
9
|
+
|
|
10
|
+
globalThis.platformatic ??= {}
|
|
11
|
+
globalThis.platformatic.valkeyClients = new Map()
|
|
12
|
+
|
|
13
|
+
export function keyFor (prefix, subprefix, section, key) {
|
|
14
|
+
let result = prefix?.length ? prefix + ':' : ''
|
|
15
|
+
|
|
16
|
+
result += 'cache:next'
|
|
17
|
+
|
|
18
|
+
if (subprefix?.length) {
|
|
19
|
+
result += ':' + subprefix
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (section?.length) {
|
|
23
|
+
result += ':' + section
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (key?.length) {
|
|
27
|
+
result += Buffer.from(key).toString('base64url')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getConnection (url) {
|
|
34
|
+
let client = globalThis.platformatic.valkeyClients.get(url)
|
|
35
|
+
|
|
36
|
+
if (!client) {
|
|
37
|
+
client = new Redis(url, { enableAutoPipelining: true })
|
|
38
|
+
globalThis.platformatic.valkeyClients.set(url, client)
|
|
39
|
+
|
|
40
|
+
globalThis.platformatic.events.on('plt:next:close', () => {
|
|
41
|
+
client.disconnect(false)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return client
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createPlatformaticLogger () {
|
|
49
|
+
const loggerConfig = globalThis.platformatic?.config?.logger
|
|
50
|
+
|
|
51
|
+
const pinoOptions = {
|
|
52
|
+
...loggerConfig,
|
|
53
|
+
level: globalThis.platformatic?.logLevel ?? loggerConfig?.level ?? 'info'
|
|
54
|
+
}
|
|
55
|
+
if (pinoOptions.formatters) {
|
|
56
|
+
pinoOptions.formatters = buildPinoFormatters(pinoOptions.formatters)
|
|
57
|
+
}
|
|
58
|
+
if (pinoOptions.timestamp) {
|
|
59
|
+
pinoOptions.timestamp = buildPinoTimestamp(pinoOptions.timestamp)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (globalThis.platformatic?.applicationId) {
|
|
63
|
+
pinoOptions.name = `cache:${globalThis.platformatic.applicationId}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (pinoOptions.base !== null) {
|
|
67
|
+
pinoOptions.base = {
|
|
68
|
+
...(pinoOptions.base ?? {}),
|
|
69
|
+
pid: process.pid,
|
|
70
|
+
hostname: hostname(),
|
|
71
|
+
worker: globalThis.platformatic?.workerId
|
|
72
|
+
}
|
|
73
|
+
} else if (pinoOptions.base === null) {
|
|
74
|
+
pinoOptions.base = undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return pino(pinoOptions)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getPlatformaticSubprefix () {
|
|
81
|
+
const root = fileURLToPath(globalThis.platformatic.root)
|
|
82
|
+
|
|
83
|
+
return existsSync(resolve(root, '.next/BUILD_ID'))
|
|
84
|
+
? readFileSync(resolve(root, '.next/BUILD_ID'), 'utf-8').trim()
|
|
85
|
+
: 'development'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getPlatformaticMeta () {
|
|
89
|
+
return {
|
|
90
|
+
applicationId: globalThis.platformatic.applicationId,
|
|
91
|
+
workerId: globalThis.platformatic.workerId
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function serialize (data) {
|
|
96
|
+
return pack(data).toString('base64url')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function deserialize (data) {
|
|
100
|
+
return unpack(Buffer.from(data, 'base64url'))
|
|
101
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { ensureLoggableError } from '@platformatic/foundation'
|
|
2
|
+
import { ReadableStream } from 'node:stream/web'
|
|
3
|
+
import {
|
|
4
|
+
createPlatformaticLogger,
|
|
5
|
+
deserialize,
|
|
6
|
+
getConnection,
|
|
7
|
+
getPlatformaticMeta,
|
|
8
|
+
getPlatformaticSubprefix,
|
|
9
|
+
keyFor,
|
|
10
|
+
serialize
|
|
11
|
+
} from './valkey-common.js'
|
|
12
|
+
|
|
13
|
+
export const CACHE_HIT_METRIC = {
|
|
14
|
+
name: 'next_components_cache_valkey_hit_count',
|
|
15
|
+
help: 'Next.js Components Cache (Valkey) Hit Count'
|
|
16
|
+
}
|
|
17
|
+
export const CACHE_MISS_METRIC = {
|
|
18
|
+
name: 'next_components_cache_valkey_miss_count',
|
|
19
|
+
help: 'Next.js Components Cache (Valkey) Miss Count'
|
|
20
|
+
}
|
|
21
|
+
export const MAX_BATCH_SIZE = 100
|
|
22
|
+
|
|
23
|
+
export const sections = {
|
|
24
|
+
values: 'components:values',
|
|
25
|
+
tags: 'components:tags'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class CacheHandler {
|
|
29
|
+
#config
|
|
30
|
+
#logger
|
|
31
|
+
#store
|
|
32
|
+
#prefix
|
|
33
|
+
#subprefix
|
|
34
|
+
#meta
|
|
35
|
+
#maxTTL
|
|
36
|
+
#cacheHitMetric
|
|
37
|
+
#cacheMissMetric
|
|
38
|
+
|
|
39
|
+
constructor () {
|
|
40
|
+
this.#config ??= globalThis.platformatic.config.cache
|
|
41
|
+
this.#logger ??= createPlatformaticLogger()
|
|
42
|
+
this.#store ??= getConnection(this.#config.url)
|
|
43
|
+
this.#maxTTL ??= this.#config.maxTTL
|
|
44
|
+
this.#prefix ??= this.#config.prefix
|
|
45
|
+
this.#subprefix ??= getPlatformaticSubprefix()
|
|
46
|
+
this.#meta ??= getPlatformaticMeta()
|
|
47
|
+
|
|
48
|
+
if (!this.#config) {
|
|
49
|
+
throw new Error('Please provide a the "config" option.')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!this.#logger) {
|
|
53
|
+
throw new Error('Please provide a the "logger" option.')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!this.#store) {
|
|
57
|
+
throw new Error('Please provide a the "store" option.')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (globalThis.platformatic) {
|
|
61
|
+
this.#registerMetrics()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async get (cacheKey, _, isRedisKey) {
|
|
66
|
+
this.#logger.trace({ key: cacheKey }, 'cache get')
|
|
67
|
+
|
|
68
|
+
const key = isRedisKey ? cacheKey : this.#keyFor(cacheKey, sections.values)
|
|
69
|
+
|
|
70
|
+
let rawValue
|
|
71
|
+
try {
|
|
72
|
+
rawValue = await this.#store.get(key)
|
|
73
|
+
|
|
74
|
+
if (!rawValue) {
|
|
75
|
+
this.#cacheMissMetric?.inc()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
this.#cacheMissMetric?.inc()
|
|
80
|
+
this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot read cache value from Valkey')
|
|
81
|
+
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let raw
|
|
86
|
+
try {
|
|
87
|
+
raw = deserialize(rawValue)
|
|
88
|
+
} catch (e) {
|
|
89
|
+
this.#cacheMissMetric?.inc()
|
|
90
|
+
this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot deserialize cache value from Valkey')
|
|
91
|
+
|
|
92
|
+
// Avoid useless reads the next time
|
|
93
|
+
// Note that since the value was unserializable, we don't know its tags and thus
|
|
94
|
+
// we cannot remove it from the tags sets. TTL will take care of them.
|
|
95
|
+
await this.#store.del(key)
|
|
96
|
+
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { maxTTL: _maxTTL, meta: _meta, ...value } = raw
|
|
101
|
+
|
|
102
|
+
if (this.#maxTTL < raw.revalidate) {
|
|
103
|
+
try {
|
|
104
|
+
await this.#refreshKey(key, value)
|
|
105
|
+
} catch (e) {
|
|
106
|
+
this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot refresh cache key expiration in Valkey')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Convert the value back to a web ReadableStream. Sob.
|
|
111
|
+
const buffer = value.value
|
|
112
|
+
value.value = new ReadableStream({
|
|
113
|
+
start (controller) {
|
|
114
|
+
controller.enqueue(buffer)
|
|
115
|
+
controller.close()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
this.#cacheHitMetric?.inc()
|
|
120
|
+
return value
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async set (cacheKey, dataPromise, isRedisKey) {
|
|
124
|
+
const { value, ...data } = await dataPromise
|
|
125
|
+
const { expire: expireSec, tags, revalidate } = data
|
|
126
|
+
|
|
127
|
+
this.#logger.trace({ key: cacheKey, value, tags, revalidate }, 'cache set')
|
|
128
|
+
|
|
129
|
+
const key = isRedisKey ? cacheKey : this.#keyFor(cacheKey, sections.values)
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Gather the value
|
|
133
|
+
const chunks = []
|
|
134
|
+
for await (const chunk of value) {
|
|
135
|
+
chunks.push(chunk)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Compute the parameters to save
|
|
139
|
+
const toSerialize = serialize({
|
|
140
|
+
maxTTL: this.#maxTTL,
|
|
141
|
+
meta: this.#meta,
|
|
142
|
+
value: Buffer.concat(chunks),
|
|
143
|
+
...data
|
|
144
|
+
})
|
|
145
|
+
const expire = Math.min(revalidate, expireSec, this.#maxTTL)
|
|
146
|
+
|
|
147
|
+
if (expire < 1) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Enqueue all the operations to perform in Valkey
|
|
152
|
+
const promises = []
|
|
153
|
+
promises.push(this.#store.set(key, toSerialize, 'EX', expire))
|
|
154
|
+
|
|
155
|
+
// As Next.js limits tags to 64, we don't need to manage batches here
|
|
156
|
+
if (Array.isArray(tags)) {
|
|
157
|
+
for (const tag of tags) {
|
|
158
|
+
const tagsKey = this.#keyFor(tag, sections.tags)
|
|
159
|
+
promises.push(this.#store.sadd(tagsKey, key))
|
|
160
|
+
promises.push(this.#store.expire(tagsKey, expire))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Execute all the operations
|
|
165
|
+
await Promise.all(promises)
|
|
166
|
+
} catch (e) {
|
|
167
|
+
this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot write cache value in Valkey')
|
|
168
|
+
throw new Error('Cannot write cache value in Valkey', { cause: e })
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async refreshTags () {
|
|
173
|
+
// this.#logger.trace('refreshTags - Not implemented')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getExpiration (_tags) {
|
|
177
|
+
// this.#logger.trace({ tags: _tags }, 'getExpiration - Not implemented')
|
|
178
|
+
return Number.POSITIVE_INFINITY
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
updateTags (_tags) {
|
|
182
|
+
// this.#logger.trace({ tags: _tags }, 'updateTags - Not implemented')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async #refreshKey (key, value) {
|
|
186
|
+
const life = Math.round((Date.now() - value.timestamp) / 1000)
|
|
187
|
+
const expire = Math.min(value.revalidate - life, this.#maxTTL)
|
|
188
|
+
|
|
189
|
+
if (expire < 1) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const promises = []
|
|
194
|
+
promises.push(this.#store.expire(key, expire, 'gt'))
|
|
195
|
+
|
|
196
|
+
if (Array.isArray(value.tags)) {
|
|
197
|
+
for (const tag of value.tags) {
|
|
198
|
+
const tagsKey = this.#keyFor(tag, sections.tags)
|
|
199
|
+
promises.push(this.#store.expire(tagsKey, expire, 'gt'))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await Promise.all(promises)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#keyFor (key, section) {
|
|
207
|
+
return keyFor(this.#prefix, this.#subprefix, section, key)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#registerMetrics () {
|
|
211
|
+
const { client, registry } = globalThis.platformatic.prometheus
|
|
212
|
+
|
|
213
|
+
this.#cacheHitMetric =
|
|
214
|
+
registry.getSingleMetric(CACHE_HIT_METRIC.name) ??
|
|
215
|
+
new client.Counter({
|
|
216
|
+
name: CACHE_HIT_METRIC.name,
|
|
217
|
+
help: CACHE_HIT_METRIC.help,
|
|
218
|
+
registers: [registry]
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
this.#cacheMissMetric =
|
|
222
|
+
registry.getSingleMetric(CACHE_MISS_METRIC.name) ??
|
|
223
|
+
new client.Counter({
|
|
224
|
+
name: CACHE_MISS_METRIC.name,
|
|
225
|
+
help: CACHE_MISS_METRIC.help,
|
|
226
|
+
registers: [registry]
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default new CacheHandler()
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
1
|
+
import { ensureLoggableError } from '@platformatic/foundation'
|
|
2
|
+
import {
|
|
3
|
+
createPlatformaticLogger,
|
|
4
|
+
deserialize,
|
|
5
|
+
getConnection,
|
|
6
|
+
getPlatformaticMeta,
|
|
7
|
+
getPlatformaticSubprefix,
|
|
8
|
+
keyFor,
|
|
9
|
+
serialize
|
|
10
|
+
} from './valkey-common.js'
|
|
11
|
+
|
|
12
|
+
export const CACHE_HIT_METRIC = { name: 'next_cache_valkey_hit_count', help: 'Next.js Cache (Valkey) Hit Count' }
|
|
13
|
+
export const CACHE_MISS_METRIC = { name: 'next_cache_valkey_miss_count', help: 'Next.js Cache (Valkey) Miss Count' }
|
|
15
14
|
export const MAX_BATCH_SIZE = 100
|
|
16
15
|
|
|
17
16
|
export const sections = {
|
|
@@ -19,27 +18,6 @@ export const sections = {
|
|
|
19
18
|
tags: 'tags'
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
export function keyFor (prefix, subprefix, section, key) {
|
|
23
|
-
return [prefix, 'cache:next', subprefix, section, key ? Buffer.from(key).toString('base64url') : undefined]
|
|
24
|
-
.filter(c => c)
|
|
25
|
-
.join(':')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function getConnection (url) {
|
|
29
|
-
let client = clients.get(url)
|
|
30
|
-
|
|
31
|
-
if (!client) {
|
|
32
|
-
client = new Redis(url, { enableAutoPipelining: true })
|
|
33
|
-
clients.set(url, client)
|
|
34
|
-
|
|
35
|
-
globalThis.platformatic.events.on('plt:next:close', () => {
|
|
36
|
-
client.disconnect(false)
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return client
|
|
41
|
-
}
|
|
42
|
-
|
|
43
21
|
export class CacheHandler {
|
|
44
22
|
#standalone
|
|
45
23
|
#config
|
|
@@ -64,14 +42,14 @@ export class CacheHandler {
|
|
|
64
42
|
this.#subprefix = options.subprefix
|
|
65
43
|
this.#meta = options.meta
|
|
66
44
|
|
|
67
|
-
if (!this.#standalone && globalThis.platformatic) {
|
|
45
|
+
if (!this.#standalone && globalThis.platformatic?.config) {
|
|
68
46
|
this.#config ??= globalThis.platformatic.config.cache
|
|
69
|
-
this.#logger ??=
|
|
47
|
+
this.#logger ??= createPlatformaticLogger()
|
|
70
48
|
this.#store ??= getConnection(this.#config.url)
|
|
71
49
|
this.#maxTTL ??= this.#config.maxTTL
|
|
72
50
|
this.#prefix ??= this.#config.prefix
|
|
73
|
-
this.#subprefix ??=
|
|
74
|
-
this.#meta ??=
|
|
51
|
+
this.#subprefix ??= getPlatformaticSubprefix()
|
|
52
|
+
this.#meta ??= getPlatformaticMeta()
|
|
75
53
|
} else {
|
|
76
54
|
this.#config ??= {}
|
|
77
55
|
this.#maxTTL ??= 86_400
|
|
@@ -92,7 +70,7 @@ export class CacheHandler {
|
|
|
92
70
|
throw new Error('Please provide a the "store" option.')
|
|
93
71
|
}
|
|
94
72
|
|
|
95
|
-
if (globalThis.platformatic) {
|
|
73
|
+
if (globalThis.platformatic?.prometheus) {
|
|
96
74
|
this.#registerMetrics()
|
|
97
75
|
}
|
|
98
76
|
}
|
|
@@ -118,7 +96,7 @@ export class CacheHandler {
|
|
|
118
96
|
|
|
119
97
|
let value
|
|
120
98
|
try {
|
|
121
|
-
value =
|
|
99
|
+
value = deserialize(rawValue)
|
|
122
100
|
} catch (e) {
|
|
123
101
|
this.#cacheMissMetric?.inc()
|
|
124
102
|
this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot deserialize cache value from Valkey')
|
|
@@ -155,7 +133,7 @@ export class CacheHandler {
|
|
|
155
133
|
|
|
156
134
|
try {
|
|
157
135
|
// Compute the parameters to save
|
|
158
|
-
const data =
|
|
136
|
+
const data = serialize({
|
|
159
137
|
value,
|
|
160
138
|
tags,
|
|
161
139
|
lastModified: Date.now(),
|
|
@@ -170,7 +148,6 @@ export class CacheHandler {
|
|
|
170
148
|
}
|
|
171
149
|
|
|
172
150
|
// Enqueue all the operations to perform in Valkey
|
|
173
|
-
|
|
174
151
|
const promises = []
|
|
175
152
|
promises.push(this.#store.set(key, data, 'EX', expire))
|
|
176
153
|
|
|
@@ -210,7 +187,7 @@ export class CacheHandler {
|
|
|
210
187
|
|
|
211
188
|
let value
|
|
212
189
|
try {
|
|
213
|
-
value =
|
|
190
|
+
value = deserialize(rawValue)
|
|
214
191
|
} catch (e) {
|
|
215
192
|
this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot deserialize cache value from Valkey')
|
|
216
193
|
|
|
@@ -299,65 +276,10 @@ export class CacheHandler {
|
|
|
299
276
|
await Promise.all(promises)
|
|
300
277
|
}
|
|
301
278
|
|
|
302
|
-
#createPlatformaticLogger () {
|
|
303
|
-
const loggerConfig = globalThis.platformatic?.config?.logger
|
|
304
|
-
|
|
305
|
-
const pinoOptions = {
|
|
306
|
-
...loggerConfig,
|
|
307
|
-
level: globalThis.platformatic?.logLevel ?? loggerConfig?.level ?? 'info'
|
|
308
|
-
}
|
|
309
|
-
if (pinoOptions.formatters) {
|
|
310
|
-
pinoOptions.formatters = buildPinoFormatters(pinoOptions.formatters)
|
|
311
|
-
}
|
|
312
|
-
if (pinoOptions.timestamp) {
|
|
313
|
-
pinoOptions.timestamp = buildPinoTimestamp(pinoOptions.timestamp)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (this.applicationId) {
|
|
317
|
-
pinoOptions.name = `cache:${this.applicationId}`
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (pinoOptions.base !== null) {
|
|
321
|
-
pinoOptions.base = {
|
|
322
|
-
...(pinoOptions.base ?? {}),
|
|
323
|
-
pid: process.pid,
|
|
324
|
-
hostname: hostname(),
|
|
325
|
-
worker: this.workerId
|
|
326
|
-
}
|
|
327
|
-
} else if (pinoOptions.base === null) {
|
|
328
|
-
pinoOptions.base = undefined
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return pino(pinoOptions)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
#getPlatformaticSubprefix () {
|
|
335
|
-
const root = fileURLToPath(globalThis.platformatic.root)
|
|
336
|
-
|
|
337
|
-
return existsSync(resolve(root, '.next/BUILD_ID'))
|
|
338
|
-
? (this.#subprefix = readFileSync(resolve(root, '.next/BUILD_ID'), 'utf-8').trim())
|
|
339
|
-
: 'development'
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
#getPlatformaticMeta () {
|
|
343
|
-
return {
|
|
344
|
-
applicationId: globalThis.platformatic.applicationId,
|
|
345
|
-
workerId: globalThis.platformatic.workerId
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
279
|
#keyFor (key, section) {
|
|
350
280
|
return keyFor(this.#prefix, this.#subprefix, section, key)
|
|
351
281
|
}
|
|
352
282
|
|
|
353
|
-
#serialize (data) {
|
|
354
|
-
return pack(data).toString('base64url')
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
#deserialize (data) {
|
|
358
|
-
return unpack(Buffer.from(data, 'base64url'))
|
|
359
|
-
}
|
|
360
|
-
|
|
361
283
|
#registerMetrics () {
|
|
362
284
|
const { client, registry } = globalThis.platformatic.prometheus
|
|
363
285
|
|
package/lib/capability.js
CHANGED
|
@@ -13,11 +13,10 @@ import { ChildProcess } from 'node:child_process'
|
|
|
13
13
|
import { once } from 'node:events'
|
|
14
14
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
15
15
|
import { dirname, resolve as resolvePath } from 'node:path'
|
|
16
|
-
import { pathToFileURL } from 'node:url'
|
|
17
16
|
import { parse, satisfies } from 'semver'
|
|
18
17
|
import { version } from './schema.js'
|
|
19
18
|
|
|
20
|
-
const supportedVersions = ['^14.0.0', '^15.0.0']
|
|
19
|
+
const supportedVersions = ['^14.0.0', '^15.0.0', '^16.0.0']
|
|
21
20
|
|
|
22
21
|
export class NextCapability extends BaseCapability {
|
|
23
22
|
#basePath
|
|
@@ -164,7 +163,10 @@ export class NextCapability extends BaseCapability {
|
|
|
164
163
|
async getChildManagerContext (basePath) {
|
|
165
164
|
const context = await super.getChildManagerContext(basePath)
|
|
166
165
|
|
|
166
|
+
const { major, minor } = this.#nextVersion
|
|
167
167
|
context.exitOnUnhandledErrors = false
|
|
168
|
+
context.wantsAbsoluteUrls = true
|
|
169
|
+
context.nextVersion = { major, minor }
|
|
168
170
|
|
|
169
171
|
return context
|
|
170
172
|
}
|
|
@@ -190,16 +192,13 @@ export class NextCapability extends BaseCapability {
|
|
|
190
192
|
|
|
191
193
|
this.childManager = new ChildManager({
|
|
192
194
|
loader: loaderUrl,
|
|
193
|
-
context: {
|
|
194
|
-
...context,
|
|
195
|
-
port: false,
|
|
196
|
-
wantsAbsoluteUrls: true
|
|
197
|
-
},
|
|
195
|
+
context: { ...context, port: false },
|
|
198
196
|
scripts: this.#getChildManagerScripts()
|
|
199
197
|
})
|
|
200
198
|
|
|
201
199
|
const promise = once(this.childManager, 'url')
|
|
202
200
|
await this.#startDevelopmentNext(serverOptions)
|
|
201
|
+
|
|
203
202
|
const [url, clientWs] = await promise
|
|
204
203
|
this.url = url
|
|
205
204
|
this.clientWs = clientWs
|
|
@@ -235,7 +234,7 @@ export class NextCapability extends BaseCapability {
|
|
|
235
234
|
}
|
|
236
235
|
}
|
|
237
236
|
|
|
238
|
-
async #startProduction (
|
|
237
|
+
async #startProduction () {
|
|
239
238
|
const config = this.config
|
|
240
239
|
const loaderUrl = new URL('./loader.js', import.meta.url)
|
|
241
240
|
const command = this.config.application.commands.production
|
|
@@ -253,19 +252,7 @@ export class NextCapability extends BaseCapability {
|
|
|
253
252
|
|
|
254
253
|
this.childManager = new ChildManager({
|
|
255
254
|
loader: loaderUrl,
|
|
256
|
-
context:
|
|
257
|
-
config: this.config,
|
|
258
|
-
applicationId: this.applicationId,
|
|
259
|
-
workerId: this.workerId,
|
|
260
|
-
// Always use URL to avoid serialization problem in Windows
|
|
261
|
-
root: pathToFileURL(this.root).toString(),
|
|
262
|
-
basePath: this.#basePath,
|
|
263
|
-
logLevel: this.logger.level,
|
|
264
|
-
isEntrypoint: this.isEntrypoint,
|
|
265
|
-
runtimeBasePath: this.runtimeConfig.basePath,
|
|
266
|
-
wantsAbsoluteUrls: true,
|
|
267
|
-
telemetryConfig: this.telemetryConfig
|
|
268
|
-
},
|
|
255
|
+
context: await this.getChildManagerContext(this.#basePath),
|
|
269
256
|
scripts: this.#getChildManagerScripts()
|
|
270
257
|
})
|
|
271
258
|
|
|
@@ -312,7 +299,7 @@ export class NextCapability extends BaseCapability {
|
|
|
312
299
|
#getChildManagerScripts () {
|
|
313
300
|
const scripts = []
|
|
314
301
|
|
|
315
|
-
if (this.#nextVersion.major
|
|
302
|
+
if (this.#nextVersion.major >= 15) {
|
|
316
303
|
scripts.push(new URL('./loader-next-15.cjs', import.meta.url))
|
|
317
304
|
}
|
|
318
305
|
|
package/lib/loader-next-15.cjs
CHANGED
|
@@ -42,8 +42,7 @@ fsPromises.readFile = async function readAndPatchNextConfigTS (url, options) {
|
|
|
42
42
|
|
|
43
43
|
const { code } = transformSync(contents.toString('utf-8'), { mode: 'strip-only' })
|
|
44
44
|
|
|
45
|
-
const { transformESM, transformCJS
|
|
46
|
-
setLoaderData({ basePath: globalThis.platformatic.basePath, config: globalThis.platformatic.config })
|
|
45
|
+
const { transformESM, transformCJS } = await import('./loader.js')
|
|
47
46
|
const transformer = detectFormat(code) === 'esm' ? transformESM : transformCJS
|
|
48
47
|
const transformed = transformer(code)
|
|
49
48
|
|
package/lib/loader.js
CHANGED
|
@@ -11,83 +11,26 @@ import {
|
|
|
11
11
|
variableDeclarator
|
|
12
12
|
} from '@babel/types'
|
|
13
13
|
import { readFile, realpath } from 'node:fs/promises'
|
|
14
|
-
import { sep } from 'node:path'
|
|
15
14
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
16
15
|
|
|
17
|
-
const originalId = '
|
|
18
|
-
|
|
19
|
-
let config
|
|
20
|
-
let candidates
|
|
21
|
-
let basePath = ''
|
|
16
|
+
const originalId = '__pltEnhanceNextConfig'
|
|
17
|
+
// Keep in sync with https://github.com/vercel/next.js/blob/main/packages/next/src/shared/lib/constants.ts
|
|
18
|
+
let candidates = ['next.config.js', 'next.config.mjs']
|
|
22
19
|
|
|
23
20
|
function parseSingleExpression (expr) {
|
|
24
21
|
return parse(expr, { allowAwaitOutsideFunction: true }).program.body[0]
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
/*
|
|
28
|
-
Generates:
|
|
29
|
-
async function (...args) {
|
|
30
|
-
let __pltOriginalNextConfig = $ORIGINAL;
|
|
31
|
-
|
|
32
|
-
if (typeof __pltOriginalNextConfig === 'function') {
|
|
33
|
-
__pltOriginalNextConfig = await __pltOriginalNextConfig(...args);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if(typeof __pltOriginalNextConfig.basePath === 'undefined') {
|
|
37
|
-
__pltOriginalNextConfig.basePath = basePath
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if(typeof __pltOriginalNextConfig.cacheHandler === 'undefined') {
|
|
41
|
-
__pltOriginalNextConfig.cacheHandler = $PATH
|
|
42
|
-
__pltOriginalNextConfig.cacheMaxMemorySize = 0
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// This is to send the configuraion when Next is executed in a child process
|
|
46
|
-
globalThis.platformatic.notifyConfig(__pltOriginalNextConfig)
|
|
47
|
-
|
|
48
|
-
return __pltOriginalNextConfig;
|
|
49
|
-
}
|
|
50
|
-
*/
|
|
51
24
|
function createEvaluatorWrapperFunction (original) {
|
|
52
|
-
const cacheHandler = config?.cache?.adapter
|
|
53
|
-
? fileURLToPath(new URL(`./caching/${config.cache.adapter}.js`, import.meta.url)).replaceAll(sep, '/')
|
|
54
|
-
: undefined
|
|
55
|
-
|
|
56
|
-
const trailingSlash = config?.next?.trailingSlash
|
|
57
|
-
|
|
58
25
|
return functionDeclaration(
|
|
59
|
-
|
|
26
|
+
identifier(originalId),
|
|
60
27
|
[restElement(identifier('args'))],
|
|
61
|
-
blockStatement(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`if (typeof ${originalId} === 'function') { ${originalId} = await ${originalId}(...args) }`
|
|
68
|
-
),
|
|
69
|
-
parseSingleExpression(
|
|
70
|
-
`if (typeof ${originalId}.basePath === 'undefined') { ${originalId}.basePath = "${basePath}" }`
|
|
71
|
-
),
|
|
72
|
-
cacheHandler
|
|
73
|
-
? parseSingleExpression(`
|
|
74
|
-
if (typeof ${originalId}.cacheHandler === 'undefined') {
|
|
75
|
-
${originalId}.cacheHandler = '${cacheHandler}'
|
|
76
|
-
${originalId}.cacheMaxMemorySize = 0
|
|
77
|
-
}
|
|
78
|
-
`)
|
|
79
|
-
: undefined,
|
|
80
|
-
trailingSlash
|
|
81
|
-
? parseSingleExpression(`
|
|
82
|
-
if (typeof ${originalId}.trailingSlash === 'undefined') {
|
|
83
|
-
${originalId}.trailingSlash = true
|
|
84
|
-
}
|
|
85
|
-
`)
|
|
86
|
-
: undefined,
|
|
87
|
-
parseSingleExpression(`globalThis.platformatic.notifyConfig(${originalId})`),
|
|
88
|
-
returnStatement(identifier(originalId))
|
|
89
|
-
].filter(e => e)
|
|
90
|
-
),
|
|
28
|
+
blockStatement([
|
|
29
|
+
parseSingleExpression("const { enhanceNextConfig } = await import('@platformatic/next')"),
|
|
30
|
+
variableDeclaration('const', [variableDeclarator(identifier('original'), original)]),
|
|
31
|
+
parseSingleExpression('const enhanced = await enhanceNextConfig(original, ...args)'),
|
|
32
|
+
returnStatement(identifier('enhanced'))
|
|
33
|
+
]),
|
|
91
34
|
false,
|
|
92
35
|
true
|
|
93
36
|
)
|
|
@@ -143,12 +86,6 @@ export function transformESM (source) {
|
|
|
143
86
|
return generate.default(ast).code
|
|
144
87
|
}
|
|
145
88
|
|
|
146
|
-
export function setLoaderData (data) {
|
|
147
|
-
candidates = data.candidates
|
|
148
|
-
basePath = data.basePath ?? ''
|
|
149
|
-
config = data.config
|
|
150
|
-
}
|
|
151
|
-
|
|
152
89
|
export async function initialize (data) {
|
|
153
90
|
const realRoot = pathToFileURL(await realpath(fileURLToPath(data.root)))
|
|
154
91
|
|
|
@@ -156,12 +93,7 @@ export async function initialize (data) {
|
|
|
156
93
|
realRoot.pathname += '/'
|
|
157
94
|
}
|
|
158
95
|
|
|
159
|
-
|
|
160
|
-
// Keep in sync with https://github.com/vercel/next.js/blob/main/packages/next/src/shared/lib/constants.ts
|
|
161
|
-
candidates: ['next.config.js', 'next.config.mjs'].map(c => new URL(c, realRoot).toString()),
|
|
162
|
-
basePath: data.basePath ?? '',
|
|
163
|
-
config: data.config
|
|
164
|
-
})
|
|
96
|
+
candidates = candidates.map(c => new URL(c, realRoot).toString())
|
|
165
97
|
}
|
|
166
98
|
|
|
167
99
|
export async function load (url, context, nextLoad) {
|
package/lib/schema.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/next",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.18.0",
|
|
4
4
|
"description": "Platformatic Next.js Capability",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"iovalkey": "^0.3.0",
|
|
24
24
|
"msgpackr": "^1.11.2",
|
|
25
25
|
"semver": "^7.6.3",
|
|
26
|
-
"@platformatic/basic": "3.
|
|
27
|
-
"@platformatic/foundation": "3.
|
|
26
|
+
"@platformatic/basic": "3.18.0",
|
|
27
|
+
"@platformatic/foundation": "3.18.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@fastify/reply-from": "^12.0.0",
|
|
@@ -37,17 +37,21 @@
|
|
|
37
37
|
"fastify": "^5.0.0",
|
|
38
38
|
"json-schema-to-typescript": "^15.0.1",
|
|
39
39
|
"neostandard": "^0.12.0",
|
|
40
|
-
"next": "^
|
|
40
|
+
"next": "^16.0.0",
|
|
41
41
|
"typescript": "^5.5.4",
|
|
42
42
|
"ws": "^8.18.0",
|
|
43
|
-
"@platformatic/
|
|
44
|
-
"@platformatic/
|
|
43
|
+
"@platformatic/service": "3.18.0",
|
|
44
|
+
"@platformatic/gateway": "3.18.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=22.19.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
|
-
"test": "
|
|
50
|
+
"test": "npm run test:main && npm run test:caching && npm run test:compatibility && npm run test:integration",
|
|
51
|
+
"test:main": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js",
|
|
52
|
+
"test:caching": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/caching/*.test.js",
|
|
53
|
+
"test:compatibility": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/compatibility/*.test.js",
|
|
54
|
+
"test:integration": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/integration/*.test.js",
|
|
51
55
|
"gen-schema": "node lib/schema.js > schema.json",
|
|
52
56
|
"gen-types": "json2ts > config.d.ts < schema.json",
|
|
53
57
|
"build": "npm run gen-schema && npm run gen-types",
|
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.18.0.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Next.js Config",
|
|
5
5
|
"type": "object",
|
|
@@ -1142,8 +1142,7 @@
|
|
|
1142
1142
|
{
|
|
1143
1143
|
"type": "string"
|
|
1144
1144
|
}
|
|
1145
|
-
]
|
|
1146
|
-
"default": 4294967296
|
|
1145
|
+
]
|
|
1147
1146
|
},
|
|
1148
1147
|
"maxYoungGeneration": {
|
|
1149
1148
|
"anyOf": [
|
|
@@ -1915,6 +1914,9 @@
|
|
|
1915
1914
|
"prefix": {
|
|
1916
1915
|
"type": "string"
|
|
1917
1916
|
},
|
|
1917
|
+
"cacheComponents": {
|
|
1918
|
+
"type": "boolean"
|
|
1919
|
+
},
|
|
1918
1920
|
"maxTTL": {
|
|
1919
1921
|
"default": 604800,
|
|
1920
1922
|
"anyOf": [
|