@vevedh/create-feathers-app 0.1.0-rc.1 → 0.1.0-rc.5

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 (46) hide show
  1. package/README.md +55 -18
  2. package/bin/create-feathers-app.mjs +13 -2
  3. package/lib/scaffold.mjs +196 -14
  4. package/lib/secure-runtime/connect.mjs +28 -0
  5. package/lib/secure-runtime/policy.mjs +85 -0
  6. package/lib/secure-runtime/prepare-storage.mjs +14 -0
  7. package/lib/secure-runtime/providers.mjs +74 -0
  8. package/lib/secure-runtime/register.mjs +80 -0
  9. package/lib/secure-runtime/storage.mjs +31 -0
  10. package/lib/services.mjs +227 -0
  11. package/lib/typescript.mjs +148 -0
  12. package/package.json +2 -2
  13. package/templates/common/dev-all-quasar-ssr.mjs +37 -0
  14. package/templates/daisyui-ssg/gitignore.template +20 -0
  15. package/templates/daisyui-ssg/src/style.css +3 -1
  16. package/templates/daisyui-ssr/gitignore.template +20 -0
  17. package/templates/daisyui-ssr/src/style.css +3 -1
  18. package/templates/quasar-ssr/.env.example +11 -0
  19. package/templates/quasar-ssr/api/server.mjs +45 -0
  20. package/templates/quasar-ssr/api/services/health/health.class.mjs +3 -0
  21. package/templates/quasar-ssr/api/services/health/health.service.mjs +4 -0
  22. package/templates/quasar-ssr/api/services/notes/notes.class.mjs +25 -0
  23. package/templates/quasar-ssr/api/services/notes/notes.service.mjs +10 -0
  24. package/templates/quasar-ssr/gitignore.template +36 -0
  25. package/templates/quasar-ssr/index.html +12 -0
  26. package/templates/quasar-ssr/quasar.config.ts +27 -0
  27. package/templates/quasar-ssr/scripts/edge-proxy.mjs +30 -0
  28. package/templates/quasar-ssr/scripts/edge-routing.mjs +14 -0
  29. package/templates/quasar-ssr/src/App.vue +1 -0
  30. package/templates/quasar-ssr/src/css/app.css +20 -0
  31. package/templates/quasar-ssr/src/pages/IndexPage.vue +146 -0
  32. package/templates/quasar-ssr/src/pages/NotFoundPage.vue +11 -0
  33. package/templates/quasar-ssr/src/router/index.ts +8 -0
  34. package/templates/quasar-ssr/src/router/routes.ts +6 -0
  35. package/templates/quasar-ssr/src/stores/index.ts +4 -0
  36. package/templates/quasar-ssr/src/stores/notes.ts +73 -0
  37. package/templates/quasar-ssr/src-ssr/middlewares/render.ts +15 -0
  38. package/templates/quasar-ssr/src-ssr/server.ts +42 -0
  39. package/templates/quasar-ssr/tsconfig.json +4 -0
  40. package/templates/spa/common/.env.example +1 -0
  41. package/templates/spa/common/api/server.mjs +2 -1
  42. package/templates/spa/common/gitignore.template +20 -0
  43. package/templates/spa/common/src/stores/notes.ts +71 -12
  44. package/templates/spa/daisyui/src/components/NotesPanel.vue +24 -7
  45. package/templates/spa/daisyui/src/style.css +3 -1
  46. 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
- Create local FeathersJS v5 applications with Vue 3, Pinia and Vite. Choose Quasar 2 or Tailwind CSS + daisyUI.
3
+ Générez des applications FeathersJS v5 avec Vue 3, Pinia, Vite, Quasar 2 ou Tailwind CSS + daisyUI.
4
4
 
