@platformatic/foundation 3.0.0-alpha.2
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/LICENSE +201 -0
- package/NOTICE +13 -0
- package/eslint.config.js +64 -0
- package/index.d.ts +248 -0
- package/index.js +12 -0
- package/lib/cli.js +231 -0
- package/lib/configuration.js +525 -0
- package/lib/errors.js +58 -0
- package/lib/execution.js +13 -0
- package/lib/file-system.js +188 -0
- package/lib/logger.js +180 -0
- package/lib/module.js +175 -0
- package/lib/node.js +24 -0
- package/lib/object.js +35 -0
- package/lib/schema.js +1189 -0
- package/lib/string.js +95 -0
- package/package.json +53 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import generateName from 'boring-name-generator'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { access, glob, mkdir, rm, watch } from 'node:fs/promises'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join, matchesGlob, resolve } from 'node:path'
|
|
7
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
8
|
+
import { PathOptionRequiredError } from './errors.js'
|
|
9
|
+
|
|
10
|
+
let tmpCount = 0
|
|
11
|
+
const ALLOWED_FS_EVENTS = ['change', 'rename']
|
|
12
|
+
|
|
13
|
+
export function removeDotSlash (path) {
|
|
14
|
+
return path.replace(/^\.[/\\]/, '')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function generateDashedName () {
|
|
18
|
+
return generateName().dashed.replace(/\s+/g, '')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function isFileAccessible (filename, directory) {
|
|
22
|
+
try {
|
|
23
|
+
const filePath = directory ? resolve(directory, filename) : filename
|
|
24
|
+
await access(filePath)
|
|
25
|
+
return true
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function createDirectory (path, empty = false) {
|
|
32
|
+
if (empty) {
|
|
33
|
+
await safeRemove(path)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return mkdir(path, { recursive: true, maxRetries: 10, retryDelay: 1000 })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function createTemporaryDirectory (prefix) {
|
|
40
|
+
const directory = join(tmpdir(), `plt-utils-${prefix}-${process.pid}-${tmpCount++}`)
|
|
41
|
+
|
|
42
|
+
return createDirectory(directory)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function safeRemove (path) {
|
|
46
|
+
let i = 0
|
|
47
|
+
while (i++ < 10) {
|
|
48
|
+
if (!existsSync(path)) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* c8 ignore start - Hard to test */
|
|
53
|
+
try {
|
|
54
|
+
await rm(path, { force: true, recursive: true })
|
|
55
|
+
break
|
|
56
|
+
} catch {
|
|
57
|
+
// This means that we might not delete the folder at all.
|
|
58
|
+
// This is ok as we can't really trust Windows to behave.
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await sleep(1000)
|
|
62
|
+
/* c8 ignore end - Hard to test */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function searchFilesWithExtensions (root, extensions, globOptions = {}) {
|
|
67
|
+
const globSuffix = Array.isArray(extensions) ? `{${extensions.join(',')}}` : extensions
|
|
68
|
+
return Array.fromAsync(glob(`**/*.${globSuffix}`, { ...globOptions, cwd: root }))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function searchJavascriptFiles (projectDir, globOptions = {}) {
|
|
72
|
+
return searchFilesWithExtensions(projectDir, ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts'], {
|
|
73
|
+
...globOptions,
|
|
74
|
+
ignore: ['node_modules', '**/node_modules/**']
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function hasFilesWithExtensions (root, extensions, globOptions = {}) {
|
|
79
|
+
const files = await searchFilesWithExtensions(root, extensions, globOptions)
|
|
80
|
+
return files.length > 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function hasJavascriptFiles (projectDir, globOptions = {}) {
|
|
84
|
+
const files = await searchJavascriptFiles(projectDir, globOptions)
|
|
85
|
+
return files.length > 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class FileWatcher extends EventEmitter {
|
|
89
|
+
constructor (opts) {
|
|
90
|
+
super()
|
|
91
|
+
|
|
92
|
+
if (typeof opts.path !== 'string') {
|
|
93
|
+
throw new PathOptionRequiredError()
|
|
94
|
+
}
|
|
95
|
+
this.path = opts.path
|
|
96
|
+
this.allowToWatch = opts.allowToWatch?.map(removeDotSlash) || null
|
|
97
|
+
this.watchIgnore = opts.watchIgnore?.map(removeDotSlash) || null
|
|
98
|
+
this.handlePromise = null
|
|
99
|
+
this.abortController = null
|
|
100
|
+
|
|
101
|
+
if (this.allowToWatch) {
|
|
102
|
+
this.allowToWatch = Array.from(new Set(this.allowToWatch))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.watchIgnore) {
|
|
106
|
+
this.watchIgnore = Array.from(new Set(this.watchIgnore))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.isWatching = false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
startWatching () {
|
|
113
|
+
if (this.isWatching) return
|
|
114
|
+
this.isWatching = true
|
|
115
|
+
|
|
116
|
+
this.abortController = new AbortController()
|
|
117
|
+
const signal = this.abortController.signal
|
|
118
|
+
|
|
119
|
+
// Recursive watch is unreliable on platforms besides macOS and Windows.
|
|
120
|
+
// See: https://github.com/nodejs/node/issues/48437
|
|
121
|
+
const fsWatcher = watch(this.path, {
|
|
122
|
+
signal,
|
|
123
|
+
recursive: true
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
let updateTimeout = null
|
|
127
|
+
|
|
128
|
+
this.on('update', () => {
|
|
129
|
+
clearTimeout(updateTimeout)
|
|
130
|
+
updateTimeout = null
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const eventHandler = async () => {
|
|
134
|
+
for await (const { eventType, filename } of fsWatcher) {
|
|
135
|
+
/* c8 ignore next */
|
|
136
|
+
if (filename === null) return
|
|
137
|
+
const isTimeoutSet = updateTimeout === null
|
|
138
|
+
const isTrackedEvent = ALLOWED_FS_EVENTS.includes(eventType)
|
|
139
|
+
const isTrackedFile = this.shouldFileBeWatched(filename)
|
|
140
|
+
|
|
141
|
+
if (isTimeoutSet && isTrackedEvent && isTrackedFile) {
|
|
142
|
+
updateTimeout = setTimeout(() => this.emit('update', filename), 100)
|
|
143
|
+
updateTimeout.unref()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} /* c8 ignore next */
|
|
147
|
+
this.handlePromise = eventHandler()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async stopWatching () {
|
|
151
|
+
if (!this.isWatching) return
|
|
152
|
+
this.isWatching = false
|
|
153
|
+
|
|
154
|
+
this.abortController.abort()
|
|
155
|
+
await this.handlePromise.catch(() => {})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
shouldFileBeWatched (fileName) {
|
|
159
|
+
return this.isFileAllowed(fileName) && !this.isFileIgnored(fileName)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
isFileAllowed (fileName) {
|
|
163
|
+
if (this.allowToWatch === null) {
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return this.allowToWatch.some(allowedFile => matchesGlob(fileName, allowedFile))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
isFileIgnored (fileName) {
|
|
171
|
+
// Always ignore the node_modules folder - This can be overriden by the allow list
|
|
172
|
+
if (fileName.startsWith('node_modules')) {
|
|
173
|
+
return true
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (this.watchIgnore === null) {
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const ignoredFile of this.watchIgnore) {
|
|
181
|
+
if (matchesGlob(fileName, ignoredFile)) {
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
}
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { hostname } from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import pino from 'pino'
|
|
5
|
+
|
|
6
|
+
// Utilities to build pino options from a config object
|
|
7
|
+
// There are many variants to fit better the different use cases
|
|
8
|
+
|
|
9
|
+
export function setPinoFormatters (options) {
|
|
10
|
+
const r = createRequire(path.dirname(options.formatters.path))
|
|
11
|
+
const formatters = loadFormatters(r, options.formatters.path)
|
|
12
|
+
if (formatters.bindings) {
|
|
13
|
+
if (typeof formatters.bindings === 'function') {
|
|
14
|
+
options.formatters.bindings = formatters.bindings
|
|
15
|
+
} else {
|
|
16
|
+
throw new Error('logger.formatters.bindings must be a function')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (formatters.level) {
|
|
20
|
+
if (typeof formatters.level === 'function') {
|
|
21
|
+
options.formatters.level = formatters.level
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error('logger.formatters.level must be a function')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildPinoFormatters (formatters) {
|
|
29
|
+
const r = createRequire(path.dirname(formatters.path))
|
|
30
|
+
const f = loadFormatters(r, formatters.path)
|
|
31
|
+
const pinoFormatters = {}
|
|
32
|
+
if (f.bindings) {
|
|
33
|
+
if (typeof f.bindings === 'function') {
|
|
34
|
+
pinoFormatters.bindings = f.bindings
|
|
35
|
+
} else {
|
|
36
|
+
throw new Error('logger.formatters.bindings must be a function')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (f.level) {
|
|
40
|
+
if (typeof f.level === 'function') {
|
|
41
|
+
pinoFormatters.level = f.level
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error('logger.formatters.level must be a function')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return pinoFormatters
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function setPinoTimestamp (options) {
|
|
50
|
+
options.timestamp = stdTimeFunctions[options.timestamp]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildPinoTimestamp (timestamp) {
|
|
54
|
+
return stdTimeFunctions[timestamp]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildPinoOptions (loggerConfig, serverConfig, serviceId, workerId, context, root) {
|
|
58
|
+
const pinoOptions = {
|
|
59
|
+
level: loggerConfig?.level ?? serverConfig?.level ?? 'trace'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (serviceId) {
|
|
63
|
+
pinoOptions.name = serviceId
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (loggerConfig?.base) {
|
|
67
|
+
for (const [key, value] of Object.entries(loggerConfig.base)) {
|
|
68
|
+
if (typeof value !== 'string') {
|
|
69
|
+
throw new Error(`logger.base.${key} must be a string`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/* c8 ignore next - else */
|
|
73
|
+
} else if (loggerConfig?.base === null) {
|
|
74
|
+
pinoOptions.base = undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof context.worker?.index !== 'undefined' && loggerConfig?.base !== null) {
|
|
78
|
+
pinoOptions.base = {
|
|
79
|
+
...(pinoOptions.base ?? {}),
|
|
80
|
+
pid: process.pid,
|
|
81
|
+
hostname: hostname(),
|
|
82
|
+
worker: workerId
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (loggerConfig?.formatters) {
|
|
87
|
+
pinoOptions.formatters = {}
|
|
88
|
+
const formatters = loadFormatters(createRequire(root), loggerConfig.formatters.path)
|
|
89
|
+
if (formatters.bindings) {
|
|
90
|
+
if (typeof formatters.bindings === 'function') {
|
|
91
|
+
pinoOptions.formatters.bindings = formatters.bindings
|
|
92
|
+
} else {
|
|
93
|
+
throw new Error('logger.formatters.bindings must be a function')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (formatters.level) {
|
|
97
|
+
if (typeof formatters.level === 'function') {
|
|
98
|
+
pinoOptions.formatters.level = formatters.level
|
|
99
|
+
} else {
|
|
100
|
+
throw new Error('logger.formatters.level must be a function')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (loggerConfig?.timestamp !== undefined) {
|
|
106
|
+
pinoOptions.timestamp = stdTimeFunctions[loggerConfig.timestamp]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (loggerConfig?.redact) {
|
|
110
|
+
pinoOptions.redact = {
|
|
111
|
+
paths: loggerConfig.redact.paths,
|
|
112
|
+
censor: loggerConfig.redact.censor
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (loggerConfig?.messageKey) {
|
|
117
|
+
pinoOptions.messageKey = loggerConfig.messageKey
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (loggerConfig?.customLevels) {
|
|
121
|
+
for (const [key, value] of Object.entries(loggerConfig.customLevels)) {
|
|
122
|
+
if (typeof value !== 'number') {
|
|
123
|
+
throw new Error(`logger.customLevels.${key} must be a number`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
pinoOptions.customLevels = loggerConfig.customLevels
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// This is used by standalone CLI like start-platformatic-node in @platformatic/node
|
|
130
|
+
if (loggerConfig?.pretty) {
|
|
131
|
+
pinoOptions.transport = { target: 'pino-pretty' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return pinoOptions
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function loadFormatters (require, file) {
|
|
138
|
+
try {
|
|
139
|
+
// Check if the file is a valid path
|
|
140
|
+
const resolvedPath = require.resolve(file)
|
|
141
|
+
|
|
142
|
+
// Load the module
|
|
143
|
+
const loaded = require(resolvedPath)
|
|
144
|
+
|
|
145
|
+
return loaded?.default ?? loaded
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new Error(`Failed to load function from ${file}: ${error.message}`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// This is needed so that pino detects a tampered stdout and avoid writing directly to the FD.
|
|
152
|
+
// Writing directly to the FD would bypass worker.stdout, which is currently piped in the parent process.
|
|
153
|
+
// See: https://github.com/pinojs/pino/blob/ad864b7ae02b314b9a548614f705a437e0db78c3/lib/tools.js#L330
|
|
154
|
+
export function disablePinoDirectWrite () {
|
|
155
|
+
process.stdout.write = process.stdout.write.bind(process.stdout)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* c8 ignore start - Nothing to test */
|
|
159
|
+
export function noop () {}
|
|
160
|
+
|
|
161
|
+
export const abstractLogger = {
|
|
162
|
+
fatal: noop,
|
|
163
|
+
error: noop,
|
|
164
|
+
warn: noop,
|
|
165
|
+
info: noop,
|
|
166
|
+
debug: noop,
|
|
167
|
+
trace: noop,
|
|
168
|
+
done: noop,
|
|
169
|
+
child () {
|
|
170
|
+
return abstractLogger
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/* c8 ignore end */
|
|
174
|
+
|
|
175
|
+
export const stdTimeFunctions = {
|
|
176
|
+
epochTime: pino.stdTimeFunctions.epochTime,
|
|
177
|
+
unixTime: pino.stdTimeFunctions.unixTime,
|
|
178
|
+
nullTime: pino.stdTimeFunctions.nullTime,
|
|
179
|
+
isoTime: pino.stdTimeFunctions.isoTime
|
|
180
|
+
}
|
package/lib/module.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { request } from 'undici'
|
|
6
|
+
import { hasJavascriptFiles } from './file-system.js'
|
|
7
|
+
|
|
8
|
+
let platformaticPackageVersion
|
|
9
|
+
|
|
10
|
+
export const kFailedImport = Symbol('plt.utils.failedImport')
|
|
11
|
+
|
|
12
|
+
export const defaultPackageManager = 'npm'
|
|
13
|
+
|
|
14
|
+
export async function getLatestNpmVersion (pkg) {
|
|
15
|
+
const res = await request(`https://registry.npmjs.org/${pkg}`)
|
|
16
|
+
if (res.statusCode === 200) {
|
|
17
|
+
const json = await res.body.json()
|
|
18
|
+
return json['dist-tags'].latest
|
|
19
|
+
}
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getPkgManager () {
|
|
24
|
+
const userAgent = process.env.npm_config_user_agent
|
|
25
|
+
if (!userAgent) {
|
|
26
|
+
return 'npm'
|
|
27
|
+
}
|
|
28
|
+
const pmSpec = userAgent.split(' ')[0]
|
|
29
|
+
const separatorPos = pmSpec.lastIndexOf('/')
|
|
30
|
+
const name = pmSpec.substring(0, separatorPos)
|
|
31
|
+
return name || 'npm'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the package manager used in the project by looking at the lock file
|
|
36
|
+
*
|
|
37
|
+
* if `search` is true, will search for the package manager in a nested directory
|
|
38
|
+
*/
|
|
39
|
+
export async function getPackageManager (root, defaultManager = defaultPackageManager, search = false) {
|
|
40
|
+
if (existsSync(resolve(root, 'pnpm-lock.yaml'))) {
|
|
41
|
+
return 'pnpm'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (existsSync(resolve(root, 'yarn.lock'))) {
|
|
45
|
+
return 'yarn'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (existsSync(resolve(root, 'package-lock.json'))) {
|
|
49
|
+
return 'npm'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// search for the package manager in a nested directory
|
|
53
|
+
if (search) {
|
|
54
|
+
// look in the first level nested directory
|
|
55
|
+
for (const dir of await readdir(root)) {
|
|
56
|
+
const p = await getPackageManager(resolve(root, dir), null)
|
|
57
|
+
if (p) {
|
|
58
|
+
return p
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return defaultManager
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getInstallationCommand (packageManager, production) {
|
|
67
|
+
const args = ['install']
|
|
68
|
+
if (production) {
|
|
69
|
+
switch (packageManager) {
|
|
70
|
+
case 'pnpm':
|
|
71
|
+
args.push('--prod')
|
|
72
|
+
break
|
|
73
|
+
case 'yarn':
|
|
74
|
+
args.push('--production')
|
|
75
|
+
break
|
|
76
|
+
case 'npm':
|
|
77
|
+
args.push('--omit=dev')
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return args
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function getPlatformaticVersion () {
|
|
85
|
+
if (!platformaticPackageVersion) {
|
|
86
|
+
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf-8'))
|
|
87
|
+
platformaticPackageVersion = packageJson.version
|
|
88
|
+
return platformaticPackageVersion
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return platformaticPackageVersion
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function hasDependency (packageJson, dependency) {
|
|
95
|
+
return packageJson.dependencies?.[dependency] || packageJson.devDependencies?.[dependency]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function splitModuleFromVersion (module) {
|
|
99
|
+
if (!module) {
|
|
100
|
+
return {}
|
|
101
|
+
}
|
|
102
|
+
const versionMatcher = module.match(/(.+)@(\d+.\d+.\d+)/)
|
|
103
|
+
let version
|
|
104
|
+
if (versionMatcher) {
|
|
105
|
+
module = versionMatcher[1]
|
|
106
|
+
version = versionMatcher[2]
|
|
107
|
+
}
|
|
108
|
+
return { module, version }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function detectApplicationType (root, packageJson) {
|
|
112
|
+
if (!packageJson) {
|
|
113
|
+
try {
|
|
114
|
+
packageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf-8'))
|
|
115
|
+
} catch {
|
|
116
|
+
packageJson = {}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let name
|
|
121
|
+
let label
|
|
122
|
+
|
|
123
|
+
if (hasDependency(packageJson, '@nestjs/core')) {
|
|
124
|
+
name = '@platformatic/nest'
|
|
125
|
+
label = 'NestJS'
|
|
126
|
+
} else if (hasDependency(packageJson, 'next')) {
|
|
127
|
+
name = '@platformatic/next'
|
|
128
|
+
label = 'Next.js'
|
|
129
|
+
} else if (hasDependency(packageJson, '@remix-run/dev')) {
|
|
130
|
+
name = '@platformatic/remix'
|
|
131
|
+
label = 'Remix'
|
|
132
|
+
} else if (hasDependency(packageJson, 'astro')) {
|
|
133
|
+
name = '@platformatic/astro'
|
|
134
|
+
label = 'Astro'
|
|
135
|
+
// Since Vite is often used with other frameworks, we must check for Vite last
|
|
136
|
+
} else if (hasDependency(packageJson, 'vite')) {
|
|
137
|
+
name = '@platformatic/vite'
|
|
138
|
+
label = 'Vite'
|
|
139
|
+
} else if (await hasJavascriptFiles(root)) {
|
|
140
|
+
// If no specific framework is detected, we assume it's a generic Node.js application
|
|
141
|
+
name = '@platformatic/node'
|
|
142
|
+
label = 'Node.js'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return name ? { name, label } : null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function loadModule (require, path) {
|
|
149
|
+
if (path.startsWith('file://')) {
|
|
150
|
+
path = fileURLToPath(path)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let loaded
|
|
154
|
+
try {
|
|
155
|
+
try {
|
|
156
|
+
loaded = require(path)
|
|
157
|
+
} catch (err) {
|
|
158
|
+
/* c8 ignore next 4 */
|
|
159
|
+
if (err.code === 'ERR_REQUIRE_ESM') {
|
|
160
|
+
const toLoad = require.resolve(path)
|
|
161
|
+
loaded = await import('file://' + toLoad)
|
|
162
|
+
} else {
|
|
163
|
+
throw err
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
|
|
168
|
+
err[kFailedImport] = path
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw err
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return loaded?.default ?? loaded
|
|
175
|
+
}
|
package/lib/node.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { platform } from 'node:os'
|
|
2
|
+
import { lt, satisfies } from 'semver'
|
|
3
|
+
|
|
4
|
+
const currentPlatform = platform()
|
|
5
|
+
|
|
6
|
+
export function checkNodeVersionForServices () {
|
|
7
|
+
const currentVersion = process.version
|
|
8
|
+
const minimumVersion = '22.18.0'
|
|
9
|
+
|
|
10
|
+
if (lt(currentVersion, minimumVersion)) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Your current Node.js version is ${currentVersion}, while the minimum supported version is v${minimumVersion}. Please upgrade Node.js and try again.`
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const features = {
|
|
18
|
+
node: {
|
|
19
|
+
reusePort: satisfies(process.version, '^22.12.0 || ^23.1.0') && !['win32', 'darwin'].includes(currentPlatform),
|
|
20
|
+
worker: {
|
|
21
|
+
getHeapStatistics: satisfies(process.version, '^22.18.0')
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
package/lib/object.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { deepmerge as fastifyDeepMerge } from '@fastify/deepmerge'
|
|
2
|
+
|
|
3
|
+
// This function merges arrays recursively. When the source is shorter than the target, the target value is cloned.
|
|
4
|
+
function deepmergeArray (options) {
|
|
5
|
+
const { deepmerge, clone } = options
|
|
6
|
+
|
|
7
|
+
return function mergeArray (target, source) {
|
|
8
|
+
const sourceLength = source.length
|
|
9
|
+
const targetLength = Math.max(target.length, source.length)
|
|
10
|
+
|
|
11
|
+
const result = new Array(targetLength)
|
|
12
|
+
for (let i = 0; i < targetLength; i++) {
|
|
13
|
+
result[i] = i < sourceLength ? deepmerge(target[i], source[i]) : clone(target[i])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return result
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const deepmerge = fastifyDeepMerge({ all: true, mergeArray: deepmergeArray })
|
|
21
|
+
|
|
22
|
+
export function isKeyEnabled (key, config) {
|
|
23
|
+
if (config === undefined) return false
|
|
24
|
+
if (typeof config[key] === 'boolean') {
|
|
25
|
+
return config[key]
|
|
26
|
+
}
|
|
27
|
+
if (config[key] === undefined) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getPrivateSymbol (obj, name) {
|
|
34
|
+
return Object.getOwnPropertySymbols(obj).find(s => s.description === name)
|
|
35
|
+
}
|