umpordez 0.0.3 → 1.0.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/.claude/settings.local.json +20 -0
- package/README.md +123 -0
- package/build.mjs +409 -0
- package/cli.mjs +85 -107
- package/create.mjs +334 -0
- package/package.json +2 -7
- package/template/.claude/settings.local.json +20 -0
- package/template/.editorconfig +11 -0
- package/template/.github/copilot-instructions.md +186 -0
- package/template/.nvimrc +7 -0
- package/template/.vscode/extensions.json +9 -0
- package/template/.vscode/settings.json +28 -0
- package/template/AGENTS.md +305 -0
- package/template/CLAUDE.md +140 -0
- package/template/architecture.md +518 -0
- package/template/code.md +211 -0
- package/template/dev.sh +45 -0
- package/template/install.sh +47 -0
- package/template/misc/nginx.conf +151 -0
- package/template/misc/systemd/{{PROJECT_SLUG}}-api.service +31 -0
- package/template/misc/systemd/{{PROJECT_SLUG}}-site-api.service +31 -0
- package/template/misc/systemd/{{PROJECT_SLUG}}-site.service +30 -0
- package/template/seed.sh +23 -0
- package/template/server/.env.sample +40 -0
- package/template/server/.prettierrc +4 -0
- package/template/server/apps/api/app.ts +42 -0
- package/template/server/apps/api/routes/account.ts +98 -0
- package/template/server/apps/api/routes/admin.ts +80 -0
- package/template/server/apps/api/routes/auth.ts +222 -0
- package/template/server/apps/api/routes/files.ts +89 -0
- package/template/server/apps/api/routes/index.ts +19 -0
- package/template/server/apps/api/routes/user.ts +17 -0
- package/template/server/apps/shared/middlewares/demand-account-access.ts +52 -0
- package/template/server/apps/shared/middlewares/demand-admin-user.ts +25 -0
- package/template/server/apps/shared/middlewares/demand-user.ts +17 -0
- package/template/server/apps/shared/middlewares/error-handler.ts +28 -0
- package/template/server/apps/shared/middlewares/request-logger.ts +40 -0
- package/template/server/apps/shared/middlewares/try-set-user-by-token.ts +26 -0
- package/template/server/apps/shared/utils.ts +71 -0
- package/template/server/apps/site-api/app.ts +42 -0
- package/template/server/apps/site-api/routes/index.ts +10 -0
- package/template/server/apps/site-api/routes/public.ts +25 -0
- package/template/server/console/index.ts +26 -0
- package/template/server/console/runner.ts +67 -0
- package/template/server/console/tasks/example.ts +29 -0
- package/template/server/console/tasks/seed-admin.ts +71 -0
- package/template/server/core/context.ts +49 -0
- package/template/server/core/db.ts +12 -0
- package/template/server/core/email.ts +87 -0
- package/template/server/core/errors.ts +44 -0
- package/template/server/core/knexfile.ts +28 -0
- package/template/server/core/logger.ts +60 -0
- package/template/server/core/models/account.ts +319 -0
- package/template/server/core/models/auth.ts +317 -0
- package/template/server/core/models/base.ts +19 -0
- package/template/server/core/models/user.ts +343 -0
- package/template/server/core/s3.ts +183 -0
- package/template/server/emails/_styles.css +5 -0
- package/template/server/emails/_template.html +28 -0
- package/template/server/emails/accountWelcome.html +4 -0
- package/template/server/emails/forgetPassword.html +5 -0
- package/template/server/eslint.config.js +16 -0
- package/template/server/knex.sh +4 -0
- package/template/server/migrations/20260208000000_initial-schema.ts +56 -0
- package/template/server/migrations/20260208000001_seed-admin.ts +42 -0
- package/template/server/migrations/20260208000002_add-user-avatar.ts +13 -0
- package/template/server/package.json +58 -0
- package/template/server/tsconfig.json +27 -0
- package/template/server/types/api.ts +20 -0
- package/template/server/types/express.d.ts +35 -0
- package/template/ui/admin/.prettierrc +4 -0
- package/template/ui/admin/components.json +20 -0
- package/template/ui/admin/eslint.config.js +16 -0
- package/template/ui/admin/index.html +12 -0
- package/template/ui/admin/package.json +57 -0
- package/template/ui/admin/postcss.config.js +6 -0
- package/template/ui/admin/src/app.tsx +13 -0
- package/template/ui/admin/src/components/nav-user.tsx +95 -0
- package/template/ui/admin/src/components/theme-provider.tsx +65 -0
- package/template/ui/admin/src/components/theme-toggle.tsx +17 -0
- package/template/ui/admin/src/components/ui/badge.tsx +36 -0
- package/template/ui/admin/src/components/ui/button.tsx +56 -0
- package/template/ui/admin/src/components/ui/calendar.tsx +68 -0
- package/template/ui/admin/src/components/ui/card.tsx +79 -0
- package/template/ui/admin/src/components/ui/checkbox.tsx +28 -0
- package/template/ui/admin/src/components/ui/date-picker.tsx +78 -0
- package/template/ui/admin/src/components/ui/date-range-picker.tsx +208 -0
- package/template/ui/admin/src/components/ui/dialog.tsx +121 -0
- package/template/ui/admin/src/components/ui/dropdown-menu.tsx +200 -0
- package/template/ui/admin/src/components/ui/input.tsx +22 -0
- package/template/ui/admin/src/components/ui/label.tsx +24 -0
- package/template/ui/admin/src/components/ui/popover.tsx +29 -0
- package/template/ui/admin/src/components/ui/select.tsx +158 -0
- package/template/ui/admin/src/components/ui/separator.tsx +29 -0
- package/template/ui/admin/src/components/ui/sheet.tsx +136 -0
- package/template/ui/admin/src/components/ui/sidebar.tsx +759 -0
- package/template/ui/admin/src/components/ui/skeleton.tsx +15 -0
- package/template/ui/admin/src/components/ui/sonner.tsx +26 -0
- package/template/ui/admin/src/components/ui/switch.tsx +27 -0
- package/template/ui/admin/src/components/ui/table.tsx +117 -0
- package/template/ui/admin/src/components/ui/tabs.tsx +53 -0
- package/template/ui/admin/src/components/ui/textarea.tsx +22 -0
- package/template/ui/admin/src/components/ui/tooltip.tsx +28 -0
- package/template/ui/admin/src/hooks/queries/use-accounts.ts +68 -0
- package/template/ui/admin/src/hooks/queries/use-auth.ts +61 -0
- package/template/ui/admin/src/hooks/queries/use-dashboard.ts +70 -0
- package/template/ui/admin/src/hooks/queries/use-members.ts +90 -0
- package/template/ui/admin/src/hooks/queries/use-users.ts +99 -0
- package/template/ui/admin/src/hooks/use-confirm.tsx +92 -0
- package/template/ui/admin/src/hooks/use-mobile.tsx +22 -0
- package/template/ui/admin/src/hooks/use-sidebar.tsx +4 -0
- package/template/ui/admin/src/hooks/use-user.tsx +86 -0
- package/template/ui/admin/src/index.css +22 -0
- package/template/ui/admin/src/layouts/account.tsx +278 -0
- package/template/ui/admin/src/layouts/admin.tsx +182 -0
- package/template/ui/admin/src/layouts/public.tsx +9 -0
- package/template/ui/admin/src/layouts/user.tsx +107 -0
- package/template/ui/admin/src/lib/config.ts +9 -0
- package/template/ui/admin/src/lib/fetch-api.ts +101 -0
- package/template/ui/admin/src/lib/query-client.ts +12 -0
- package/template/ui/admin/src/lib/query-keys.ts +44 -0
- package/template/ui/admin/src/lib/utils.ts +24 -0
- package/template/ui/admin/src/main.tsx +17 -0
- package/template/ui/admin/src/pages/account/dashboard.tsx +128 -0
- package/template/ui/admin/src/pages/account/members.tsx +414 -0
- package/template/ui/admin/src/pages/account/settings.tsx +210 -0
- package/template/ui/admin/src/pages/admin/accounts.tsx +164 -0
- package/template/ui/admin/src/pages/admin/dashboard.tsx +243 -0
- package/template/ui/admin/src/pages/admin/users.tsx +395 -0
- package/template/ui/admin/src/pages/public/error.tsx +24 -0
- package/template/ui/admin/src/pages/public/forgot-password.tsx +74 -0
- package/template/ui/admin/src/pages/public/login.tsx +102 -0
- package/template/ui/admin/src/pages/public/reset-password.tsx +106 -0
- package/template/ui/admin/src/pages/public/signup.tsx +118 -0
- package/template/ui/admin/src/pages/user/profile.tsx +313 -0
- package/template/ui/admin/src/pages/user/select-account.tsx +114 -0
- package/template/ui/admin/src/router.tsx +71 -0
- package/template/ui/admin/tailwind.config.ts +71 -0
- package/template/ui/admin/tsconfig.json +24 -0
- package/template/ui/admin/vite.config.ts +15 -0
- package/template/ui/site/.env.sample +3 -0
- package/template/ui/site/.prettierrc +4 -0
- package/template/ui/site/app.ts +50 -0
- package/template/ui/site/eslint.config.js +16 -0
- package/template/ui/site/package.json +33 -0
- package/template/ui/site/styles/input.css +3 -0
- package/template/ui/site/tailwind.config.js +8 -0
- package/template/ui/site/tsconfig.json +15 -0
- package/template/ui/site/views/pages/home.ejs +305 -0
- package/template/ui/site/views/partials/footer.ejs +44 -0
- package/template/ui/site/views/partials/header.ejs +83 -0
- package/template-variables.mjs +186 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(chmod:*)",
|
|
5
|
+
"Bash(node -e:*)",
|
|
6
|
+
"Bash(grep:*)",
|
|
7
|
+
"Bash(find:*)",
|
|
8
|
+
"Bash(xargs cat:*)",
|
|
9
|
+
"Bash(node:*)",
|
|
10
|
+
"Bash(echo:*)",
|
|
11
|
+
"WebFetch(domain:umpordez.com)",
|
|
12
|
+
"Bash(curl:*)",
|
|
13
|
+
"Bash(python3:*)",
|
|
14
|
+
"WebFetch(domain:github.com)",
|
|
15
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
16
|
+
"Bash(gh api:*)",
|
|
17
|
+
"WebFetch(domain:api.github.com)"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# umpordez 🚀
|
|
2
|
+
|
|
3
|
+
SaaS starter kit generator.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why?
|
|
8
|
+
|
|
9
|
+
We all know the drill; you want to build a SaaS, you spend two weeks
|
|
10
|
+
setting up auth, multi-tenancy, file uploads, admin panels, role-based
|
|
11
|
+
access... and then you still haven't written a single line of your
|
|
12
|
+
actual product.
|
|
13
|
+
|
|
14
|
+
I've built this same architecture across multiple production apps
|
|
15
|
+
(different domains, same bones). At some point I got tired of
|
|
16
|
+
copying between repos, adapting folder names, and forgetting to
|
|
17
|
+
update that one hardcoded port somewhere.
|
|
18
|
+
|
|
19
|
+
So I made a CLI that generates the whole thing. One command, answer a
|
|
20
|
+
few questions, and you get a production-ready multi-tenant SaaS with
|
|
21
|
+
everything wired up.
|
|
22
|
+
|
|
23
|
+
**No magic, no hidden abstractions.** The generated code is yours;
|
|
24
|
+
plain TypeScript, plain React, plain SQL. Read it, change it, own it.
|
|
25
|
+
|
|
26
|
+
## What you get
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
server/
|
|
30
|
+
apps/api/ Admin API (Express + TypeScript)
|
|
31
|
+
apps/site-api/ Public API (Express + TypeScript)
|
|
32
|
+
apps/shared/ Shared middlewares + utilities
|
|
33
|
+
core/ Models, DB, S3, email, auth
|
|
34
|
+
console/ Task runner (migrations, seeds, custom tasks)
|
|
35
|
+
migrations/ Raw SQL migrations (Knex)
|
|
36
|
+
|
|
37
|
+
ui/admin/ React SPA (Vite + shadcn/ui + Tailwind)
|
|
38
|
+
ui/site/ Public site (Express + EJS + Tailwind)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The architecture:
|
|
42
|
+
|
|
43
|
+
- **Multi-tenant**; users > accounts with role-based access
|
|
44
|
+
(admin, owner, manager, user)
|
|
45
|
+
- **Multi-API**; separate Express servers sharing core logic,
|
|
46
|
+
add more as you need
|
|
47
|
+
- **Cookie auth**; httpOnly JWT, 30-day expiry, no tokens
|
|
48
|
+
in localStorage
|
|
49
|
+
- **S3 uploads**; multer-s3 with signed URLs (never public-read)
|
|
50
|
+
- **Context DI**; fresh context per request with all models
|
|
51
|
+
instantiated, no singletons
|
|
52
|
+
- **PostgreSQL**; Knex.js with raw SQL migrations, because ORMs
|
|
53
|
+
lie to you eventually :X
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install -g umpordez
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
### Create a project
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
umpordez
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That's it. It asks you a few questions (name, domain, ports, colors)
|
|
70
|
+
and generates everything. Then:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cd my-project
|
|
74
|
+
./install.sh # install deps
|
|
75
|
+
./seed.sh # create db + migrate + seed admin
|
|
76
|
+
./dev.sh # start all services
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Build for production
|
|
80
|
+
|
|
81
|
+
The build system is split in two steps on purpose; compilation is
|
|
82
|
+
expensive and should run on your machine, dependency installation
|
|
83
|
+
needs to happen on the target machine (native bindings, OS-specific
|
|
84
|
+
stuff).
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Step 1: compile locally, push artifacts to builds repo
|
|
88
|
+
umpordez build ../app ../builds
|
|
89
|
+
|
|
90
|
+
# Step 2: on prod/staging, install production deps
|
|
91
|
+
umpordez build-deps ../builds
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
After `build-deps`, the builds repo is ready to clone and run. Add
|
|
95
|
+
your `.env`, start the services, done.
|
|
96
|
+
|
|
97
|
+
### Other commands
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
umpordez --help # see all commands
|
|
101
|
+
umpordez --version # check version
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Tech stack
|
|
105
|
+
|
|
106
|
+
| Layer | Tech |
|
|
107
|
+
|------------|------|
|
|
108
|
+
| Backend | TypeScript, Express, PostgreSQL, Knex.js, Zod |
|
|
109
|
+
| Admin UI | React 18, Vite, Tailwind, shadcn/ui (Radix), React Query v5 |
|
|
110
|
+
| Public site| Express + EJS + Tailwind |
|
|
111
|
+
| Auth | httpOnly JWT cookies (bcrypt + 30-day expiry) |
|
|
112
|
+
| Uploads | AWS S3 via multer-s3, signed URLs |
|
|
113
|
+
| Email | Nodemailer + HTML templates |
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT; do whatever you want with it.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
*May the speedy force be with you.* ✌️
|
|
122
|
+
|
|
123
|
+
[youtube.com/ligeiro](https://youtube.com/ligeiro)
|
package/build.mjs
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
// umpordez.com design tokens
|
|
7
|
+
const green = chalk.hex('#02e027');
|
|
8
|
+
const cyan = chalk.hex('#00fff9');
|
|
9
|
+
const red = chalk.hex('#ff3c00');
|
|
10
|
+
const dim = chalk.hex('#a0a0a0');
|
|
11
|
+
|
|
12
|
+
function run(cmd, cwd) {
|
|
13
|
+
execSync(cmd, { cwd, stdio: 'inherit' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runSilent(cmd, cwd) {
|
|
17
|
+
execSync(cmd, { cwd, stdio: 'ignore' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── build ───────────────────────────────────────────────────────────────────
|
|
21
|
+
// Run locally. Compiles TypeScript + Vite, moves artifacts to builds repo.
|
|
22
|
+
// Does NOT install node_modules — that's build-deps on the target machine.
|
|
23
|
+
|
|
24
|
+
export async function buildProject(appDir, buildDir) {
|
|
25
|
+
appDir = path.resolve(appDir);
|
|
26
|
+
buildDir = path.resolve(buildDir);
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(appDir)) {
|
|
29
|
+
console.log(red(`App directory not found: ${appDir}`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(buildDir)) {
|
|
34
|
+
console.log(red(`Build directory not found: ${buildDir}`));
|
|
35
|
+
console.log(dim('Create it first: mkdir -p ' + buildDir));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(green.bold('=== umpordez build ==='));
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(` ${chalk.bold('Source:')} ${cyan(appDir)}`);
|
|
43
|
+
console.log(` ${chalk.bold('Output:')} ${cyan(buildDir)}`);
|
|
44
|
+
console.log('');
|
|
45
|
+
|
|
46
|
+
// Pull builds repo if git
|
|
47
|
+
if (fs.existsSync(path.join(buildDir, '.git'))) {
|
|
48
|
+
console.log(dim(' Pulling builds repo...'));
|
|
49
|
+
runSilent('git pull', buildDir);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Bump build number
|
|
53
|
+
const buildNumberFile = path.join(buildDir, 'build-number');
|
|
54
|
+
let number = 0;
|
|
55
|
+
if (fs.existsSync(buildNumberFile)) {
|
|
56
|
+
number = parseInt(
|
|
57
|
+
fs.readFileSync(buildNumberFile, 'utf-8').trim(),
|
|
58
|
+
10,
|
|
59
|
+
) || 0;
|
|
60
|
+
}
|
|
61
|
+
const nextNumber = number + 1;
|
|
62
|
+
|
|
63
|
+
// ─── Admin UI ────────────────────────────────────────────────
|
|
64
|
+
// Static files (HTML/CSS/JS) — no node_modules needed at runtime
|
|
65
|
+
|
|
66
|
+
const adminDir = path.join(appDir, 'ui', 'admin');
|
|
67
|
+
if (fs.existsSync(adminDir)) {
|
|
68
|
+
console.log(green('[1/3]') + ' Building Admin UI...');
|
|
69
|
+
|
|
70
|
+
const distDir = path.join(adminDir, 'dist');
|
|
71
|
+
if (fs.existsSync(distDir)) {
|
|
72
|
+
fs.rmSync(distDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
run('npm run build', adminDir);
|
|
76
|
+
|
|
77
|
+
const buildAdminOut = path.join(buildDir, 'ui', 'admin');
|
|
78
|
+
if (fs.existsSync(buildAdminOut)) {
|
|
79
|
+
fs.rmSync(buildAdminOut, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
fs.mkdirSync(buildAdminOut, { recursive: true });
|
|
82
|
+
|
|
83
|
+
run(`rsync -av dist/* "${buildAdminOut}"`, adminDir);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Site UI ─────────────────────────────────────────────────
|
|
87
|
+
// Express + EJS — needs node_modules at runtime (installed by build-deps)
|
|
88
|
+
|
|
89
|
+
const siteDir = path.join(appDir, 'ui', 'site');
|
|
90
|
+
if (fs.existsSync(siteDir)) {
|
|
91
|
+
console.log(green('[2/3]') + ' Building Site UI...');
|
|
92
|
+
|
|
93
|
+
const siteBuildDir = path.join(siteDir, 'build');
|
|
94
|
+
if (fs.existsSync(siteBuildDir)) {
|
|
95
|
+
fs.rmSync(siteBuildDir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
run('npm run build', siteDir);
|
|
99
|
+
|
|
100
|
+
const buildSiteOut = path.join(buildDir, 'ui', 'site');
|
|
101
|
+
|
|
102
|
+
// Preserve node_modules + .env from previous build-deps
|
|
103
|
+
const tmpModules = `/tmp/_umpordez_site_modules_${nextNumber}`;
|
|
104
|
+
const siteModules = path.join(buildSiteOut, 'node_modules');
|
|
105
|
+
if (fs.existsSync(siteModules)) {
|
|
106
|
+
fs.renameSync(siteModules, tmpModules);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const tmpEnv = `/tmp/_umpordez_site_env_${nextNumber}`;
|
|
110
|
+
const siteEnv = path.join(buildSiteOut, '.env');
|
|
111
|
+
if (fs.existsSync(siteEnv)) {
|
|
112
|
+
fs.renameSync(siteEnv, tmpEnv);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (fs.existsSync(buildSiteOut)) {
|
|
116
|
+
fs.rmSync(buildSiteOut, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
fs.mkdirSync(buildSiteOut, { recursive: true });
|
|
119
|
+
|
|
120
|
+
run(`rsync -av build/ "${buildSiteOut}/"`, siteDir);
|
|
121
|
+
|
|
122
|
+
const viewsDir = path.join(siteDir, 'views');
|
|
123
|
+
if (fs.existsSync(viewsDir)) {
|
|
124
|
+
run(`rsync -av views "${buildSiteOut}/"`, siteDir);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const publicDir = path.join(siteDir, 'public');
|
|
128
|
+
if (fs.existsSync(publicDir)) {
|
|
129
|
+
run(`rsync -av public "${buildSiteOut}/"`, siteDir);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fs.copyFileSync(
|
|
133
|
+
path.join(siteDir, 'package.json'),
|
|
134
|
+
path.join(buildSiteOut, 'package.json'),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const lockFile = path.join(siteDir, 'package-lock.json');
|
|
138
|
+
if (fs.existsSync(lockFile)) {
|
|
139
|
+
fs.copyFileSync(
|
|
140
|
+
lockFile,
|
|
141
|
+
path.join(buildSiteOut, 'package-lock.json'),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Restore .env and node_modules from previous build-deps
|
|
146
|
+
if (fs.existsSync(tmpEnv)) {
|
|
147
|
+
fs.renameSync(tmpEnv, path.join(buildSiteOut, '.env'));
|
|
148
|
+
}
|
|
149
|
+
if (fs.existsSync(tmpModules)) {
|
|
150
|
+
fs.renameSync(
|
|
151
|
+
tmpModules,
|
|
152
|
+
path.join(buildSiteOut, 'node_modules'),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Server ──────────────────────────────────────────────────
|
|
158
|
+
// Node.js — needs node_modules at runtime (installed by build-deps)
|
|
159
|
+
|
|
160
|
+
const serverDir = path.join(appDir, 'server');
|
|
161
|
+
if (fs.existsSync(serverDir)) {
|
|
162
|
+
console.log(green('[3/3]') + ' Building Server...');
|
|
163
|
+
|
|
164
|
+
const serverBuildDir = path.join(serverDir, 'build');
|
|
165
|
+
if (fs.existsSync(serverBuildDir)) {
|
|
166
|
+
fs.rmSync(serverBuildDir, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
run('npm run build', serverDir);
|
|
170
|
+
|
|
171
|
+
fs.copyFileSync(
|
|
172
|
+
path.join(serverDir, 'package.json'),
|
|
173
|
+
path.join(serverBuildDir, 'package.json'),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const serverLock = path.join(serverDir, 'package-lock.json');
|
|
177
|
+
if (fs.existsSync(serverLock)) {
|
|
178
|
+
fs.copyFileSync(
|
|
179
|
+
serverLock,
|
|
180
|
+
path.join(serverBuildDir, 'package-lock.json'),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Copy email templates
|
|
185
|
+
const emailsDir = path.join(serverDir, 'emails');
|
|
186
|
+
if (fs.existsSync(emailsDir)) {
|
|
187
|
+
const buildEmails = path.join(serverBuildDir, 'emails');
|
|
188
|
+
fs.mkdirSync(buildEmails, { recursive: true });
|
|
189
|
+
for (const f of fs.readdirSync(emailsDir)) {
|
|
190
|
+
fs.copyFileSync(
|
|
191
|
+
path.join(emailsDir, f),
|
|
192
|
+
path.join(buildEmails, f),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Remove .ts and .map from compiled migrations
|
|
198
|
+
const migrationsDir = path.join(serverBuildDir, 'migrations');
|
|
199
|
+
if (fs.existsSync(migrationsDir)) {
|
|
200
|
+
for (const f of fs.readdirSync(migrationsDir)) {
|
|
201
|
+
if (f.endsWith('.ts') || f.endsWith('.map')) {
|
|
202
|
+
fs.unlinkSync(path.join(migrationsDir, f));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const buildServerOut = path.join(buildDir, 'server');
|
|
208
|
+
|
|
209
|
+
// Preserve node_modules and .env from previous build-deps
|
|
210
|
+
const tmpServerModules = `/tmp/_umpordez_server_modules_${nextNumber}`;
|
|
211
|
+
const serverModules = path.join(buildServerOut, 'node_modules');
|
|
212
|
+
if (fs.existsSync(serverModules)) {
|
|
213
|
+
fs.renameSync(serverModules, tmpServerModules);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const tmpServerEnv = `/tmp/_umpordez_server_env_${nextNumber}`;
|
|
217
|
+
const serverEnv = path.join(buildServerOut, '.env');
|
|
218
|
+
if (fs.existsSync(serverEnv)) {
|
|
219
|
+
fs.renameSync(serverEnv, tmpServerEnv);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (fs.existsSync(buildServerOut)) {
|
|
223
|
+
fs.rmSync(buildServerOut, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
fs.mkdirSync(buildServerOut, { recursive: true });
|
|
226
|
+
|
|
227
|
+
run(`rsync -av build/* "${buildServerOut}"`, serverDir);
|
|
228
|
+
|
|
229
|
+
// Write knex.sh for the build repo (local knex + .js knexfile)
|
|
230
|
+
const knexSh = '#!/bin/bash\n'
|
|
231
|
+
+ 'node ./node_modules/knex/bin/cli.js \\\n'
|
|
232
|
+
+ ' --knexfile core/knexfile.js \\\n'
|
|
233
|
+
+ ' --migrations-directory ../migrations $@\n';
|
|
234
|
+
fs.writeFileSync(
|
|
235
|
+
path.join(buildServerOut, 'knex.sh'),
|
|
236
|
+
knexSh,
|
|
237
|
+
{ mode: 0o755 }
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Restore node_modules and .env
|
|
241
|
+
if (fs.existsSync(tmpServerEnv)) {
|
|
242
|
+
fs.renameSync(tmpServerEnv, path.join(buildServerOut, '.env'));
|
|
243
|
+
}
|
|
244
|
+
if (fs.existsSync(tmpServerModules)) {
|
|
245
|
+
fs.renameSync(
|
|
246
|
+
tmpServerModules,
|
|
247
|
+
path.join(buildServerOut, 'node_modules')
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── Version & Git ───────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
fs.writeFileSync(buildNumberFile, String(nextNumber), 'utf-8');
|
|
255
|
+
|
|
256
|
+
if (fs.existsSync(path.join(buildDir, '.git'))) {
|
|
257
|
+
console.log(dim(' Committing build...'));
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
run('git add .', buildDir);
|
|
261
|
+
run(
|
|
262
|
+
`git commit -m "build: #${nextNumber}"`,
|
|
263
|
+
buildDir,
|
|
264
|
+
);
|
|
265
|
+
} catch {
|
|
266
|
+
// nothing to commit
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Tag if doesn't exist
|
|
270
|
+
try {
|
|
271
|
+
execSync(
|
|
272
|
+
`git rev-parse "${nextNumber}"`,
|
|
273
|
+
{ cwd: buildDir, stdio: 'ignore' },
|
|
274
|
+
);
|
|
275
|
+
} catch {
|
|
276
|
+
run(
|
|
277
|
+
`git tag -a "${nextNumber}" -m "#${nextNumber}"`,
|
|
278
|
+
buildDir,
|
|
279
|
+
);
|
|
280
|
+
run('git push origin --tags', buildDir);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
run('git push', buildDir);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log(green.bold(`=== Build #${nextNumber} complete ===`));
|
|
288
|
+
console.log('');
|
|
289
|
+
console.log(` ${chalk.bold('Admin UI:')} ${cyan(path.join(buildDir, 'ui', 'admin'))}`);
|
|
290
|
+
console.log(` ${chalk.bold('Site UI:')} ${cyan(path.join(buildDir, 'ui', 'site'))}`);
|
|
291
|
+
console.log(` ${chalk.bold('Server:')} ${cyan(path.join(buildDir, 'server'))}`);
|
|
292
|
+
console.log('');
|
|
293
|
+
console.log(dim(' Next: run umpordez build-deps on the target machine'));
|
|
294
|
+
console.log('');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─── build-deps ──────────────────────────────────────────────────────────────
|
|
298
|
+
// Run on prod/staging. Installs production node_modules in the builds repo
|
|
299
|
+
// so native bindings match the target OS. After this, the repo is ready to run.
|
|
300
|
+
|
|
301
|
+
export async function buildDeps(buildDir) {
|
|
302
|
+
buildDir = path.resolve(buildDir);
|
|
303
|
+
|
|
304
|
+
if (!fs.existsSync(buildDir)) {
|
|
305
|
+
console.log(red(`Build directory not found: ${buildDir}`));
|
|
306
|
+
console.log(dim('Run umpordez build first.'));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log('');
|
|
311
|
+
console.log(green.bold('=== umpordez build-deps ==='));
|
|
312
|
+
console.log('');
|
|
313
|
+
console.log(` ${chalk.bold('Output:')} ${cyan(buildDir)}`);
|
|
314
|
+
console.log('');
|
|
315
|
+
|
|
316
|
+
// Pull builds repo if git
|
|
317
|
+
if (fs.existsSync(path.join(buildDir, '.git'))) {
|
|
318
|
+
console.log(dim(' Pulling builds repo...'));
|
|
319
|
+
runSilent('git pull', buildDir);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let step = 1;
|
|
323
|
+
const parts = [];
|
|
324
|
+
|
|
325
|
+
const buildServerOut = path.join(buildDir, 'server');
|
|
326
|
+
const buildSiteOut = path.join(buildDir, 'ui', 'site');
|
|
327
|
+
|
|
328
|
+
if (fs.existsSync(buildServerOut)) {
|
|
329
|
+
parts.push('server');
|
|
330
|
+
}
|
|
331
|
+
if (fs.existsSync(buildSiteOut)) {
|
|
332
|
+
parts.push('site');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (parts.length === 0) {
|
|
336
|
+
console.log(red('No build output found.'));
|
|
337
|
+
console.log(dim('Run umpordez build first.'));
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const total = parts.length;
|
|
342
|
+
|
|
343
|
+
// ─── Server deps ─────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
if (parts.includes('server')) {
|
|
346
|
+
console.log(
|
|
347
|
+
green(`[${step}/${total}]`)
|
|
348
|
+
+ ' Installing server dependencies...',
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const serverModules = path.join(buildServerOut, 'node_modules');
|
|
352
|
+
if (fs.existsSync(serverModules)) {
|
|
353
|
+
fs.rmSync(serverModules, { recursive: true });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
run('npm ci --omit=dev', buildServerOut);
|
|
358
|
+
} catch {
|
|
359
|
+
run('npm install --omit=dev', buildServerOut);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
step++;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Site UI deps ────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
if (parts.includes('site')) {
|
|
368
|
+
console.log(
|
|
369
|
+
green(`[${step}/${total}]`)
|
|
370
|
+
+ ' Installing site dependencies...',
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const siteModules = path.join(buildSiteOut, 'node_modules');
|
|
374
|
+
if (fs.existsSync(siteModules)) {
|
|
375
|
+
fs.rmSync(siteModules, { recursive: true });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
run('npm ci --omit=dev', buildSiteOut);
|
|
380
|
+
} catch {
|
|
381
|
+
run('npm install --omit=dev', buildSiteOut);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
step++;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ─── Git commit ──────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
if (fs.existsSync(path.join(buildDir, '.git'))) {
|
|
390
|
+
console.log(dim(' Committing dependencies...'));
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
run('git add .', buildDir);
|
|
394
|
+
run(
|
|
395
|
+
'git commit -m "deps: update node_modules"',
|
|
396
|
+
buildDir,
|
|
397
|
+
);
|
|
398
|
+
run('git push', buildDir);
|
|
399
|
+
} catch {
|
|
400
|
+
console.log(dim(' No dependency changes to commit.'));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
console.log('');
|
|
405
|
+
console.log(green.bold('=== Dependencies ready ==='));
|
|
406
|
+
console.log('');
|
|
407
|
+
console.log(dim(' Add .env and start the services.'));
|
|
408
|
+
console.log('');
|
|
409
|
+
}
|