5
- This package is a separate command-line tool. The runtime integration is published as [`@vevedh/vite-plugin-feathersjs`](https://www.npmjs.com/package/@vevedh/vite-plugin-feathersjs).
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
- npm exec --package @vevedh/create-feathers-app -- create-feathers-app portail --ui quasar2 --render spa --adapter koa --transport rest
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
- After npm publication, `npm create @vevedh/feathers-app@latest portail -- --ui daisyui --render ssr` can be used (depending on npm's argument forwarding).
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
- For a local candidate, run `node bin/create-feathers-app.mjs --help` from this package.
22
+ ## PWA optionnelle (SPA uniquement)
16
23
 
17
- | UI | SPA | SSR | SSG |
18
- | --- | --- | --- | --- |
19
- | Quasar 2 | Yes | R16 standalone example only | Unsupported on certified CLI |
20
- | daisyUI 5 | Yes | Yes | Yes |
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
- Generated projects include an independent Feathers REST API (`health`, `notes`), Koa or Express, Pinia, a `.env.example` and a `dev:all` command. Socket.IO is optional for the SPA variants only.
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
- ## Options
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
- `--name`, `--dir`, `--ui`, `--render`, `--adapter`, `--transport`, `--dry-run`, `--json`, `--help`, `--version`.
41
+ ## Profils d'identité et de bases
27
42
 
28
- ## Windows development
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
- Generated apps are local demonstrations with an in-memory notes service and no authentication. Do not expose them directly on the Internet.
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
- The CLI does not generate OAuth/JWT, persistent databases, PWA or Quasar SSR/SSG yet.
60
+ ## API Feathers intégrée à Vite en développement (RC4)
40
61
 
41
- ## Documentation
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
- A complete French VitePress guide is included in the companion repository under `docs/cli` (installation, rendering modes, Windows, Feathers services, security and publishing). The documentation site is built and hosted separately from this npm package.
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
- Available: Quasar 2 SPA; daisyUI SPA, SSR, SSG.
24
- Quasar SSR: certified example in the companion repository; Quasar SSG not supported.`)
29
+ Service: create-feathers-app service add <name> [--project <path>] [--schema typebox|json|zod] [--db <alias>] [--auth none|jwt] [--lang js|ts] [--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,100 @@ 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
- : 'Le frontend est une SPA Vite. L’API Feathers s’exécute séparément.'
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
- 'Les données sont en mémoire, sans authentification. Cet exemple est réservé au laboratoire local et ne doit pas être exposé sur Internet.\n'
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
+ lifecycle: 'managed',
98
+ cors: false,
99
+ socketio: ${socket},
100
+ })`
101
+ // 'vite preview' serves static assets: the Feathers Vite plugin is dev-only.
102
+ // Proxy API traffic to the separately started, loopback-bound demo API.
103
+ const previewProxy = `preview: { host: '127.0.0.1', port: 4173, strictPort: true,
104
+ proxy: { '/api': 'http://127.0.0.1:3030',${realtime ? " '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true }" : ''} } },`
105
+ const socketProxy = realtime
106
+ ? ",\n proxy: { '/socket.io': { target: 'ws://127.0.0.1:3031', ws: true } }"
107
+ : ''
108
+ files.set('vite.config.ts',
109
+ `import feathers from '@vevedh/vite-plugin-feathersjs'\n` +
110
+ `import { assertCapabilities } from './api/config/capabilities.mjs'\n` +
111
+ source.replace(marker, `${marker} ${runtime},`).replace(oldProxy, socketProxy)
112
+ .replace('export default defineConfig({',
113
+ '// Do not silently start an unprotected or unconfigured identity/database service.\nassertCapabilities()\nexport default defineConfig({\n' +
114
+ ` ${previewProxy}\n` +
115
+ ` define: { 'import.meta.env.VITE_FEATHERS_REALTIME': '${JSON.stringify(realtime ? '1' : '0')}' },`))
116
+ }
117
+
118
+ /** PWA is opt-in, SPA-only, and must never cache API or realtime responses. */
119
+ function enableSpaPwa(files, options) {
120
+ const config = files.get('vite.config.ts')
121
+ const marker = 'plugins: [vue(),'
122
+ if (typeof config !== 'string' || !config.includes(marker) || config.includes('vite-plugin-pwa')) {
123
+ throw new Error('Unsupported Vite SPA template for PWA; refusing partial generation')
124
+ }
125
+ const pwa = `VitePWA({
126
+ registerType: 'prompt',
127
+ injectRegister: 'auto',
128
+ devOptions: { enabled: false },
129
+ includeAssets: ['pwa-icon.svg'],
130
+ manifest: {
131
+ name: ${JSON.stringify(options.name)},
132
+ short_name: ${JSON.stringify(options.name)},
133
+ description: 'Application Feathers et Vue',
134
+ lang: 'fr',
135
+ start_url: '/',
136
+ scope: '/',
137
+ display: 'standalone',
138
+ background_color: '#ffffff',
139
+ theme_color: '#ffffff',
140
+ icons: [{ src: '/pwa-icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' }],
141
+ },
142
+ workbox: {
143
+ globPatterns: ['**/*.{js,css,html,svg}'],
144
+ globIgnores: ['**/api/**', '**/socket.io/**'],
145
+ navigateFallback: '/index.html',
146
+ navigateFallbackDenylist: [/^\\/api(?:\\/|$)/, /^\\/socket\\.io(?:\\/|$)/],
147
+ runtimeCaching: [],
148
+ },
149
+ })`
150
+ files.set('vite.config.ts', `import { VitePWA } from 'vite-plugin-pwa'\n${config.replace(marker, `${marker} ${pwa},`)}`)
151
+ if (files.has('public/pwa-icon.svg')) throw new Error('PWA icon conflict in source template')
152
+ 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')
153
+ }
154
+
72
155
  export function createGenerator({outputRoot, templatesRoot, examplesRoot = templatesRoot, reportRoot = outputRoot}) {
73
156
  const output = resolve(outputRoot), templates = resolve(templatesRoot), report = resolve(reportRoot)
74
157
  function parse(args) {
75
- const o={ui:'daisyui',render:'spa',adapter:'koa',transport:'rest',dryRun:false,json:false}
158
+ 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
159
  const seen=new Set()
77
160
  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
161
  for(let i=0;i<args.length;i++) {
@@ -80,20 +163,30 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
80
163
  if(!KEYS.has(flag)||seen.has(flag))throw new Error(`Unknown or duplicate option: ${flag}`)
81
164
  seen.add(flag)
82
165
  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 }
166
+ else { const value=args[++i];if(!value||value.startsWith('--'))throw new Error(`Missing ${flag}`);o[flag==='--dev-api'?'devApi':flag.slice(2)]=value }
84
167
  }
85
168
  if(!safeProjectName(o.name))throw new Error('Invalid or missing --name (lowercase kebab-case, max 48 characters)')
169
+ o.devApi ??= o.render === 'spa' ? 'vite' : 'separate'
170
+ if (!['vite','separate'].includes(o.devApi)) throw new Error('Invalid --dev-api: expected vite|separate')
171
+ if (o.devApi === 'vite' && o.render !== 'spa') throw new Error('--dev-api vite is supported for SPA only; SSR/SSG require separate API')
86
172
  if(!['koa','express'].includes(o.adapter))throw new Error('Only koa|express adapters are supported')
87
173
  if(!['rest','both'].includes(o.transport))throw new Error('Only rest|both are supported')
174
+ if (!['on','off'].includes(o.pwa)) throw new Error('Invalid --pwa: expected on|off')
175
+ if (o.pwa==='on' && o.render!=='spa') throw new Error('PWA is supported for SPA only; SSR/SSG require separate certification')
176
+ if (!['typebox','json','zod'].includes(o.schema)) throw new Error('Invalid --schema: expected typebox|json|zod')
177
+ if (!['none','jwt','oauth2','oidc','keycloak'].includes(o.auth)) throw new Error('Invalid --auth: expected none|jwt|oauth2|oidc|keycloak')
178
+ 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]')
179
+ const dbPairs=o.db.split(',').map(x=>{const [alias,adapter]=x.split(':');return {alias,adapter}})
180
+ if(new Set(dbPairs.map(x=>x.alias)).size!==dbPairs.length)throw new Error('Duplicate database alias')
88
181
  // Reuse the actual R15 planner as the capability validator, not a separate feature catalogue.
89
182
  const plan=buildProjectPlan({name:o.name,ui:o.ui,render:o.render,transport:o.transport,
90
- engine:'auto',auth:'none',db:'main:memory',pwa:'off',schema:'typebox'})
91
- if(!CASES[`${o.ui}:${o.render}`])throw new Error('Unsupported combination: Quasar2 supports SPA; daisyui supports SPA, SSR, SSG')
183
+ 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})
184
+ if(!CASES[`${o.ui}:${o.render}`])throw new Error('Unsupported combination: Quasar2 supports SPA/SSR; daisyui supports SPA/SSR/SSG')
92
185
  if(o.render!=='spa' && o.transport!=='rest')throw new Error('SSR/SSG supports REST only; Socket.IO proxy not certified')
