create-kelpie 0.2.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/LICENSE +661 -0
- package/README.md +72 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +41 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +172 -0
- package/dist/options.js.map +1 -0
- package/dist/scaffold.d.ts +36 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +106 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +53 -0
- package/src/index.ts +59 -0
- package/src/options.ts +229 -0
- package/src/scaffold.ts +146 -0
- package/templates/README.md +97 -0
- package/templates/docker-compose.yml +26 -0
- package/templates/env +27 -0
- package/templates/gitignore +6 -0
- package/templates/kelpie.config.ts +21 -0
- package/templates/kelpie.ui.config.ts +14 -0
- package/templates/package.json +35 -0
- package/templates/src/server.ts +118 -0
- package/templates/tsconfig.server.json +20 -0
- package/templates/tsconfig.web.json +21 -0
- package/templates/vite.config.ts +60 -0
- package/templates/web/index.html +12 -0
- package/templates/web/main.tsx +23 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server'
|
|
2
|
+
import {
|
|
3
|
+
ConfigurationError,
|
|
4
|
+
ModuleBootError,
|
|
5
|
+
ModuleConfigFileError,
|
|
6
|
+
connectDatabase,
|
|
7
|
+
createApp,
|
|
8
|
+
createEmailSender,
|
|
9
|
+
createEventBus,
|
|
10
|
+
createIdFactory,
|
|
11
|
+
createLogger,
|
|
12
|
+
createTransactionScope,
|
|
13
|
+
loadConfig,
|
|
14
|
+
readModuleConfigFile,
|
|
15
|
+
registerModules,
|
|
16
|
+
resolveActorFrom,
|
|
17
|
+
runMigrations,
|
|
18
|
+
} from '@kelpie/server'
|
|
19
|
+
import type { CredentialDependencies } from '@kelpie/server'
|
|
20
|
+
|
|
21
|
+
import { modules } from '../kelpie.config.ts'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The entry point. It reads the environment, registers the configured modules,
|
|
25
|
+
* applies migrations, wires the dependencies, and serves.
|
|
26
|
+
*
|
|
27
|
+
* Registration runs before migrations. Modules declare their migrations
|
|
28
|
+
* directory while registering, so there is nothing to migrate until that pass
|
|
29
|
+
* has finished.
|
|
30
|
+
*
|
|
31
|
+
* `--no-migrate` skips the migration step, for deployments where one release
|
|
32
|
+
* step migrates and many instances then start.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
function reportFatal(message: string): void {
|
|
36
|
+
process.stderr.write(`${message}\n`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function start(): Promise<void> {
|
|
40
|
+
const config = loadConfig(process.env)
|
|
41
|
+
const logger = createLogger(config.logLevel)
|
|
42
|
+
const database = connectDatabase(config.databaseUrl, logger)
|
|
43
|
+
const events = createEventBus(logger)
|
|
44
|
+
const createId = createIdFactory()
|
|
45
|
+
const credentials: CredentialDependencies = { db: database.db, now: () => new Date() }
|
|
46
|
+
const moduleConfig = readModuleConfigFile(config.moduleConfigPath)
|
|
47
|
+
const contributions = await registerModules({
|
|
48
|
+
modules,
|
|
49
|
+
environment: process.env,
|
|
50
|
+
logger,
|
|
51
|
+
events,
|
|
52
|
+
moduleConfig,
|
|
53
|
+
resolveActor: (context) => resolveActorFrom(credentials, context),
|
|
54
|
+
services: {
|
|
55
|
+
db: database.db,
|
|
56
|
+
transaction: createTransactionScope({ db: database.db, bus: events, logger }),
|
|
57
|
+
email: createEmailSender(config.email, logger),
|
|
58
|
+
createId,
|
|
59
|
+
now: () => new Date(),
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (process.argv.includes('--no-migrate')) {
|
|
64
|
+
logger.info('skipping migrations', { reason: '--no-migrate' })
|
|
65
|
+
} else {
|
|
66
|
+
await runMigrations(database.db, contributions.schemas, logger)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const app = createApp({
|
|
70
|
+
logger,
|
|
71
|
+
probeDatabase: database.probe,
|
|
72
|
+
contributions,
|
|
73
|
+
credentials,
|
|
74
|
+
createId,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const server = serve({ fetch: app.fetch, port: config.port }, (address) => {
|
|
78
|
+
logger.info('listening', { port: address.port, runtimeMode: config.runtimeMode })
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const shutdown = (signal: string): void => {
|
|
82
|
+
logger.info('shutting down', { signal })
|
|
83
|
+
server.close(() => {
|
|
84
|
+
// Drain before closing the pool: a handler mid-flight may still be writing.
|
|
85
|
+
void contributions.events
|
|
86
|
+
.drain()
|
|
87
|
+
.then(() => database.close())
|
|
88
|
+
.then(() => process.exit(0))
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
93
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await start()
|
|
98
|
+
} catch (error: unknown) {
|
|
99
|
+
if (error instanceof ConfigurationError) {
|
|
100
|
+
reportFatal(error.message)
|
|
101
|
+
reportFatal('Check .env against the table in README.md.')
|
|
102
|
+
process.exit(1)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (error instanceof ModuleConfigFileError) {
|
|
106
|
+
reportFatal(error.message)
|
|
107
|
+
reportFatal('Fix the file at KELPIE_MODULE_CONFIG_PATH, or unset it to let workspaces decide for themselves.')
|
|
108
|
+
process.exit(1)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (error instanceof ModuleBootError) {
|
|
112
|
+
reportFatal(error.message)
|
|
113
|
+
reportFatal('Fix the module list in kelpie.config.ts, or the configuration it needs.')
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw error
|
|
118
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"exactOptionalPropertyTypes": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"erasableSyntaxOnly": true,
|
|
15
|
+
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"noEmit": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src", "kelpie.config.ts"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"types": ["vite/client", "node"],
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"exactOptionalPropertyTypes": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"erasableSyntaxOnly": true,
|
|
16
|
+
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"noEmit": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["web", "vite.config.ts", "kelpie.ui.config.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import react from '@vitejs/plugin-react'
|
|
5
|
+
import { defineConfig, loadEnv } from 'vite'
|
|
6
|
+
|
|
7
|
+
const projectRoot = fileURLToPath(new URL('./', import.meta.url))
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The dev server proxies the API rather than pointing the browser at a second
|
|
11
|
+
* origin, so the UI runs against same-origin `/v1` in development exactly as it
|
|
12
|
+
* does in production.
|
|
13
|
+
*/
|
|
14
|
+
export default defineConfig(({ mode }) => {
|
|
15
|
+
// `loadEnv` rather than `process.env`, because `npm run dev:web` runs `vite`
|
|
16
|
+
// directly and nothing has read `.env` into the environment by then. Reading
|
|
17
|
+
// `process.env.WEB_PORT` here means the port in `.env` is silently ignored and
|
|
18
|
+
// Vite takes 5173, or the next free one, while README and .env both say
|
|
19
|
+
// otherwise.
|
|
20
|
+
//
|
|
21
|
+
// `API_PORT`, not `PORT`. Every process manager sets `PORT` to the port it
|
|
22
|
+
// wants the process it is launching to listen on, and a prefix that matched it
|
|
23
|
+
// would let that value win over `.env`. Vite would then proxy to itself: the
|
|
24
|
+
// page loads and every `/v1` call times out against the dev server it came
|
|
25
|
+
// from.
|
|
26
|
+
const environment = loadEnv(mode, projectRoot, ['API_', 'WEB_'])
|
|
27
|
+
const apiPort = environment.API_PORT
|
|
28
|
+
|
|
29
|
+
if (apiPort === undefined || apiPort.length === 0) {
|
|
30
|
+
throw new Error('API_PORT is not set. It belongs in .env, with the same value as PORT.')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const apiOrigin = `http://localhost:${apiPort}`
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
root: fileURLToPath(new URL('./web', import.meta.url)),
|
|
37
|
+
envDir: projectRoot,
|
|
38
|
+
plugins: [react(), tailwindcss()],
|
|
39
|
+
build: {
|
|
40
|
+
outDir: fileURLToPath(new URL('./dist', import.meta.url)),
|
|
41
|
+
emptyOutDir: true,
|
|
42
|
+
},
|
|
43
|
+
server: {
|
|
44
|
+
port: Number(environment.WEB_PORT ?? 5173),
|
|
45
|
+
// Refuse rather than quietly taking the next port. A launcher that was
|
|
46
|
+
// told 5173 and got 5174 proxies nothing, and the README hands out an
|
|
47
|
+
// address that answers nothing.
|
|
48
|
+
strictPort: true,
|
|
49
|
+
proxy: {
|
|
50
|
+
'/v1': { target: apiOrigin, changeOrigin: false },
|
|
51
|
+
'/healthz': { target: apiOrigin, changeOrigin: false },
|
|
52
|
+
// The MCP page shows the endpoint at the origin the browser reached the
|
|
53
|
+
// app on, which in development is this dev server. Without the proxy
|
|
54
|
+
// that address is right in production and dead here, so anyone copying
|
|
55
|
+
// it out of the page would be debugging the wrong thing.
|
|
56
|
+
'/mcp': { target: apiOrigin, changeOrigin: false },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__PROJECT_NAME__</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="./main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { KelpieApp, registerUiModules } from '@kelpie/ui'
|
|
2
|
+
import '@kelpie/ui/styles.css'
|
|
3
|
+
import { StrictMode } from 'react'
|
|
4
|
+
import { createRoot } from 'react-dom/client'
|
|
5
|
+
|
|
6
|
+
import { uiModules } from '../kelpie.ui.config.ts'
|
|
7
|
+
|
|
8
|
+
const container = document.getElementById('root')
|
|
9
|
+
|
|
10
|
+
if (container === null) {
|
|
11
|
+
throw new Error('Expected an element with id "root" in index.html')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Registration is build-time and happens once, above the root. A module
|
|
15
|
+
// clashing with another fails here, at startup, rather than as a tab somebody
|
|
16
|
+
// notices is missing a week later.
|
|
17
|
+
const extensions = registerUiModules(uiModules)
|
|
18
|
+
|
|
19
|
+
createRoot(container).render(
|
|
20
|
+
<StrictMode>
|
|
21
|
+
<KelpieApp extensions={extensions} />
|
|
22
|
+
</StrictMode>,
|
|
23
|
+
)
|