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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -2
  3. package/bin/create-feathers-app.mjs +33 -0
  4. package/lib/planner/options.mjs +74 -0
  5. package/lib/planner/plan.mjs +107 -0
  6. package/lib/scaffold.mjs +136 -0
  7. package/package.json +32 -4
  8. package/templates/common/dev-all.mjs +46 -0
  9. package/templates/daisyui-ssg/.env.example +6 -0
  10. package/templates/daisyui-ssg/README.md +21 -0
  11. package/templates/daisyui-ssg/api/server.mjs +45 -0
  12. package/templates/daisyui-ssg/api/services/health/health.class.mjs +3 -0
  13. package/templates/daisyui-ssg/api/services/health/health.service.mjs +4 -0
  14. package/templates/daisyui-ssg/api/services/notes/notes.class.mjs +25 -0
  15. package/templates/daisyui-ssg/api/services/notes/notes.service.mjs +10 -0
  16. package/templates/daisyui-ssg/gitignore.template +16 -0
  17. package/templates/daisyui-ssg/index.html +2 -0
  18. package/templates/daisyui-ssg/package.json +39 -0
  19. package/templates/daisyui-ssg/scripts/prerender.mjs +14 -0
  20. package/templates/daisyui-ssg/scripts/serve-static.mjs +32 -0
  21. package/templates/daisyui-ssg/src/App.vue +14 -0
  22. package/templates/daisyui-ssg/src/components/NotesPanel.vue +22 -0
  23. package/templates/daisyui-ssg/src/entry-client.ts +9 -0
  24. package/templates/daisyui-ssg/src/entry-server.ts +9 -0
  25. package/templates/daisyui-ssg/src/stores/notes.ts +46 -0
  26. package/templates/daisyui-ssg/src/style.css +3 -0
  27. package/templates/daisyui-ssg/src/vite-env.d.ts +1 -0
  28. package/templates/daisyui-ssg/tsconfig.json +4 -0
  29. package/templates/daisyui-ssg/vite.config.ts +6 -0
  30. package/templates/daisyui-ssr/.env.example +6 -0
  31. package/templates/daisyui-ssr/README.md +21 -0
  32. package/templates/daisyui-ssr/api/server.mjs +45 -0
  33. package/templates/daisyui-ssr/api/services/health/health.class.mjs +3 -0
  34. package/templates/daisyui-ssr/api/services/health/health.service.mjs +4 -0
  35. package/templates/daisyui-ssr/api/services/notes/notes.class.mjs +25 -0
  36. package/templates/daisyui-ssr/api/services/notes/notes.service.mjs +10 -0
  37. package/templates/daisyui-ssr/gitignore.template +16 -0
  38. package/templates/daisyui-ssr/index.html +2 -0
  39. package/templates/daisyui-ssr/package.json +39 -0
  40. package/templates/daisyui-ssr/scripts/serve-ssr.mjs +47 -0
  41. package/templates/daisyui-ssr/src/App.vue +14 -0
  42. package/templates/daisyui-ssr/src/components/NotesPanel.vue +22 -0
  43. package/templates/daisyui-ssr/src/entry-client.ts +9 -0
  44. package/templates/daisyui-ssr/src/entry-server.ts +9 -0
  45. package/templates/daisyui-ssr/src/stores/notes.ts +46 -0
  46. package/templates/daisyui-ssr/src/style.css +3 -0
  47. package/templates/daisyui-ssr/src/vite-env.d.ts +1 -0
  48. package/templates/daisyui-ssr/tsconfig.json +4 -0
  49. package/templates/daisyui-ssr/vite.config.ts +7 -0
  50. package/templates/spa/common/.env.example +7 -0
  51. package/templates/spa/common/api/server.mjs +45 -0
  52. package/templates/spa/common/api/services/health/health.class.mjs +3 -0
  53. package/templates/spa/common/api/services/health/health.service.mjs +4 -0
  54. package/templates/spa/common/api/services/notes/notes.class.mjs +25 -0
  55. package/templates/spa/common/api/services/notes/notes.service.mjs +10 -0
  56. package/templates/spa/common/gitignore.template +19 -0
  57. package/templates/spa/common/index.html +3 -0
  58. package/templates/spa/common/src/components/NotesPanel.vue +25 -0
  59. package/templates/spa/common/src/stores/notes.ts +46 -0
  60. package/templates/spa/common/src/vite-env.d.ts +1 -0
  61. package/templates/spa/common/tsconfig.json +4 -0
  62. package/templates/spa/daisyui/src/App.vue +2 -0
  63. package/templates/spa/daisyui/src/components/NotesPanel.vue +22 -0
  64. package/templates/spa/daisyui/src/main.ts +5 -0
  65. package/templates/spa/daisyui/src/style.css +3 -0
  66. package/templates/spa/daisyui/vite.config.ts +8 -0
  67. package/templates/spa/quasar2/src/App.vue +3 -0
  68. package/templates/spa/quasar2/src/components/NotesPanel.vue +23 -0
  69. package/templates/spa/quasar2/src/main.ts +7 -0
  70. package/templates/spa/quasar2/src/style.css +1 -0
  71. package/templates/spa/quasar2/vite.config.ts +8 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vite-plugin-feathersjs contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,43 @@