93
186
  if(o.dir!==undefined && !safeProjectName(o.dir))throw new Error('--dir must be a single safe directory name')
94
187
  o.destination=resolve(output,o.dir||o.name)
95
188
  if(dirname(o.destination)!==output)throw new Error('Destination escapes output root')
96
- o.case=CASES[`${o.ui}:${o.render}`];o.plan=plan
189
+ o.case=CASES[`${o.ui}:${o.render}`];o.plan=plan;o.databases=dbPairs
97
190
  return o
98
191
  }
99
192
  async function generate(o) {
@@ -103,22 +196,83 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
103
196
  const files=new Map(o.render==='spa' ? [
104
197
  ...await walk(join(templates,'spa','common')),
105
198
  ...await walk(join(templates,'spa',o.ui)),
106
- ] : await walk(join(resolve(examplesRoot),`daisyui-${o.render}`)))
199
+ ] : await walk(join(resolve(examplesRoot),`${o.ui==='quasar2'?'quasar':'daisyui'}-${o.render}`)))
107
200
  // npm pack excludes files literally named .gitignore, even in subdirectories.
108
201
  // The published templates use gitignore.template, rewritten here to real .gitignore.
109
202
  if(files.has('gitignore.template')){files.set('.gitignore',files.get('gitignore.template'));files.delete('gitignore.template')}
