@voxgig/build 4.1.0 → 4.3.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/build.ts +23 -0
- package/dist/build.d.ts +34 -1
- package/dist/build.js +19 -1
- package/dist/build.js.map +1 -1
- package/dist/env/env_gen.d.ts +17 -0
- package/dist/env/env_gen.js +154 -0
- package/dist/env/env_gen.js.map +1 -0
- package/dist/env/lambda/generate.d.ts +3 -2
- package/dist/env/lambda/generate.js +13 -9
- package/dist/env/lambda/generate.js.map +1 -1
- package/dist/env/web/web_gen.d.ts +19 -0
- package/dist/env/web/web_gen.js +93 -0
- package/dist/env/web/web_gen.js.map +1 -0
- package/env/env_gen.ts +179 -0
- package/env/lambda/generate.ts +15 -8
- package/env/web/web_gen.ts +115 -0
- package/package.json +1 -1
- package/tm/env/aws/README.md.frag +9 -0
- package/tm/env/aws/lambda.entry.ts.frag +89 -0
- package/tm/env/aws/serverless.yml.frag +23 -0
- package/tm/env/azure/README.md.frag +7 -0
- package/tm/env/azure/entry.ts.frag +49 -0
- package/tm/env/azure/host.json.frag +8 -0
- package/tm/env/basic/README.md.frag +7 -0
- package/tm/env/basic/backend.service.frag +14 -0
- package/tm/env/basic/deploy.sh.frag +13 -0
- package/tm/env/cloudflare/README.md.frag +7 -0
- package/tm/env/cloudflare/worker.ts.frag +13 -0
- package/tm/env/cloudflare/wrangler.toml.frag +7 -0
- package/tm/env/docker/Dockerfile.frag +10 -0
- package/tm/env/docker/README.md.frag +7 -0
- package/tm/env/docker/compose.yml.frag +10 -0
- package/tm/env/local/README.md.frag +7 -0
- package/tm/env/local/run.sh.frag +6 -0
- package/tm/env/server.entry.ts.frag +48 -0
- package/tm/env/vm/README.md.frag +5 -0
- package/tm/env/vm/cloud-init.yaml.frag +23 -0
- package/tm/web/backend/env/web/web.ts.frag +120 -0
- package/tm/web/backend/srv/auth/auth-srv.ts.frag +4 -0
- package/tm/web/backend/srv/auth/get_info.ts.frag +5 -0
- package/tm/web/backend/srv/auth/load_auth.ts.frag +6 -0
- package/tm/web/backend/srv/auth/signin_user.ts.frag +14 -0
- package/tm/web/backend/srv/auth/signout_user.ts.frag +5 -0
- package/tm/web/backend/srv/auth/web_load_auth.ts.frag +12 -0
- package/tm/web/backend/srv/auth/web_signin_user.ts.frag +25 -0
- package/tm/web/backend/srv/auth/web_signout_user.ts.frag +9 -0
- package/tm/web/web/e2e/smoke.spec.js.frag +21 -0
- package/tm/web/web/index.html.frag +13 -0
- package/tm/web/web/package.json.frag +17 -0
- package/tm/web/web/playwright.config.js.frag +36 -0
- package/tm/web/web/src/bus.js.frag +61 -0
- package/tm/web/web/src/cmp/admin.js.frag +254 -0
- package/tm/web/web/src/cmp/app.js.frag +30 -0
- package/tm/web/web/src/cmp/auth.js.frag +97 -0
- package/tm/web/web/src/main.js.frag +10 -0
- package/tm/web/web/src/style.css.frag +26 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
// $$Name$$ backend - $$env$$ environment entry (generated once; this file
|
|
3
|
+
// is yours to edit). A long-lived server process using the shared Seneca
|
|
4
|
+
// setup (src/env/shared/basic.ts); reads its env config from the model
|
|
5
|
+
// (main.env.$$env$$).
|
|
6
|
+
|
|
7
|
+
import Seneca from 'seneca'
|
|
8
|
+
import { Local } from '@voxgig/system'
|
|
9
|
+
|
|
10
|
+
import { basic, base } from '../shared/basic'
|
|
11
|
+
|
|
12
|
+
import Pkg from '../../../package.json'
|
|
13
|
+
import Model from '../../../model/model.json'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
run()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async function run() {
|
|
20
|
+
const { deep } = Seneca.util
|
|
21
|
+
|
|
22
|
+
const envdef: any = (Model as any).main.env?.['$$env$$'] || {}
|
|
23
|
+
|
|
24
|
+
const seneca = Seneca(deep(base.seneca, {
|
|
25
|
+
tag: '$$name$$-$$env$$',
|
|
26
|
+
log: { level: envdef.log || 'warn' },
|
|
27
|
+
}, envdef.seneca || {}))
|
|
28
|
+
|
|
29
|
+
seneca.context.model = Model
|
|
30
|
+
seneca.context.env = '$$env$$'
|
|
31
|
+
seneca.context.stage = process.env.STAGE || '$$env$$'
|
|
32
|
+
seneca.context.srvname = 'all'
|
|
33
|
+
seneca.context.pkg = Pkg
|
|
34
|
+
|
|
35
|
+
basic(seneca)
|
|
36
|
+
|
|
37
|
+
seneca.use(Local, {
|
|
38
|
+
srv: {
|
|
39
|
+
folder: __dirname + '/../../../dist/srv',
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
await seneca.ready()
|
|
44
|
+
|
|
45
|
+
console.log('$$name$$-backend started', {
|
|
46
|
+
env: '$$env$$', version: Pkg.version, port: $$port$$,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#cloud-config
|
|
2
|
+
# $$Name$$ backend VM bootstrap (generated; customize via tm/env/vm/)
|
|
3
|
+
packages:
|
|
4
|
+
- nodejs
|
|
5
|
+
- npm
|
|
6
|
+
- rsync
|
|
7
|
+
write_files:
|
|
8
|
+
- path: /etc/systemd/system/$$name$$-backend.service
|
|
9
|
+
content: |
|
|
10
|
+
[Unit]
|
|
11
|
+
Description=$$Name$$ backend
|
|
12
|
+
After=network.target
|
|
13
|
+
[Service]
|
|
14
|
+
WorkingDirectory=/opt/$$name$$/backend
|
|
15
|
+
ExecStart=/usr/bin/node dist/env/local/local.js
|
|
16
|
+
Restart=always
|
|
17
|
+
Environment=NODE_ENV=production
|
|
18
|
+
[Install]
|
|
19
|
+
WantedBy=multi-user.target
|
|
20
|
+
runcmd:
|
|
21
|
+
- mkdir -p /opt/$$name$$/backend
|
|
22
|
+
- systemctl daemon-reload
|
|
23
|
+
- systemctl enable $$name$$-backend
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
|
|
2
|
+
// Local web runner: the local in-memory backend plus an HTTP layer for
|
|
3
|
+
// the SPA - express serving web/dist, the model, and a single seneca
|
|
4
|
+
// gateway endpoint (/seneca) that the browser-side Seneca bus posts
|
|
5
|
+
// messages to (seneca-browser fetch transport). Cookie auth via
|
|
6
|
+
// @seneca/gateway-auth (express_cookie): signin sets the todo-auth
|
|
7
|
+
// cookie, after which the gateway attaches the principal so @seneca/owner
|
|
8
|
+
// scopes todos per user.
|
|
9
|
+
//
|
|
10
|
+
// Predefined users are seeded at startup (local testing only).
|
|
11
|
+
|
|
12
|
+
import Path from 'node:path'
|
|
13
|
+
|
|
14
|
+
import Express from 'express'
|
|
15
|
+
import CookieParser from 'cookie-parser'
|
|
16
|
+
|
|
17
|
+
import Seneca from 'seneca'
|
|
18
|
+
import { Local } from '@voxgig/system'
|
|
19
|
+
|
|
20
|
+
import { basic, base } from '../shared/basic'
|
|
21
|
+
|
|
22
|
+
import Pkg from '../../../package.json'
|
|
23
|
+
import Model from '../../../model/model.json'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const PORT = parseInt(process.env.PORT || '', 10) ||
|
|
27
|
+
(Model as any).main.conf.port.backend
|
|
28
|
+
|
|
29
|
+
// Predefined users seeded at startup (local testing only). Edit freely.
|
|
30
|
+
const USERS = $$users$$
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
run()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async function run() {
|
|
37
|
+
const { deep } = Seneca.util
|
|
38
|
+
|
|
39
|
+
const seneca = Seneca(deep(base.seneca, { tag: '$$name$$-web' }))
|
|
40
|
+
|
|
41
|
+
seneca.context.model = Model
|
|
42
|
+
seneca.context.env = 'local'
|
|
43
|
+
seneca.context.stage = 'local'
|
|
44
|
+
seneca.context.srvname = 'all'
|
|
45
|
+
seneca.context.pkg = Pkg
|
|
46
|
+
|
|
47
|
+
seneca.test()
|
|
48
|
+
|
|
49
|
+
basic(seneca)
|
|
50
|
+
|
|
51
|
+
seneca
|
|
52
|
+
.use('gateway', {
|
|
53
|
+
// Allow auth + every service the model declares (aim:<srv>).
|
|
54
|
+
allow: Object.keys((Model as any).main.srv).reduce(
|
|
55
|
+
(a: any, n: string) => (a['aim:' + n] = true, a),
|
|
56
|
+
{ 'aim:req,on:auth': true }),
|
|
57
|
+
})
|
|
58
|
+
.use('gateway-express', {
|
|
59
|
+
// gateway-express sets/clears the auth cookie when a result carries
|
|
60
|
+
// gateway$.auth (see web_signin_user / web_signout_user). Cookie
|
|
61
|
+
// attributes use the plugin defaults (httpOnly, sameSite).
|
|
62
|
+
auth: {
|
|
63
|
+
token: {
|
|
64
|
+
name: '$$name$$-auth',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
.use('gateway-auth', {
|
|
69
|
+
spec: {
|
|
70
|
+
express_cookie: {
|
|
71
|
+
active: true,
|
|
72
|
+
token: {
|
|
73
|
+
name: '$$name$$-auth',
|
|
74
|
+
},
|
|
75
|
+
user: {
|
|
76
|
+
auth: true,
|
|
77
|
+
require: false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
seneca.use(Local, {
|
|
84
|
+
srv: {
|
|
85
|
+
folder: __dirname + '/../../../dist/srv',
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await seneca.ready()
|
|
90
|
+
|
|
91
|
+
// Seed the predefined users (idempotent per boot: in-memory store).
|
|
92
|
+
for (const u of USERS) {
|
|
93
|
+
const res = await seneca.post('sys:user,register:user', u)
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
console.log('SEED-USER-FAILED', u.email, res.why)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const app = Express()
|
|
100
|
+
// web/ is a sibling of backend/; from dist/env/local go up to the
|
|
101
|
+
// project root, then web/dist.
|
|
102
|
+
const webdist = Path.join(
|
|
103
|
+
__dirname, '..', '..', '..', '..', 'web', 'dist')
|
|
104
|
+
|
|
105
|
+
app
|
|
106
|
+
.use(Express.json())
|
|
107
|
+
.use(new (CookieParser as any)())
|
|
108
|
+
.post('/seneca', seneca.export('gateway-express/handler'))
|
|
109
|
+
.get('/model.json',
|
|
110
|
+
(_req: any, res: any) => res.sendFile(
|
|
111
|
+
Path.join(__dirname, '..', '..', '..', 'model', 'model.json')))
|
|
112
|
+
.use(Express.static(webdist))
|
|
113
|
+
.listen(PORT)
|
|
114
|
+
|
|
115
|
+
console.log('$$name$$-web started', {
|
|
116
|
+
port: PORT,
|
|
117
|
+
users: USERS.map((u) => u.email),
|
|
118
|
+
version: Pkg.version,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Sign a user in via @seneca/user login (email + password).
|
|
2
|
+
module.exports = function make_signin_user() {
|
|
3
|
+
return async function signin_user(this: any, msg: any) {
|
|
4
|
+
const seneca = this
|
|
5
|
+
|
|
6
|
+
const out = await seneca.post('sys:user,login:user', {
|
|
7
|
+
email: msg.email,
|
|
8
|
+
password: msg.password,
|
|
9
|
+
fields: ['id', 'email', 'name', 'handle'],
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
return out
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Current auth state, from the principal the gateway resolved (cookie).
|
|
2
|
+
module.exports = function make_web_load_auth() {
|
|
3
|
+
return async function web_load_auth(this: any, _msg: any, meta: any) {
|
|
4
|
+
const user = meta.custom?.principal?.user
|
|
5
|
+
const out: any = { ok: true, state: 'signedout' }
|
|
6
|
+
if (user) {
|
|
7
|
+
out.state = 'signedin'
|
|
8
|
+
out.user = { id: user.id, email: user.email, handle: user.handle }
|
|
9
|
+
}
|
|
10
|
+
return out
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Gateway signin: on success the gateway$ auth token result field makes
|
|
2
|
+
// gateway-auth (express_cookie) set the auth cookie.
|
|
3
|
+
module.exports = function make_web_signin_user() {
|
|
4
|
+
return async function web_signin_user(this: any, msg: any) {
|
|
5
|
+
const res = await this.post('aim:auth,signin:user', {
|
|
6
|
+
email: msg.email,
|
|
7
|
+
password: msg.password,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const out: any = { ok: res.ok }
|
|
11
|
+
if (res.ok) {
|
|
12
|
+
out.gateway$ = { auth: { token: res.login.token } }
|
|
13
|
+
out.user = {
|
|
14
|
+
id: res.user.id,
|
|
15
|
+
email: res.user.email,
|
|
16
|
+
name: res.user.name,
|
|
17
|
+
handle: res.user.handle,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
out.why = res.why
|
|
22
|
+
}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module.exports = function make_web_signout_user() {
|
|
2
|
+
return async function web_signout_user(this: any, _msg: any, meta: any) {
|
|
3
|
+
const token = meta.custom?.principal?.token
|
|
4
|
+
if (token) {
|
|
5
|
+
await this.post('aim:auth,signout:user', { token })
|
|
6
|
+
}
|
|
7
|
+
return { ok: true, gateway$: { auth: { remove: true } } }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
// Generic smoke test: the SPA boots, a seeded user can sign in, and the
|
|
4
|
+
// model-driven entity admin renders with at least one entity. Add
|
|
5
|
+
// project-specific CRUD flows alongside this file.
|
|
6
|
+
|
|
7
|
+
test('sign in and load the entity admin', async ({ page }) => {
|
|
8
|
+
await page.goto('/')
|
|
9
|
+
|
|
10
|
+
await expect(page.locator('vg-auth h2')).toHaveText('Sign in')
|
|
11
|
+
|
|
12
|
+
await page.fill('input[name=email]', '$$seedEmail$$')
|
|
13
|
+
await page.fill('input[name=password]', '$$seedPassword$$')
|
|
14
|
+
await page.click('vg-auth button[type=submit]')
|
|
15
|
+
|
|
16
|
+
await expect(page.locator('.vg-auth-bar b')).toHaveText('$$seedEmail$$')
|
|
17
|
+
await expect(page.locator('vg-entity-admin nav a')).not.toHaveCount(0)
|
|
18
|
+
|
|
19
|
+
// The first entity's list loads (count line rendered).
|
|
20
|
+
await expect(page.locator('#vg-count')).toBeVisible()
|
|
21
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>$$Name$$</title>
|
|
7
|
+
<script src="/seneca-browser.js"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<vg-app></vg-app>
|
|
11
|
+
<script type="module" src="/src/main.js"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "$$name$$-web",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "$$Name$$ SPA: web components on a Seneca service bus",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"postinstall": "node -e \"require('fs').copyFileSync(require.resolve('seneca-browser'),'public/seneca-browser.js')\"",
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"e2e": "playwright test"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@playwright/test": "^1.58.2",
|
|
14
|
+
"seneca-browser": "^8.0.0-rc4",
|
|
15
|
+
"vite": "^7.3.4"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Headless-chrome E2E for the $$Name$$ SPA. The backend web runner (which
|
|
2
|
+
// serves the built SPA and the /seneca gateway) is started as the test
|
|
3
|
+
// webServer; Playwright waits for it, then drives the SPA.
|
|
4
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
5
|
+
|
|
6
|
+
const PORT = $$e2eport$$
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
testDir: './e2e',
|
|
10
|
+
timeout: 30000,
|
|
11
|
+
fullyParallel: false,
|
|
12
|
+
reporter: [['list']],
|
|
13
|
+
use: {
|
|
14
|
+
baseURL: `http://localhost:${PORT}`,
|
|
15
|
+
trace: 'off',
|
|
16
|
+
},
|
|
17
|
+
projects: [
|
|
18
|
+
{
|
|
19
|
+
name: 'chromium',
|
|
20
|
+
use: {
|
|
21
|
+
...devices['Desktop Chrome'],
|
|
22
|
+
// Self-contained chromium (no system chrome / no desktop): install
|
|
23
|
+
// with `npx playwright install chromium` (no --with-deps).
|
|
24
|
+
launchOptions: { args: ['--no-sandbox'] },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
webServer: {
|
|
29
|
+
command: `npm run build && cd ../backend && PORT=${PORT} node dist/env/web/web.js`,
|
|
30
|
+
url: `http://localhost:${PORT}/`,
|
|
31
|
+
timeout: 60000,
|
|
32
|
+
reuseExistingServer: false,
|
|
33
|
+
stdout: 'ignore',
|
|
34
|
+
stderr: 'pipe',
|
|
35
|
+
},
|
|
36
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The browser-side Seneca service bus. All component state and all
|
|
2
|
+
// backend communication flow through messages on this bus:
|
|
3
|
+
//
|
|
4
|
+
// aim:* -> the backend gateway (/seneca) via the
|
|
5
|
+
// seneca-browser fetch transport (cookie auth)
|
|
6
|
+
// cmp:* -> local component plugins (state)
|
|
7
|
+
//
|
|
8
|
+
// Cross-component notification uses the tiny `evt` plugin below:
|
|
9
|
+
// components post cmp:bus,emit:<name> and subscribe with onEvent().
|
|
10
|
+
|
|
11
|
+
/* global Seneca */
|
|
12
|
+
|
|
13
|
+
const bus = Seneca({
|
|
14
|
+
legacy: false,
|
|
15
|
+
timeout: 8888,
|
|
16
|
+
plugin: {
|
|
17
|
+
browser: {
|
|
18
|
+
endpoint: '/seneca',
|
|
19
|
+
fetch: {
|
|
20
|
+
credentials: 'same-origin',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
.test()
|
|
26
|
+
.client({ type: 'browser', pin: 'aim:*' })
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// Event fanout: seneca actions have single handlers, so the bus plugin
|
|
30
|
+
// keeps a listener registry for UI notification.
|
|
31
|
+
const listeners = {}
|
|
32
|
+
|
|
33
|
+
bus.use(function evt() {
|
|
34
|
+
this.add('cmp:bus,emit:evt', function (msg, reply) {
|
|
35
|
+
const subs = listeners[msg.name] || []
|
|
36
|
+
for (const fn of subs) {
|
|
37
|
+
try {
|
|
38
|
+
fn(msg.data || {})
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.error('evt listener failed', msg.name, e)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
reply({ ok: true, count: subs.length })
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
function onEvent(name, fn) {
|
|
49
|
+
(listeners[name] = listeners[name] || []).push(fn)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function emit(name, data) {
|
|
53
|
+
bus.post('cmp:bus,emit:evt', { name, data })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
bus,
|
|
59
|
+
onEvent,
|
|
60
|
+
emit,
|
|
61
|
+
}
|