@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.
Files changed (56) hide show
  1. package/build.ts +23 -0
  2. package/dist/build.d.ts +34 -1
  3. package/dist/build.js +19 -1
  4. package/dist/build.js.map +1 -1
  5. package/dist/env/env_gen.d.ts +17 -0
  6. package/dist/env/env_gen.js +154 -0
  7. package/dist/env/env_gen.js.map +1 -0
  8. package/dist/env/lambda/generate.d.ts +3 -2
  9. package/dist/env/lambda/generate.js +13 -9
  10. package/dist/env/lambda/generate.js.map +1 -1
  11. package/dist/env/web/web_gen.d.ts +19 -0
  12. package/dist/env/web/web_gen.js +93 -0
  13. package/dist/env/web/web_gen.js.map +1 -0
  14. package/env/env_gen.ts +179 -0
  15. package/env/lambda/generate.ts +15 -8
  16. package/env/web/web_gen.ts +115 -0
  17. package/package.json +1 -1
  18. package/tm/env/aws/README.md.frag +9 -0
  19. package/tm/env/aws/lambda.entry.ts.frag +89 -0
  20. package/tm/env/aws/serverless.yml.frag +23 -0
  21. package/tm/env/azure/README.md.frag +7 -0
  22. package/tm/env/azure/entry.ts.frag +49 -0
  23. package/tm/env/azure/host.json.frag +8 -0
  24. package/tm/env/basic/README.md.frag +7 -0
  25. package/tm/env/basic/backend.service.frag +14 -0
  26. package/tm/env/basic/deploy.sh.frag +13 -0
  27. package/tm/env/cloudflare/README.md.frag +7 -0
  28. package/tm/env/cloudflare/worker.ts.frag +13 -0
  29. package/tm/env/cloudflare/wrangler.toml.frag +7 -0
  30. package/tm/env/docker/Dockerfile.frag +10 -0
  31. package/tm/env/docker/README.md.frag +7 -0
  32. package/tm/env/docker/compose.yml.frag +10 -0
  33. package/tm/env/local/README.md.frag +7 -0
  34. package/tm/env/local/run.sh.frag +6 -0
  35. package/tm/env/server.entry.ts.frag +48 -0
  36. package/tm/env/vm/README.md.frag +5 -0
  37. package/tm/env/vm/cloud-init.yaml.frag +23 -0
  38. package/tm/web/backend/env/web/web.ts.frag +120 -0
  39. package/tm/web/backend/srv/auth/auth-srv.ts.frag +4 -0
  40. package/tm/web/backend/srv/auth/get_info.ts.frag +5 -0
  41. package/tm/web/backend/srv/auth/load_auth.ts.frag +6 -0
  42. package/tm/web/backend/srv/auth/signin_user.ts.frag +14 -0
  43. package/tm/web/backend/srv/auth/signout_user.ts.frag +5 -0
  44. package/tm/web/backend/srv/auth/web_load_auth.ts.frag +12 -0
  45. package/tm/web/backend/srv/auth/web_signin_user.ts.frag +25 -0
  46. package/tm/web/backend/srv/auth/web_signout_user.ts.frag +9 -0
  47. package/tm/web/web/e2e/smoke.spec.js.frag +21 -0
  48. package/tm/web/web/index.html.frag +13 -0
  49. package/tm/web/web/package.json.frag +17 -0
  50. package/tm/web/web/playwright.config.js.frag +36 -0
  51. package/tm/web/web/src/bus.js.frag +61 -0
  52. package/tm/web/web/src/cmp/admin.js.frag +254 -0
  53. package/tm/web/web/src/cmp/app.js.frag +30 -0
  54. package/tm/web/web/src/cmp/auth.js.frag +97 -0
  55. package/tm/web/web/src/main.js.frag +10 -0
  56. 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,5 @@
1
+ # $$Name$$ - vm environment
2
+
3
+ Boot any cloud VM with gen/env/vm/cloud-init.yaml as user-data, then push
4
+ code with the basic deploy script (BASIC_HOST=user@vm sh
5
+ gen/env/basic/deploy.sh) or your own pipeline.
@@ -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,4 @@
1
+
2
+ import { MakeSrv } from '@voxgig/system'
3
+
4
+ module.exports = MakeSrv('auth', require)
@@ -0,0 +1,5 @@
1
+ module.exports = function make_get_info() {
2
+ return async function get_info(this: any, _msg: any) {
3
+ return { ok: true, srv: 'auth' }
4
+ }
5
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = function make_load_auth() {
2
+ return async function load_auth(this: any, msg: any) {
3
+ const user = await this.entity('sys/user').load$(msg.user_id)
4
+ return { ok: !!user, state: user ? 'signedin' : 'signedout', user }
5
+ }
6
+ }
@@ -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,5 @@
1
+ module.exports = function make_signout_user() {
2
+ return async function signout_user(this: any, msg: any) {
3
+ return this.post('sys:user,logout:user', { token: msg.token })
4
+ }
5
+ }
@@ -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
+ }