@platformatic/next 2.18.0 → 2.19.0-alpha.10

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
@@ -93,4 +93,10 @@ export interface PlatformaticNextJsStackable {
93
93
  production?: string;
94
94
  };
95
95
  };
96
+ cache?: {
97
+ adapter: "redis" | "valkey";
98
+ url: string;
99
+ prefix?: string;
100
+ maxTTL?: number | string;
101
+ };
96
102
  }
package/index.js CHANGED
@@ -16,14 +16,15 @@ import { once } from 'node:events'
16
16
  import { readFile } from 'node:fs/promises'
17
17
  import { dirname, resolve as pathResolve } from 'node:path'
18
18
  import { pathToFileURL } from 'node:url'
19
- import { satisfies } from 'semver'
19
+ import { parse, satisfies } from 'semver'
20
20
  import { packageJson, schema } from './lib/schema.js'
21
21
 
22
- const supportedVersions = '^14.0.0'
22
+ const supportedVersions = ['^14.0.0', '^15.0.0']
23
23
 
24
24
  export class NextStackable extends BaseStackable {
25
25
  #basePath
26
26
  #next
27
+ #nextVersion
27
28
  #child
28
29
  #server
29
30
 
@@ -34,9 +35,10 @@ export class NextStackable extends BaseStackable {
34
35
  async init () {
35
36
  this.#next = pathResolve(dirname(resolvePackage(this.root, 'next')), '../..')
36
37
  const nextPackage = JSON.parse(await readFile(pathResolve(this.#next, 'package.json'), 'utf-8'))
38
+ this.#nextVersion = parse(nextPackage.version)
37
39
 
38
40
  /* c8 ignore next 3 */
39
- if (!satisfies(nextPackage.version, supportedVersions)) {
41
+ if (!supportedVersions.some(v => satisfies(nextPackage.version, v))) {
40
42
  throw new errors.UnsupportedVersion('next', nextPackage.version, supportedVersions)
41
43
  }
42
44
  }
@@ -59,8 +61,10 @@ export class NextStackable extends BaseStackable {
59
61
  return this.stopCommand()
60
62
  }
61
63
 
64
+ globalThis.platformatic.events.emit('plt:next:close')
65
+
62
66
  if (this.isProduction) {
63
- return new Promise((resolve, reject) => {
67
+ await new Promise((resolve, reject) => {
64
68
  this.#server.close(error => {
65
69
  /* c8 ignore next 3 */
66
70
  if (error) {
@@ -70,6 +74,8 @@ export class NextStackable extends BaseStackable {
70
74
  resolve()
71
75
  })
72
76
  })
77
+
78
+ await this.childManager.close()
73
79
  } else {
74
80
  const exitPromise = once(this.#child, 'exit')
75
81
  await this.childManager.close()
@@ -79,6 +85,10 @@ export class NextStackable extends BaseStackable {
79
85
  }
80
86
 
81
87
  async build () {
88
+ if (!this.#nextVersion) {
89
+ await this.init()
90
+ }
91
+
82
92
  const config = this.configManager.current
83
93
  const loader = new URL('./lib/loader.js', import.meta.url)
84
94
  this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
@@ -90,13 +100,14 @@ export class NextStackable extends BaseStackable {
90
100
  command = ['node', pathResolve(this.#next, './dist/bin/next'), 'build', this.root]
91
101
  }
92
102
 
93
- return this.buildWithCommand(command, this.#basePath, loader)
103
+ return this.buildWithCommand(command, this.#basePath, loader, this.#getChildManagerScripts())
94
104
  }
95
105
 
96
106
  /* c8 ignore next 5 */
97
107
  async getWatchConfig () {
98
108
  return {
99
- enabled: false
109
+ enabled: false,
110
+ path: this.root
100
111
  }
101
112
  }
102
113
 
@@ -119,7 +130,7 @@ export class NextStackable extends BaseStackable {
119
130
  this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
120
131
 
121
132
  if (command) {
122
- return this.startWithCommand(command, loaderUrl)
133
+ return this.startWithCommand(command, loaderUrl, this.#getChildManagerScripts())
123
134
  }
124
135
 
125
136
  const { hostname, port } = this.serverConfig ?? {}
@@ -131,6 +142,7 @@ export class NextStackable extends BaseStackable {
131
142
  this.childManager = new ChildManager({
132
143
  loader: loaderUrl,
133
144
  context: {
145
+ config: this.configManager.current,
134
146
  serviceId: this.serviceId,
135
147
  workerId: this.workerId,
136
148
  // Always use URL to avoid serialization problem in Windows
@@ -142,7 +154,8 @@ export class NextStackable extends BaseStackable {
142
154
  runtimeBasePath: this.runtimeConfig.basePath,
143
155
  wantsAbsoluteUrls: true,
144
156
  telemetryConfig: this.telemetryConfig
145
- }
157
+ },
158
+ scripts: this.#getChildManagerScripts()
146
159
  })
147
160
 
148
161
  const promise = once(this.childManager, 'url')
@@ -162,7 +175,17 @@ export class NextStackable extends BaseStackable {
162
175
  try {
163
176
  await this.childManager.inject()
164
177
  const childPromise = createChildProcessListener()
165
- await nextDev(serverOptions, 'default', this.root)
178
+
179
+ if (this.#nextVersion.major === 14 && this.#nextVersion.minor < 2) {
180
+ await nextDev({
181
+ '--hostname': serverOptions.host,
182
+ '--port': serverOptions.port,
183
+ _: [this.root]
184
+ })
185
+ } else {
186
+ await nextDev(serverOptions, 'default', this.root)
187
+ }
188
+
166
189
  this.#child = await childPromise
167
190
  } finally {
168
191
  await this.childManager.eject()
@@ -177,12 +200,13 @@ export class NextStackable extends BaseStackable {
177
200
  this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
178
201
 
179
202
  if (command) {
180
- return this.startWithCommand(command, loaderUrl)
203
+ return this.startWithCommand(command, loaderUrl, this.#getChildManagerScripts())
181
204
  }
182
205
 
183
206
  this.childManager = new ChildManager({
184
207
  loader: loaderUrl,
185
208
  context: {
209
+ config: this.configManager.current,
186
210
  serviceId: this.serviceId,
187
211
  workerId: this.workerId,
188
212
  // Always use URL to avoid serialization problem in Windows
@@ -193,7 +217,8 @@ export class NextStackable extends BaseStackable {
193
217
  runtimeBasePath: this.runtimeConfig.basePath,
194
218
  wantsAbsoluteUrls: true,
195
219
  telemetryConfig: this.telemetryConfig
196
- }
220
+ },
221
+ scripts: this.#getChildManagerScripts()
197
222
  })
198
223
 
199
224
  this.verifyOutputDirectory(pathResolve(this.root, '.next'))
@@ -202,6 +227,7 @@ export class NextStackable extends BaseStackable {
202
227
 
203
228
  async #startProductionNext () {
204
229
  try {
230
+ globalThis.platformatic.config = this.configManager.current
205
231
  await this.childManager.inject()
206
232
  const { nextStart } = await importFile(pathResolve(this.#next, './dist/cli/next-start.js'))
207
233
 
@@ -222,7 +248,15 @@ export class NextStackable extends BaseStackable {
222
248
  (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
223
249
  )
224
250
 
225
- await nextStart(serverOptions, this.root)
251
+ if (this.#nextVersion.major === 14 && this.#nextVersion.minor < 2) {
252
+ await nextStart({
253
+ '--hostname': serverOptions.host,
254
+ '--port': serverOptions.port,
255
+ _: [this.root]
256
+ })
257
+ } else {
258
+ await nextStart(serverOptions, this.root)
259
+ }
226
260
 
227
261
  this.#server = await serverPromise
228
262
  this.url = getServerUrl(this.#server)
@@ -230,6 +264,16 @@ export class NextStackable extends BaseStackable {
230
264
  await this.childManager.eject()
231
265
  }
232
266
  }
267
+
268
+ #getChildManagerScripts () {
269
+ const scripts = []
270
+
271
+ if (this.#nextVersion.major === 15) {
272
+ scripts.push(new URL('./lib/loader-next-15.cjs', import.meta.url))
273
+ }
274
+
275
+ return scripts
276
+ }
233
277
  }
234
278
 
235
279
  /* c8 ignore next 9 */
@@ -242,6 +286,10 @@ function transformConfig () {
242
286
  this.current.watch = { enabled: this.current.watch || false }
243
287
  }
244
288
 
289
+ if (this.current.cache?.adapter === 'redis') {
290
+ this.current.cache.adapter = 'valkey'
291
+ }
292
+
245
293
  basicTransformConfig.call(this)
246
294
  }
247
295
 
@@ -0,0 +1,229 @@
1
+ import { ensureLoggableError } from '@platformatic/utils'
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
+ export const MAX_BATCH_SIZE = 100
11
+
12
+ const sections = {
13
+ values: 'values',
14
+ tags: 'tags'
15
+ }
16
+
17
+ const clients = new Map()
18
+
19
+ export function keyFor (prefix, subprefix, section, key) {
20
+ return [prefix, 'cache:next', subprefix, section, key ? Buffer.from(key).toString('base64url') : undefined]
21
+ .filter(c => c)
22
+ .join(':')
23
+ }
24
+
25
+ export function getConnection (url) {
26
+ let client = clients.get(url)
27
+
28
+ if (!client) {
29
+ client = new Redis(url, { enableAutoPipelining: true })
30
+ clients.set(url, client)
31
+
32
+ globalThis.platformatic.events.on('plt:next:close', () => {
33
+ client.disconnect(false)
34
+ })
35
+ }
36
+
37
+ return client
38
+ }
39
+
40
+ export class CacheHandler {
41
+ #config
42
+ #logger
43
+ #store
44
+ #subprefix
45
+ #maxTTL
46
+
47
+ constructor () {
48
+ this.#logger = this.#createLogger()
49
+ this.#config = globalThis.platformatic.config.cache
50
+ this.#store = getConnection(this.#config.url)
51
+ this.#maxTTL = this.#config.maxTTL
52
+ this.#subprefix = this.#getSubprefix()
53
+ }
54
+
55
+ async get (cacheKey) {
56
+ this.#logger.trace({ key: cacheKey }, 'get')
57
+
58
+ const key = this.#keyFor(cacheKey, sections.values)
59
+
60
+ let rawValue
61
+ try {
62
+ rawValue = await this.#store.get(key)
63
+
64
+ if (!rawValue) {
65
+ return
66
+ }
67
+ } catch (e) {
68
+ this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot read cache value from Valkey')
69
+ throw new Error('Cannot read cache value from Valkey', { cause: e })
70
+ }
71
+
72
+ let value
73
+ try {
74
+ value = this.#deserialize(rawValue)
75
+ } catch (e) {
76
+ this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot deserialize cache value from Valkey')
77
+
78
+ // Avoid useless reads the next time
79
+ // Note that since the value was unserializable, we don't know its tags and thus
80
+ // we cannot remove it from the tags sets. TTL will take care of them.
81
+ await this.#store.del(key)
82
+
83
+ throw new Error('Cannot deserialize cache value from Valkey', { cause: e })
84
+ }
85
+
86
+ if (this.#maxTTL < value.revalidate) {
87
+ try {
88
+ await this.#refreshKey(key, value)
89
+ } catch (e) {
90
+ this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot refresh cache key expiration in Valkey')
91
+
92
+ // We don't throw here since we want to use the cached value anyway
93
+ }
94
+ }
95
+
96
+ return value
97
+ }
98
+
99
+ async set (cacheKey, value, { tags, revalidate }) {
100
+ this.#logger.trace({ key: cacheKey, value, tags, revalidate }, 'set')
101
+
102
+ try {
103
+ // Compute the parameters to save
104
+ const key = this.#keyFor(cacheKey, sections.values)
105
+ const data = this.#serialize({ value, tags, lastModified: Date.now(), revalidate, maxTTL: this.#maxTTL })
106
+ const expire = Math.min(revalidate, this.#maxTTL)
107
+
108
+ if (expire < 1) {
109
+ return
110
+ }
111
+
112
+ // Enqueue all the operations to perform in Valkey
113
+
114
+ const promises = []
115
+ promises.push(this.#store.set(key, data, 'EX', expire))
116
+
117
+ // As Next.js limits tags to 64, we don't need to manage batches here
118
+ if (Array.isArray(tags)) {
119
+ for (const tag of tags) {
120
+ const tagsKey = this.#keyFor(tag, sections.tags)
121
+ promises.push(this.#store.sadd(tagsKey, key))
122
+ promises.push(this.#store.expire(tagsKey, expire))
123
+ }
124
+ }
125
+
126
+ // Execute all the operations
127
+ await Promise.all(promises)
128
+ } catch (e) {
129
+ this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot write cache value in Valkey')
130
+ throw new Error('Cannot write cache value in Valkey', { cause: e })
131
+ }
132
+ }
133
+
134
+ async revalidateTag (tags) {
135
+ this.#logger.trace({ tags }, 'revalidateTag')
136
+
137
+ if (typeof tags === 'string') {
138
+ tags = [tags]
139
+ }
140
+
141
+ try {
142
+ let promises = []
143
+
144
+ for (const tag of tags) {
145
+ const tagsKey = this.#keyFor(tag, sections.tags)
146
+
147
+ // For each key in the tag set, expire the key
148
+ for await (const keys of this.#store.sscanStream(tagsKey)) {
149
+ for (const key of keys) {
150
+ promises.push(this.#store.del(key))
151
+
152
+ // Batch full, execute it
153
+ if (promises.length >= MAX_BATCH_SIZE) {
154
+ await Promise.all(promises)
155
+ promises = []
156
+ }
157
+ }
158
+ }
159
+
160
+ // Delete the set, this will also take care of executing pending operation for a non full batch
161
+ promises.push(this.#store.del(tagsKey))
162
+ await Promise.all(promises)
163
+ promises = []
164
+ }
165
+ } catch (e) {
166
+ this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot expire cache tags in Valkey')
167
+ throw new Error('Cannot expire cache tags in Valkey', { cause: e })
168
+ }
169
+ }
170
+
171
+ async #refreshKey (key, value) {
172
+ const life = Math.round((Date.now() - value.lastModified) / 1000)
173
+ const expire = Math.min(value.revalidate - life, this.#maxTTL)
174
+
175
+ if (expire < 1) {
176
+ return
177
+ }
178
+
179
+ const promises = []
180
+ promises.push(this.#store.expire(key, expire, 'gt'))
181
+
182
+ if (Array.isArray(value.tags)) {
183
+ for (const tag of value.tags) {
184
+ const tagsKey = this.#keyFor(tag, sections.tags)
185
+ promises.push(this.#store.expire(tagsKey, expire, 'gt'))
186
+ }
187
+ }
188
+
189
+ await Promise.all(promises)
190
+ }
191
+
192
+ #createLogger () {
193
+ const pinoOptions = {
194
+ level: globalThis.platformatic?.logLevel ?? 'info'
195
+ }
196
+
197
+ if (this.serviceId) {
198
+ pinoOptions.name = `cache:${this.serviceId}`
199
+ }
200
+
201
+ if (typeof globalThis.platformatic.workerId !== 'undefined') {
202
+ pinoOptions.base = { pid: process.pid, hostname: hostname(), worker: this.workerId }
203
+ }
204
+
205
+ return pino(pinoOptions)
206
+ }
207
+
208
+ #getSubprefix () {
209
+ const root = fileURLToPath(globalThis.platformatic.root)
210
+
211
+ return existsSync(resolve(root, '.next/BUILD_ID'))
212
+ ? (this.#subprefix = readFileSync(resolve(root, '.next/BUILD_ID'), 'utf-8').trim())
213
+ : 'development'
214
+ }
215
+
216
+ #keyFor (key, section) {
217
+ return keyFor(this.#config.prefix, this.#subprefix, section, key)
218
+ }
219
+
220
+ #serialize (data) {
221
+ return pack(data).toString('base64url')
222
+ }
223
+
224
+ #deserialize (data) {
225
+ return unpack(Buffer.from(data, 'base64url'))
226
+ }
227
+ }
228
+
229
+ export default CacheHandler
@@ -0,0 +1,52 @@
1
+ const { resolve } = require('node:path')
2
+ const { fileURLToPath } = require('node:url')
3
+ const fsPromises = require('node:fs').promises
4
+ const { transformSync } = require('amaro')
5
+ const { parse } = require('@babel/parser')
6
+ const traverse = require('@babel/traverse')
7
+
8
+ const originalReadFile = fsPromises.readFile
9
+ const targetFile = resolve(fileURLToPath(globalThis.platformatic.root), 'next.config.ts')
10
+
11
+ function detectFormat (code) {
12
+ let format = 'esm'
13
+
14
+ const ast = parse(code, { sourceType: 'module' })
15
+
16
+ // Manipulate the AST
17
+ traverse.default(ast, {
18
+ AssignmentExpression (path) {
19
+ const { left } = path.node
20
+
21
+ // module.exports = $EXPRESSION
22
+ if (left.object.name === 'module' && left.property.name === 'exports') {
23
+ format = 'cjs'
24
+ path.stop()
25
+ }
26
+ }
27
+ })
28
+
29
+ return format
30
+ }
31
+
32
+ fsPromises.readFile = async function WTF (url, options) {
33
+ if (url.startsWith('file://')) {
34
+ url = fileURLToPath(url)
35
+ }
36
+
37
+ const contents = await originalReadFile(url, options)
38
+
39
+ if (url !== targetFile) {
40
+ return contents
41
+ }
42
+
43
+ const { code } = transformSync(contents.toString('utf-8'), { mode: 'strip-only' })
44
+
45
+ const { transformESM, transformCJS } = await import('./loader.js')
46
+ const transformer = detectFormat(code) === 'esm' ? transformESM : transformCJS
47
+ const transformed = transformer(code)
48
+
49
+ // Restore the original method
50
+ fsPromises.readFile = originalReadFile
51
+ return transformed
52
+ }
package/lib/loader.js CHANGED
@@ -11,12 +11,14 @@ import {
11
11
  variableDeclarator
12
12
  } from '@babel/types'
13
13
  import { readFile, realpath } from 'node:fs/promises'
14
+ import { sep } from 'node:path'
14
15
  import { fileURLToPath, pathToFileURL } from 'node:url'
15
16
 
16
17
  const originalId = '__pltOriginalNextConfig'
17
18
 
19
+ let config
18
20
  let candidates
19
- let basePath
21
+ let basePath = ''
20
22
 
21
23
  function parseSingleExpression (expr) {
22
24
  return parse(expr, { allowAwaitOutsideFunction: true }).program.body[0]
@@ -35,6 +37,11 @@ function parseSingleExpression (expr) {
35
37
  __pltOriginalNextConfig.basePath = basePath
36
38
  }
37
39
 
40
+ if(typeof __pltOriginalNextConfig.cacheHandler === 'undefined') {
41
+ __pltOriginalNextConfig.cacheHandler = $PATH
42
+ __pltOriginalNextConfig.cacheMaxMemorySize = 0
43
+ }
44
+
38
45
  // This is to send the configuraion when Next is executed in a child process (development)
39
46
  globalThis[Symbol.for('plt.children.itc')]?.notify('config', __pltOriginalNextConfig)
40
47
 
@@ -45,27 +52,41 @@ function parseSingleExpression (expr) {
45
52
  }
46
53
  */
47
54
  function createEvaluatorWrapperFunction (original) {
55
+ const cacheHandler = config?.cache
56
+ ? fileURLToPath(new URL(`./caching/${config.cache.adapter ?? 'foo'}.js`, import.meta.url)).replaceAll(sep, '/')
57
+ : undefined
58
+
48
59
  return functionDeclaration(
49
60
  null,
50
61
  [restElement(identifier('args'))],
51
- blockStatement([
52
- variableDeclaration('let', [variableDeclarator(identifier(originalId), original)]),
53
- parseSingleExpression(
54
- `if (typeof ${originalId} === 'function') { ${originalId} = await ${originalId}(...args) }`
55
- ),
56
- parseSingleExpression(
57
- `if (typeof ${originalId}.basePath === 'undefined') { ${originalId}.basePath = "${basePath}" }`
58
- ),
59
- parseSingleExpression(`globalThis[Symbol.for('plt.children.itc')]?.notify('config', ${originalId})`),
60
- parseSingleExpression(`process.emit('plt:next:config', ${originalId})`),
61
- returnStatement(identifier(originalId))
62
- ]),
62
+ blockStatement(
63
+ [
64
+ variableDeclaration('let', [variableDeclarator(identifier(originalId), original)]),
65
+ parseSingleExpression(
66
+ `if (typeof ${originalId} === 'function') { ${originalId} = await ${originalId}(...args) }`
67
+ ),
68
+ parseSingleExpression(
69
+ `if (typeof ${originalId}.basePath === 'undefined') { ${originalId}.basePath = "${basePath}" }`
70
+ ),
71
+ cacheHandler
72
+ ? parseSingleExpression(`
73
+ if (typeof ${originalId}.cacheHandler === 'undefined') {
74
+ ${originalId}.cacheHandler = '${cacheHandler}'
75
+ ${originalId}.cacheMaxMemorySize = 0
76
+ }
77
+ `)
78
+ : undefined,
79
+ parseSingleExpression(`globalThis[Symbol.for('plt.children.itc')]?.notify('config', ${originalId})`),
80
+ parseSingleExpression(`process.emit('plt:next:config', ${originalId})`),
81
+ returnStatement(identifier(originalId))
82
+ ].filter(e => e)
83
+ ),
63
84
  false,
64
85
  true
65
86
  )
66
87
  }
67
88
 
68
- function transformCJS (source) {
89
+ export function transformCJS (source) {
69
90
  const ast = parse(source.toString(), { sourceType: 'module' })
70
91
 
71
92
  // Manipulate the AST
@@ -84,7 +105,7 @@ function transformCJS (source) {
84
105
  return generate.default(ast).code
85
106
  }
86
107
 
87
- function transformESM (source) {
108
+ export function transformESM (source) {
88
109
  const ast = parse(source.toString(), { sourceType: 'module' })
89
110
 
90
111
  // Manipulate the AST
@@ -125,6 +146,7 @@ export async function initialize (data) {
125
146
  // Keep in sync with https://github.com/vercel/next.js/blob/main/packages/next/src/shared/lib/constants.ts
126
147
  candidates = ['next.config.js', 'next.config.mjs'].map(c => new URL(c, realRoot).toString())
127
148
  basePath = data.basePath ?? ''
149
+ config = data.config
128
150
  }
129
151
 
130
152
  export async function load (url, context, nextLoad) {
package/lib/schema.js CHANGED
@@ -4,6 +4,36 @@ import { readFileSync } from 'node:fs'
4
4
 
5
5
  export const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
6
6
 
7
+ export const cache = {
8
+ type: 'object',
9
+ properties: {
10
+ adapter: {
11
+ type: 'string',
12
+ enum: ['redis', 'valkey']
13
+ },
14
+ url: {
15
+ type: 'string'
16
+ },
17
+ prefix: {
18
+ type: 'string'
19
+ },
20
+ maxTTL: {
21
+ default: 86400 * 7, // One week
22
+ anyOf: [
23
+ {
24
+ type: 'number',
25
+ minimum: 0
26
+ },
27
+ {
28
+ type: 'string'
29
+ }
30
+ ]
31
+ }
32
+ },
33
+ required: ['adapter', 'url'],
34
+ additionalProperties: false
35
+ }
36
+
7
37
  export const schema = {
8
38
  $id: `https://schemas.platformatic.dev/@platformatic/next/${packageJson.version}.json`,
9
39
  $schema: 'http://json-schema.org/draft-07/schema#',
@@ -16,7 +46,8 @@ export const schema = {
16
46
  logger: utilsSchemaComponents.logger,
17
47
  server: utilsSchemaComponents.server,
18
48
  watch: schemaComponents.watch,
19
- application: schemaComponents.application
49
+ application: schemaComponents.application,
50
+ cache
20
51
  },
21
52
  additionalProperties: false
22
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/next",
3
- "version": "2.18.0",
3
+ "version": "2.19.0-alpha.10",
4
4
  "description": "Platformatic Next.js Stackable",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -19,25 +19,30 @@
19
19
  "@babel/parser": "^7.25.3",
20
20
  "@babel/traverse": "^7.25.3",
21
21
  "@babel/types": "^7.25.2",
22
+ "iovalkey": "^0.2.1",
23
+ "msgpackr": "^1.11.2",
24
+ "amaro": "^0.2.0",
22
25
  "semver": "^7.6.3",
23
- "@platformatic/basic": "2.18.0",
24
- "@platformatic/config": "2.18.0",
25
- "@platformatic/utils": "2.18.0"
26
+ "@platformatic/basic": "2.19.0-alpha.10",
27
+ "@platformatic/config": "2.19.0-alpha.10",
28
+ "@platformatic/utils": "2.19.0-alpha.10"
26
29
  },
27
30
  "devDependencies": {
28
31
  "@fastify/reply-from": "^11.0.0",
32
+ "@types/node": "^22.5.0",
29
33
  "borp": "^0.19.0",
30
34
  "eslint": "9",
35
+ "execa": "^9.5.1",
31
36
  "fastify": "^5.0.0",
32
37
  "json-schema-to-typescript": "^15.0.1",
33
38
  "neostandard": "^0.11.1",
34
- "next": "^14.2.5",
39
+ "next": "^15.0.0",
35
40
  "react": "^18.3.1",
36
41
  "react-dom": "^18.3.1",
37
42
  "typescript": "^5.5.4",
38
43
  "ws": "^8.18.0",
39
- "@platformatic/composer": "2.18.0",
40
- "@platformatic/service": "2.18.0"
44
+ "@platformatic/composer": "2.19.0-alpha.10",
45
+ "@platformatic/service": "2.19.0-alpha.10"
41
46
  },
42
47
  "scripts": {
43
48
  "test": "npm run lint && borp --concurrency=1 --no-timeout",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/next/2.18.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/next/2.19.0-alpha.10.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Next.js Stackable",
5
5
  "type": "object",
@@ -296,6 +296,41 @@
296
296
  },
297
297
  "additionalProperties": false,
298
298
  "default": {}
299
+ },
300
+ "cache": {
301
+ "type": "object",
302
+ "properties": {
303
+ "adapter": {
304
+ "type": "string",
305
+ "enum": [
306
+ "redis",
307
+ "valkey"
308
+ ]
309
+ },
310
+ "url": {
311
+ "type": "string"
312
+ },
313
+ "prefix": {
314
+ "type": "string"
315
+ },
316
+ "maxTTL": {
317
+ "default": 604800,
318
+ "anyOf": [
319
+ {
320
+ "type": "number",
321
+ "minimum": 0
322
+ },
323
+ {
324
+ "type": "string"
325
+ }
326
+ ]
327
+ }
328
+ },
329
+ "required": [
330
+ "adapter",
331
+ "url"
332
+ ],
333
+ "additionalProperties": false
299
334
  }
300
335
  },
301
336
  "additionalProperties": false