create-platformatic 1.49.1 → 1.50.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createPlatformatic } from '@platformatic/create-platformatic-auto'
2
+ import { createPlatformatic } from './src/index.mjs'
3
3
  import isMain from 'es-main'
4
4
  import parseArgs from 'minimist'
5
5
  import { readFile } from 'fs/promises'
@@ -30,7 +30,4 @@ if (isMain(import.meta)) {
30
30
  await createPlatformatic(_args)
31
31
  }
32
32
 
33
- export {
34
- createStaticWorkspaceGHAction,
35
- createDynamicWorkspaceGHAction
36
- } from './src/ghaction.mjs'
33
+ export { createPlatformatic }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-platformatic",
3
- "version": "1.49.1",
4
- "description": "Create platformatic-db interactive tool",
3
+ "version": "1.50.0",
4
+ "description": "Create platformatic application interactive tool",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/platformatic/platformatic.git"
@@ -10,10 +10,10 @@
10
10
  ".": "./create-platformatic.mjs"
11
11
  },
12
12
  "bin": {
13
- "create-platformatic": "./create-platformatic.mjs"
13
+ "create-platformatic-auto": "./create-platformatic.mjs"
14
14
  },
15
15
  "license": "Apache-2.0",
16
- "author": "Marco Piraccini <marco.piraccini@gmail.com>",
16
+ "author": "Matteo Collina <hello@matteocollina.com>",
17
17
  "dependencies": {
18
18
  "@types/node": "^20.11.28",
19
19
  "boring-name-generator": "^1.0.3",
@@ -30,17 +30,19 @@
30
30
  "ora": "^6.3.1",
31
31
  "pino": "^8.19.0",
32
32
  "pino-pretty": "^11.0.0",
33
+ "resolve": "^1.22.8",
33
34
  "semver": "^7.6.0",
34
35
  "strip-ansi": "^7.1.0",
35
36
  "undici": "^6.9.0",
36
37
  "which": "^3.0.1",
37
- "@platformatic/config": "1.49.1",
38
- "@platformatic/utils": "1.49.1",
39
- "@platformatic/create-platformatic-auto": "1.49.1"
38
+ "@platformatic/config": "1.50.0",
39
+ "@platformatic/generators": "1.50.0",
40
+ "@platformatic/authenticate": "1.50.0",
41
+ "@platformatic/utils": "1.50.0"
40
42
  },
41
43
  "devDependencies": {
42
44
  "ajv": "^8.12.0",
43
- "borp": "^0.15.0",
45
+ "borp": "^0.16.0",
44
46
  "c8": "^10.0.0",
45
47
  "cross-env": "^7.0.3",
46
48
  "dotenv": "^16.4.5",
@@ -50,12 +52,14 @@
50
52
  "standard": "^17.1.0",
51
53
  "typescript": "~5.5.0",
52
54
  "yaml": "^2.4.1",
53
- "@platformatic/db": "1.49.1",
54
- "@platformatic/service": "1.49.1"
55
+ "@platformatic/composer": "1.50.0",
56
+ "@platformatic/db": "1.50.0",
57
+ "@platformatic/runtime": "1.50.0",
58
+ "@platformatic/service": "1.50.0"
55
59
  },
56
60
  "scripts": {
57
- "test:cli": "borp --pattern \"test/cli/*.test.mjs\" --timeout 120000 --concurrency=1",
58
- "test:unit": "pnpm run lint && cross-env NODE_OPTIONS=\"--loader=esmock --no-warnings\" borp --coverage --pattern \"test/unit/*.test.mjs\" --timeout 120000",
61
+ "test:cli": "borp --pattern \"test/cli/*test.mjs\" --timeout 120000 --concurrency=1",
62
+ "test:unit": "pnpm run lint && cross-env NODE_OPTIONS=\"--loader=esmock --no-warnings\" borp --pattern \"test/unit/*test.mjs\" --timeout 120000 --concurrency=1",
59
63
  "test": "npm run test:unit && npm run test:cli",
60
64
  "lint": "standard | snazzy"
61
65
  }