203
+ if(files.has('.gitignore')) {
204
+ const ignores=files.get('.gitignore')
205
+ 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','api/.create-feathers-service.lock']
206
+ 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')
207
+ }
110
208
  if(!files.has('api/server.mjs')||!files.has('.env.example')||!files.has('index.html'))throw new Error('Incomplete source template')
111
209
  files.set('.env.example',envChange(files.get('.env.example'),o))
210
+ if (o.devApi === 'vite') {
211
+ enableSpaViteRuntime(files, o)
212
+ files.set('api/config/capabilities.d.mts', 'export declare function assertCapabilities(): void\n')
213
+ }
214
+ if (o.pwa === 'on') enableSpaPwa(files,o)
215
+ if(o.ui==='quasar2' && o.render==='ssr') {
216
+ // R16 HTML has no Vite SPA identity marker; add one without touching frozen R16 sources.
217
+ const html=files.get('index.html')
218
+ 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">'))
219
+ }
112
220
  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'))
221
+ const manifest=o.render==='spa'?spaManifest(o):o.ui==='quasar2'?quasarSSRManifest(o):JSON.parse(files.get('package.json'))
114
222
  manifest.name=o.name;manifest.private=true
115
- manifest.scripts={...manifest.scripts,'dev:all':'node scripts/dev-all.mjs'}
223
+ manifest.scripts={...manifest.scripts,'dev:all':o.devApi==='vite'?'vite --host 127.0.0.1':'node scripts/dev-all.mjs'}
116
224
  files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
117
225
  files.set('README.md',generatedReadme(o))
