create-platformatic 2.63.4 → 2.65.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.
@@ -23,4 +23,5 @@ if (isMain(import.meta)) {
23
23
  await createPlatformatic(_args)
24
24
  }
25
25
 
26
- export { createPlatformatic }
26
+ export * from './src/index.mjs'
27
+ export * from './src/utils.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-platformatic",
3
- "version": "2.63.4",
3
+ "version": "2.65.0",
4
4
  "description": "Create platformatic application interactive tool",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,7 +15,6 @@
15
15
  "license": "Apache-2.0",
16
16
  "author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
17
17
  "dependencies": {
18
- "chalk": "^5.3.0",
19
18
  "columnify": "^1.6.0",
20
19
  "commist": "^3.2.0",
21
20
  "desm": "^1.3.1",
@@ -23,7 +22,6 @@
23
22
  "execa": "^9.0.0",
24
23
  "help-me": "^5.0.0",
25
24
  "inquirer": "^9.2.16",
26
- "log-update": "^6.0.0",
27
25
  "minimist": "^1.2.8",
28
26
  "ora": "^6.3.1",
29
27
  "pino": "^9.0.0",
@@ -33,9 +31,9 @@
33
31
  "strip-ansi": "^7.1.0",
34
32
  "undici": "^7.0.0",
35
33
  "which": "^3.0.1",
36
- "@platformatic/config": "2.63.4",
37
- "@platformatic/generators": "2.63.4",
38
- "@platformatic/utils": "2.63.4"
34
+ "@platformatic/config": "2.65.0",
35
+ "@platformatic/utils": "2.65.0",
36
+ "@platformatic/generators": "2.65.0"
39
37
  },
40
38
  "devDependencies": {
41
39
  "@types/node": "^22.5.0",
@@ -50,10 +48,10 @@
50
48
  "neostandard": "^0.12.0",
51
49
  "typescript": "~5.8.0",
52
50
  "yaml": "^2.4.1",
53
- "@platformatic/composer": "2.63.4",
54
- "@platformatic/db": "2.63.4",
55
- "@platformatic/runtime": "2.63.4",
56
- "@platformatic/service": "2.63.4"
51
+ "@platformatic/composer": "2.65.0",
52
+ "@platformatic/db": "2.65.0",
53
+ "@platformatic/runtime": "2.65.0",
54
+ "@platformatic/service": "2.65.0"
57
55
  },
