@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 +6 -0
- package/index.js +60 -12
- package/lib/caching/valkey.js +229 -0
- package/lib/loader-next-15.cjs +52 -0
- package/lib/loader.js +37 -15
- package/lib/schema.js +32 -1
- package/package.json +12 -7
- package/schema.json +36 -1
package/config.d.ts
CHANGED
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
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.
|
|
24
|
-
"@platformatic/config": "2.
|
|
25
|
-
"@platformatic/utils": "2.
|
|
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": "^
|
|
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.
|
|
40
|
-
"@platformatic/service": "2.
|
|
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.
|
|
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
|