@platformatic/next 2.19.0-alpha.1 → 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/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
  }
@@ -83,6 +85,10 @@ export class NextStackable extends BaseStackable {
83
85
  }
84
86
 
85
87
  async build () {
88
+ if (!this.#nextVersion) {
89
+ await this.init()
90
+ }
91
+
86
92
  const config = this.configManager.current
87
93
  const loader = new URL('./lib/loader.js', import.meta.url)
88
94
  this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
@@ -94,7 +100,7 @@ export class NextStackable extends BaseStackable {
94
100
  command = ['node', pathResolve(this.#next, './dist/bin/next'), 'build', this.root]
95
101
  }
96
102
 
97
- return this.buildWithCommand(command, this.#basePath, loader)
103
+ return this.buildWithCommand(command, this.#basePath, loader, this.#getChildManagerScripts())
98
104
  }
99
105
 
100
106
  /* c8 ignore next 5 */
@@ -124,7 +130,7 @@ export class NextStackable extends BaseStackable {
124
130
  this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
125
131
 
126
132
  if (command) {
127
- return this.startWithCommand(command, loaderUrl)
133
+ return this.startWithCommand(command, loaderUrl, this.#getChildManagerScripts())
128
134
  }
129
135
 
130
136
  const { hostname, port } = this.serverConfig ?? {}
@@ -148,7 +154,8 @@ export class NextStackable extends BaseStackable {
148
154
  runtimeBasePath: this.runtimeConfig.basePath,
149
155
  wantsAbsoluteUrls: true,
150
156
  telemetryConfig: this.telemetryConfig
151
- }
157
+ },
158
+ scripts: this.#getChildManagerScripts()
152
159
  })
153
160
 
154
161
  const promise = once(this.childManager, 'url')
@@ -168,7 +175,17 @@ export class NextStackable extends BaseStackable {
168
175
  try {
169
176
  await this.childManager.inject()
170
177
  const childPromise = createChildProcessListener()
171
- 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
+
172
189
  this.#child = await childPromise
173
190
  } finally {
174
191
  await this.childManager.eject()
@@ -183,7 +200,7 @@ export class NextStackable extends BaseStackable {
183
200
  this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
184
201
 
185
202
  if (command) {
186
- return this.startWithCommand(command, loaderUrl)
203
+ return this.startWithCommand(command, loaderUrl, this.#getChildManagerScripts())
187
204
  }
188
205
 
189
206
  this.childManager = new ChildManager({
@@ -200,7 +217,8 @@ export class NextStackable extends BaseStackable {
200
217
  runtimeBasePath: this.runtimeConfig.basePath,
201
218
  wantsAbsoluteUrls: true,
202
219
  telemetryConfig: this.telemetryConfig
203
- }
220
+ },
221
+ scripts: this.#getChildManagerScripts()
204
222
  })
205
223
 
206
224
  this.verifyOutputDirectory(pathResolve(this.root, '.next'))
@@ -230,7 +248,15 @@ export class NextStackable extends BaseStackable {
230
248
  (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
231
249
  )
232
250
 
233
- 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
+ }
234
260
 
235
261
  this.#server = await serverPromise
236
262
  this.url = getServerUrl(this.#server)
@@ -238,6 +264,16 @@ export class NextStackable extends BaseStackable {
238
264
  await this.childManager.eject()
239
265
  }
240
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
+ }
241
277
  }
242
278
 
243
279
  /* c8 ignore next 9 */
@@ -14,12 +14,29 @@ const sections = {
14
14
  tags: 'tags'
15
15
  }
16
16
 
17
+ const clients = new Map()
18
+
17
19
  export function keyFor (prefix, subprefix, section, key) {
18
20
  return [prefix, 'cache:next', subprefix, section, key ? Buffer.from(key).toString('base64url') : undefined]
19
21
  .filter(c => c)
20
22
  .join(':')
21
23
  }
22
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
+
23
40
  export class CacheHandler {
24
41
  #config
25
42
  #logger
@@ -30,14 +47,9 @@ export class CacheHandler {
30
47
  constructor () {
31
48
  this.#logger = this.#createLogger()
32
49
  this.#config = globalThis.platformatic.config.cache
33
- this.#store = new Redis(this.#config.url, { enableAutoPipelining: true })
50
+ this.#store = getConnection(this.#config.url)
34
51
  this.#maxTTL = this.#config.maxTTL
35
52
  this.#subprefix = this.#getSubprefix()
36
-
37
- // Handle disconnection not to hang the process on exit
38
- globalThis.platformatic.events.on('plt:next:close', () => {
39
- this.#store.disconnect(false)
40
- })
41
53
  }
42
54
 
43
55
  async get (cacheKey) {
@@ -93,7 +105,12 @@ export class CacheHandler {
93
105
  const data = this.#serialize({ value, tags, lastModified: Date.now(), revalidate, maxTTL: this.#maxTTL })
94
106
  const expire = Math.min(revalidate, this.#maxTTL)
95
107
 
108
+ if (expire < 1) {
109
+ return
110
+ }
111
+
96
112
  // Enqueue all the operations to perform in Valkey
113
+
97
114
  const promises = []
98
115
  promises.push(this.#store.set(key, data, 'EX', expire))
99
116
 
@@ -155,19 +172,21 @@ export class CacheHandler {
155
172
  const life = Math.round((Date.now() - value.lastModified) / 1000)
156
173
  const expire = Math.min(value.revalidate - life, this.#maxTTL)
157
174
 
158
- if (expire > 0) {
159
- const promises = []
160
- promises.push(this.#store.expire(key, expire, 'gt'))
175
+ if (expire < 1) {
176
+ return
177
+ }
161
178
 
162
- if (Array.isArray(value.tags)) {
163
- for (const tag of value.tags) {
164
- const tagsKey = this.#keyFor(tag, sections.tags)
165
- promises.push(this.#store.expire(tagsKey, expire, 'gt'))
166
- }
167
- }
179
+ const promises = []
180
+ promises.push(this.#store.expire(key, expire, 'gt'))
168
181
 
169
- await Promise.all(promises)
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
+ }
170
187
  }
188
+
189
+ await Promise.all(promises)
171
190
  }
172
191
 
173
192
  #createLogger () {
@@ -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
@@ -18,7 +18,7 @@ const originalId = '__pltOriginalNextConfig'
18
18
 
19
19
  let config
20
20
  let candidates
21
- let basePath
21
+ let basePath = ''
22
22
 
23
23
  function parseSingleExpression (expr) {
24
24
  return parse(expr, { allowAwaitOutsideFunction: true }).program.body[0]
@@ -86,7 +86,7 @@ function createEvaluatorWrapperFunction (original) {
86
86
  )
87
87
  }
88
88
 
89
- function transformCJS (source) {
89
+ export function transformCJS (source) {
90
90
  const ast = parse(source.toString(), { sourceType: 'module' })
91
91
 
92
92
  // Manipulate the AST
@@ -105,7 +105,7 @@ function transformCJS (source) {
105
105
  return generate.default(ast).code
106
106
  }
107
107
 
108
- function transformESM (source) {
108
+ export function transformESM (source) {
109
109
  const ast = parse(source.toString(), { sourceType: 'module' })
110
110
 
111
111
  // Manipulate the AST
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/next",
3
- "version": "2.19.0-alpha.1",
3
+ "version": "2.19.0-alpha.10",
4
4
  "description": "Platformatic Next.js Stackable",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -21,25 +21,28 @@
21
21
  "@babel/types": "^7.25.2",
22
22
  "iovalkey": "^0.2.1",
23
23
  "msgpackr": "^1.11.2",
24
+ "amaro": "^0.2.0",
24
25
  "semver": "^7.6.3",
25
- "@platformatic/basic": "2.19.0-alpha.1",
26
- "@platformatic/config": "2.19.0-alpha.1",
27
- "@platformatic/utils": "2.19.0-alpha.1"
26
+ "@platformatic/basic": "2.19.0-alpha.10",
27
+ "@platformatic/config": "2.19.0-alpha.10",
28
+ "@platformatic/utils": "2.19.0-alpha.10"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@fastify/reply-from": "^11.0.0",
32
+ "@types/node": "^22.5.0",
31
33
  "borp": "^0.19.0",
32
34
  "eslint": "9",
35
+ "execa": "^9.5.1",
33
36
  "fastify": "^5.0.0",
34
37
  "json-schema-to-typescript": "^15.0.1",
35
38
  "neostandard": "^0.11.1",
36
- "next": "^14.2.5",
39
+ "next": "^15.0.0",
37
40
  "react": "^18.3.1",
38
41
  "react-dom": "^18.3.1",
39
42
  "typescript": "^5.5.4",
40
43
  "ws": "^8.18.0",
41
- "@platformatic/composer": "2.19.0-alpha.1",
42
- "@platformatic/service": "2.19.0-alpha.1"
44
+ "@platformatic/composer": "2.19.0-alpha.10",
45
+ "@platformatic/service": "2.19.0-alpha.10"
43
46
  },
44
47
  "scripts": {
45
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.19.0-alpha.1.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",