@vevedh/create-feathers-app 0.1.0-rc.1 → 0.1.0-rc.4
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/README.md +55 -18
- package/bin/create-feathers-app.mjs +13 -2
- package/lib/scaffold.mjs +178 -14
- package/lib/services.mjs +110 -0
- package/package.json +1 -1
- package/templates/common/dev-all-quasar-ssr.mjs +37 -0
- package/templates/daisyui-ssg/gitignore.template +20 -0
- package/templates/daisyui-ssg/src/style.css +3 -1
- package/templates/daisyui-ssr/gitignore.template +20 -0
- package/templates/daisyui-ssr/src/style.css +3 -1
- package/templates/quasar-ssr/.env.example +11 -0
- package/templates/quasar-ssr/api/server.mjs +45 -0
- package/templates/quasar-ssr/api/services/health/health.class.mjs +3 -0
- package/templates/quasar-ssr/api/services/health/health.service.mjs +4 -0
- package/templates/quasar-ssr/api/services/notes/notes.class.mjs +25 -0
- package/templates/quasar-ssr/api/services/notes/notes.service.mjs +10 -0
- package/templates/quasar-ssr/gitignore.template +36 -0
- package/templates/quasar-ssr/index.html +12 -0
- package/templates/quasar-ssr/quasar.config.ts +27 -0
- package/templates/quasar-ssr/scripts/edge-proxy.mjs +30 -0
- package/templates/quasar-ssr/scripts/edge-routing.mjs +14 -0
- package/templates/quasar-ssr/src/App.vue +1 -0
- package/templates/quasar-ssr/src/css/app.css +20 -0
- package/templates/quasar-ssr/src/pages/IndexPage.vue +146 -0
- package/templates/quasar-ssr/src/pages/NotFoundPage.vue +11 -0
- package/templates/quasar-ssr/src/router/index.ts +8 -0
- package/templates/quasar-ssr/src/router/routes.ts +6 -0
- package/templates/quasar-ssr/src/stores/index.ts +4 -0
- package/templates/quasar-ssr/src/stores/notes.ts +73 -0
- package/templates/quasar-ssr/src-ssr/middlewares/render.ts +15 -0
- package/templates/quasar-ssr/src-ssr/server.ts +42 -0
- package/templates/quasar-ssr/tsconfig.json +4 -0
- package/templates/spa/common/.env.example +1 -0
- package/templates/spa/common/api/server.mjs +2 -1
- package/templates/spa/common/gitignore.template +20 -0
- package/templates/spa/common/src/stores/notes.ts +71 -12
- package/templates/spa/daisyui/src/components/NotesPanel.vue +24 -7
- package/templates/spa/daisyui/src/style.css +3 -1
- package/templates/spa/quasar2/src/components/NotesPanel.vue +24 -6
package/README.md
CHANGED
|
@@ -1,43 +1,80 @@
|
|
|
1
1
|
# create-feathers-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Générez des applications FeathersJS v5 avec Vue 3, Pinia, Vite, Quasar 2 ou Tailwind CSS + daisyUI.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Le CLI est indépendant du runtime [`@vevedh/vite-plugin-feathersjs`](https://www.npmjs.com/package/@vevedh/vite-plugin-feathersjs) (version 0.2.6). La version candidate `0.1.0-rc.4` **n'est pas encore publiée**. Elle ajoute la PWA optionnelle au rendu SPA.
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
9
|
```powershell
|
|
10
|
-
|
|
10
|
+
# Préversion disponible sur npm : 0.1.0-rc.1
|
|
11
|
+
npm exec --yes --package=@vevedh/create-feathers-app@0.1.0-rc.1 -- create-feathers-app portail --ui daisyui --render spa
|
|
12
|
+
|
|
13
|
+
# R17-r3 candidat, depuis les sources mises en stage :
|
|
14
|
+
node .release/create-feathers-app/bin/create-feathers-app.mjs portail-ssr --ui quasar2 --render ssr
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
| Interface | SPA | SSR | SSG |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| Quasar 2 | Générable | Générable, validation Windows en cours | Bloqué |
|
|
20
|
+
| daisyUI 5 | Générable | Générable | Générable |
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
## PWA optionnelle (SPA uniquement)
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
Le paramètre `--pwa on` ajoute `vite-plugin-pwa` v2 en dépendance de développement, une icône SVG locale de démonstration et un manifeste Web App. Le service worker, désactivé en développement, est généré au build et conserve uniquement des ressources statiques, jamais les réponses `/api` ou les échanges Socket.IO. Par défaut, `--pwa off` ne change pas les templates existants.
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
node .release/create-feathers-app/bin/create-feathers-app.mjs portail-spa --ui quasar2 --render spa --pwa on --dry-run
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
SSR/SSG avec PWA est refusé tant que leur cycle de vie n'est pas certifié. Pour une installation sur les navigateurs ciblés, remplacer l'icône SVG de démonstration par des icônes PNG 192 et 512 pixels, puis contrôler HTTPS, les mises à jour et le mode hors-ligne.
|
|
31
|
+
|
|
32
|
+
## Ajouter un service Feathers
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
```powershell
|
|
35
|
+
cd portail
|
|
36
|
+
node ../chemin/vers/create-feathers-app/bin/create-feathers-app.mjs service add tickets --schema zod --db main
|
|
37
|
+
```
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
Le générateur ajoute le module `api/services/tickets` et son inscription dans le registre Feathers. Les schémas disponibles sont `typebox`, `json` et `zod`. Le service mémoire est destiné au laboratoire local, avec CRUD REST et validation des données.
|
|
25
40
|
|
|
26
|
-
|
|
41
|
+
## Profils d'identité et de bases
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
La création de projet accepte `--auth none|jwt|oauth2|oidc|keycloak`, `--schema typebox|json|zod` et `--db main:memory,audit:mongodb` avec alias nommés. **Les authentifications externes et les connexions persistantes sont des profils de configuration, pas des intégrations opérationnelles.** L'API échoue volontairement au démarrage jusqu'au raccordement et à la certification du fournisseur ou du SGBD.
|
|
44
|
+
|
|
45
|
+
Les projets sans authentification sont exclusivement des démonstrations locales. Ils ne doivent jamais être exposés tels quels sur Internet. Aucun secret, mot de passe ou jeton n'est inclus dans les templates.
|
|
46
|
+
|
|
47
|
+
## Windows
|
|
29
48
|
|
|
30
49
|
```powershell
|
|
31
50
|
corepack pnpm install
|
|
32
|
-
corepack pnpm run dev:all
|
|
33
51
|
corepack pnpm run typecheck
|
|
34
52
|
corepack pnpm run build
|
|
53
|
+
corepack pnpm run dev:all
|
|
35
54
|
```
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
SPA et daisyUI SSR/SSG utilisent `http://127.0.0.1:5173`. Quasar SSR utilise `http://127.0.0.1:8080` via un proxy local ; le SSR brut écoute sur 9100.
|
|
57
|
+
|
|
58
|
+
La documentation publique VitePress figure dans `docs/cli` du dépôt et sera déployée sur GitHub Pages après activation du workflow de publication.
|
|
38
59
|
|
|
39
|
-
|
|
60
|
+
## API Feathers intégrée à Vite en développement (RC4)
|
|
40
61
|
|
|
41
|
-
|
|
62
|
+
Pour les applications SPA, `--dev-api vite` est désormais la valeur par défaut.
|
|
63
|
+
Le plugin public `@vevedh/vite-plugin-feathersjs@0.2.6` est chargé depuis
|
|
64
|
+
`vite.config.ts` et monte les services sur `/api` pendant `vite dev`.
|
|
65
|
+
Il n'y a pas de second serveur REST ni de proxy REST à démarrer.
|
|
66
|
+
|
|
67
|
+
L'option `--dev-api separate` conserve l'ancien comportement à deux processus.
|
|
68
|
+
Pour SSR/SSG, le mode séparé reste obligatoire. Avec `--transport both`,
|
|
69
|
+
Socket.IO reste sur le port privé `127.0.0.1:3031`, accessible depuis la SPA
|
|
70
|
+
via le proxy WebSocket Vite. La configuration de Feathers échoue au chargement
|
|
71
|
+
si les profils d'identité ou bases externes ne sont pas opérationnels.
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
node .release/create-feathers-app/bin/create-feathers-app.mjs mon-spa --ui quasar2 --render spa --dev-api vite --transport both
|
|
75
|
+
```
|
|
42
76
|
|
|
43
|
-
|
|
77
|
+
**Production** : le plugin Vite s'exécute uniquement en développement.
|
|
78
|
+
Le serveur `api/server.mjs` autonome reste disponible et obligatoire pour
|
|
79
|
+
le déploiement ; `vite preview` n'héberge pas l'API.
|
|
80
|
+
Le runtime `0.2.6` n'a pas été modifié.
|
|
@@ -3,6 +3,7 @@ import { dirname,resolve } from 'node:path'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { readFile } from 'node:fs/promises'
|
|
5
5
|
import { createGenerator } from '../lib/scaffold.mjs'
|
|
6
|
+
import { runServiceCommand } from '../lib/services.mjs'
|
|
6
7
|
const packageRoot=resolve(dirname(fileURLToPath(import.meta.url)),'..')
|
|
7
8
|
const {version}=JSON.parse(await readFile(resolve(packageRoot,'package.json'),'utf8'))
|
|
8
9
|
const args=process.argv.slice(2)
|
|
@@ -16,13 +17,23 @@ Usage: create-feathers-app <name> [options]
|
|
|
16
17
|
--render <spa|ssr|ssg> Default spa
|
|
17
18
|
--adapter <koa|express> Default koa
|
|
18
19
|
--transport <rest|both> Default rest (both only for SPA)
|
|
20
|
+
--pwa <on|off> Optional offline SPA shell (default off, no API caching)
|
|
21
|
+
--dev-api <vite|separate> SPA default vite; SSR/SSG always separate
|
|
19
22
|
--dry-run Preview files without writing
|
|
20
23
|
--json Machine-readable output
|
|
24
|
+
--schema <typebox|json|zod> Service schema preset (default typebox)
|
|
25
|
+
--auth <none|jwt|oauth2|oidc|keycloak> Identity configuration blueprint
|
|
26
|
+
--db <alias:adapter[,alias:adapter]> Named connections (default main:memory)
|
|
21
27
|
--version Print CLI version
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
Service: create-feathers-app service add <name> [--project <path>] [--schema typebox|json|zod] [--db <alias>] [--auth none|jwt] [--dry-run]
|
|
30
|
+
|
|
31
|
+
Available: Quasar 2 SPA/SSR; daisyUI SPA, SSR, SSG.
|
|
32
|
+
Quasar SSR uses the R16-certified architecture; R17-generated SSR requires its own Windows E2E certification. Quasar SSG not supported.`)
|
|
25
33
|
}else if(args.includes('--version')||args.includes('-v'))console.log(version)
|
|
34
|
+
else if(args[0]==='service') {
|
|
35
|
+
try { await runServiceCommand(args.slice(1),process.cwd()) } catch(error){console.error(`FAIL: ${error.message}`);process.exitCode=1}
|
|
36
|
+
}
|
|
26
37
|
else {
|
|
27
38
|
const {parse,generate}=createGenerator({outputRoot:process.cwd(),templatesRoot:resolve(packageRoot,'templates'),reportRoot:process.cwd()})
|
|
28
39
|
try {
|
package/lib/scaffold.mjs
CHANGED
|
@@ -3,9 +3,10 @@ import { existsSync } from 'node:fs'
|
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
4
4
|
import { buildProjectPlan } from './planner/plan.mjs'
|
|
5
5
|
|
|
6
|
-
const KEYS = new Set(['--name', '--dir', '--ui', '--render', '--adapter', '--transport', '--dry-run', '--json'])
|
|
6
|
+
const KEYS = new Set(['--name', '--dir', '--ui', '--render', '--adapter', '--transport', '--dry-run', '--json', '--schema', '--auth', '--db', '--pwa', '--dev-api'])
|
|
7
7
|
const CASES = Object.freeze({
|
|
8
8
|
'quasar2:spa': 'quasar-spa',
|
|
9
|
+
'quasar2:ssr': 'quasar-ssr',
|
|
9
10
|
'daisyui:spa': 'daisyui-spa',
|
|
10
11
|
'daisyui:ssr': 'daisyui-ssr',
|
|
11
12
|
'daisyui:ssg': 'daisyui-ssg',
|
|
@@ -50,6 +51,7 @@ function spaManifest(o) {
|
|
|
50
51
|
vite:'^7.3.7', '@vitejs/plugin-vue':'^6.0.0', 'vue-tsc':'^3.0.7',
|
|
51
52
|
typescript:'^5.9.3', '@types/node':'^22.0.0',
|
|
52
53
|
}
|
|
54
|
+
if (o.pwa === 'on') devDependencies['vite-plugin-pwa'] = '^2.0.0'
|
|
53
55
|
if (o.ui === 'quasar2') { dependencies.quasar = '^2.35.2'; devDependencies['@quasar/vite-plugin']='^1.12.0';devDependencies['sass-embedded']='^1.93.2' }
|
|
54
56
|
else { devDependencies.tailwindcss='^4.1.0';devDependencies['@tailwindcss/vite']='^4.1.0';devDependencies.daisyui='^5.0.0' }
|
|
55
57
|
return {name:o.name,version:'0.1.0',private:true,type:'module',packageManager:'pnpm@10.30.2',engines:{node:'>=22.12'},
|
|
@@ -60,19 +62,99 @@ function spaManifest(o) {
|
|
|
60
62
|
function generatedReadme(o) {
|
|
61
63
|
const renderInfo = o.render === 'ssg' ? 'Le build pré-rend les routes publiques / et /about. Les notes restent dynamiques via /api.'
|
|
62
64
|
: o.render === 'ssr' ? 'Le serveur Node effectue le SSR avec un état Pinia distinct par requête.'
|
|
63
|
-
:
|
|
65
|
+
: o.devApi === 'vite'
|
|
66
|
+
? 'En développement, le plugin Vite monte les services Feathers directement sur /api. En production, l’API doit être lancée et déployée séparément.'
|
|
67
|
+
: 'Le frontend est une SPA Vite. En développement, une API Feathers indépendante écoute sur 127.0.0.1:3030.'
|
|
64
68
|
return `# ${o.name}\n\nProjet ${o.ui} ${o.render.toUpperCase()} avec Vue 3, Pinia et FeathersJS v5 (${o.adapter}, ${o.transport}).\n\n`+
|
|
65
69
|
`${renderInfo}\n\n## Démarrage local (Windows / PowerShell)\n\n`+
|
|
66
70
|
'```powershell\ncorepack pnpm install\ncorepack pnpm run dev:all\n```\n\n' +
|
|
67
|
-
'Ouvrir http://127.0.0.1:5173. L’API utilise http://127.0.0.1:3030.\n\n' +
|
|
71
|
+
(o.devApi === 'vite' ? 'Ouvrir http://127.0.0.1:5173. Pendant le développement, Feathers REST est servi sur le même port via /api.\n\n' : 'Ouvrir http://127.0.0.1:5173. L’API indépendante utilise http://127.0.0.1:3030.\n\n') +
|
|
68
72
|
'```powershell\ncorepack pnpm run typecheck\ncorepack pnpm run build\n```\n\n' +
|
|
69
73
|
(o.render !== 'spa' ? 'Pour vérifier le build, lancer `corepack pnpm run serve:production` avec l’API active.\n\n' : '') +
|
|
70
|
-
'
|
|
74
|
+
(o.devApi === 'vite' ? 'Le plugin Vite est actif en développement uniquement. `pnpm run preview` sert les fichiers statiques et ne démarre pas l’API. Pour tester la SPA construite avec CRUD, lancer `corepack pnpm run dev:api` dans un premier terminal puis `corepack pnpm run preview` dans un second. Le proxy `preview.proxy` relaie /api vers 127.0.0.1:3030 (et Socket.IO vers 3031 si activé). En l’absence de backend, l’interface affiche « API indisponible » ; hors ligne, seule la coque statique reste disponible. Ne jamais exposer le serveur de démonstration sans authentification.\n\n' : '') +
|
|
75
|
+
`Profil: schema ${o.schema}; auth ${o.auth}; databases ${o.db}. Les profils d'identité et les bases externes nécessitent un raccordement explicite.\n\n`+
|
|
76
|
+
(o.ui==='quasar2' && o.render==='ssr'?'Quasar SSR : ouvrir http://127.0.0.1:8080 (reverse proxy local). Ne pas exposer le serveur SSR directement.\n\n':'')+
|
|
77
|
+
(o.pwa === 'on' ? 'PWA SPA activée : le service worker n’est produit qu’au build. Il met en cache les ressources statiques uniquement, jamais les réponses API ou Socket.IO.\nL’icône SVG d’exemple doit être remplacée par des icônes PNG 192/512 adaptées avant une distribution publique. En HTTPS, tester les mises à jour et l’installation sur le navigateur cible.\n\n' : '') +
|
|
78
|
+
'Les données de démonstration sont en mémoire ; sans authentification les services ne doivent pas être exposés publiquement.\n'
|
|
71
79
|
}
|
|
80
|
+
/** Replace the SPA proxy with the public Vite plugin. The standalone API remains for production. */
|
|
81
|
+
function enableSpaViteRuntime(files, options) {
|
|
82
|
+
const source = files.get('vite.config.ts')
|
|
83
|
+
const marker = 'plugins: [vue(),'
|
|
84
|
+
const oldProxy = ",\n proxy: { '/api': 'http://127.0.0.1:3030', '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true } }"
|
|
85
|
+
if (typeof source !== 'string' || !source.includes(marker) || !source.includes(oldProxy)) {
|
|
86
|
+
throw new Error('Unsupported SPA Vite template for embedded Feathers; refusing partial generation')
|
|
87
|
+
}
|
|
88
|
+
const realtime = options.transport === 'both'
|
|
89
|
+
const socket = realtime
|
|
90
|
+
? "{ port: 3031, host: '127.0.0.1', origins: ['http://127.0.0.1:5173'] }"
|
|
91
|
+
: 'false'
|
|
92
|
+
const runtime = `feathers({
|
|
93
|
+
serviceFiles: 'api/services/*/*.service.mjs',
|
|
94
|
+
watchDirectories: ['api/services'],
|
|
95
|
+
adapter: ${JSON.stringify(options.adapter)},
|
|
96
|
+
prefix: '/api',
|
|
97
|
+
cors: false,
|
|
98
|
+
socketio: ${socket},
|
|
99
|
+
})`
|
|
100
|
+
// 'vite preview' serves static assets: the Feathers Vite plugin is dev-only.
|
|
101
|
+
// Proxy API traffic to the separately started, loopback-bound demo API.
|
|
102
|
+
const previewProxy = `preview: { host: '127.0.0.1', port: 4173, strictPort: true,
|
|
103
|
+
proxy: { '/api': 'http://127.0.0.1:3030',${realtime ? " '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true }" : ''} } },`
|
|
104
|
+
const socketProxy = realtime
|
|
105
|
+
? ",\n proxy: { '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true } }"
|
|
106
|
+
: ''
|
|
107
|
+
files.set('vite.config.ts',
|
|
108
|
+
`import feathers from '@vevedh/vite-plugin-feathersjs'\n` +
|
|
109
|
+
`import { assertCapabilities } from './api/config/capabilities.mjs'\n` +
|
|
110
|
+
source.replace(marker, `${marker} ${runtime},`).replace(oldProxy, socketProxy)
|
|
111
|
+
.replace('export default defineConfig({',
|
|
112
|
+
'// Do not silently start an unprotected or unconfigured identity/database service.\nassertCapabilities()\nexport default defineConfig({\n' +
|
|
113
|
+
` ${previewProxy}\n` +
|
|
114
|
+
` define: { 'import.meta.env.VITE_FEATHERS_REALTIME': '${JSON.stringify(realtime ? '1' : '0')}' },`))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** PWA is opt-in, SPA-only, and must never cache API or realtime responses. */
|
|
118
|
+
function enableSpaPwa(files, options) {
|
|
119
|
+
const config = files.get('vite.config.ts')
|
|
120
|
+
const marker = 'plugins: [vue(),'
|
|
121
|
+
if (typeof config !== 'string' || !config.includes(marker) || config.includes('vite-plugin-pwa')) {
|
|
122
|
+
throw new Error('Unsupported Vite SPA template for PWA; refusing partial generation')
|
|
123
|
+
}
|
|
124
|
+
const pwa = `VitePWA({
|
|
125
|
+
registerType: 'prompt',
|
|
126
|
+
injectRegister: 'auto',
|
|
127
|
+
devOptions: { enabled: false },
|
|
128
|
+
includeAssets: ['pwa-icon.svg'],
|
|
129
|
+
manifest: {
|
|
130
|
+
name: ${JSON.stringify(options.name)},
|
|
131
|
+
short_name: ${JSON.stringify(options.name)},
|
|
132
|
+
description: 'Application Feathers et Vue',
|
|
133
|
+
lang: 'fr',
|
|
134
|
+
start_url: '/',
|
|
135
|
+
scope: '/',
|
|
136
|
+
display: 'standalone',
|
|
137
|
+
background_color: '#ffffff',
|
|
138
|
+
theme_color: '#ffffff',
|
|
139
|
+
icons: [{ src: '/pwa-icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' }],
|
|
140
|
+
},
|
|
141
|
+
workbox: {
|
|
142
|
+
globPatterns: ['**/*.{js,css,html,svg}'],
|
|
143
|
+
globIgnores: ['**/api/**', '**/socket.io/**'],
|
|
144
|
+
navigateFallback: '/index.html',
|
|
145
|
+
navigateFallbackDenylist: [/^\\/api(?:\\/|$)/, /^\\/socket\\.io(?:\\/|$)/],
|
|
146
|
+
runtimeCaching: [],
|
|
147
|
+
},
|
|
148
|
+
})`
|
|
149
|
+
files.set('vite.config.ts', `import { VitePWA } from 'vite-plugin-pwa'\n${config.replace(marker, `${marker} ${pwa},`)}`)
|
|
150
|
+
if (files.has('public/pwa-icon.svg')) throw new Error('PWA icon conflict in source template')
|
|
151
|
+
files.set('public/pwa-icon.svg', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" role="img" aria-label="Feathers app"><rect width="192" height="192" rx="40" fill="#1e293b"/><path d="M44 96l40 40 64-80" fill="none" stroke="#fff" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/></svg>\n')
|
|
152
|
+
}
|
|
153
|
+
|
|
72
154
|
export function createGenerator({outputRoot, templatesRoot, examplesRoot = templatesRoot, reportRoot = outputRoot}) {
|
|
73
155
|
const output = resolve(outputRoot), templates = resolve(templatesRoot), report = resolve(reportRoot)
|
|
74
156
|
function parse(args) {
|
|
75
|
-
const o={ui:'daisyui',render:'spa',adapter:'koa',transport:'rest',dryRun:false,json:false}
|
|
157
|
+
const o={ui:'daisyui',render:'spa',adapter:'koa',transport:'rest',schema:'typebox',auth:'none',db:'main:memory',pwa:'off',devApi:undefined,dryRun:false,json:false}
|
|
76
158
|
const seen=new Set()
|
|
77
159
|
if (args.length && !args[0].startsWith('-')) {o.name=args[0];args=args.slice(1);if(args.includes('--name'))throw new Error('Use either positional name or --name, not both')}
|
|
78
160
|
for(let i=0;i<args.length;i++) {
|
|
@@ -80,20 +162,30 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
|
|
|
80
162
|
if(!KEYS.has(flag)||seen.has(flag))throw new Error(`Unknown or duplicate option: ${flag}`)
|
|
81
163
|
seen.add(flag)
|
|
82
164
|
if(flag==='--dry-run'||flag==='--json') o[flag==='--dry-run'?'dryRun':'json']=true
|
|
83
|
-
else { const value=args[++i];if(!value||value.startsWith('--'))throw new Error(`Missing ${flag}`);o[flag.slice(2)]=value }
|
|
165
|
+
else { const value=args[++i];if(!value||value.startsWith('--'))throw new Error(`Missing ${flag}`);o[flag==='--dev-api'?'devApi':flag.slice(2)]=value }
|
|
84
166
|
}
|
|
85
167
|
if(!safeProjectName(o.name))throw new Error('Invalid or missing --name (lowercase kebab-case, max 48 characters)')
|
|
168
|
+
o.devApi ??= o.render === 'spa' ? 'vite' : 'separate'
|
|
169
|
+
if (!['vite','separate'].includes(o.devApi)) throw new Error('Invalid --dev-api: expected vite|separate')
|
|
170
|
+
if (o.devApi === 'vite' && o.render !== 'spa') throw new Error('--dev-api vite is supported for SPA only; SSR/SSG require separate API')
|
|
86
171
|
if(!['koa','express'].includes(o.adapter))throw new Error('Only koa|express adapters are supported')
|
|
87
172
|
if(!['rest','both'].includes(o.transport))throw new Error('Only rest|both are supported')
|
|
173
|
+
if (!['on','off'].includes(o.pwa)) throw new Error('Invalid --pwa: expected on|off')
|
|
174
|
+
if (o.pwa==='on' && o.render!=='spa') throw new Error('PWA is supported for SPA only; SSR/SSG require separate certification')
|
|
175
|
+
if (!['typebox','json','zod'].includes(o.schema)) throw new Error('Invalid --schema: expected typebox|json|zod')
|
|
176
|
+
if (!['none','jwt','oauth2','oidc','keycloak'].includes(o.auth)) throw new Error('Invalid --auth: expected none|jwt|oauth2|oidc|keycloak')
|
|
177
|
+
if (!/^[a-z][a-z0-9-]{0,31}:(?:memory|mongodb|postgresql|mariadb|mysql|sqlite)(?:,[a-z][a-z0-9-]{0,31}:(?:memory|mongodb|postgresql|mariadb|mysql|sqlite))*$/.test(o.db)) throw new Error('Invalid --db: use alias:adapter[,alias:adapter]')
|
|
178
|
+
const dbPairs=o.db.split(',').map(x=>{const [alias,adapter]=x.split(':');return {alias,adapter}})
|
|
179
|
+
if(new Set(dbPairs.map(x=>x.alias)).size!==dbPairs.length)throw new Error('Duplicate database alias')
|
|
88
180
|
// Reuse the actual R15 planner as the capability validator, not a separate feature catalogue.
|
|
89
181
|
const plan=buildProjectPlan({name:o.name,ui:o.ui,render:o.render,transport:o.transport,
|
|
90
|
-
engine:'auto',auth:'
|
|
91
|
-
if(!CASES[`${o.ui}:${o.render}`])throw new Error('Unsupported combination: Quasar2 supports SPA; daisyui supports SPA
|
|
182
|
+
engine:'auto',auth:o.auth==='oauth2'?'jwt,oauth':o.auth==='oidc'||o.auth==='keycloak'?'jwt,oidc':o.auth,db:o.db,primary:dbPairs[0].alias,pwa:o.pwa,schema:o.schema})
|
|
183
|
+
if(!CASES[`${o.ui}:${o.render}`])throw new Error('Unsupported combination: Quasar2 supports SPA/SSR; daisyui supports SPA/SSR/SSG')
|
|
92
184
|
if(o.render!=='spa' && o.transport!=='rest')throw new Error('SSR/SSG supports REST only; Socket.IO proxy not certified')
|
|
93
185
|
if(o.dir!==undefined && !safeProjectName(o.dir))throw new Error('--dir must be a single safe directory name')
|
|
94
186
|
o.destination=resolve(output,o.dir||o.name)
|
|
95
187
|
if(dirname(o.destination)!==output)throw new Error('Destination escapes output root')
|
|
96
|
-
o.case=CASES[`${o.ui}:${o.render}`];o.plan=plan
|
|
188
|
+
o.case=CASES[`${o.ui}:${o.render}`];o.plan=plan;o.databases=dbPairs
|
|
97
189
|
return o
|
|
98
190
|
}
|
|
99
191
|
async function generate(o) {
|
|
@@ -103,22 +195,66 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
|
|
|
103
195
|
const files=new Map(o.render==='spa' ? [
|
|
104
196
|
...await walk(join(templates,'spa','common')),
|
|
105
197
|
...await walk(join(templates,'spa',o.ui)),
|
|
106
|
-
] : await walk(join(resolve(examplesRoot)
|
|
198
|
+
] : await walk(join(resolve(examplesRoot),`${o.ui==='quasar2'?'quasar':'daisyui'}-${o.render}`)))
|
|
107
199
|
// npm pack excludes files literally named .gitignore, even in subdirectories.
|
|
108
200
|
// The published templates use gitignore.template, rewritten here to real .gitignore.
|
|
109
201
|
if(files.has('gitignore.template')){files.set('.gitignore',files.get('gitignore.template'));files.delete('gitignore.template')}
|
|
202
|
+
if(files.has('.gitignore')) {
|
|
203
|
+
const ignores=files.get('.gitignore')
|
|
204
|
+
const additions=['.agents/','AGENTS.md','SKILLS.md','*.prompt.md','.ai/','.codex/','.cursor/','.claude/','.gemini/','.windsurf/','.aider*','.mcp.json','.vscode/mcp.json','.github/copilot-instructions.md','docs-private/','.npmrc','.env*','!.env.example']
|
|
205
|
+
files.set('.gitignore',ignores.trimEnd()+'\n\n# Local editor tooling and private maintenance files\n'+additions.filter(x=>!ignores.split(/\r?\n/).includes(x)).join('\n')+'\n')
|
|
206
|
+
}
|
|
110
207
|
if(!files.has('api/server.mjs')||!files.has('.env.example')||!files.has('index.html'))throw new Error('Incomplete source template')
|
|
111
208
|
files.set('.env.example',envChange(files.get('.env.example'),o))
|
|
209
|
+
if (o.devApi === 'vite') {
|
|
210
|
+
enableSpaViteRuntime(files, o)
|
|
211
|
+
files.set('api/config/capabilities.d.mts', 'export declare function assertCapabilities(): void\n')
|
|
212
|
+
}
|
|
213
|
+
if (o.pwa === 'on') enableSpaPwa(files,o)
|
|
214
|
+
if(o.ui==='quasar2' && o.render==='ssr') {
|
|
215
|
+
// R16 HTML has no Vite SPA identity marker; add one without touching frozen R16 sources.
|
|
216
|
+
const html=files.get('index.html')
|
|
217
|
+
files.set('index.html',html.replace('<meta charset="utf-8">','<meta charset="utf-8">\n <meta name="r17-example" content="quasar-ssr">\n <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\'/%3E">'))
|
|
218
|
+
}
|
|
112
219
|
files.set('index.html',files.get('index.html').replace('__R17_CASE__',o.case).replace(/(<meta name="r17-example" content=")[^"]+("\s*\/?>)/,`$1${o.case}$2`))
|
|
113
|
-
const manifest=o.render==='spa'?spaManifest(o):JSON.parse(files.get('package.json'))
|
|
220
|
+
const manifest=o.render==='spa'?spaManifest(o):o.ui==='quasar2'?quasarSSRManifest(o):JSON.parse(files.get('package.json'))
|
|
114
221
|
manifest.name=o.name;manifest.private=true
|
|
115
|
-
manifest.scripts={...manifest.scripts,'dev:all':'node scripts/dev-all.mjs'}
|
|
222
|
+
manifest.scripts={...manifest.scripts,'dev:all':o.devApi==='vite'?'vite --host 127.0.0.1':'node scripts/dev-all.mjs'}
|
|
116
223
|
files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
|
|
117
224
|
files.set('README.md',generatedReadme(o))
|
|
118
|
-
|
|
225
|
+
// Keep the visible application description consistent with the actual dev API mode.
|
|
226
|
+
// The standalone production API is intentionally unaffected.
|
|
227
|
+
if (o.render === 'spa' && o.devApi === 'vite') {
|
|
228
|
+
const app = files.get('src/App.vue')
|
|
229
|
+
const separateDescription = 'Le service Feathers est exécuté dans un processus séparé.'
|
|
230
|
+
if (typeof app !== 'string' || !app.includes(separateDescription)) {
|
|
231
|
+
throw new Error('SPA description contract changed: refusing inconsistent Vite-native scaffold')
|
|
232
|
+
}
|
|
233
|
+
files.set('src/App.vue', app.replace(separateDescription,
|
|
234
|
+
'En développement, les services Feathers sont intégrés directement à Vite.'))
|
|
235
|
+
}
|
|
236
|
+
if (o.devApi !== 'vite') files.set('scripts/dev-all.mjs',await readFile(join(templates,'common',o.ui==='quasar2'&&o.render==='ssr'?'dev-all-quasar-ssr.mjs':'dev-all.mjs'),'utf8'))
|
|
237
|
+
// A configuration contract, not a simulated authentication or persistence layer.
|
|
238
|
+
// Non-memory databases and external identity providers fail closed at startup
|
|
239
|
+
// until explicitly integrated and tested with a real provider.
|
|
240
|
+
files.set('api/config/capabilities.mjs',capabilityGuard(o))
|
|
241
|
+
const server=files.get('api/server.mjs').replace("from 'vite-plugin-feathersjs'","from '@vevedh/vite-plugin-feathersjs'")
|
|
242
|
+
files.set('api/server.mjs',"import { assertCapabilities } from './config/capabilities.mjs'\nassertCapabilities()\n" + server)
|
|
243
|
+
files.set('api/config/README.md',`# Configuration des capacités\n\nProfil sélectionné : schéma ${o.schema}, identité ${o.auth}, connexions ${o.db}.\n\nLes services en mémoire et sans authentification sont destinés au laboratoire local. Les bases externes nécessitent une instance réelle, un client Feathers/Knex ou MongoDB, ses migrations et la vérification de son cycle de vie.\n\nL'authentification JWT exige un service utilisateurs, un secret hors Git, une stratégie JWT active, des contrôles d'accès et des tests. OAuth2, OIDC et Keycloak exigent en plus un fournisseur HTTPS, la vérification de l'issuer, audience, JWKS, PKCE et le mapping des identités. Ne pas exposer l'API avant validation.\n\nLa commande \`service add tickets --schema zod --db main\` crée un service et l'enregistre dans l'API.\n`)
|
|
244
|
+
files.set('api/config/profile.json',JSON.stringify({schema:o.schema,auth:o.auth,databases:o.databases,primary:o.databases[0].alias,operational:o.auth==='none' && o.databases.every(db=>db.adapter==='memory')},null,2)+'\n')
|
|
245
|
+
const depMap={typebox:['@feathersjs/typebox','ajv'],json:['ajv'],zod:['zod']}
|
|
246
|
+
// Dependencies for the service subcommand; project options alone create no arbitrary services.
|
|
247
|
+
for(const dep of depMap[o.schema])manifest.dependencies[dep] ||= dep==='ajv'?'^8.17.1':dep==='zod'?'^4.1.0':'^5.0.50'
|
|
248
|
+
if(o.auth==='jwt')manifest.dependencies['@feathersjs/authentication']='^5.0.50'
|
|
249
|
+
if(o.auth==='oauth2')manifest.dependencies['@feathersjs/authentication-oauth']='^5.0.50'
|
|
250
|
+
for(const db of o.databases) {
|
|
251
|
+
const drivers={mongodb:['@feathersjs/mongodb','mongodb'],postgresql:['@feathersjs/knex','knex','pg'],mariadb:['@feathersjs/knex','knex','mysql2'],mysql:['@feathersjs/knex','knex','mysql2'],sqlite:['@feathersjs/knex','knex','better-sqlite3']}
|
|
252
|
+
for(const dep of drivers[db.adapter]||[])manifest.dependencies[dep]||='*'
|
|
253
|
+
}
|
|
254
|
+
files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
|
|
119
255
|
const list=[...files.keys()].sort()
|
|
120
256
|
const result={name:o.name,ui:o.ui,render:o.render,adapter:o.adapter,transport:o.transport,
|
|
121
|
-
destination:relative(report,o.destination).split(sep).join('/'),files:list}
|
|
257
|
+
destination:relative(report,o.destination).split(sep).join('/'),schema:o.schema,auth:o.auth,pwa:o.pwa,devApi:o.devApi,databases:o.databases,files:list}
|
|
122
258
|
if(o.dryRun)return result
|
|
123
259
|
// Do not follow symlinks and refuse any existing destination, including empty ones.
|
|
124
260
|
await mkdir(output,{recursive:true})
|
|
@@ -134,3 +270,31 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
|
|
|
134
270
|
}
|
|
135
271
|
return {parse,generate}
|
|
136
272
|
}
|
|
273
|
+
|
|
274
|
+
function quasarSSRManifest(o){
|
|
275
|
+
return {name:o.name,version:'0.1.0',private:true,type:'module',packageManager:'pnpm@10.30.2',engines:{node:'>=22.12'},
|
|
276
|
+
scripts:{'dev:ssr':'quasar dev -m ssr','build:ssr':'quasar build -m ssr',build:'quasar build -m ssr',
|
|
277
|
+
typecheck:'vue-tsc --noEmit', 'dev:api':'node --env-file=.env.example api/server.mjs',
|
|
278
|
+
'serve:production':'node dist/ssr/index.js',postinstall:'quasar prepare', 'dev:edge':'node scripts/edge-proxy.mjs'},
|
|
279
|
+
dependencies:{'@vevedh/vite-plugin-feathersjs':'0.2.6', '@feathersjs/feathers':'^5.0.50', '@feathersjs/koa':'^5.0.50',
|
|
280
|
+
'@feathersjs/express':'^5.0.50', '@feathersjs/errors':'^5.0.50', '@feathersjs/socketio':'^5.0.50',
|
|
281
|
+
'@feathersjs/rest-client':'^5.0.50', '@feathersjs/socketio-client':'^5.0.50',
|
|
282
|
+
'@quasar/extras':'^1.18.0',pinia:'^3.0.4',quasar:'^2.35.2','socket.io-client':'^4.8.4',
|
|
283
|
+
vue:'^3.5.44','vue-router':'^4.6.4',express:'^4.22.3','serialize-javascript':'^7.1.4'},
|
|
284
|
+
devDependencies:{'@quasar/app-vite':'2.6.2','@types/node':'^22.0.0',typescript:'^5.9.3','vue-tsc':'^3.3.14',
|
|
285
|
+
// Quasar app-vite 2.6.2 runs Express v4. Keep its ambient types in
|
|
286
|
+
// the same major as Express runtime, including nested pnpm dependencies.
|
|
287
|
+
'@types/express':'4.17.25','@types/express-serve-static-core':'4.19.9',vite:'^7.3.7'},
|
|
288
|
+
pnpm:{overrides:{'@types/express':'4.17.25','@types/express-serve-static-core':'4.19.9'}}}
|
|
289
|
+
}
|
|
290
|
+
function capabilityGuard(o) {
|
|
291
|
+
const external=o.databases.filter(x=>x.adapter!=='memory')
|
|
292
|
+
const desc=JSON.stringify({auth:o.auth,databases:o.databases})
|
|
293
|
+
return `// Generated capability contract: never silently downgrade identity or persistence.\n`+
|
|
294
|
+
`export const profile=Object.freeze(${desc})\n`+
|
|
295
|
+
`export function assertCapabilities(){\n`+
|
|
296
|
+
` if (profile.auth !== 'none') throw new Error('Authentication '+profile.auth+' is a configuration blueprint; identity provider and JWT validation must be integrated and certified before serving requests')\n`+
|
|
297
|
+
` const missing=profile.databases.filter(item=>item.adapter!=='memory')\n`+
|
|
298
|
+
` if(missing.length)throw new Error('Database adapters not provisioned: '+missing.map(x=>x.alias+':'+x.adapter).join(', ')+'. Configure named adapters before serving requests')\n`+
|
|
299
|
+
`}\n`
|
|
300
|
+
}
|
package/lib/services.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { lstat, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const VALID_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
|
6
|
+
const VALID_SCHEMA = new Set(['typebox','json','zod'])
|
|
7
|
+
const VALID_AUTH = new Set(['none','jwt'])
|
|
8
|
+
function safeName(s){return typeof s==='string' && s.length<=48 && VALID_NAME.test(s) && !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(s)}
|
|
9
|
+
async function rejectLinks(path){
|
|
10
|
+
let current=path
|
|
11
|
+
while(true){if(existsSync(current)&&(await lstat(current)).isSymbolicLink())throw new Error('Symlink path refused');const next=dirname(current);if(next===current)break;current=next}
|
|
12
|
+
}
|
|
13
|
+
export function parseServiceArguments(args){
|
|
14
|
+
if(args[0]!=='add')throw new Error('Use: service add <name> [--project <dir>] [--schema typebox|json|zod] [--db <alias>] [--auth none|jwt] [--dry-run]')
|
|
15
|
+
if(!safeName(args[1]))throw new Error('Invalid service name (lowercase kebab-case)')
|
|
16
|
+
const options={name:args[1],project:'.',schema:undefined,db:undefined,auth:undefined,dryRun:false}
|
|
17
|
+
const keys=new Map([['--project','project'],['--schema','schema'],['--db','db'],['--auth','auth']])
|
|
18
|
+
const seen=new Set()
|
|
19
|
+
for(let i=2;i<args.length;i++){
|
|
20
|
+
const flag=args[i]
|
|
21
|
+
if(seen.has(flag))throw new Error('Duplicate option: '+flag)
|
|
22
|
+
seen.add(flag)
|
|
23
|
+
if(flag==='--dry-run'){options.dryRun=true;continue}
|
|
24
|
+
if(!keys.has(flag))throw new Error('Unknown service option: '+flag)
|
|
25
|
+
const value=args[++i]
|
|
26
|
+
if(!value||value.startsWith('--'))throw new Error('Missing service option: '+flag)
|
|
27
|
+
options[keys.get(flag)]=value
|
|
28
|
+
}
|
|
29
|
+
if(!VALID_SCHEMA.has(options.schema??'typebox'))throw new Error('Invalid schema')
|
|
30
|
+
if(!VALID_AUTH.has(options.auth??'none'))throw new Error('Invalid service auth: none|jwt')
|
|
31
|
+
if(options.db!==undefined&&!safeName(options.db))throw new Error('Invalid database alias')
|
|
32
|
+
if(!/^\.?\/?[a-zA-Z0-9._/-]*$/.test(options.project)||options.project.includes('..')||options.project.includes('\\')||options.project.startsWith('/'))throw new Error('Unsafe project path; use a local relative directory')
|
|
33
|
+
return options
|
|
34
|
+
}
|
|
35
|
+
function schemaSource(schema){
|
|
36
|
+
const opening="import { BadRequest } from '@feathersjs/errors'\n"
|
|
37
|
+
if(schema==='zod')return opening+`import { z } from 'zod'
|
|
38
|
+
export const dataSchema=z.object({title:z.string().trim().min(1).max(120)}).strict()
|
|
39
|
+
export function validateData(value){const parsed=dataSchema.safeParse(value);if(!parsed.success)throw new BadRequest('Invalid service data');return parsed.data}
|
|
40
|
+
`
|
|
41
|
+
const def=schema==='typebox'?`import { Type } from '@feathersjs/typebox'
|
|
42
|
+
export const dataSchema=Type.Object({title:Type.String({minLength:1,maxLength:120})},{additionalProperties:false})
|
|
43
|
+
`:`export const dataSchema={type:'object',additionalProperties:false,required:['title'],properties:{title:{type:'string',minLength:1,maxLength:120}}}
|
|
44
|
+
`
|
|
45
|
+
return opening+`import Ajv from 'ajv'
|
|
46
|
+
`+def+`const valid=new Ajv({allErrors:true}).compile(dataSchema)
|
|
47
|
+
export function validateData(value){if(!valid(value))throw new BadRequest('Invalid service data');return {title:value.title.trim()}}
|
|
48
|
+
`
|
|
49
|
+
}
|
|
50
|
+
function classSource(){return `import { NotFound, BadRequest } from '@feathersjs/errors'
|
|
51
|
+
import { validateData } from './schema.mjs'
|
|
52
|
+
export class GeneratedService {
|
|
53
|
+
#items=new Map()
|
|
54
|
+
#counter=1
|
|
55
|
+
constructor(){for(const method of ['find','get','create','patch','remove'])this[method]=this[method].bind(this)}
|
|
56
|
+
#id(value){const n=Number(value);if(!Number.isSafeInteger(n)||n<1)throw new BadRequest('Invalid ID');return n}
|
|
57
|
+
async find(){return [...this.#items.values()].map(x=>({...x}))}
|
|
58
|
+
async get(id){const item=this.#items.get(this.#id(id));if(!item)throw new NotFound('Not found');return {...item}}
|
|
59
|
+
async create(data){const row={id:this.#counter++,...validateData(data)};this.#items.set(row.id,row);return {...row}}
|
|
60
|
+
async patch(id,data){const key=this.#id(id);if(!this.#items.has(key))throw new NotFound('Not found');const row={id:key,...validateData(data)};this.#items.set(key,row);return {...row}}
|
|
61
|
+
async remove(id){const key=this.#id(id);const row=this.#items.get(key);if(!row)throw new NotFound('Not found');this.#items.delete(key);return {...row}}
|
|
62
|
+
}
|
|
63
|
+
`}
|
|
64
|
+
function registrar(name,auth){
|
|
65
|
+
return `import { GeneratedService } from './service.class.mjs'
|
|
66
|
+
${auth==='jwt'?"import { authenticate } from '@feathersjs/authentication'\n":''}export default function register(app){
|
|
67
|
+
app.use('${name}',new GeneratedService(),{methods:['find','get','create','patch','remove']})
|
|
68
|
+
${auth==='jwt'?`app.service('${name}').hooks({before:{all:[authenticate('jwt')]}})`:'// This demonstration service has no authentication; bind HTTP only to loopback.'}
|
|
69
|
+
}
|
|
70
|
+
`
|
|
71
|
+
}
|
|
72
|
+
export async function generateService(opts, cwd=process.cwd()){
|
|
73
|
+
const project=resolve(cwd,opts.project)
|
|
74
|
+
if(opts.project!=='.' && !project.startsWith(resolve(cwd)+sep))throw new Error('Project escapes current directory')
|
|
75
|
+
await rejectLinks(project)
|
|
76
|
+
const manifestPath=join(project,'package.json'),serverPath=join(project,'api/server.mjs'),profilePath=join(project,'api/config/profile.json')
|
|
77
|
+
if(!existsSync(manifestPath)||!existsSync(serverPath)||!existsSync(profilePath))throw new Error('Not a CLI generated project')
|
|
78
|
+
const manifest=JSON.parse(await readFile(manifestPath,'utf8'))
|
|
79
|
+
const profile=JSON.parse(await readFile(profilePath,'utf8'))
|
|
80
|
+
const schema=opts.schema??profile.schema,auth=opts.auth??profile.auth,db=opts.db??profile.primary
|
|
81
|
+
if(!VALID_SCHEMA.has(schema))throw new Error('Invalid schema')
|
|
82
|
+
if(!VALID_AUTH.has(auth))throw new Error('Service supports none|jwt; OAuth2/OIDC/Keycloak require audited identity integration')
|
|
83
|
+
if(auth!=='none' && profile.auth!==auth)throw new Error('Service auth must match configured project profile')
|
|
84
|
+
const connection=profile.databases.find(x=>x.alias===db)
|
|
85
|
+
if(!connection)throw new Error('Unknown database alias: '+db)
|
|
86
|
+
const destination=join(project,'api/services',opts.name)
|
|
87
|
+
await rejectLinks(destination)
|
|
88
|
+
if(existsSync(destination))throw new Error('Service already exists; refusing overwrite')
|
|
89
|
+
const text=await readFile(serverPath,'utf8')
|
|
90
|
+
const marker='const modules = [\n'
|
|
91
|
+
if(!text.includes(marker))throw new Error('Missing Feathers modules registry; no changes made')
|
|
92
|
+
const moduleEntry=` new URL('./services/${opts.name}/${opts.name}.service.mjs', import.meta.url),\n`
|
|
93
|
+
if(text.includes(moduleEntry))throw new Error('Duplicate service registration')
|
|
94
|
+
// Never replace a user-controlled arbitrary string outside of the explicit registry.
|
|
95
|
+
const output={name:opts.name,project,database:{alias:db,adapter:connection.adapter},auth,schema,files:[`api/services/${opts.name}/schema.mjs`,`api/services/${opts.name}/service.class.mjs`,`api/services/${opts.name}/${opts.name}.service.mjs`],ready:connection.adapter==='memory'&&auth==='none'}
|
|
96
|
+
if(opts.dryRun)return output
|
|
97
|
+
const dependencies=manifest.dependencies||{}
|
|
98
|
+
const deps=schema==='zod'?{'zod':'^4.1.0'}:schema==='typebox'?{'@feathersjs/typebox':'^5.0.50','ajv':'^8.17.1'}:{'ajv':'^8.17.1'}
|
|
99
|
+
if(auth==='jwt')deps['@feathersjs/authentication']='^5.0.50'
|
|
100
|
+
for(const [name,version] of Object.entries(deps))dependencies[name]??=version
|
|
101
|
+
await mkdir(destination,{recursive:false})
|
|
102
|
+
for(const [name,contents] of Object.entries({'schema.mjs':schemaSource(schema),'service.class.mjs':classSource(),[`${opts.name}.service.mjs`]:registrar(opts.name,auth)}))await writeFile(join(destination,name),contents,{flag:'wx'})
|
|
103
|
+
await writeFile(serverPath,text.replace(marker,marker+moduleEntry))
|
|
104
|
+
await writeFile(manifestPath,JSON.stringify({...manifest,dependencies},null,2)+'\n')
|
|
105
|
+
return output
|
|
106
|
+
}
|
|
107
|
+
export async function runServiceCommand(args,cwd){
|
|
108
|
+
const result=await generateService(parseServiceArguments(args),cwd)
|
|
109
|
+
console.log(JSON.stringify(result,null,2))
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { loadEnvFile } from 'node:process'
|
|
5
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
6
|
+
const file=existsSync('.env')?'.env':'.env.example'
|
|
7
|
+
if (!existsSync(file))throw new Error('Missing .env.example')
|
|
8
|
+
loadEnvFile(file)
|
|
9
|
+
const env={...process.env}
|
|
10
|
+
const children=[]
|
|
11
|
+
let closed=false
|
|
12
|
+
function stop(code=0){if(closed)return;closed=true;for(const child of children.slice().reverse())if(child.exitCode===null)try{child.kill()}catch{};process.exitCode=code}
|
|
13
|
+
for(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>stop(signal==='SIGINT'?130:143))
|
|
14
|
+
function start(label,command,args,opts={}) {
|
|
15
|
+
const proc=spawn(command,args,{env,stdio:'inherit',windowsHide:true,...opts})
|
|
16
|
+
children.push(proc)
|
|
17
|
+
proc.on('error',error=>{console.error(label,error.message);stop(1)})
|
|
18
|
+
proc.on('exit',code=>{if(!closed){console.error(label,'stopped',code);stop(code||1)}})
|
|
19
|
+
return proc
|
|
20
|
+
}
|
|
21
|
+
start('Feathers',process.execPath,[`--env-file=${file}`,'api/server.mjs'])
|
|
22
|
+
let ready=false
|
|
23
|
+
for(let attempt=0;attempt<90 && !closed;attempt++){
|
|
24
|
+
try{const res=await fetch('http://127.0.0.1:3030/api/health',{signal:AbortSignal.timeout(650)});if(res.ok){ready=true;break}}catch{}
|
|
25
|
+
await sleep(200)
|
|
26
|
+
}
|
|
27
|
+
if(!ready){console.error('Feathers health unavailable on loopback');stop(1)}
|
|
28
|
+
if(!closed){
|
|
29
|
+
start('Quasar SSR',process.platform==='win32'?'corepack.cmd':'corepack',['pnpm','run','dev:ssr'],{shell:process.platform==='win32'})
|
|
30
|
+
let ssrReady=false
|
|
31
|
+
for(let attempt=0;attempt<150 && !closed;attempt++){
|
|
32
|
+
try{const res=await fetch('http://127.0.0.1:9100/',{signal:AbortSignal.timeout(800)});if(res.ok){ssrReady=true;break}}catch{}
|
|
33
|
+
await sleep(350)
|
|
34
|
+
}
|
|
35
|
+
if(!ssrReady){console.error('Quasar SSR unavailable on loopback :9100');stop(1)}
|
|
36
|
+
else start('Edge',process.execPath,['scripts/edge-proxy.mjs'])
|
|
37
|
+
}
|
|
@@ -14,3 +14,23 @@ playwright-report/
|
|
|
14
14
|
.mcp.json
|
|
15
15
|
.vscode/mcp.json
|
|
16
16
|
docs-private/
|
|
17
|
+
|
|
18
|
+
# Development-only configuration and internal artifacts
|
|
19
|
+
.agents/
|
|
20
|
+
.agent/
|
|
21
|
+
.ai/
|
|
22
|
+
.codex/
|
|
23
|
+
.claude/
|
|
24
|
+
.cursor/
|
|
25
|
+
.gemini/
|
|
26
|
+
.windsurf/
|
|
27
|
+
.ecc/
|
|
28
|
+
.mcp.json
|
|
29
|
+
.vscode/mcp.json
|
|
30
|
+
AGENTS.md
|
|
31
|
+
SKILLS.md
|
|
32
|
+
CLAUDE.md
|
|
33
|
+
GEMINI.md
|
|
34
|
+
*.prompt.md
|
|
35
|
+
docs-private/
|
|
36
|
+
.npmrc
|
|
@@ -14,3 +14,23 @@ playwright-report/
|
|
|
14
14
|
.mcp.json
|
|
15
15
|
.vscode/mcp.json
|
|
16
16
|
docs-private/
|
|
17
|
+
|
|
18
|
+
# Development-only configuration and internal artifacts
|
|
19
|
+
.agents/
|
|
20
|
+
.agent/
|
|
21
|
+
.ai/
|
|
22
|
+
.codex/
|
|
23
|
+
.claude/
|
|
24
|
+
.cursor/
|
|
25
|
+
.gemini/
|
|
26
|
+
.windsurf/
|
|
27
|
+
.ecc/
|
|
28
|
+
.mcp.json
|
|
29
|
+
.vscode/mcp.json
|
|
30
|
+
AGENTS.md
|
|
31
|
+
SKILLS.md
|
|
32
|
+
CLAUDE.md
|
|
33
|
+
GEMINI.md
|
|
34
|
+
*.prompt.md
|
|
35
|
+
docs-private/
|
|
36
|
+
.npmrc
|