58
56
  "scripts": {
59
57
  "test:cli": "borp --pattern \"test/cli/*test.mjs\" --timeout=300000 --concurrency=1",
package/src/index.mjs CHANGED
@@ -1,11 +1,10 @@
1
1
  import { ConfigManager } from '@platformatic/config'
2
- import { createDirectory, generateDashedName, getPkgManager } from '@platformatic/utils'
2
+ import { createDirectory, executeWithTimeout, generateDashedName, getPkgManager } from '@platformatic/utils'
3
3
  import { execa } from 'execa'
4
- import inquirer from 'inquirer'
4
+ import defaultInquirer from 'inquirer'
5
5
  import parseArgs from 'minimist'
6
6
  import { readFile, writeFile } from 'node:fs/promises'
7
- import path, { basename, join } from 'node:path'
8
- import { setTimeout } from 'node:timers/promises'
7
+ import { basename, join, resolve as pathResolve } from 'node:path'
9
8
  import { pathToFileURL } from 'node:url'
10
9
  import ora from 'ora'
11
10
  import pino from 'pino'
@@ -13,40 +12,41 @@ import pretty from 'pino-pretty'
13
12
  import resolve from 'resolve'
14
13
  import { request } from 'undici'
15
14
  import { createGitRepository } from './create-git-repository.mjs'
16
- import { say } from './say.mjs'
17
- import { getUsername, getVersion } from './utils.mjs'
18
-
15
+ import { getUsername, getVersion, say } from './utils.mjs'
19
16
  const MARKETPLACE_HOST = 'https://marketplace.platformatic.dev'
20
- const defaultStackables = ['@platformatic/composer', '@platformatic/db', '@platformatic/service']
17
+ const defaultStackables = ['@platformatic/service', '@platformatic/composer', '@platformatic/db']
18
+
19
+ export async function fetchStackables (marketplaceHost, modules = []) {
20
+ const stackables = new Set([...modules, ...defaultStackables])
21
21
 
22
- export async function fetchStackables (marketplaceHost) {
23
22
  // Skip the remote network request if we are running tests
24
23
  if (process.env.MARKETPLACE_TEST) {
25
- return [...defaultStackables]
24
+ return Array.from(stackables)
26
25
  }
27
26
 
28
- marketplaceHost = marketplaceHost || MARKETPLACE_HOST
29
-
30
- const stackablesRequest = request(marketplaceHost + '/templates')
31
- const stackablesRequestTimeout = setTimeout(5000, new Error('Request timed out'))
32
-
27
+ let response
33
28
  try {
34
- const { statusCode, body } = await Promise.race([stackablesRequest, stackablesRequestTimeout])
35
- if (statusCode === 200) {
36
- return (await body.json()).map(stackable => stackable.name)
29
+ response = await executeWithTimeout(request(new URL('/templates', marketplaceHost || MARKETPLACE_HOST)), 5000)
30
+ } catch (err) {
31
+ // No-op: we just use the default stackables
32
+ }
33
+
34
+ if (response && response.statusCode === 200) {
35
+ for (const stackable of await response.body.json()) {
36
+ stackables.add(stackable.name)
37
37
  }
38
- } catch (err) {}
38
+ }
39
39
 
40
- return [...defaultStackables]
40
+ return Array.from(stackables)
41
41
  }
42
42
 
43
- export async function chooseStackable (stackables) {
43
+ export async function chooseStackable (inquirer, stackables) {
44
44
  const options = await inquirer.prompt({
45
45
  type: 'list',
46
46
  name: 'type',
47
- message: 'Which kind of project do you want to create?',
48
- default: stackables.indexOf('@platformatic/service'),
49
- choices: stackables,
47
+ message: 'Which kind of service do you want to create?',
48
+ default: stackables[0],
49
+ choices: stackables
50
50
  })
51
51
 
52
52
  return options.type
@@ -59,7 +59,9 @@ async function importOrLocal ({ pkgManager, name, projectDir, pkg }) {
59
59
  try {
60
60
  const fileToImport = resolve.sync(pkg, { basedir: projectDir })
61
61
  return await import(pathToFileURL(fileToImport))
62
- } catch {}
62
+ } catch {
63
+ // No-op
64
+ }
63
65
 
64
66
  let version = ''
65
67
 
@@ -84,13 +86,14 @@ async function importOrLocal ({ pkgManager, name, projectDir, pkg }) {
84
86
  }
85
87
  }
86
88
 
87
- export const createPlatformatic = async argv => {
89
+ export async function createPlatformatic (argv) {
88
90
  const args = parseArgs(argv, {
89
91
  default: {
90
92
  install: true,
93
+ module: []
91
94
  },
92
95
  boolean: ['install'],
93
- string: ['global-config', 'marketplace-host'],
96
+ string: ['global-config', 'marketplace-host', 'module']
94
97
  })
95
98
 
96
99
  const username = await getUsername()
@@ -101,65 +104,78 @@ export const createPlatformatic = async argv => {
101
104
  const logger = pino(
102
105
  pretty({
103
106
  translateTime: 'SYS:HH:MM:ss',
104
- ignore: 'hostname,pid',
107
+ ignore: 'hostname,pid'
105
108
  })
106
109
  )
107
110
 
108
111
  const pkgManager = getPkgManager()
109
- await createApplication(args, logger, pkgManager)
112
+ const modules = Array.isArray(args.module) ? args.module : [args.module]
113
+ await createApplication(logger, pkgManager, modules, args['marketplace-host'], args['install'])
110
114
  }
111
115
 
112
- async function createApplication (args, logger, pkgManager) {
116
+ export async function createApplication (
117
+ logger,
118
+ packageManager,
119
+ modules,
120
+ marketplaceHost,
121
+ install,
122
+ additionalGeneratorOptions = {}
123
+ ) {
124
+ // This is only used for testing for now, but might be useful in the future
125
+ const inquirer = process.env.INQUIRER_PATH ? await import(process.env.INQUIRER_PATH) : defaultInquirer
126
+
113
127
  let projectDir = process.cwd()
114
128
  if (!(await ConfigManager.findConfigFile())) {
115
129
  const optionsDir = await inquirer.prompt({
116
130
  type: 'input',
117
131
  name: 'dir',
118
132
  message: 'Where would you like to create your project?',
119
- default: 'platformatic',
133
+ default: 'platformatic'
120
134
  })
121
135
 
122
- projectDir = path.resolve(process.cwd(), optionsDir.dir)
136
+ projectDir = pathResolve(process.cwd(), optionsDir.dir)
123
137
  }
124
138
  const projectName = basename(projectDir)
125
139
 
126
140
  await createDirectory(projectDir)
127
141
 
128
142
  const runtime = await importOrLocal({
129
- pkgManager,
143
+ pkgManager: packageManager,
130
144
  name: projectName,
131
145
  projectDir,
132
- pkg: '@platformatic/runtime',
146
+ pkg: '@platformatic/runtime'
133
147
  })
134
148
 
135
149
  const generator = new runtime.Generator({
136
150
  logger,
137
151
  name: projectName,
138
152
  inquirer,
153
+ ...additionalGeneratorOptions
139
154
  })
155
+
140
156
  generator.setConfig({
141
157
  ...generator.config,
142
- targetDirectory: projectDir,
158
+ targetDirectory: projectDir
143
159
  })
144
160
 
145
161
  await generator.populateFromExistingConfig()
146
162
  if (generator.existingConfig) {
147
- await say('Using existing configuration')
163
+ await say('Using existing configuration ...')
148
164
  }
149
165
 
150
- const stackables = await fetchStackables(args['marketplace-host'])
166
+ const stackables = await fetchStackables(marketplaceHost, modules)
151
167
 
152
- const names = []
168
+ const names = generator.existingServices ?? []
153
169
 
154
170
  while (true) {
155
- const stackableName = await chooseStackable(stackables)
171
+ const stackableName = await chooseStackable(inquirer, stackables)
156
172
  // await say(`Creating a ${stackable} project in ${projectDir}...`)
157
173
 
158
174
  const stackable = await importOrLocal({
159
- pkgManager,
175
+ pkgManager: packageManager,
160
176
  name: projectName,
161
177
  projectDir,
162
- pkg: stackableName,
178
+ pkg: stackableName
163
179
  })
164
180
 
165
181
  const { serviceName } = await inquirer.prompt({
@@ -181,19 +197,19 @@ async function createApplication (args, logger, pkgManager) {
181
197
  }
182
198
 
183
199
  return true
184
- },
200
+ }
185
201
  })
186
202
 
187
203
  names.push(serviceName)
188
204
 
189
205
  const stackableGenerator = new stackable.Generator({
190
206
  logger,
191
- inquirer,
207
+ inquirer
192
208
  })
193
209
 
194
210
  stackableGenerator.setConfig({
195
211
  ...stackableGenerator.config,
196
- serviceName,
212
+ serviceName
197
213
  })
198
214
 
199
215
  generator.addService(stackableGenerator, serviceName)
@@ -208,9 +224,9 @@ async function createApplication (args, logger, pkgManager) {
208
224
  default: false,
209
225
  choices: [
210
226
  { name: 'yes', value: false },
211
- { name: 'no', value: true },
212
- ],
213
- },
227
+ { name: 'no', value: true }
228
+ ]
229
+ }
214
230
  ])
215
231
 
216
232
  if (shouldBreak) {
@@ -219,15 +235,16 @@ async function createApplication (args, logger, pkgManager) {
219
235
  }
220
236
 
221
237
  let entrypoint = ''
238
+ const chooseEntrypoint = names.length > 1 && (!generator.existingConfigRaw || !generator.existingConfigRaw.entrypoint)
222
239
 
223
- if (names.length > 1) {
240
+ if (chooseEntrypoint) {
224
241
  const results = await inquirer.prompt([
225
242
  {
226
243
  type: 'list',
227
244
  name: 'entrypoint',
228
245
  message: 'Which service should be exposed?',
229
- choices: names.map(name => ({ name, value: name })),
230
- },
246
+ choices: names.map(name => ({ name, value: name }))
247
+ }
231
248
  ])
232
249
  entrypoint = results.entrypoint
233
250
  } else {
@@ -238,40 +255,54 @@ async function createApplication (args, logger, pkgManager) {
238
255
 
239
256
  await generator.ask()
240
257
  await generator.prepare()
258
+
259
+ if (chooseEntrypoint) {
260
+ // This can return null if the generator was not supposed to modify the config
261
+ const configObject = generator.getFileObject(generator.runtimeConfig)
262
+ const config = configObject ? JSON.parse(configObject.contents) : generator.existingConfigRaw
263
+ config.entrypoint = entrypoint
264
+
265
+ generator.addFile({ path: '', file: generator.runtimeConfig, contents: JSON.stringify(config, null, 2) })
266
+ }
267
+
241
268
  await generator.writeFiles()
242
269
 
243
270
  // Create project here
271
+ if (!generator.existingConfigRaw) {
272
+ const { initGitRepository } = await inquirer.prompt({
273
+ type: 'list',
274
+ name: 'initGitRepository',
275
+ message: 'Do you want to init the git repository?',
276
+ default: false,
277
+ choices: [
278
+ { name: 'yes', value: true },
279
+ { name: 'no', value: false }
280
+ ]
281
+ })
244
282
 
245
- const { initGitRepository } = await inquirer.prompt({
246
- type: 'list',
247
- name: 'initGitRepository',
248
- message: 'Do you want to init the git repository?',
249
- default: false,
250
- choices: [
251
- { name: 'yes', value: true },
252
- { name: 'no', value: false },
253
- ],
254
- })
255
-
256
- if (initGitRepository) {
257
- await createGitRepository(logger, projectDir)
283
+ if (initGitRepository) {
284
+ await createGitRepository(logger, projectDir)
285
+ }
258
286
  }
259
287
 
260
- if (pkgManager === 'pnpm') {
288
+ if (packageManager === 'pnpm') {
261
289
  // add pnpm-workspace.yaml file if needed
262
290
  const content = `packages:
263
291
  # all packages in direct subdirs of packages/
264
- - 'services/*'`
292
+ - 'services/*'
293
+ - 'web/*'`
265
294
  await writeFile(join(projectDir, 'pnpm-workspace.yaml'), content)
266
295
  }
267
296
 
268
- if (args.install) {
297
+ if (typeof install === 'function') {
298
+ await install(projectDir, generator.runtimeConfig, packageManager)
299
+ } else if (install) {
269
300
  const spinner = ora('Installing dependencies...').start()
270
- await execa(pkgManager, ['install'], { cwd: projectDir })
301
+ await execa(packageManager, ['install'], { cwd: projectDir })
271
302
  spinner.succeed()
272
303
  }
273
304
 
274
305
  logger.info('Project created successfully, executing post-install actions...')
275
306
  await generator.postInstallActions()
276
- logger.info('You are all set! Run `npm start` to start your project.')
307
+ logger.info(`You are all set! Run \`${packageManager} start\` to start your project.`)
277
308
  }
package/src/utils.mjs CHANGED
@@ -11,6 +11,14 @@ export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
11
11
  export const randomBetween = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
12
12
  const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
13
13
 
14
+ const ansiCodes = {
15
+ // Platformatic Green: #21FA90
16
+ pltGreen: '\u001B[38;2;33;250;144m',
17
+ bell: '\u0007',
18
+ reset: '\u001b[0m',
19
+ erasePreviousLine: '\u001b[1K',
20
+ }
21
+
14
22
  export async function isFileAccessible (filename, directory) {
15
23
  try {
16
24
  const filePath = directory ? resolve(directory, filename) : filename
@@ -118,6 +126,7 @@ export const getDependencyVersion = async dependencyName => {
118
126
  export function convertServiceNameToPrefix (serviceName) {
119
127
  return serviceName.replace(/-/g, '_').toUpperCase()
120
128
  }
129
+
121
130
  export function addPrefixToEnv (env, prefix) {
122
131
  const output = {}
123
132
  Object.entries(env).forEach(([key, value]) => {
@@ -125,3 +134,25 @@ export function addPrefixToEnv (env, prefix) {
125
134
  })
126
135
  return output
127
136
  }
137
+
138
+ export async function say (message) {
139
+ // Disable if not supporting colors
140
+ if (process.env.NO_COLOR) {
141
+ console.log(message)
142
+ return
143
+ }
144
+
145
+ const words = message.split(' ')
146
+
147
+ for (let i = 0; i <= words.length; i++) {
148
+ if (i > 0) {
149
+ process.stdout.write('\r' + ansiCodes.erasePreviousLine)
150
+ }
151
+
152
+ process.stdout.write(ansiCodes.pltGreen + words.slice(0, i).join(' ') + ansiCodes.reset + ansiCodes.bell)
153
+ await sleep(randomBetween(75, 100))
154
+ }
155
+
156
+ process.stdout.write('\n')
157
+ await sleep(randomBetween(75, 200))
158
+ }
package/src/colors.mjs DELETED
@@ -1,4 +0,0 @@
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
package/src/say.mjs DELETED
@@ -1,29 +0,0 @@
1
- import logUpdate from 'log-update'
2
- import { pltGreen } from './colors.mjs'
3
- import { randomBetween, sleep } from './utils.mjs'
4
-
5
- export async function say (messages) {
6
- const _messages = Array.isArray(messages) ? messages : [messages]
7
-
8
- if (process.env.NO_COLOR) {
9
- for (const message of _messages) {
10
- console.log(message)
11
- }
12
-
13
- logUpdate.done()
14
- return
15
- }
16
-
17
- for (const message of _messages) {
18
- const _message = Array.isArray(message) ? message : message.split(' ')
19
- const msg = []
20
- for (const word of [''].concat(_message)) {
21
- msg.push(word)
22
- logUpdate(pltGreen(msg.join(' ')))
23
- await sleep(randomBetween(75, 100))
24
- process.stdout.write('\u0007') // Do we want to enable terminal bell?
25
- }
26
- await sleep(randomBetween(75, 200))
27
- }
28
- logUpdate.done()
29
- }