package/src/colors.mjs ADDED
@@ -0,0 +1,4 @@
1
+ import chalk, { supportsColor } from 'chalk'
2
+ const useColor = supportsColor && !process.env.NO_COLOR
3
+ const noop = (str) => str
4
+ export const pltGreen = useColor ? chalk.hex('#21FA90') : noop
@@ -0,0 +1,108 @@
1
+ import { execa } from 'execa'
2
+
3
+ export const GIT_FIRST_COMMIT_MESSAGE = 'Platformatic project started! 🚀'
4
+ export const GIT_MAIN_BRANCH = 'main'
5
+
6
+ /**
7
+ * Creates a Git repository and performs the initial commit if it doesn't already exist.
8
+ *
9
+ * This function checks if Git is installed, initializes a Git repository in the specified
10
+ * directory if it's not already a Git repository, and performs the initial commit.
11
+ *
12
+ * @param {import('pino.').BaseLogger} logger - The logger interface for logging messages.
13
+ * @param {string} [dir='.'] - The target directory where the Git repository should be created.
14
+ */
15
+ export async function createGitRepository (logger, dir = '.') {
16
+ if (!await isGitInstalled()) {
17
+ logger.error('Git is not installed')
18
+ return
19
+ }
20
+
21
+ if (!await gitInit(logger, dir)) {
22
+ return
23
+ }
24
+
25
+ if (!await gitCommit(logger, dir)) {
26
+ return
27
+ }
28
+
29
+ logger.info('Git repository initialized.')
30
+ }
31
+
32
+ /**
33
+ * Checks if Git is installed on the system.
34
+ *
35
+ * @async
36
+ * @returns {Promise<boolean>} A Promise that resolves to true if Git is installed, false otherwise.
37
+ */
38
+ async function isGitInstalled () {
39
+ try {
40
+ await execa('git', ['--version'])
41
+ return true
42
+ } catch (err) {
43
+ return false
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Checks if a Git repository exists in the specified directory.
49
+ *
50
+ * @async
51
+ * @param {string} dir - The directory to check for a Git repository.
52
+ * @returns {Promise<boolean>} A Promise that resolves to true if a Git repository exists in the directory, false otherwise.
53
+ */
54
+ async function doesGitRepositoryExist (dir) {
55
+ try {
56
+ await execa('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir })
57
+ return true
58
+ } catch (e) {
59
+ return false
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Initializes a Git repository in the specified directory if it doesn't already exist.
65
+ *
66
+ * @async
67
+ * @param {import('pino.').BaseLogger} - The logger object for logging messages.
68
+ * @param {string} dir - The directory where the Git repository should be initialized.
69
+ * @returns {Promise<boolean>} A Promise that resolves to true if the Git repository is successfully initialized, false otherwise.
70
+ */
71
+ async function gitInit (logger, dir) {
72
+ try {
73
+ if (await doesGitRepositoryExist(dir)) {
74
+ logger.info('Git repository already exists.')
75
+ return false
76
+ }
77
+
78
+ await execa('git', ['init', '-b', GIT_MAIN_BRANCH], { cwd: dir })
79
+ logger.debug('Git repository initialized.')
80
+ return true
81
+ } catch (err) {
82
+ logger.error('Git repository init failed.')
83
+ logger.debug({ err })
84
+ return false
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Commits changes in a Git repository located in the specified directory.
90
+ *
91
+ * @async
92
+ * @param {import('pino.').BaseLogger} - The logger object for logging messages.
93
+ * @param {string} dir - The directory of the Git repository where changes should be committed.
94
+ * @returns {Promise<boolean>} A Promise that resolves to true if the Git commit is successful, false otherwise.
95
+ */
96
+ async function gitCommit (logger, dir) {
97
+ try {
98
+ await execa('git', ['add', '-A'], { cwd: dir })
99
+ await execa('git', ['commit', '-n', '-m', GIT_FIRST_COMMIT_MESSAGE], { cwd: dir })
100
+ logger.debug('Git commit done.')
101
+ return true
102
+ } catch (err) {
103
+ console.log(err)
104
+ logger.error('Git commit failed.')
105
+ logger.debug({ err })
106
+ return false
107
+ }
108
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,317 @@
1
+ import { say } from './say.mjs'
2
+ import path, { basename, join } from 'node:path'
3
+ import inquirer from 'inquirer'
4
+ import generateName from 'boring-name-generator'
5
+ import { getUsername, getVersion, minimumSupportedNodeVersions, isCurrentVersionSupported, safeMkdir } from './utils.mjs'
6
+ import { createGitRepository } from './create-git-repository.mjs'
7
+ import { getPkgManager } from '@platformatic/utils'
8
+ import { StackableGenerator } from '@platformatic/generators'
9
+ import { getUserApiKey } from '@platformatic/authenticate'
10
+ import pino from 'pino'
11
+ import pretty from 'pino-pretty'
12
+ import { execa } from 'execa'
13
+ import parseArgs from 'minimist'
14
+ import ora from 'ora'
15
+ import { pathToFileURL } from 'node:url'
16
+ import { writeFile } from 'node:fs/promises'
17
+ import { request } from 'undici'
18
+ import { setTimeout } from 'node:timers/promises'
19
+ import resolve from 'resolve'
20
+
21
+ const MARKETPLACE_HOST = 'https://marketplace.platformatic.dev'
22
+
23
+ export async function fetchStackables (marketplaceHost, userApiKey) {
24
+ marketplaceHost = marketplaceHost || MARKETPLACE_HOST
25
+
26
+ const stackablesRequest = request(marketplaceHost + '/templates', {
27
+ method: 'GET',
28
+ headers: {
29
+ 'x-platformatic-user-api-key': userApiKey
30
+ }
31
+ })
32
+ const stackablesRequestTimeout = setTimeout(5000, new Error('Request timed out'))
33
+
34
+ try {
35
+ const { statusCode, body } = await Promise.race([
36
+ stackablesRequest,
37
+ stackablesRequestTimeout
38
+ ])
39
+ if (statusCode === 401 && userApiKey) {
40
+ return fetchStackables(marketplaceHost)
41
+ }
42
+ if (statusCode === 200) {
43
+ return (await body.json()).map(stackable => stackable.name)
44
+ }
45
+ } catch (err) {}
46
+
47
+ return ['@platformatic/composer', '@platformatic/db', '@platformatic/service']
48
+ }
49
+
50
+ export async function chooseStackable (stackables) {
51
+ const options = await inquirer.prompt({
52
+ type: 'list',
53
+ name: 'type',
54
+ message: 'Which kind of project do you want to create?',
55
+ default: stackables.indexOf('@platformatic/service'),
56
+ choices: stackables
57
+ })
58
+
59
+ return options.type
60
+ }
61
+
62
+ async function importOrLocal ({ pkgManager, name, projectDir, pkg }) {
63
+ try {
64
+ return await import(pkg)
65
+ } catch (err) {
66
+ try {
67
+ const fileToImport = resolve.sync(pkg, { basedir: projectDir })
68
+ return await import(pathToFileURL(fileToImport))
69
+ } catch { }
70
+
71
+ const spinner = ora(`Installing ${pkg}...`).start()
72
+ await execa(pkgManager, ['install', pkg], { cwd: projectDir })
73
+ spinner.succeed()
74
+
75
+ const fileToImport = resolve.sync(pkg, { basedir: projectDir })
76
+ return await import(pathToFileURL(fileToImport))
77
+ }
78
+ }
79
+
80
+ export const createPlatformatic = async (argv) => {
81
+ const args = parseArgs(argv, {
82
+ default: {
83
+ install: true
84
+ },
85
+ boolean: ['install'],
86
+ string: ['global-config', 'marketplace-host']
87
+ })
88
+
89
+ const username = await getUsername()
90
+ const version = await getVersion()
91
+ const greeting = username ? `Hello ${username},` : 'Hello,'
92
+ await say(`${greeting} welcome to ${version ? `Platformatic ${version}!` : 'Platformatic!'}`)
93
+
94
+ const currentVersion = process.versions.node
95
+ const supported = isCurrentVersionSupported(currentVersion)
96
+ if (!supported) {
97
+ const supportedVersions = minimumSupportedNodeVersions.join(' or >= ')
98
+ await say(`Platformatic is not supported on Node.js v${currentVersion}.`)
99
+ await say(`Please use one of the following Node.js versions >= ${supportedVersions}.`)
100
+ }
101
+
102
+ const logger = pino(pretty({
103
+ translateTime: 'SYS:HH:MM:ss',
104
+ ignore: 'hostname,pid'
105
+ }))
106
+
107
+ const pkgManager = getPkgManager()
108
+
109
+ const { projectType } = await inquirer.prompt({
110
+ type: 'list',
111
+ name: 'projectType',
112
+ message: 'What kind of project do you want to create?',
113
+ default: 'application',
114
+ choices: [
115
+ { name: 'Application', value: 'application' },
116
+ { name: 'Stackable', value: 'stackable' }
117
+ ]
118
+ })
119
+
120
+ if (projectType === 'application') {
121
+ await createApplication(args, logger, pkgManager)
122
+ } else {
123
+ await createStackable(args, logger, pkgManager)
124
+ }
125
+ }
126
+
127
+ async function createApplication (args, logger, pkgManager) {
128
+ const optionsDir = await inquirer.prompt({
129
+ type: 'input',
130
+ name: 'dir',
131
+ message: 'Where would you like to create your project?',
132
+ default: 'platformatic'
133
+ })
134
+
135
+ const projectDir = path.resolve(process.cwd(), optionsDir.dir)
136
+ const projectName = basename(projectDir)
137
+
138
+ await safeMkdir(projectDir)
139
+
140
+ const runtime = await importOrLocal({
141
+ pkgManager,
142
+ name: projectName,
143
+ projectDir,
144
+ pkg: '@platformatic/runtime'
145
+ })
146
+
147
+ const generator = new runtime.Generator({
148
+ logger,
149
+ name: projectName,
150
+ inquirer
151
+ })
152
+ generator.setConfig({
153
+ ...generator.config,
154
+ targetDirectory: projectDir
155
+ })
156
+
157
+ await generator.populateFromExistingConfig()
158
+ if (generator.existingConfig) {
159
+ await say('Using existing configuration')
160
+ }
161
+
162
+ const globalConfigPath = args['global-config']
163
+ const userApiKey = await getUserApiKey(globalConfigPath).catch(() => {})
164
+ const stackables = await fetchStackables(args['marketplace-host'], userApiKey)
165
+
166
+ const names = []
167
+
168
+ while (true) {
169
+ const stackableName = await chooseStackable(stackables)
170
+ // await say(`Creating a ${stackable} project in ${projectDir}...`)
171
+
172
+ const stackable = await importOrLocal({
173
+ pkgManager,
174
+ name: projectName,
175
+ projectDir,
176
+ pkg: stackableName
177
+ })
178
+
179
+ const { serviceName } = await inquirer.prompt({
180
+ type: 'input',
181
+ name: 'serviceName',
182
+ message: 'What is the name of the service?',
183
+ default: generateName().dashed,
184
+ validate: (value) => {
185
+ if (value.length === 0) {
186
+ return 'Please enter a name'
187
+ }
188
+
189
+ if (value.includes(' ')) {
190
+ return 'Please enter a name without spaces'
191
+ }
192
+
193
+ if (names.includes(value)) {
194
+ return 'This name is already used, please choose another one.'
195
+ }
196
+
197
+ return true
198
+ }
199
+ })
200
+
201
+ names.push(serviceName)
202
+
203
+ const stackableGenerator = new stackable.Generator({
204
+ logger,
205
+ inquirer
206
+ })
207
+
208
+ stackableGenerator.setConfig({
209
+ ...stackableGenerator.config,
210
+ serviceName
211
+ })
212
+
213
+ generator.addService(stackableGenerator, serviceName)
214
+
215
+ await stackableGenerator.ask()
216
+
217
+ const { shouldBreak } = await inquirer.prompt([
218
+ {
219
+ type: 'list',
220
+ name: 'shouldBreak',
221
+ message: 'Do you want to create another service?',
222
+ default: false,
223
+ choices: [{ name: 'yes', value: false }, { name: 'no', value: true }]
224
+ }
225
+ ])
226
+
227
+ if (shouldBreak) {
228
+ break
229
+ }
230
+ }
231
+
232
+ let entrypoint = ''
233
+
234
+ if (names.length > 1) {
235
+ const results = await inquirer.prompt([
236
+ {
237
+ type: 'list',
238
+ name: 'entrypoint',
239
+ message: 'Which service should be exposed?',
240
+ choices: names.map(name => ({ name, value: name }))
241
+ }
242
+ ])
243
+ entrypoint = results.entrypoint
244
+ } else {
245
+ entrypoint = names[0]
246
+ }
247
+
248
+ generator.setEntryPoint(entrypoint)
249
+
250
+ await generator.ask()
251
+ await generator.prepare()
252
+ await generator.writeFiles()
253
+
254
+ // Create project here
255
+
256
+ const { initGitRepository } = await inquirer.prompt({
257
+ type: 'list',
258
+ name: 'initGitRepository',
259
+ message: 'Do you want to init the git repository?',
260
+ default: false,
261
+ choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
262
+ })
263
+
264
+ if (initGitRepository) {
265
+ await createGitRepository(logger, projectDir)
266
+ }
267
+
268
+ if (pkgManager === 'pnpm') {
269
+ // add pnpm-workspace.yaml file if needed
270
+ const content = `packages:
271
+ # all packages in direct subdirs of packages/
272
+ - 'services/*'`
273
+ await (writeFile(join(projectDir, 'pnpm-workspace.yaml'), content))
274
+ }
275
+
276
+ if (args.install) {
277
+ const spinner = ora('Installing dependencies...').start()
278
+ await execa(pkgManager, ['install'], { cwd: projectDir })
279
+ spinner.succeed()
280
+ }
281
+
282
+ logger.info('Project created successfully, executing post-install actions...')
283
+ await generator.postInstallActions()
284
+ logger.info('You are all set! Run `npm start` to start your project.')
285
+ }
286
+
287
+ async function createStackable (args, logger, pkgManager) {
288
+ logger.info('Creating a stackable project...')
289
+
290
+ const generator = new StackableGenerator({ logger, inquirer })
291
+ await generator.ask()
292
+ await generator.prepare()
293
+ await generator.writeFiles()
294
+
295
+ const projectDir = path.resolve(process.cwd(), generator.config.targetDirectory)
296
+
297
+ const { initGitRepository } = await inquirer.prompt({
298
+ type: 'list',
299
+ name: 'initGitRepository',
300
+ message: 'Do you want to init the git repository?',
301
+ default: false,
302
+ choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
303
+ })
304
+
305
+ if (initGitRepository) {
306
+ await createGitRepository(logger, projectDir)
307
+ }
308
+
309
+ if (args.install) {
310
+ const spinner = ora('Installing dependencies...').start()
311
+ await execa(pkgManager, ['install'], { cwd: projectDir })
312
+ spinner.succeed()
313
+ }
314
+
315
+ await generator.postInstallActions()
316
+ logger.info('Stackable created successfully! Run `npm run create` to create an application.')
317
+ }
package/src/say.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import logUpdate from 'log-update'
2
+ import { pltGreen } from './colors.mjs'
3
+ import { sleep, randomBetween } from './utils.mjs'
4
+
5
+ export const say = async (messages) => {
6
+ const _messages = Array.isArray(messages) ? messages : [messages]
7
+
8
+ for (const message of _messages) {
9
+ const _message = Array.isArray(message) ? message : message.split(' ')
10
+ const msg = []
11
+ for (const word of [''].concat(_message)) {
12
+ msg.push(word)
13
+ logUpdate(pltGreen(msg.join(' ')))
14
+ await sleep(randomBetween(75, 100))
15
+ process.stdout.write('\u0007') // Do we want to enable terminal bell?
16
+ }
17
+ await sleep(randomBetween(75, 200))
18
+ }
19
+ logUpdate.done()
20
+ }
package/src/utils.mjs CHANGED
@@ -1,5 +1,16 @@
1
- import { access, mkdir } from 'fs/promises'
2
- import { resolve } from 'path'
1
+ import { execa } from 'execa'
2
+ import { access, constants, mkdir, readFile } from 'fs/promises'
3
+ import { resolve, join, dirname } from 'path'
4
+ import { createRequire } from 'module'
5
+ import semver from 'semver'
6
+ import * as desm from 'desm'
7
+ import * as url from 'url'
8
+
9
+ import ConfigManager from '@platformatic/config'
10
+
11
+ export const sleep = ms => new Promise((resolve) => setTimeout(resolve, ms))
12
+ export const randomBetween = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
13
+ const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
3
14
 
4
15
  export async function isFileAccessible (filename, directory) {
5
16
  try {
@@ -10,6 +21,123 @@ export async function isFileAccessible (filename, directory) {
10
21
  return false
11
22
  }
12
23
  }
24
+ /**
25
+ * Gets the username from git config or `whoami` command
26
+ * @returns string | null
27
+ */
28
+ export const getUsername = async () => {
29
+ try {
30
+ const { stdout } = await execa('git', ['config', 'user.name'])
31
+ if (stdout?.trim()) {
32
+ return stdout.trim()
33
+ }
34
+ } catch (err) {
35
+ // ignore: git failed
36
+ }
37
+ try {
38
+ const { stdout } = await execa('whoami')
39
+ if (stdout?.trim()) {
40
+ return stdout.trim()
41
+ }
42
+ } catch (err) {
43
+ // ignore: whoami failed
44
+ }
45
+
46
+ return null
47
+ }
48
+ /**
49
+ * Get the platformatic package version from package.json
50
+ * @returns string
51
+ */
52
+ /* c8 ignore next 4 */
53
+ export const getVersion = async () => {
54
+ const data = await readFile(desm.join(import.meta.url, '..', 'package.json'), 'utf8')
55
+ return JSON.parse(data).version
56
+ }
57
+
58
+ export async function isDirectoryWriteable (directory) {
59
+ try {
60
+ await access(directory, constants.R_OK | constants.W_OK)
61
+ return true
62
+ } catch (err) {
63
+ return false
64
+ }
65
+ }
66
+
67
+ export const findConfigFile = async (directory) => (ConfigManager.findConfigFile(directory))
68
+ export const findDBConfigFile = async (directory) => (ConfigManager.findConfigFile(directory, 'db'))
69
+ export const findServiceConfigFile = async (directory) => (ConfigManager.findConfigFile(directory, 'service'))
70
+ export const findComposerConfigFile = async (directory) => (ConfigManager.findConfigFile(directory, 'composer'))
71
+ export const findRuntimeConfigFile = async (directory) => (ConfigManager.findConfigFile(directory, 'runtime'))
72
+
73
+ /**
74
+ * Gets the version of the specified dependency package from package.json
75
+ * @param {string} dependencyName
76
+ * @returns string
77
+ */
78
+ export const getDependencyVersion = async (dependencyName) => {
79
+ const rootPackageJson = join(__dirname, '..', 'package.json')
80
+ const packageJsonContents = JSON.parse(await readFile(rootPackageJson, 'utf8'))
81
+ const dependencies = packageJsonContents.dependencies
82
+ const devDependencies = packageJsonContents.devDependencies
83
+ const regexp = /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/
84
+ if (dependencies[dependencyName]) {
85
+ const match = dependencies[dependencyName].match(regexp)
86
+ if (!match) {
87
+ return await resolveWorkspaceDependency(dependencyName)
88
+ }
89
+ return match[0]
90
+ }
91
+
92
+ if (devDependencies[dependencyName]) {
93
+ const match = devDependencies[dependencyName].match(regexp)
94
+ if (!match) {
95
+ return await resolveWorkspaceDependency(dependencyName)
96
+ }
97
+ return match[0]
98
+ }
99
+
100
+ async function resolveWorkspaceDependency (dependencyName) {
101
+ const require = createRequire(import.meta.url)
102
+ let dependencyPath = dirname(require.resolve(dependencyName))
103
+ // some deps are resolved not at their root level
104
+ // for instance 'typescript' will be risolved in its own ./lib directory
105
+ // next loop is to find the nearest parent directory that contains a package.json file
106
+ while (!await isFileAccessible(join(dependencyPath, 'package.json'))) {
107
+ dependencyPath = join(dependencyPath, '..')
108
+ if (dependencyPath === '/') {
109
+ throw new Error(`Cannot find package.json for ${dependencyName}`)
110
+ }
111
+ }
112
+ const pathToPackageJson = join(dependencyPath, 'package.json')
113
+ const packageJsonFile = await readFile(pathToPackageJson, 'utf-8')
114
+ const packageJson = JSON.parse(packageJsonFile)
115
+ return packageJson.version
116
+ }
117
+ }
118
+
119
+ export const minimumSupportedNodeVersions = ['18.8.0', '20.6.0']
120
+
121
+ export const isCurrentVersionSupported = (currentVersion) => {
122
+ // TODO: add try/catch if some unsupported node version is passed
123
+ for (const version of minimumSupportedNodeVersions) {
124
+ if (semver.major(currentVersion) === semver.major(version) && semver.gte(currentVersion, version)) {
125
+ return true
126
+ }
127
+ }
128
+ return false
129
+ }
130
+
131
+ export function convertServiceNameToPrefix (serviceName) {
132
+ return serviceName.replace(/-/g, '_').toUpperCase()
133
+ }
134
+ export function addPrefixToEnv (env, prefix) {
135
+ const output = {}
136
+ Object.entries(env).forEach(([key, value]) => {
137
+ output[`${prefix}_${key}`] = value
138
+ })
139
+ return output
140
+ }
13
141
 
14
142
  export async function safeMkdir (dir) {
15
143
  try {
File without changes
@@ -0,0 +1,8 @@
1
+ To have this test working, you need to link 'platformatic' globally with:
2
+
3
+ ```bash
4
+ # in platformatic root
5
+ cd packages/cli
6
+ pnpm link --global
7
+ ```
8
+ The reason is that the type generation is done expecting the 'platformatic' command to be in the cli, see: https://github.com/platformatic/platformatic/blob/main/packages/create-platformatic/src/service/create-service-cli.mjs#L120