@adula/create-app 0.2.0-alpha.1
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 +10 -0
- package/README.md +63 -0
- package/build/cli.mjs +330 -0
- package/build/progress.mjs +40 -0
- package/build/project.mjs +237 -0
- package/build/system.mjs +182 -0
- package/build/template.json +1 -0
- package/package.json +45 -0
package/build/system.mjs
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { access, readFile, open } from 'node:fs/promises'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { createServer, createConnection } from 'node:net'
|
|
5
|
+
import { dirname, join, resolve } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import pg from 'pg'
|
|
8
|
+
|
|
9
|
+
export async function run(
|
|
10
|
+
command,
|
|
11
|
+
args,
|
|
12
|
+
{ cwd = undefined, env = process.env, quiet = false, logFile = undefined } = {}
|
|
13
|
+
) {
|
|
14
|
+
const log = logFile ? await open(logFile, 'a', 0o600) : undefined
|
|
15
|
+
try {
|
|
16
|
+
return await new Promise((resolve, reject) => {
|
|
17
|
+
const child = spawn(command, args, {
|
|
18
|
+
cwd,
|
|
19
|
+
env,
|
|
20
|
+
shell: false,
|
|
21
|
+
windowsHide: true,
|
|
22
|
+
stdio: log ? ['ignore', log.fd, log.fd] : quiet ? 'ignore' : 'inherit',
|
|
23
|
+
})
|
|
24
|
+
child.once('error', () => {
|
|
25
|
+
reject(
|
|
26
|
+
new Error(`Cannot start ${command}. Check that it is installed and available in PATH.`)
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
child.once('close', (code, signal) => {
|
|
30
|
+
code === 0
|
|
31
|
+
? resolve(undefined)
|
|
32
|
+
: reject(
|
|
33
|
+
new Error(
|
|
34
|
+
`Command failed: ${args[0] ?? command} (${signal ?? code}). Review ${logFile ?? 'the output above'}.`
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
} finally {
|
|
40
|
+
await log?.close()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function packageManager() {
|
|
45
|
+
// Keep the installer outside the project whose dependency tree it updates.
|
|
46
|
+
// Direct JavaScript entries also avoid shell interpretation on Windows.
|
|
47
|
+
const require = createRequire(import.meta.url)
|
|
48
|
+
const manifest = require.resolve('pnpm')
|
|
49
|
+
const packageJson = JSON.parse(await readFile(manifest, 'utf8'))
|
|
50
|
+
return [process.execPath, resolve(dirname(manifest), packageJson.bin.pnpm)]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function packageManagerBin() {
|
|
54
|
+
let directory = fileURLToPath(new URL('../', import.meta.url))
|
|
55
|
+
while (directory) {
|
|
56
|
+
const bin = join(directory, 'node_modules/.bin')
|
|
57
|
+
try {
|
|
58
|
+
await access(join(bin, process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'))
|
|
59
|
+
return bin
|
|
60
|
+
} catch {
|
|
61
|
+
const parent = dirname(directory)
|
|
62
|
+
if (parent === directory) break
|
|
63
|
+
directory = parent
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw new Error('Cannot find the bundled pnpm. Run npm create again.')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function npmEntry() {
|
|
70
|
+
const candidates = [
|
|
71
|
+
process.env.npm_execpath,
|
|
72
|
+
join(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'),
|
|
73
|
+
join(dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js'),
|
|
74
|
+
].filter(Boolean)
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
if (!candidate.endsWith('npm-cli.js')) continue
|
|
77
|
+
try {
|
|
78
|
+
await access(candidate)
|
|
79
|
+
} catch {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
return candidate
|
|
83
|
+
}
|
|
84
|
+
throw new Error('Cannot find npm. Run npm create with Node.js 24 or later.')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function freePort() {
|
|
88
|
+
const server = createServer()
|
|
89
|
+
await new Promise((resolve, reject) => {
|
|
90
|
+
server.once('error', reject)
|
|
91
|
+
server.listen(0, '127.0.0.1', () => resolve(undefined))
|
|
92
|
+
})
|
|
93
|
+
const address = server.address()
|
|
94
|
+
const port = typeof address === 'object' && address ? address.port : 0
|
|
95
|
+
await new Promise((resolve) => server.close(resolve))
|
|
96
|
+
return port
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function pingRedis(connection) {
|
|
100
|
+
const frame = (parts) =>
|
|
101
|
+
`*${parts.length}\r\n${parts.map((part) => `$${Buffer.byteLength(String(part))}\r\n${part}\r\n`).join('')}`
|
|
102
|
+
await new Promise((resolve, reject) => {
|
|
103
|
+
const socket = createConnection({
|
|
104
|
+
host: connection.host,
|
|
105
|
+
port: connection.port,
|
|
106
|
+
})
|
|
107
|
+
let output = ''
|
|
108
|
+
let finished = false
|
|
109
|
+
const fail = () => {
|
|
110
|
+
if (finished) return
|
|
111
|
+
finished = true
|
|
112
|
+
socket.destroy()
|
|
113
|
+
reject(new Error('Cannot connect to Redis. Check the connection and password.'))
|
|
114
|
+
}
|
|
115
|
+
socket.setTimeout(10000, fail)
|
|
116
|
+
socket.on('error', fail)
|
|
117
|
+
socket.on('close', fail)
|
|
118
|
+
socket.on('connect', () =>
|
|
119
|
+
socket.write(
|
|
120
|
+
(connection.password ? frame(['AUTH', connection.password]) : '') + frame(['PING'])
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
socket.on('data', (chunk) => {
|
|
124
|
+
output += chunk.toString()
|
|
125
|
+
if (output.includes('-')) return fail()
|
|
126
|
+
if (output.endsWith('+PONG\r\n')) {
|
|
127
|
+
finished = true
|
|
128
|
+
socket.destroy()
|
|
129
|
+
resolve(undefined)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function databaseNames(name, suffix) {
|
|
136
|
+
const database = `${name.replaceAll('-', '_').slice(0, 35)}_${suffix}`
|
|
137
|
+
if (!/^[a-z][a-z0-9_]{0,49}$/.test(database)) throw new Error('Invalid database name.')
|
|
138
|
+
return { database, testDatabase: `${database}_test` }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function createDatabases(connection, names) {
|
|
142
|
+
// Always create new databases. A failed retry never migrates or erases a
|
|
143
|
+
// pre-existing database, even if it happens to be empty.
|
|
144
|
+
for (const name of [names.database, names.testDatabase])
|
|
145
|
+
if (!/^[a-z][a-z0-9_]{0,60}$/.test(name)) throw new Error('Invalid database name.')
|
|
146
|
+
const client = new pg.Client({
|
|
147
|
+
...connection,
|
|
148
|
+
database: connection.maintenanceDatabase ?? 'postgres',
|
|
149
|
+
connectionTimeoutMillis: 10000,
|
|
150
|
+
})
|
|
151
|
+
try {
|
|
152
|
+
await client.connect()
|
|
153
|
+
const version = Number(
|
|
154
|
+
(await client.query('SHOW server_version_num')).rows[0].server_version_num
|
|
155
|
+
)
|
|
156
|
+
if (version < 170000 || version >= 180000)
|
|
157
|
+
throw new Error('This release requires PostgreSQL 17.')
|
|
158
|
+
const found = await client.query('SELECT datname FROM pg_database WHERE datname = ANY($1)', [
|
|
159
|
+
[names.database, names.testDatabase],
|
|
160
|
+
])
|
|
161
|
+
if (found.rowCount)
|
|
162
|
+
throw new Error('Database already exists and will not be modified. Choose a new project.')
|
|
163
|
+
for (const name of [names.database, names.testDatabase])
|
|
164
|
+
await client.query(`CREATE DATABASE "${name}"`)
|
|
165
|
+
} finally {
|
|
166
|
+
await client.end()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function childEnvironment(values = {}) {
|
|
171
|
+
// Calling this from a CI/test shell must not redirect migrations or cached
|
|
172
|
+
// authorization to the caller's database. The new application's .env owns it.
|
|
173
|
+
const env = { ...process.env }
|
|
174
|
+
for (const key of Object.keys(env))
|
|
175
|
+
if (
|
|
176
|
+
/^(?:DB_|REDIS_|APP_|VITE_|ADULA_|COMPOSE_|GITHUB_CLIENT_|GOOGLE_CLIENT_|BACKUP_|AWS_|S3_|SMTP_|MAIL_|SESSION_|LIMITER_|DRIVE_|NODE_ENV$|HOST$|PORT$|LOG_LEVEL$)/i.test(
|
|
177
|
+
key
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
delete env[key]
|
|
181
|
+
return { ...env, ...values }
|
|
182
|
+
}
|