118
- files.set('scripts/dev-all.mjs',await readFile(join(templates,'common','dev-all.mjs'),'utf8'))
226
+ // Keep the visible application description consistent with the actual dev API mode.
227
+ // The standalone production API is intentionally unaffected.
228
+ if (o.render === 'spa' && o.devApi === 'vite') {
229
+ const app = files.get('src/App.vue')
230
+ const separateDescription = 'Le service Feathers est exécuté dans un processus séparé.'
231
+ if (typeof app !== 'string' || !app.includes(separateDescription)) {
232
+ throw new Error('SPA description contract changed: refusing inconsistent Vite-native scaffold')
233
+ }
234
+ files.set('src/App.vue', app.replace(separateDescription,
235
+ 'En développement, les services Feathers sont intégrés directement à Vite.'))
236
+ }
237
+ 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'))
238
+ // A configuration contract, not a simulated authentication or persistence layer.
239
+ // Non-memory databases and external identity providers fail closed at startup
240
+ // until explicitly integrated and tested with a real provider.
241
+ files.set('api/config/capabilities.mjs',capabilityGuard(o))
242
+ const server=files.get('api/server.mjs').replace("from 'vite-plugin-feathersjs'","from '@vevedh/vite-plugin-feathersjs'")
243
+ files.set('api/server.mjs',"import { assertCapabilities } from './config/capabilities.mjs'\nassertCapabilities()\n" + server)
244
+ 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`)
245
+ 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')
246
+ const depMap={typebox:['@feathersjs/typebox','ajv'],json:['ajv'],zod:['zod']}
247
+ // Dependencies for the service subcommand; project options alone create no arbitrary services.
248
+ for(const dep of depMap[o.schema])manifest.dependencies[dep] ||= dep==='ajv'?'^8.17.1':dep==='zod'?'^4.1.0':'^5.0.50'
249
+ if(o.auth==='jwt')manifest.dependencies['@feathersjs/authentication']='^5.0.50'
250
+ if(o.auth==='oauth2')manifest.dependencies['@feathersjs/authentication-oauth']='^5.0.50'
251
+ for(const db of o.databases) {
252
+ 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']}
253
+ for(const dep of drivers[db.adapter]||[])manifest.dependencies[dep] ||= dep === 'mongodb' ? '6.19.0' : dep === '@feathersjs/mongodb' ? '5.0.51' : '*'
254
+ }
255
+ files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
256
+ if (o.auth === 'jwt' && o.databases.some(db => ['postgresql','mariadb','mongodb'].includes(db.adapter))) {
257
+ for (const name of ['register.mjs','policy.mjs']) files.set('api/security/' + name, await readFile(new URL('./secure-runtime/' + name, import.meta.url), 'utf8'))
258
+ Object.assign(manifest.dependencies, { '@feathersjs/authentication-local': '5.0.50', '@casl/ability': '6.8.1', 'feathers-casl': '3.1.0' })
259
+ manifest.dependencies.zod ||= '^4.1.0'
260
+ files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
261
+ }
262
+ const persistentDatabases = o.databases.filter(db => ['postgresql','mariadb','mongodb'].includes(db.adapter))
263
+ if (o.auth === 'jwt' && persistentDatabases.length) {
264
+ const declarations = persistentDatabases.map(db => ({ alias: db.alias, adapter: db.adapter, envPrefix: 'P5_DB_' + db.alias.replaceAll('-', '_').toUpperCase() }))
265
+ if (new Set(declarations.map(db => db.envPrefix)).size !== declarations.length) throw new Error('Database environment prefix collision')
266
+ files.set('api/config/database-connections.generated.mjs', 'export const databaseDeclarations = Object.freeze(' + JSON.stringify(declarations, null, 2) + ')\n')
267
+ for (const [source, target] of [['providers.mjs','api/runtime/database-providers.generated.mjs'], ['storage.mjs','api/runtime/storage.mjs'], ['prepare-storage.mjs','scripts/prepare-storage.mjs'], ['connect.mjs','api/security/connect.mjs']]) {
268
+ files.set(target, await readFile(new URL('./secure-runtime/' + source, import.meta.url), 'utf8'))
269
+ }
270
+ const env = declarations.flatMap(db => [db.envPrefix + '_HOST=127.0.0.1', db.envPrefix + '_PORT=' + (db.adapter === 'postgresql' ? '5432' : db.adapter === 'mariadb' ? '3306' : '27017'), db.envPrefix + '_USER=', db.envPrefix + '_PASSWORD=', db.envPrefix + '_NAME='])
271
+ files.set('database.env.example', env.join('\n') + '\n')
272
+ }
119
273
  const list=[...files.keys()].sort()
120
274
  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}
275
+ 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
276
  if(o.dryRun)return result
123
277
  // Do not follow symlinks and refuse any existing destination, including empty ones.
124
278
  await mkdir(output,{recursive:true})
@@ -134,3 +288,31 @@ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templ
134
288
  }
135
289
  return {parse,generate}
136
290
  }
291
+
292
+ function quasarSSRManifest(o){
293
+ return {name:o.name,version:'0.1.0',private:true,type:'module',packageManager:'pnpm@10.30.2',engines:{node:'>=22.12'},
294
+ scripts:{'dev:ssr':'quasar dev -m ssr','build:ssr':'quasar build -m ssr',build:'quasar build -m ssr',
295
+ typecheck:'vue-tsc --noEmit', 'dev:api':'node --env-file=.env.example api/server.mjs',
296
+ 'serve:production':'node dist/ssr/index.js',postinstall:'quasar prepare', 'dev:edge':'node scripts/edge-proxy.mjs'},
297
+ dependencies:{'@vevedh/vite-plugin-feathersjs':'0.2.6', '@feathersjs/feathers':'^5.0.50', '@feathersjs/koa':'^5.0.50',
298
+ '@feathersjs/express':'^5.0.50', '@feathersjs/errors':'^5.0.50', '@feathersjs/socketio':'^5.0.50',
299
+ '@feathersjs/rest-client':'^5.0.50', '@feathersjs/socketio-client':'^5.0.50',
300
+ '@quasar/extras':'^1.18.0',pinia:'^3.0.4',quasar:'^2.35.2','socket.io-client':'^4.8.4',
301
+ vue:'^3.5.44','vue-router':'^4.6.4',express:'^4.22.3','serialize-javascript':'^7.1.4'},
302
+ devDependencies:{'@quasar/app-vite':'2.6.2','@types/node':'^22.0.0',typescript:'^5.9.3','vue-tsc':'^3.3.14',
303
+ // Quasar app-vite 2.6.2 runs Express v4. Keep its ambient types in
304
+ // the same major as Express runtime, including nested pnpm dependencies.
305
+ '@types/express':'4.17.25','@types/express-serve-static-core':'4.19.9',vite:'^7.3.7'},
306
+ pnpm:{overrides:{'@types/express':'4.17.25','@types/express-serve-static-core':'4.19.9'}}}
307
+ }
308
+ function capabilityGuard(o) {
309
+ const external=o.databases.filter(x=>x.adapter!=='memory')
310
+ const desc=JSON.stringify({auth:o.auth,databases:o.databases})
311
+ return `// Generated capability contract: never silently downgrade identity or persistence.\n`+
312
+ `export const profile=Object.freeze(${desc})\n`+
313
+ `export function assertCapabilities(){\n`+
314
+ ` 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`+
315
+ ` const missing=profile.databases.filter(item=>item.adapter!=='memory')\n`+
316
+ ` 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`+
317
+ `}\n`
318
+ }
@@ -0,0 +1,28 @@
1
+ import { openNamedDatabaseProviders } from '../runtime/database-providers.generated.mjs'
2
+ import { storageNames } from '../runtime/storage.mjs'
3
+ import { registerSecuredPersistence } from './register.mjs'
4
+
5
+ /** Attach real persistence/auth to an unstarted Feathers app; caller owns listener and shutdown.
6
+ * No schema creation, account seeding or socket broadcast occurs during startup.
7
+ */
8
+ export async function connectSecuredPersistence(app, config) {
9
+ if (!config || Object.keys(config).sort().join(',') !== 'alias,audience,issuer,secret') throw Error('INVALID_SECURITY_CONFIG')
10
+ let providers
11
+ try {
12
+ providers = await openNamedDatabaseProviders()
13
+ const provider = providers.get(config.alias)
14
+ // Readiness checks the actual schema/collections before registration.
15
+ if (provider.adapter === 'mongodb') {
16
+ for (const name of Object.values(storageNames)) {
17
+ if (!await provider.db.listCollections({ name }).hasNext()) throw Error('STORAGE_NOT_PREPARED')
18
+ }
19
+ } else {
20
+ for (const name of Object.values(storageNames)) if (!await provider.db.schema.hasTable(name)) throw Error('STORAGE_NOT_PREPARED')
21
+ }
22
+ registerSecuredPersistence(app, providers, { ...config, ...storageNames })
23
+ return Object.freeze({ close: () => providers.close() })
24
+ } catch (error) {
25
+ try { await providers?.close() } catch {}
26
+ throw error
27
+ }
28
+ }
@@ -0,0 +1,85 @@
1
+ /** P5-R5-R4-R2: only server-validated identities determine Feathers/CASL permissions. */
2
+ import { randomUUID } from 'node:crypto'
3
+ import { AbilityBuilder, createMongoAbility } from '@casl/ability'
4
+ import { BadRequest, NotAuthenticated } from '@feathersjs/errors'
5
+
6
+ import { z } from 'zod'
7
+
8
+ const id = z.string().uuid()
9
+ const title = z.string().trim().min(1).max(80).regex(/^[A-Za-z0-9 _.-]+$/)
10
+ const create = z.object({ title }).strict()
11
+ const patch = z.object({ title }).strict()
12
+ const list = z.object({
13
+ $limit: z.coerce.number().int().min(0).max(50).optional(),
14
+ $skip: z.coerce.number().int().min(0).max(100).optional(),
15
+ }).strict()
16
+ const empty = z.object({}).strict()
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+ export function validateIdentity(input ) {
27
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
28
+ throw new NotAuthenticated('Identity unavailable')
29
+ }
30
+ const value = input
31
+ const parsed = z.object({
32
+ id,
33
+ role: z.enum(['viewer','editor','admin']),
34
+ status: z.literal('active'),
35
+ sessionVersion: z.number().int().positive().safe(),
36
+ }).passthrough().safeParse(value)
37
+ if (!parsed.success) throw new NotAuthenticated('Identity unavailable')
38
+ return parsed.data
39
+ }
40
+
41
+ export function serverAbility(identity ) {
42
+ const { can, rules } = new AbilityBuilder(createMongoAbility)
43
+ if (identity.role === 'admin') {
44
+ can('manage', 'all')
45
+ } else {
46
+ can('find', 'documents', ['id','title','ownerId'], { ownerId: identity.id })
47
+ can('get', 'documents', ['id','title','ownerId'], { ownerId: identity.id })
48
+ if (identity.role === 'editor') {
49
+ can('create', 'documents')
50
+ can('patch', 'documents', ['title'], { ownerId: identity.id })
51
+ }
52
+ }
53
+ return createMongoAbility(rules)
54
+ }
55
+
56
+
57
+
58
+
59
+
60
+
61
+ /** Must run after real Feathers authenticate('jwt') and before CASL authorize(). */
62
+ export function documentPolicy(revalidate ) {
63
+ return async (context ) => {
64
+ if (['get', 'patch', 'remove'].includes(context.method) && !id.safeParse(context.id).success) throw new BadRequest('Invalid id')
65
+ const params = context.params
66
+ if (!params.provider) throw new NotAuthenticated('Authenticated transport required')
67
+ const token = params.authentication?.accessToken
68
+ if (typeof token !== 'string' || token.length < 24 || token.length > 4096) {
69
+ throw new NotAuthenticated('Authentication required')
70
+ }
71
+ // Never use params.user, client JWT roles, client-supplied ability, or stale socket permissions.
72
+ const identity = validateIdentity(await revalidate(token))
73
+ params.ability = serverAbility(identity)
74
+ const query = (context.method === 'find' ? list : empty).safeParse(params.query ?? {})
75
+ if (!query.success) throw new BadRequest('Invalid query')
76
+ context.params.query = query.data
77
+ if (context.method === 'create' || context.method === 'patch') {
78
+ const parsed = (context.method === 'create' ? create : patch).safeParse(context.data )
79
+ if (!parsed.success) throw new BadRequest('Invalid data')
80
+ context.data = context.method === 'create'
81
+ ? { ...parsed.data, id: randomUUID(), ownerId: identity.id, internal: null }
82
+ : parsed.data
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,14 @@
1
+ import { openNamedDatabaseProviders } from '../api/runtime/database-providers.generated.mjs'
2
+ import { prepareIdentityStorage } from '../api/runtime/storage.mjs'
3
+ const [mode, flag, alias, ...extra] = process.argv.slice(2)
4
+ if (mode !== '--apply' || flag !== '--alias' || !/^[a-z][a-z0-9-]{0,31}$/.test(alias ?? '') || extra.length) {
5
+ console.error('STORAGE=REFUSED | EXPECTED=--apply --alias NAME'); process.exitCode = 2
6
+ } else {
7
+ let providers
8
+ try {
9
+ providers = await openNamedDatabaseProviders()
10
+ await prepareIdentityStorage(providers.get(alias))
11
+ console.log('STORAGE=PREPARED | ACCOUNTS_CREATED=0 | PUBLIC_RUNTIME=BLOCKED')
12
+ } catch { console.error('STORAGE=FAILED | PUBLIC_RUNTIME=BLOCKED'); process.exitCode = 2 }
13
+ finally { try { await providers?.close() } catch { console.error('STORAGE=CLEANUP_FAILED'); process.exitCode = 2 } }
14
+ }