1
- # Temporary Holding Version
1
+ # create-feathers-app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Create local FeathersJS v5 applications with Vue 3, Pinia and Vite. Choose Quasar 2 or Tailwind CSS + daisyUI.
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).
6
+
7
+ ## Usage
8
+
9
+ ```powershell
10
+ npm exec --package @vevedh/create-feathers-app -- create-feathers-app portail --ui quasar2 --render spa --adapter koa --transport rest
11
+ ```
12
+
13
+ After npm publication, `npm create @vevedh/feathers-app@latest portail -- --ui daisyui --render ssr` can be used (depending on npm's argument forwarding).
14
+
15
+ For a local candidate, run `node bin/create-feathers-app.mjs --help` from this package.
16
+
17
+ | UI | SPA | SSR | SSG |
18
+ | --- | --- | --- | --- |
19
+ | Quasar 2 | Yes | R16 standalone example only | Unsupported on certified CLI |
20
+ | daisyUI 5 | Yes | Yes | Yes |
21
+
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.
23
+
24
+ ## Options
25
+
26
+ `--name`, `--dir`, `--ui`, `--render`, `--adapter`, `--transport`, `--dry-run`, `--json`, `--help`, `--version`.
27
+
28
+ ## Windows development
29
+
30
+ ```powershell
31
+ corepack pnpm install
32
+ corepack pnpm run dev:all
33
+ corepack pnpm run typecheck
34
+ corepack pnpm run build
35
+ ```
36
+
37
+ Generated apps are local demonstrations with an in-memory notes service and no authentication. Do not expose them directly on the Internet.
38
+
39
+ The CLI does not generate OAuth/JWT, persistent databases, PWA or Quasar SSR/SSG yet.
40
+
41
+ ## Documentation
42
+
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.
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { dirname,resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { readFile } from 'node:fs/promises'
5
+ import { createGenerator } from '../lib/scaffold.mjs'
6
+ const packageRoot=resolve(dirname(fileURLToPath(import.meta.url)),'..')
7
+ const {version}=JSON.parse(await readFile(resolve(packageRoot,'package.json'),'utf8'))
8
+ const args=process.argv.slice(2)
9
+ if(args.includes('--help')||args.includes('-h')||!args.length){
10
+ console.log(`create-feathers-app ${version}
11
+
12
+ Usage: create-feathers-app <name> [options]
13
+ --name <name> Project name (alternative to positional name)
14
+ --dir <directory> Safe folder name (within current directory)
15
+ --ui <quasar2|daisyui> Default daisyui
16
+ --render <spa|ssr|ssg> Default spa
17
+ --adapter <koa|express> Default koa
18
+ --transport <rest|both> Default rest (both only for SPA)
19
+ --dry-run Preview files without writing
20
+ --json Machine-readable output
21
+ --version Print CLI version
22
+
23
+ Available: Quasar 2 SPA; daisyUI SPA, SSR, SSG.
24
+ Quasar SSR: certified example in the companion repository; Quasar SSG not supported.`)
25
+ }else if(args.includes('--version')||args.includes('-v'))console.log(version)
26
+ else {
27
+ const {parse,generate}=createGenerator({outputRoot:process.cwd(),templatesRoot:resolve(packageRoot,'templates'),reportRoot:process.cwd()})
28
+ try {
29
+ const options=parse(args)
30
+ const result=await generate(options)
31
+ console.log(options.json?JSON.stringify(result,null,2):`${options.dryRun?'DRY RUN':'CREATED'}: ${result.destination} (${result.files.length} files)`)
32
+ } catch(error){console.error(`FAIL: ${error.message}`);process.exitCode=1}
33
+ }
@@ -0,0 +1,74 @@
1
+ /** Candidate options for a future scaffolding CLI. No files or runtime services
2
+ * are generated by this read-only planner. Keep mappings reviewable and explicit.
3
+ */
4
+ export const UI_PRESETS = Object.freeze(['quasar2', 'daisyui', 'daisyui-kit'])
5
+ export const SCHEMA_PRESETS = Object.freeze(['typebox', 'json', 'zod'])
6
+ export const AUTH_PRESETS = Object.freeze(['none', 'jwt', 'local', 'oauth', 'oidc', 'ldap'])
7
+ export const DATABASE_PRESETS = Object.freeze(['memory', 'mongodb', 'postgresql', 'mariadb', 'mysql', 'sqlite'])
8
+ export const RENDER_PRESETS = Object.freeze(['spa', 'ssr', 'ssg'])
9
+ export const RENDER_ENGINES = Object.freeze(['auto', 'vite', 'quasar-cli-vite'])
10
+ export const TRANSPORT_PRESETS = Object.freeze(['rest', 'socketio', 'both'])
11
+ export const NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
12
+
13
+ const AVAILABLE_OPTIONS = new Set(['--name', '--ui', '--render', '--pwa', '--schema', '--auth', '--db', '--primary', '--transport', '--engine', '--dry-run', '--json', '--help'])
14
+
15
+ function requiredChoice(value, choices, name) {
16
+ if (!choices.includes(value)) throw new Error(`Invalid ${name}: expected ${choices.join('|')}`)
17
+ return value
18
+ }
19
+ function parseUniqueCsv(value, name) {
20
+ if (typeof value !== 'string' || !value || value.length > 512) throw new Error(`Invalid ${name}`)
21
+ const parts = value.split(',')
22
+ if (!parts.length || parts.some(part => !part || part !== part.trim()) || new Set(parts).size !== parts.length) throw new Error(`Invalid or duplicate ${name}`)
23
+ return parts
24
+ }
25
+ export function parsePlanArguments(args) {
26
+ if (args.includes('--help') || args.includes('-h')) return { help: true }
27
+ const options = { name: 'my-feathers-app', ui: 'daisyui', render: 'spa', pwa: 'off', schema: 'typebox', auth: 'none', db: 'main:memory', primary: 'main', transport: 'rest', engine: 'auto', dryRun: true, json: false }
28
+ const seen = new Set()
29
+ for (let index = 0; index < args.length; index++) {
30
+ const flag = args[index]
31
+ if (!AVAILABLE_OPTIONS.has(flag) || flag === '--help' || seen.has(flag)) throw new Error(`Unknown or duplicate option: ${flag}`)
32
+ seen.add(flag)
33
+ if (flag === '--dry-run' || flag === '--json') options[flag === '--dry-run' ? 'dryRun' : 'json'] = true
34
+ else {
35
+ const value = args[++index]
36
+ if (!value || value.startsWith('--')) throw new Error(`Missing value for ${flag}`)
37
+ options[flag.slice(2)] = value
38
+ }
39
+ }
40
+ return options
41
+ }
42
+ export function normalizePlanOptions(input) {
43
+ const name = input.name ?? 'my-feathers-app'
44
+ if (typeof name !== 'string' || name.length > 48 || !NAME_PATTERN.test(name)) throw new Error('Invalid --name: lowercase kebab-case, maximum 48 characters')
45
+ const ui = requiredChoice(input.ui ?? 'daisyui', UI_PRESETS, '--ui')
46
+ const render = requiredChoice(input.render ?? 'spa', RENDER_PRESETS, '--render')
47
+ const pwa = requiredChoice(input.pwa ?? 'off', ['on', 'off'], '--pwa')
48
+ const schema = requiredChoice(input.schema ?? 'typebox', SCHEMA_PRESETS, '--schema')
49
+ const transport = requiredChoice(input.transport ?? 'rest', TRANSPORT_PRESETS, '--transport')
50
+ const requestedEngine = requiredChoice(input.engine ?? 'auto', RENDER_ENGINES, '--engine')
51
+ // Quasar Vite plugin works in SPA; Quasar CLI with Vite is a separate toolchain
52
+ // required for SSR/SSG. Neither integrates this Feathers plugin automatically.
53
+ const engine = requestedEngine === 'auto'
54
+ ? (ui === 'quasar2' && render !== 'spa' ? 'quasar-cli-vite' : 'vite')
55
+ : requestedEngine
56
+ if (ui !== 'quasar2' && engine === 'quasar-cli-vite') throw new Error('Quasar CLI is only compatible with --ui quasar2')
57
+ if (ui === 'quasar2' && render !== 'spa' && engine === 'vite') throw new Error('Quasar standalone Vite plugin supports SPA only; select --engine quasar-cli-vite for SSR/SSG')
58
+ const auth = parseUniqueCsv(input.auth ?? 'none', '--auth')
59
+ for (const strategy of auth) requiredChoice(strategy, AUTH_PRESETS, '--auth')
60
+ if (auth.includes('none') && auth.length > 1) throw new Error('Cannot combine --auth none with authentication strategies')
61
+ // AuthenticationService issues JWTs for the standard local and OAuth flows.
62
+ if (auth.some(strategy => ['local', 'oauth'].includes(strategy)) && !auth.includes('jwt')) auth.unshift('jwt')
63
+ const databases = parseUniqueCsv(input.db ?? 'main:memory', '--db').map(spec => {
64
+ const match = /^([a-z][a-z0-9-]{0,31}):([a-z]+)$/.exec(spec)
65
+ if (!match) throw new Error('Invalid --db: use alias:adapter[,alias:adapter]')
66
+ const [, alias, adapter] = match
67
+ requiredChoice(adapter, DATABASE_PRESETS, '--db adapter')
68
+ return { alias, adapter }
69
+ })
70
+ if (new Set(databases.map(db => db.alias)).size !== databases.length) throw new Error('Duplicate database connection alias')
71
+ const primary = input.primary ?? 'main'
72
+ if (!databases.some(db => db.alias === primary)) throw new Error('--primary must match a --db alias')
73
+ return { name, ui, render, engine, pwa, schema, transport, auth, databases, primary, dryRun: true, json: Boolean(input.json) }
74
+ }
@@ -0,0 +1,107 @@
1
+ import { normalizePlanOptions } from './options.mjs'
2
+
3
+ const DATABASE_PACKAGES = {
4
+ memory: ['@feathersjs/memory'],
5
+ mongodb: ['@feathersjs/mongodb', 'mongodb'],
6
+ postgresql: ['@feathersjs/knex', 'knex', 'pg'],
7
+ mariadb: ['@feathersjs/knex', 'knex', 'mysql2'],
8
+ mysql: ['@feathersjs/knex', 'knex', 'mysql2'],
9
+ sqlite: ['@feathersjs/knex', 'knex'],
10
+ }
11
+ const PROVIDERS = {
12
+ jwt: ['@feathersjs/authentication'],
13
+ local: ['@feathersjs/authentication-local'],
14
+ oauth: ['@feathersjs/authentication-oauth'],
15
+ oidc: [],
16
+ ldap: [],
17
+ }
18
+ /** Deterministic, read-only plan. This is a capability contract, NOT a scaffold.
19
+ * Store no connection URLs or authentication secrets in this plan.
20
+ */
21
+ export function buildProjectPlan(rawOptions) {
22
+ const options = normalizePlanOptions(rawOptions)
23
+ const packages = new Set(['vite', 'vue', 'pinia', '@feathersjs/feathers', '@feathersjs/koa', '@vevedh/vite-plugin-feathersjs'])
24
+ const tasks = []
25
+ const warnings = []
26
+ if (options.ui === 'quasar2') {
27
+ if (options.engine === 'vite') {
28
+ for (const pkg of ['quasar', '@quasar/extras', '@quasar/vite-plugin@^1', 'sass-embedded']) packages.add(pkg)
29
+ tasks.push('Vite 7 requires @quasar/vite-plugin 1.x, which supports SPA only; certify Quasar 2 component compatibility.')
30
+ }
31
+ else {
32
+ for (const pkg of ['quasar', '@quasar/extras', '@quasar/app-vite']) packages.add(pkg)
33
+ tasks.push('Use Quasar CLI with Vite rather than @quasar/vite-plugin. Scaffold independently and certify integration with Feathers v5, services, Pinia, HMR and deployment.')
34
+ if (options.render === 'ssg') warnings.push('Quasar CLI Vite SSG is in beta: APIs and deployment requirements may change.')
35
+ warnings.push('Quasar CLI SSR/SSG + vite-plugin-feathersjs integration is NOT yet implemented or certified in this project.')
36
+ }
37
+ }
38
+ else {
39
+ for (const pkg of ['tailwindcss', '@tailwindcss/vite', 'daisyui']) packages.add(pkg)
40
+ if (options.ui === 'daisyui-kit') warnings.push('daisyUI kit is an external UI-template/preset selection; its license and assets are NOT bundled or installed by the planner.')
41
+ tasks.push('Use the Tailwind 4 Vite plugin and daisyUI CSS; avoid duplicate global UnoCSS/Tailwind resets.')
42
+ if (options.render === 'ssr') {
43
+ packages.add('@vue/server-renderer')
44
+ tasks.push('Build a Vite/Vue SSR server entry, isolated per-request Vue/Pinia state, HTML template and hydration, and certify dev and production SSR.')
45
+ }
46
+ if (options.render === 'ssg') {
47
+ packages.add('vite-ssg')
48
+ tasks.push('Prerender only public/static routes; verify generated HTML and client hydration, and use a separate Feathers API for dynamic data.')
49
+ }
50
+ }
51
+ if (options.render === 'ssr') {
52
+ warnings.push('SSR must isolate Pinia and authentication state per request; never serialize tokens, secrets or other users’ data into HTML.')
53
+ tasks.push('Define a production SSR server lifecycle, security headers, cache policy and explicit separation from Feathers REST/Socket.IO endpoints.')
54
+ }
55
+ if (options.render === 'ssg') {
56
+ warnings.push('SSG must not bake user-specific, authenticated or private content into static HTML; dynamic services require a running Feathers API.')
57
+ }
58
+ if (options.pwa === 'on') {
59
+ if (options.engine === 'vite') packages.add('vite-plugin-pwa')
60
+ tasks.push(options.engine === 'quasar-cli-vite' ? 'Configure Quasar CLI PWA takeover / offline mode for the selected SSR/SSG build, and certify separately.' : 'Configure manifest, icons, service worker lifecycle and offline caching; test update/recovery and HTTPS deployment.')
61
+ }
62
+ if (options.schema === 'typebox') {
63
+ packages.add('@feathersjs/typebox')
64
+ packages.add('@feathersjs/schema')
65
+ }
66
+ else if (options.schema === 'json') {
67
+ packages.add('@feathersjs/schema')
68
+ tasks.push('Generate plain JSON Schema and typed Ajv validators/resolvers; compile schema once, do not bypass query validation.')
69
+ }
70
+ else {
71
+ packages.add('zod')
72
+ warnings.push('Zod requires a Feathers v5 validation/resolver integration. The native TypeBox/Ajv generator cannot be reused unchanged.')
73
+ tasks.push('Implement and test dedicated Zod validation hooks for data, patch and query before enabling generated services.')
74
+ }
75
+ for (const strategy of options.auth.filter(item => item !== 'none')) {
76
+ for (const pkg of PROVIDERS[strategy]) packages.add(pkg)
77
+ if (strategy === 'oidc' || strategy === 'ldap') warnings.push(`${strategy.toUpperCase()} requires a separately implemented/audited authentication strategy and identity-provider configuration.`)
78
+ }
79
+ if (!options.auth.includes('none')) {
80
+ packages.add('@feathersjs/authentication-client')
81
+ tasks.push('Register AuthenticationService and per-service authenticate/authorize hooks; require RBAC for protected services.')
82
+ }
83
+ if (options.auth.includes('oauth')) tasks.push('Choose OAuth providers explicitly and configure redirect allowlists, callback HTTPS URLs, account linking and secret storage.')
84
+ for (const db of options.databases) for (const pkg of DATABASE_PACKAGES[db.adapter]) packages.add(pkg)
85
+ if (options.databases.some(db => db.adapter === 'sqlite')) warnings.push('Choose and certify a compatible SQLite driver for the target Node/Knex versions before implementation.')
86
+ if (options.databases.some(db => db.adapter === 'memory')) warnings.push('Memory adapter is for development/test: not durable production storage.')
87
+ if (options.databases.length > 1) tasks.push('Create separate named clients and bind each service to exactly one connection; cross-database transactions are not automatic.')
88
+ const connections = options.databases.map(({ alias, adapter }) => ({ alias, adapter, envKey: `FEATHERS_DB_${alias.replaceAll('-', '_').toUpperCase()}_URL`, isPrimary: alias === options.primary }))
89
+ const rendering = {
90
+ mode: options.render,
91
+ engine: options.engine,
92
+ deployment: options.render === 'ssr' ? 'node-ssr-runtime-required' : options.render === 'ssg' ? 'static-pages-plus-separate-feathers-api' : 'static-spa-plus-feathers-api',
93
+ hydrationRequired: options.render !== 'spa',
94
+ integrationStatus: 'not-generated-or-certified',
95
+ }
96
+ return {
97
+ version: 2, status: 'planning-only', writesFiles: false,
98
+ project: options.name,
99
+ stack: { vite: options.engine === 'vite' ? '7' : 'managed-by-quasar-cli', vue: '3', feathers: '5', pinia: true, ui: options.ui, render: options.render, renderEngine: options.engine, pwa: options.pwa === 'on', schema: options.schema, transport: options.transport },
100
+ rendering,
101
+ authentication: { strategies: options.auth, customIntegrationRequired: options.auth.filter(item => ['oidc', 'ldap'].includes(item)) },
102
+ database: { primary: options.primary, connections },
103
+ proposedPackages: [...packages].sort(),
104
+ implementationTasks: tasks,
105
+ warnings,
106
+ }
107
+ }
@@ -0,0 +1,136 @@
1
+ import { mkdir, lstat, readdir, readFile, writeFile } from 'node:fs/promises'
2
+ import { existsSync } from 'node:fs'
3
+ import { dirname, join, relative, resolve, sep } from 'node:path'
4
+ import { buildProjectPlan } from './planner/plan.mjs'
5
+
6
+ const KEYS = new Set(['--name', '--dir', '--ui', '--render', '--adapter', '--transport', '--dry-run', '--json'])
7
+ const CASES = Object.freeze({
8
+ 'quasar2:spa': 'quasar-spa',
9
+ 'daisyui:spa': 'daisyui-spa',
10
+ 'daisyui:ssr': 'daisyui-ssr',
11
+ 'daisyui:ssg': 'daisyui-ssg',
12
+ })
13
+ const IGNORED = new Set(['node_modules', 'dist', 'coverage', '.env', 'test-results', 'playwright-report', 'docs-private', '.vite', '.quasar', '.git', 'pnpm-lock.yaml'])
14
+
15
+ async function walk(directory, prefix = '') {
16
+ const output = []
17
+ for (const entry of (await readdir(directory, {withFileTypes: true})).sort((a,b)=>a.name.localeCompare(b.name,'en'))) {
18
+ if (entry.isSymbolicLink()) throw new Error('Template symlink refused')
19
+ if (IGNORED.has(entry.name)) continue
20
+ const key = prefix ? `${prefix}/${entry.name}` : entry.name
21
+ if (entry.isDirectory()) output.push(...await walk(join(directory, entry.name), key))
22
+ else if (entry.isFile()) output.push([key, await readFile(join(directory, entry.name),'utf8')])
23
+ }
24
+ return output
25
+ }
26
+ async function rejectSymlinks(pathname) {
27
+ let current = pathname
28
+ while (true) {
29
+ if (existsSync(current) && (await lstat(current)).isSymbolicLink()) throw new Error('Symlink in destination path')
30
+ const parent = dirname(current)
31
+ if (parent === current) break
32
+ current = parent
33
+ }
34
+ }
35
+ function safeProjectName(value) {
36
+ return typeof value === 'string' && value.length <= 48 && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value) && !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(value)
37
+ }
38
+ function envChange(text, options) {
39
+ return text.replace(/^FEATHERS_ADAPTER=.*$/m, `FEATHERS_ADAPTER=${options.adapter}`)
40
+ .replace(/^FEATHERS_SOCKETIO=.*$/m, `FEATHERS_SOCKETIO=${options.transport === 'both' ? '1' : '0'}`)
41
+ .replace(/^VITE_FEATHERS_REALTIME=.*$/m, `VITE_FEATHERS_REALTIME=${options.transport === 'both' ? '1' : '0'}`)
42
+ }
43
+ function spaManifest(o) {
44
+ const dependencies = {
45
+ '@vevedh/vite-plugin-feathersjs':'0.2.6', '@feathersjs/feathers':'^5.0.50', '@feathersjs/errors':'^5.0.50',
46
+ '@feathersjs/koa':'^5.0.50', '@feathersjs/express':'^5.0.50', '@feathersjs/socketio':'^5.0.50',
47
+ vue:'^3.5.44', pinia:'^3.0.4', 'socket.io-client':'^4.8.4',
48
+ }
49
+ const devDependencies = {
50
+ vite:'^7.3.7', '@vitejs/plugin-vue':'^6.0.0', 'vue-tsc':'^3.0.7',
51
+ typescript:'^5.9.3', '@types/node':'^22.0.0',
52
+ }
53
+ if (o.ui === 'quasar2') { dependencies.quasar = '^2.35.2'; devDependencies['@quasar/vite-plugin']='^1.12.0';devDependencies['sass-embedded']='^1.93.2' }
54
+ else { devDependencies.tailwindcss='^4.1.0';devDependencies['@tailwindcss/vite']='^4.1.0';devDependencies.daisyui='^5.0.0' }
55
+ return {name:o.name,version:'0.1.0',private:true,type:'module',packageManager:'pnpm@10.30.2',engines:{node:'>=22.12'},
56
+ scripts:{dev:'vite --host 127.0.0.1', 'dev:api':'node --env-file=.env.example api/server.mjs',
57
+ 'dev:all':'node scripts/dev-all.mjs',build:'vue-tsc --noEmit && vite build',typecheck:'vue-tsc --noEmit',preview:'vite preview --host 127.0.0.1'},
58
+ dependencies,devDependencies}
59
+ }
60
+ function generatedReadme(o) {
61
+ const renderInfo = o.render === 'ssg' ? 'Le build pré-rend les routes publiques / et /about. Les notes restent dynamiques via /api.'
62
+ : 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.'
64
+ return `# ${o.name}\n\nProjet ${o.ui} ${o.render.toUpperCase()} avec Vue 3, Pinia et FeathersJS v5 (${o.adapter}, ${o.transport}).\n\n`+
65
+ `${renderInfo}\n\n## Démarrage local (Windows / PowerShell)\n\n`+
66
+ '```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' +
68
+ '```powershell\ncorepack pnpm run typecheck\ncorepack pnpm run build\n```\n\n' +
69
+ (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'
71
+ }
72
+ export function createGenerator({outputRoot, templatesRoot, examplesRoot = templatesRoot, reportRoot = outputRoot}) {
73
+ const output = resolve(outputRoot), templates = resolve(templatesRoot), report = resolve(reportRoot)
74
+ function parse(args) {
75
+ const o={ui:'daisyui',render:'spa',adapter:'koa',transport:'rest',dryRun:false,json:false}
76
+ const seen=new Set()
77
+ 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
+ for(let i=0;i<args.length;i++) {
79
+ const flag=args[i]
80
+ if(!KEYS.has(flag)||seen.has(flag))throw new Error(`Unknown or duplicate option: ${flag}`)
81
+ seen.add(flag)
82
+ 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 }
84
+ }
85
+ if(!safeProjectName(o.name))throw new Error('Invalid or missing --name (lowercase kebab-case, max 48 characters)')
86
+ if(!['koa','express'].includes(o.adapter))throw new Error('Only koa|express adapters are supported')
87
+ if(!['rest','both'].includes(o.transport))throw new Error('Only rest|both are supported')
88
+ // Reuse the actual R15 planner as the capability validator, not a separate feature catalogue.
89
+ 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')
92
+ if(o.render!=='spa' && o.transport!=='rest')throw new Error('SSR/SSG supports REST only; Socket.IO proxy not certified')
93
+ if(o.dir!==undefined && !safeProjectName(o.dir))throw new Error('--dir must be a single safe directory name')
94
+ o.destination=resolve(output,o.dir||o.name)
95
+ if(dirname(o.destination)!==output)throw new Error('Destination escapes output root')
96
+ o.case=CASES[`${o.ui}:${o.render}`];o.plan=plan
97
+ return o
98
+ }
99
+ async function generate(o) {
100
+ await rejectSymlinks(output)
101
+ await rejectSymlinks(o.destination)
102
+ if(existsSync(o.destination))throw new Error('Destination already exists; refusing overwrite')
103
+ const files=new Map(o.render==='spa' ? [
104
+ ...await walk(join(templates,'spa','common')),
105
+ ...await walk(join(templates,'spa',o.ui)),
106
+ ] : await walk(join(resolve(examplesRoot),`daisyui-${o.render}`)))
107
+ // npm pack excludes files literally named .gitignore, even in subdirectories.
108
+ // The published templates use gitignore.template, rewritten here to real .gitignore.
109
+ if(files.has('gitignore.template')){files.set('.gitignore',files.get('gitignore.template'));files.delete('gitignore.template')}
110
+ if(!files.has('api/server.mjs')||!files.has('.env.example')||!files.has('index.html'))throw new Error('Incomplete source template')
111
+ files.set('.env.example',envChange(files.get('.env.example'),o))
112
+ 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'))
114
+ manifest.name=o.name;manifest.private=true
115
+ manifest.scripts={...manifest.scripts,'dev:all':'node scripts/dev-all.mjs'}
116
+ files.set('package.json',JSON.stringify(manifest,null,2)+'\n')
117
+ files.set('README.md',generatedReadme(o))
118
+ files.set('scripts/dev-all.mjs',await readFile(join(templates,'common','dev-all.mjs'),'utf8'))
119
+ const list=[...files.keys()].sort()
120
+ 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}
122
+ if(o.dryRun)return result
123
+ // Do not follow symlinks and refuse any existing destination, including empty ones.
124
+ await mkdir(output,{recursive:true})
125
+ await rejectSymlinks(output)
126
+ await mkdir(o.destination,{recursive:false})
127
+ for(const [key,value] of [...files].sort(([a],[b])=>a.localeCompare(b,'en'))) {
128
+ const filepath=resolve(o.destination,key)
129
+ if(!filepath.startsWith(o.destination+sep))throw new Error('Template path escapes destination')
130
+ await mkdir(dirname(filepath),{recursive:true})
131
+ await writeFile(filepath,value.replace(/\r\n/g,'\n'),{flag:'wx'})
132
+ }
133
+ return result
134
+ }
135
+ return {parse,generate}
136
+ }
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@vevedh/create-feathers-app",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-rc.1",
4
+ "type": "module",
5
+ "description": "Scaffold FeathersJS v5 projects with Quasar 2 or daisyUI, Vite, Vue 3 and Pinia",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "create-feathers-app": "./bin/create-feathers-app.mjs"
9
+ },
10
+ "engines": {
11
+ "node": ">=22.12.0"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "lib",
16
+ "templates",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "keywords": [
24
+ "create",
25
+ "scaffold",
26
+ "vite",
27
+ "feathersjs",
28
+ "quasar",
29
+ "daisyui",
30
+ "vue",
31
+ "ssr",
32
+ "ssg"
33
+ ]
34
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ // Starts Feathers first, then the frontend, with one controlled shutdown.
3
+ import { spawn } from 'node:child_process'
4
+ import { existsSync } from 'node:fs'
5
+ import { loadEnvFile } from 'node:process'
6
+ import { setTimeout as sleep } from 'node:timers/promises'
7
+
8
+ const file=existsSync('.env') ? '.env' : '.env.example'
9
+ if(!existsSync(file))throw new Error('Missing .env.example')
10
+ loadEnvFile(file)
11
+ const usesVite=existsSync('src/main.ts')
12
+ const isSSG=existsSync('scripts/serve-static.mjs')
13
+ const env={...process.env}
14
+ const processes=[]
15
+ let stopped=false
16
+ function stop(code=0){
17
+ if(stopped)return
18
+ stopped=true
19
+ for(const p of [...processes].reverse())if(p.exitCode===null)try{p.kill()}catch{}
20
+ process.exitCode=code
21
+ }
22
+ process.on('SIGINT',()=>stop(130))
23
+ process.on('SIGTERM',()=>stop(143))
24
+ const api=spawn(process.execPath,[`--env-file=${file}`,'api/server.mjs'],{env,stdio:'inherit',windowsHide:true})
25
+ processes.push(api)
26
+ api.once('error',err=>{console.error('Feathers:',err.message);stop(1)})
27
+ api.once('exit',code=>{if(!stopped){console.error('Feathers stopped:',code);stop(code||1)}})
28
+ const port=Number(env.FEATHERS_API_PORT||3030)
29
+ let ready=false
30
+ for(let i=0;i<80 && !stopped;i++){
31
+ if(api.exitCode!==null)break
32
+ try {
33
+ const res=await fetch(`http://127.0.0.1:${port}/api/health`,{signal:AbortSignal.timeout(700)})
34
+ if(res.ok && (await res.json()).transport==='feathers-v5'){ready=true;break}
35
+ }catch{}
36
+ await sleep(200)
37
+ }
38
+ if(!ready){console.error('API Feathers non disponible sur 127.0.0.1:'+port);stop(1)}
39
+ if(!stopped){
40
+ const args=usesVite?['node_modules/vite/bin/vite.js','--host','127.0.0.1','--port','5173','--strictPort']
41
+ :[isSSG?'node_modules/vite/bin/vite.js':'scripts/serve-ssr.mjs',...(isSSG?['--host','127.0.0.1','--port','5173','--strictPort']:[])]
42
+ const ui=spawn(process.execPath,args,{env,stdio:'inherit',windowsHide:true})
43
+ processes.push(ui)
44
+ ui.once('error',err=>{console.error('Frontend:',err.message);stop(1)})
45
+ ui.once('exit',code=>{if(!stopped){console.error('Frontend stopped:',code);stop(code||1)}})
46
+ }
@@ -0,0 +1,6 @@
1
+ # Local demonstration only. No secrets or private addresses.
2
+ FEATHERS_ADAPTER=koa
3
+ FEATHERS_API_PORT=3030
4
+ FEATHERS_SOCKET_PORT=3031
5
+ FEATHERS_SOCKETIO=0
6
+ FEATHERS_PUBLIC_ORIGIN=http://127.0.0.1:5173
@@ -0,0 +1,21 @@
1
+ # daisyUI SSG — FeathersJS v5
2
+
3
+ Exemple de rendu SSG avec Vue 3, Pinia, Vite 7 et Tailwind CSS/daisyUI. Le service FeathersJS v5 est exécuté dans un processus indépendant. Les pages statiques ne contiennent pas de notes ni de secrets. Données de démonstration uniquement, sans authentification.
4
+
5
+ ```powershell
6
+ corepack pnpm install
7
+ # Terminal 1 : API Feathers
8
+ corepack pnpm run dev:api
9
+ # Terminal 2 : frontend
10
+ corepack pnpm run dev
11
+ ```
12
+
13
+ ```powershell
14
+ corepack pnpm run typecheck
15
+ corepack pnpm run build
16
+ corepack pnpm run serve:production
17
+ ```
18
+
19
+ En développement, Vite sert une application cliente. Le build pré-rend `/` et `/about` dans `dist/ssg`. Seules des pages publiques sont générées ; le service `notes` reste dynamique et nécessite une API accessible en production. Prévoir la configuration de proxy `/api` sur le serveur statique.
20
+
21
+ La certification Windows complète reste à exécuter. Ne pas exposer publiquement l’API de démonstration en mémoire, sans authentification.
@@ -0,0 +1,45 @@
1
+ import { createServer } from 'node:http'
2
+ import { createFeathersRuntime } from '@vevedh/vite-plugin-feathersjs'
3
+ const adapter = process.env.FEATHERS_ADAPTER === 'express' ? 'express' : 'koa'
4
+ const port = Number(process.env.FEATHERS_API_PORT || 3030)
5
+ const wsPort = Number(process.env.FEATHERS_SOCKET_PORT || 3031)
6
+ const realtime = process.env.FEATHERS_SOCKETIO === '1'
7
+ const origin = process.env.FEATHERS_PUBLIC_ORIGIN || 'http://127.0.0.1:5173'
8
+ if (!Number.isInteger(port) || port < 1024 || port > 65535 || !Number.isInteger(wsPort) || wsPort === port) throw new Error('Invalid port configuration')
9
+ const modules = [
10
+ new URL('./services/health/health.service.mjs', import.meta.url),
11
+ new URL('./services/notes/notes.service.mjs', import.meta.url),
12
+ ]
13
+ const runtime = await createFeathersRuntime(modules.map(String), file => import(file), {
14
+ serviceFiles: [], adapter, prefix: '/api', cors: false,
15
+ socketio: realtime ? { port: wsPort, origins: [origin], host: '127.0.0.1' } : false,
16
+ })
17
+ await runtime.start()
18
+ const httpServer = createServer((req, res) => {
19
+ const pathname = new URL(req.url || '/', 'http://localhost').pathname
20
+ if (pathname !== '/api' && !pathname.startsWith('/api/')) {
21
+ res.writeHead(404, { 'content-type': 'application/json' }); res.end('{"error":"Not found"}'); return
22
+ }
23
+ // The Feathers transport is mounted at /api, while its services register as health/notes.
24
+ req.url = (req.url || '/').slice(4) || '/'
25
+ runtime.handler(req, res, error => {
26
+ if (res.headersSent) { res.end(); return }
27
+ res.writeHead(500, { 'content-type': 'application/json' })
28
+ res.end(JSON.stringify({ error: 'Internal server error' }))
29
+ if (error) console.error('[feathers] request failed')
30
+ })
31
+ })
32
+ await new Promise((resolve, reject) => {
33
+ httpServer.once('error', reject)
34
+ httpServer.listen(port, '127.0.0.1', resolve)
35
+ })
36
+ console.log(`Feathers ${adapter} API listening on http://127.0.0.1:${port}/api (socketio=${realtime})`)
37
+ let stopping = false
38
+ async function close() {
39
+ if (stopping) return
40
+ stopping = true
41
+ await new Promise(resolve => httpServer.close(resolve))
42
+ await runtime.close()
43
+ }
44
+ process.on('SIGINT', () => { void close().then(() => process.exit(0)) })
45
+ process.on('SIGTERM', () => { void close().then(() => process.exit(0)) })
@@ -0,0 +1,3 @@
1
+ export class HealthService {
2
+ async find() { return { status: 'ok', transport: 'feathers-v5' } }
3
+ }
@@ -0,0 +1,4 @@
1
+ import { HealthService } from './health.class.mjs'
2
+ export default function register(app) {
3
+ app.use('health', new HealthService(), { methods: ['find'] })
4
+ }
@@ -0,0 +1,25 @@
1
+ import { NotFound, BadRequest } from '@feathersjs/errors'
2
+ export class NotesService {
3
+ // Feathers v5 exposes service methods through its own service wrapper. The
4
+ // wrapper is not necessarily this class instance, so native #private slots
5
+ // cannot be read through an unbound method call. Bind to the owning instance.
6
+ // Keep this per-service-instance: never hoist records or IDs to module scope.
7
+ constructor() {
8
+ for (const name of ['find', 'get', 'create', 'patch', 'remove']) {
9
+ this[name] = this[name].bind(this)
10
+ }
11
+ }
12
+ #records = new Map([[1, { id: 1, title: 'Hello from FeathersJS v5' }]])
13
+ #nextId = 2
14
+ #id(value) { const n = Number(value); if (!Number.isSafeInteger(n) || n < 1) throw new BadRequest('Invalid note ID'); return n }
15
+ #title(value) {
16
+ const title = value?.title
17
+ if (typeof title !== 'string' || title.trim().length < 1 || title.length > 120) throw new BadRequest('Invalid note title')
18
+ return title.trim()
19
+ }
20
+ async find() { return [...this.#records.values()].map(note => ({ ...note })) }
21
+ async get(id) { const note = this.#records.get(this.#id(id)); if (!note) throw new NotFound('Note not found'); return { ...note } }
22
+ async create(data) { const note = { id: this.#nextId++, title: this.#title(data) }; this.#records.set(note.id, note); return { ...note } }
23
+ async patch(id, data) { const key = this.#id(id); if (!this.#records.has(key)) throw new NotFound('Note not found'); const note = { id: key, title: this.#title(data) }; this.#records.set(key, note); return { ...note } }
24
+ async remove(id) { const key = this.#id(id); const note = this.#records.get(key); if (!note) throw new NotFound('Note not found'); this.#records.delete(key); return { ...note } }
25
+ }
@@ -0,0 +1,10 @@
1
+ import { NotesService } from './notes.class.mjs'
2
+ export default function register(app) {
3
+ app.use('notes', new NotesService(), { methods: ['find', 'get', 'create', 'patch', 'remove'] })
4
+ const service = app.service('notes')
5
+ // Demo-only channel: keep the entire stack loopback-only.
6
+ if (typeof service.publish === 'function') {
7
+ app.on('connection', connection => app.channel('local-demo').join(connection))
8
+ service.publish(() => app.channel('local-demo'))
9
+ }
10
+ }