create-platformatic 0.11.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.
- package/.taprc +1 -0
- package/LICENSE +201 -0
- package/NOTICE +13 -0
- package/README.md +13 -0
- package/create-platformatic.mjs +31 -0
- package/help/db.txt +11 -0
- package/help/help.txt +8 -0
- package/help/service.txt +7 -0
- package/package.json +47 -0
- package/src/ask-project-dir.mjs +18 -0
- package/src/colors.mjs +4 -0
- package/src/create-gitignore.mjs +34 -0
- package/src/create-package-json.mjs +32 -0
- package/src/db/README.md +38 -0
- package/src/db/create-db-cli.mjs +174 -0
- package/src/db/create-db.mjs +200 -0
- package/src/get-pkg-manager.mjs +11 -0
- package/src/ghaction.mjs +68 -0
- package/src/index.mjs +51 -0
- package/src/say.mjs +20 -0
- package/src/service/README.md +31 -0
- package/src/service/create-service-cli.mjs +88 -0
- package/src/service/create-service.mjs +94 -0
- package/src/utils.mjs +95 -0
- package/test/create-gitignore.test.mjs +35 -0
- package/test/create-package-json.test.mjs +56 -0
- package/test/db/create-db.test.mjs +241 -0
- package/test/get-pkg-manager.test.mjs +36 -0
- package/test/ghaction.test.mjs +62 -0
- package/test/service/create-service.test.mjs +88 -0
- package/test/utils.test.mjs +182 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { writeFile, mkdir } from 'fs/promises'
|
|
2
|
+
import { join, relative, resolve } from 'path'
|
|
3
|
+
import { findDBConfigFile, isFileAccessible } from '../utils.mjs'
|
|
4
|
+
|
|
5
|
+
const connectionStrings = {
|
|
6
|
+
postgres: 'postgres://postgres:postgres@localhost:5432/postgres',
|
|
7
|
+
sqlite: 'sqlite://./db.sqlite',
|
|
8
|
+
mysql: 'mysql://root@localhost:3306/graph',
|
|
9
|
+
mysql8: 'mysql://root@localhost:3308/graph',
|
|
10
|
+
mariadb: 'mysql://root@localhost:3307/graph'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const moviesMigrationDo = `
|
|
14
|
+
-- Add SQL in this file to create the database tables for your API
|
|
15
|
+
CREATE TABLE IF NOT EXISTS movies (
|
|
16
|
+
id INTEGER PRIMARY KEY,
|
|
17
|
+
title TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
`
|
|
20
|
+
|
|
21
|
+
const moviesMigrationUndo = `
|
|
22
|
+
-- Add SQL in this file to drop the database tables
|
|
23
|
+
DROP TABLE movies;
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
function getTsConfig (outDir) {
|
|
27
|
+
return {
|
|
28
|
+
compilerOptions: {
|
|
29
|
+
module: 'commonjs',
|
|
30
|
+
esModuleInterop: true,
|
|
31
|
+
target: 'es6',
|
|
32
|
+
sourceMap: true,
|
|
33
|
+
pretty: true,
|
|
34
|
+
noEmitOnError: true,
|
|
35
|
+
outDir
|
|
36
|
+
},
|
|
37
|
+
watchOptions: {
|
|
38
|
+
watchFile: 'fixedPollingInterval',
|
|
39
|
+
watchDirectory: 'fixedPollingInterval',
|
|
40
|
+
fallbackPolling: 'dynamicPriority',
|
|
41
|
+
synchronousWatchDirectory: true,
|
|
42
|
+
excludeDirectories: ['**/node_modules', outDir]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const getPluginName = (isTypescript) => isTypescript === true ? 'plugin.ts' : 'plugin.js'
|
|
48
|
+
const TS_OUT_DIR = 'dist'
|
|
49
|
+
|
|
50
|
+
function generateConfig (migrations, plugin, types, typescript) {
|
|
51
|
+
const config = {
|
|
52
|
+
$schema: './platformatic.db.schema.json',
|
|
53
|
+
server: {
|
|
54
|
+
hostname: '{PLT_SERVER_HOSTNAME}',
|
|
55
|
+
port: '{PORT}',
|
|
56
|
+
logger: {
|
|
57
|
+
level: '{PLT_SERVER_LOGGER_LEVEL}'
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
core: {
|
|
61
|
+
connectionString: '{DATABASE_URL}',
|
|
62
|
+
graphql: true,
|
|
63
|
+
openapi: true
|
|
64
|
+
},
|
|
65
|
+
migrations: { dir: migrations }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (plugin === true) {
|
|
69
|
+
config.plugin = {
|
|
70
|
+
path: getPluginName(typescript)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (types === true) {
|
|
75
|
+
config.types = {
|
|
76
|
+
autogenerate: true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typescript === true) {
|
|
81
|
+
config.plugin.typescript = {
|
|
82
|
+
outDir: TS_OUT_DIR
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return config
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function generateEnv (hostname, port, database) {
|
|
90
|
+
const connectionString = connectionStrings[database]
|
|
91
|
+
const env = `\
|
|
92
|
+
PLT_SERVER_HOSTNAME=${hostname}
|
|
93
|
+
PORT=${port}
|
|
94
|
+
PLT_SERVER_LOGGER_LEVEL=info
|
|
95
|
+
DATABASE_URL=${connectionString}
|
|
96
|
+
`
|
|
97
|
+
return env
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const JS_PLUGIN_WITH_TYPES_SUPPORT = `\
|
|
101
|
+
/// <reference path="./global.d.ts" />
|
|
102
|
+
'use strict'
|
|
103
|
+
|
|
104
|
+
/** @param {import('fastify').FastifyInstance} app */
|
|
105
|
+
module.exports = async function (app) {}
|
|
106
|
+
`
|
|
107
|
+
|
|
108
|
+
const TS_PLUGIN_WITH_TYPES_SUPPORT = `\
|
|
109
|
+
/// <reference path="./global.d.ts" />
|
|
110
|
+
import { FastifyInstance } from 'fastify'
|
|
111
|
+
|
|
112
|
+
export default async function (app: FastifyInstance) {}
|
|
113
|
+
`
|
|
114
|
+
|
|
115
|
+
async function generatePluginWithTypesSupport (logger, currentDir, isTypescript) {
|
|
116
|
+
const pluginPath = resolve(currentDir, getPluginName(isTypescript))
|
|
117
|
+
|
|
118
|
+
const isPluginExists = await isFileAccessible(pluginPath)
|
|
119
|
+
if (isPluginExists) {
|
|
120
|
+
logger.info(`Plugin file ${pluginPath} found, skipping creation of plugin file.`)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const pluginTemplate = isTypescript
|
|
125
|
+
? TS_PLUGIN_WITH_TYPES_SUPPORT
|
|
126
|
+
: JS_PLUGIN_WITH_TYPES_SUPPORT
|
|
127
|
+
|
|
128
|
+
await writeFile(pluginPath, pluginTemplate)
|
|
129
|
+
logger.info(`Plugin file created at ${relative(currentDir, pluginPath)}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function createDB ({ hostname, database = 'sqlite', port, migrations = 'migrations', plugin = true, types = true, typescript = false }, logger, currentDir) {
|
|
133
|
+
const createMigrations = !!migrations // If we don't define a migrations folder, we don't create it
|
|
134
|
+
const accessibleConfigFilename = await findDBConfigFile(currentDir)
|
|
135
|
+
if (accessibleConfigFilename === undefined) {
|
|
136
|
+
const config = generateConfig(migrations, plugin, types, typescript)
|
|
137
|
+
await writeFile(join(currentDir, 'platformatic.db.json'), JSON.stringify(config, null, 2))
|
|
138
|
+
logger.info('Configuration file platformatic.db.json successfully created.')
|
|
139
|
+
|
|
140
|
+
const env = generateEnv(hostname, port, database)
|
|
141
|
+
await writeFile(join(currentDir, '.env'), env)
|
|
142
|
+
await writeFile(join(currentDir, '.env.sample'), env)
|
|
143
|
+
logger.info('Environment file .env successfully created.')
|
|
144
|
+
} else {
|
|
145
|
+
logger.info(`Configuration file ${accessibleConfigFilename} found, skipping creation of configuration file.`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const migrationsFolderName = migrations
|
|
149
|
+
if (createMigrations) {
|
|
150
|
+
const isMigrationFolderExists = await isFileAccessible(migrationsFolderName, currentDir)
|
|
151
|
+
if (!isMigrationFolderExists) {
|
|
152
|
+
await mkdir(join(currentDir, migrationsFolderName))
|
|
153
|
+
logger.info(`Migrations folder ${migrationsFolderName} successfully created.`)
|
|
154
|
+
} else {
|
|
155
|
+
logger.info(`Migrations folder ${migrationsFolderName} found, skipping creation of migrations folder.`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const migrationFileNameDo = '001.do.sql'
|
|
160
|
+
const migrationFileNameUndo = '001.undo.sql'
|
|
161
|
+
const migrationFilePathDo = join(currentDir, migrationsFolderName, migrationFileNameDo)
|
|
162
|
+
const migrationFilePathUndo = join(currentDir, migrationsFolderName, migrationFileNameUndo)
|
|
163
|
+
const isMigrationFileDoExists = await isFileAccessible(migrationFilePathDo)
|
|
164
|
+
const isMigrationFileUndoExists = await isFileAccessible(migrationFilePathUndo)
|
|
165
|
+
if (!isMigrationFileDoExists && createMigrations) {
|
|
166
|
+
await writeFile(migrationFilePathDo, moviesMigrationDo)
|
|
167
|
+
logger.info(`Migration file ${migrationFileNameDo} successfully created.`)
|
|
168
|
+
if (!isMigrationFileUndoExists) {
|
|
169
|
+
await writeFile(migrationFilePathUndo, moviesMigrationUndo)
|
|
170
|
+
logger.info(`Migration file ${migrationFileNameUndo} successfully created.`)
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
logger.info(`Migration file ${migrationFileNameDo} found, skipping creation of migration file.`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (typescript === true) {
|
|
177
|
+
const tsConfigFileName = join(currentDir, 'tsconfig.json')
|
|
178
|
+
const isTsConfigExists = await isFileAccessible(tsConfigFileName)
|
|
179
|
+
if (!isTsConfigExists) {
|
|
180
|
+
const tsConfig = getTsConfig(TS_OUT_DIR)
|
|
181
|
+
await writeFile(tsConfigFileName, JSON.stringify(tsConfig, null, 2))
|
|
182
|
+
logger.info(`Typescript configuration file ${tsConfigFileName} successfully created.`)
|
|
183
|
+
} else {
|
|
184
|
+
logger.info(`Typescript configuration file ${tsConfigFileName} found, skipping creation of typescript configuration file.`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (plugin) {
|
|
189
|
+
await generatePluginWithTypesSupport(logger, currentDir, typescript)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
DATABASE_URL: connectionStrings[database],
|
|
194
|
+
PLT_SERVER_LOGGER_LEVEL: 'info',
|
|
195
|
+
PORT: port,
|
|
196
|
+
PLT_SERVER_HOSTNAME: hostname
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default createDB
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
export const getPkgManager = () => {
|
|
3
|
+
const userAgent = process.env.npm_config_user_agent
|
|
4
|
+
if (!userAgent) {
|
|
5
|
+
return 'npm'
|
|
6
|
+
}
|
|
7
|
+
const pmSpec = userAgent.split(' ')[0]
|
|
8
|
+
const separatorPos = pmSpec.lastIndexOf('/')
|
|
9
|
+
const name = pmSpec.substring(0, separatorPos)
|
|
10
|
+
return name || 'npm'
|
|
11
|
+
}
|
package/src/ghaction.mjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import mkdirp from 'mkdirp'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import { isFileAccessible } from './utils.mjs'
|
|
5
|
+
import { writeFile } from 'fs/promises'
|
|
6
|
+
|
|
7
|
+
export const ghTemplate = (env, type) => {
|
|
8
|
+
const envAsStr = Object.keys(env).reduce((acc, key) => {
|
|
9
|
+
acc += ` ${key}: ${env[key]} \n`
|
|
10
|
+
return acc
|
|
11
|
+
}, '')
|
|
12
|
+
|
|
13
|
+
return `name: Deploy Platformatic application to the cloud
|
|
14
|
+
on:
|
|
15
|
+
pull_request:
|
|
16
|
+
paths-ignore:
|
|
17
|
+
- 'docs/**'
|
|
18
|
+
- '**.md'
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
build_and_deploy:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout application project repository
|
|
25
|
+
uses: actions/checkout@v3
|
|
26
|
+
- name: npm install --omit=dev
|
|
27
|
+
run: npm install --omit=dev
|
|
28
|
+
- name: Deploy project
|
|
29
|
+
uses: platformatic/onestep@latest
|
|
30
|
+
with:
|
|
31
|
+
github_token: \${{ secrets.GITHUB_TOKEN }}
|
|
32
|
+
platformatic_api_key: \${{ secrets.PLATFORMATIC_API_KEY }}
|
|
33
|
+
platformatic_config_path: ./platformatic.${type}.json
|
|
34
|
+
env:
|
|
35
|
+
${envAsStr}
|
|
36
|
+
`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const createGHAction = async (logger, env, type, projectDir) => {
|
|
40
|
+
const ghActionFileName = 'platformatic-deploy.yml'
|
|
41
|
+
const ghActionFilePath = join(projectDir, '.github', 'workflows', ghActionFileName)
|
|
42
|
+
const isGithubActionExists = await isFileAccessible(ghActionFilePath)
|
|
43
|
+
if (!isGithubActionExists) {
|
|
44
|
+
await mkdirp(join(projectDir, '.github', 'workflows'))
|
|
45
|
+
await writeFile(ghActionFilePath, ghTemplate(env, type))
|
|
46
|
+
logger.info('Github action successfully created, please add PLATFORMATIC_API_KEY as repository secret.')
|
|
47
|
+
const isGitDir = await isFileAccessible('.git', projectDir)
|
|
48
|
+
if (!isGitDir) {
|
|
49
|
+
logger.warn('No git repository found. The Github action won\'t be triggered.')
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
logger.info(`Github action file ${ghActionFilePath} found, skipping creation of github action file.`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* c8 ignore next 12 */
|
|
57
|
+
export const askCreateGHAction = async (logger, env, type, projectDir = process.cwd()) => {
|
|
58
|
+
const { githubAction } = await inquirer.prompt([{
|
|
59
|
+
type: 'list',
|
|
60
|
+
name: 'githubAction',
|
|
61
|
+
message: 'Do you want to create the github action to deploy this application to Platformatic Cloud?',
|
|
62
|
+
default: true,
|
|
63
|
+
choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
|
|
64
|
+
}])
|
|
65
|
+
if (githubAction) {
|
|
66
|
+
await createGHAction(logger, env, type, projectDir)
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { say } from './say.mjs'
|
|
2
|
+
import helpMe from 'help-me'
|
|
3
|
+
import { join } from 'desm'
|
|
4
|
+
import inquirer from 'inquirer'
|
|
5
|
+
import createPlatformaticDB from './db/create-db-cli.mjs'
|
|
6
|
+
import createPlatformaticService from './service/create-service-cli.mjs'
|
|
7
|
+
import commist from 'commist'
|
|
8
|
+
import { getUsername, getVersion } from './utils.mjs'
|
|
9
|
+
|
|
10
|
+
const createPlatformatic = async (argv) => {
|
|
11
|
+
const help = helpMe({
|
|
12
|
+
dir: join(import.meta.url, '..', 'help'),
|
|
13
|
+
ext: '.txt'
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const program = commist({ maxDistance: 4 })
|
|
17
|
+
|
|
18
|
+
program.register('help', help.toStdout)
|
|
19
|
+
program.register('db help', help.toStdout.bind(null, ['db']))
|
|
20
|
+
program.register('service help', help.toStdout.bind(null, ['service']))
|
|
21
|
+
program.register('db', createPlatformaticDB)
|
|
22
|
+
program.register('service', createPlatformaticService)
|
|
23
|
+
|
|
24
|
+
const result = program.parse(argv)
|
|
25
|
+
|
|
26
|
+
if (result) {
|
|
27
|
+
const username = await getUsername()
|
|
28
|
+
const version = await getVersion()
|
|
29
|
+
const greeting = username ? `Hello, ${username}` : 'Hello,'
|
|
30
|
+
await say(`${greeting} welcome to ${version ? `Platformatic ${version}!` : 'Platformatic!'}`)
|
|
31
|
+
await say('Let\'s start by creating a new project.')
|
|
32
|
+
|
|
33
|
+
const options = await inquirer.prompt({
|
|
34
|
+
type: 'list',
|
|
35
|
+
name: 'type',
|
|
36
|
+
message: 'Which kind of project do you want to create?',
|
|
37
|
+
default: 'db',
|
|
38
|
+
choices: [{ name: 'DB', value: 'db' }, { name: 'Service', value: 'service' }]
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (options.type === 'db') {
|
|
42
|
+
await createPlatformaticDB(argv)
|
|
43
|
+
} else if (options.type === 'service') {
|
|
44
|
+
await createPlatformaticService(argv)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await say('\nAll done! Please open the project directory and check the README.')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default createPlatformatic
|
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Platformatic Service API
|
|
2
|
+
|
|
3
|
+
This is a generated [Platformatic DB](https://oss.platformatic.dev/docs/reference/service/introduction) application.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
Platformatic supports macOS, Linux and Windows ([WSL](https://docs.microsoft.com/windows/wsl/) recommended).
|
|
8
|
+
You'll need to have [Node.js](https://nodejs.org/) >= v16.17.0 or >= v18.8.0
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
Install dependencies:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
Run the API with:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm start
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Explore
|
|
27
|
+
- ⚡ The Platformatic DB server is running at http://localhost:3042/
|
|
28
|
+
- 📔 View the REST API's Swagger documentation at http://localhost:3042/documentation/
|
|
29
|
+
- 🔍 Try out the GraphiQL web UI at http://localhost:3042/graphiql
|
|
30
|
+
|
|
31
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
|
|
2
|
+
import { getVersion, getDependencyVersion, isFileAccessible } from '../utils.mjs'
|
|
3
|
+
import { createPackageJson } from '../create-package-json.mjs'
|
|
4
|
+
import { createGitignore } from '../create-gitignore.mjs'
|
|
5
|
+
import { getPkgManager } from '../get-pkg-manager.mjs'
|
|
6
|
+
import parseArgs from 'minimist'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import inquirer from 'inquirer'
|
|
9
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
10
|
+
import pino from 'pino'
|
|
11
|
+
import pretty from 'pino-pretty'
|
|
12
|
+
import { execa } from 'execa'
|
|
13
|
+
import ora from 'ora'
|
|
14
|
+
import createService from './create-service.mjs'
|
|
15
|
+
import askProjectDir from '../ask-project-dir.mjs'
|
|
16
|
+
import { askCreateGHAction } from '../ghaction.mjs'
|
|
17
|
+
import mkdirp from 'mkdirp'
|
|
18
|
+
|
|
19
|
+
export const createReadme = async (logger, dir = '.') => {
|
|
20
|
+
const readmeFileName = join(dir, 'README.md')
|
|
21
|
+
const isReadmeExists = await isFileAccessible(readmeFileName)
|
|
22
|
+
if (!isReadmeExists) {
|
|
23
|
+
const readmeFile = new URL('README.md', import.meta.url)
|
|
24
|
+
const readme = await readFile(readmeFile, 'utf-8')
|
|
25
|
+
await writeFile(readmeFileName, readme)
|
|
26
|
+
logger.debug(`${readmeFileName} successfully created.`)
|
|
27
|
+
} else {
|
|
28
|
+
logger.debug(`${readmeFileName} found, skipping creation of README.md file.`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const createPlatformaticService = async (_args) => {
|
|
33
|
+
const logger = pino(pretty({
|
|
34
|
+
translateTime: 'SYS:HH:MM:ss',
|
|
35
|
+
ignore: 'hostname,pid'
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
const args = parseArgs(_args, {
|
|
39
|
+
default: {
|
|
40
|
+
hostname: '127.0.0.1',
|
|
41
|
+
port: 3042
|
|
42
|
+
},
|
|
43
|
+
alias: {
|
|
44
|
+
h: 'hostname',
|
|
45
|
+
p: 'port'
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const version = await getVersion()
|
|
50
|
+
const pkgManager = getPkgManager()
|
|
51
|
+
|
|
52
|
+
const projectDir = await askProjectDir(logger, '.')
|
|
53
|
+
|
|
54
|
+
// Create the project directory
|
|
55
|
+
await mkdirp(projectDir)
|
|
56
|
+
|
|
57
|
+
const params = {
|
|
58
|
+
hostname: args.hostname,
|
|
59
|
+
port: args.port
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const env = await createService(params, logger, projectDir)
|
|
63
|
+
|
|
64
|
+
const fastifyVersion = await getDependencyVersion('fastify')
|
|
65
|
+
|
|
66
|
+
// Create the package.json, .gitignore, readme
|
|
67
|
+
await createPackageJson('service', version, fastifyVersion, logger, projectDir)
|
|
68
|
+
await createGitignore(logger, projectDir)
|
|
69
|
+
await createReadme(logger, projectDir)
|
|
70
|
+
|
|
71
|
+
const { runPackageManagerInstall } = await inquirer.prompt([{
|
|
72
|
+
type: 'list',
|
|
73
|
+
name: 'runPackageManagerInstall',
|
|
74
|
+
message: `Do you want to run ${pkgManager} install?`,
|
|
75
|
+
default: true,
|
|
76
|
+
choices: [{ name: 'yes', value: true }, { name: 'no', value: false }]
|
|
77
|
+
}])
|
|
78
|
+
|
|
79
|
+
if (runPackageManagerInstall) {
|
|
80
|
+
const spinner = ora('Installing dependencies...').start()
|
|
81
|
+
await execa(pkgManager, ['install'], { cwd: projectDir })
|
|
82
|
+
spinner.succeed('...done!')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await askCreateGHAction(logger, env, 'service')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default createPlatformaticService
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { writeFile, mkdir } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { findServiceConfigFile, isFileAccessible } from '../utils.mjs'
|
|
4
|
+
|
|
5
|
+
function generateConfig () {
|
|
6
|
+
const plugin = [
|
|
7
|
+
'./plugins',
|
|
8
|
+
'./routes'
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const config = {
|
|
12
|
+
server: {
|
|
13
|
+
hostname: '{PLT_SERVER_HOSTNAME}',
|
|
14
|
+
port: '{PORT}',
|
|
15
|
+
logger: {
|
|
16
|
+
level: '{PLT_SERVER_LOGGER_LEVEL}'
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
plugin
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return config
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generateEnv (hostname, port) {
|
|
26
|
+
const env = `\
|
|
27
|
+
PLT_SERVER_HOSTNAME=${hostname}
|
|
28
|
+
PORT=${port}
|
|
29
|
+
PLT_SERVER_LOGGER_LEVEL=info
|
|
30
|
+
`
|
|
31
|
+
return env
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const JS_PLUGIN_WITH_TYPES_SUPPORT = `\
|
|
35
|
+
'use strict'
|
|
36
|
+
/** @param {import('fastify').FastifyInstance} fastify */
|
|
37
|
+
module.exports = async function (fastify, opts) {
|
|
38
|
+
fastify.decorate('example', 'foobar')
|
|
39
|
+
}
|
|
40
|
+
module.exports[Symbol.for('skip-override')] = true
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
const ROUTES_WITH_TYPES_SUPPORT = `\
|
|
44
|
+
'use strict'
|
|
45
|
+
/** @param {import('fastify').FastifyInstance} fastify */
|
|
46
|
+
module.exports = async function (fastify, opts) {
|
|
47
|
+
fastify.get('/', async (request, reply) => {
|
|
48
|
+
return { hello: fastify.example }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
async function createService ({ hostname, port }, logger, currentDir = process.cwd()) {
|
|
54
|
+
const accessibleConfigFilename = await findServiceConfigFile(currentDir)
|
|
55
|
+
|
|
56
|
+
if (accessibleConfigFilename === undefined) {
|
|
57
|
+
const config = generateConfig()
|
|
58
|
+
await writeFile(join(currentDir, 'platformatic.service.json'), JSON.stringify(config, null, 2))
|
|
59
|
+
logger.info('Configuration file platformatic.service.json successfully created.')
|
|
60
|
+
|
|
61
|
+
const env = generateEnv(hostname, port)
|
|
62
|
+
await writeFile(join(currentDir, '.env'), env)
|
|
63
|
+
await writeFile(join(currentDir, '.env.sample'), env)
|
|
64
|
+
logger.info('Environment file .env successfully created.')
|
|
65
|
+
} else {
|
|
66
|
+
logger.info(`Configuration file ${accessibleConfigFilename} found, skipping creation of configuration file.`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pluginFolderExists = await isFileAccessible('plugins', currentDir)
|
|
70
|
+
if (!pluginFolderExists) {
|
|
71
|
+
await mkdir(join(currentDir, 'plugins'))
|
|
72
|
+
await writeFile(join(currentDir, 'plugins', 'example.js'), JS_PLUGIN_WITH_TYPES_SUPPORT)
|
|
73
|
+
logger.info('Plugins folder "plugins" successfully created.')
|
|
74
|
+
} else {
|
|
75
|
+
logger.info('Plugins folder "plugins" found, skipping creation of plugins folder.')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const routeFolderExists = await isFileAccessible('routes', currentDir)
|
|
79
|
+
if (!routeFolderExists) {
|
|
80
|
+
await mkdir(join(currentDir, 'routes'))
|
|
81
|
+
await writeFile(join(currentDir, 'routes', 'root.js'), ROUTES_WITH_TYPES_SUPPORT)
|
|
82
|
+
logger.info('Routes folder "routes" successfully created.')
|
|
83
|
+
} else {
|
|
84
|
+
logger.info('Routes folder "routes" found, skipping creation of routes folder.')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
PLT_SERVER_LOGGER_LEVEL: 'info',
|
|
89
|
+
PORT: port,
|
|
90
|
+
PLT_SERVER_HOSTNAME: hostname
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default createService
|
package/src/utils.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import { request } from 'undici'
|
|
3
|
+
import { access, constants, readFile } from 'fs/promises'
|
|
4
|
+
import { resolve, join, dirname } from 'path'
|
|
5
|
+
import { createRequire } from 'module'
|
|
6
|
+
|
|
7
|
+
export const sleep = ms => new Promise((resolve) => setTimeout(resolve, ms))
|
|
8
|
+
export const randomBetween = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
|
|
9
|
+
|
|
10
|
+
export async function isFileAccessible (filename, directory) {
|
|
11
|
+
try {
|
|
12
|
+
const filePath = directory ? resolve(directory, filename) : filename
|
|
13
|
+
await access(filePath)
|
|
14
|
+
return true
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const getUsername = async () => {
|
|
21
|
+
const { stdout } = await execa('git', ['config', 'user.name'])
|
|
22
|
+
if (stdout?.trim()) {
|
|
23
|
+
return stdout.trim()
|
|
24
|
+
}
|
|
25
|
+
{
|
|
26
|
+
const { stdout } = await execa('whoami')
|
|
27
|
+
if (stdout?.trim()) {
|
|
28
|
+
return stdout.trim()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const getVersion = async () => {
|
|
35
|
+
try {
|
|
36
|
+
const { body, statusCode } = await request('https://registry.npmjs.org/platformatic/latest')
|
|
37
|
+
if (statusCode !== 200) {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
const { version } = await body.json()
|
|
41
|
+
return version
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function isDirectoryWriteable (directory) {
|
|
48
|
+
try {
|
|
49
|
+
await access(directory, constants.R_OK | constants.W_OK)
|
|
50
|
+
return true
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const validatePath = async projectPath => {
|
|
57
|
+
// if the folder exists, is OK:
|
|
58
|
+
const projectDir = resolve(projectPath)
|
|
59
|
+
const canAccess = await isDirectoryWriteable(projectDir)
|
|
60
|
+
if (canAccess) {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
// if the folder does not exist, check if the parent folder exists:
|
|
64
|
+
const parentDir = dirname(projectDir)
|
|
65
|
+
const canAccessParent = await isDirectoryWriteable(parentDir)
|
|
66
|
+
if (canAccessParent) {
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const findConfigFile = async (directory, type) => {
|
|
73
|
+
const configFileNames = [
|
|
74
|
+
`platformatic.${type}.json`,
|
|
75
|
+
`platformatic.${type}.json5`,
|
|
76
|
+
`platformatic.${type}.yaml`,
|
|
77
|
+
`platformatic.${type}.yml`,
|
|
78
|
+
`platformatic.${type}.toml`,
|
|
79
|
+
`platformatic.${type}.tml`
|
|
80
|
+
]
|
|
81
|
+
const configFilesAccessibility = await Promise.all(configFileNames.map((fileName) => isFileAccessible(fileName, directory)))
|
|
82
|
+
const accessibleConfigFilename = configFileNames.find((value, index) => configFilesAccessibility[index])
|
|
83
|
+
return accessibleConfigFilename
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const findDBConfigFile = async (directory) => (findConfigFile(directory, 'db'))
|
|
87
|
+
export const findServiceConfigFile = async (directory) => (findConfigFile(directory, 'service'))
|
|
88
|
+
|
|
89
|
+
export const getDependencyVersion = async (dependencyName) => {
|
|
90
|
+
const require = createRequire(import.meta.url)
|
|
91
|
+
const pathToPackageJson = join(dirname(require.resolve(dependencyName)), 'package.json')
|
|
92
|
+
const packageJsonFile = await readFile(pathToPackageJson, 'utf-8')
|
|
93
|
+
const packageJson = JSON.parse(packageJsonFile)
|
|
94
|
+
return packageJson.version
|
|
95
|
+
}
|