create-fluxstack 1.21.1 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/client/src/App.tsx +9 -11
- package/app/client/src/components/AppLayout.tsx +1 -0
- package/app/client/src/components/BackButton.tsx +1 -0
- package/app/client/src/components/ColorWheel.tsx +1 -0
- package/app/client/src/components/DemoPage.tsx +1 -0
- package/app/client/src/components/ErrorBoundary.tsx +2 -1
- package/app/client/src/components/LiveErrorBoundary.tsx +2 -1
- package/app/client/src/components/ThemePicker.tsx +1 -0
- package/app/client/src/framework/ClientOnly.tsx +14 -0
- package/app/client/src/framework/LivePage.tsx +55 -0
- package/app/client/src/framework/RscHomePage.tsx +125 -0
- package/app/client/src/framework/RscLink.tsx +31 -0
- package/app/client/src/framework/RscNav.tsx +42 -0
- package/app/client/src/framework/RscRoot.tsx +54 -0
- package/app/client/src/framework/csrf.ts +22 -0
- package/app/client/src/framework/entry.browser.tsx +51 -0
- package/app/client/src/framework/entry.rsc.tsx +41 -0
- package/app/client/src/framework/entry.ssr.tsx +12 -0
- package/app/client/src/framework/navigation.ts +29 -0
- package/app/client/src/framework/params.tsx +22 -0
- package/app/client/src/framework/routes.ts +143 -0
- package/app/client/src/framework/vite-rsc.d.ts +21 -0
- package/app/client/src/hooks/useThemeClock.ts +16 -1
- package/app/client/src/live/AuthDemo.tsx +1 -0
- package/app/client/src/live/CounterDemo.tsx +1 -0
- package/app/client/src/live/FormDemo.tsx +1 -0
- package/app/client/src/live/PingPongDemo.tsx +1 -0
- package/app/client/src/live/RoomChatDemo.tsx +1 -0
- package/app/client/src/live/SharedCounterDemo.tsx +2 -1
- package/app/client/src/pages/ApiTestPage.tsx +1 -0
- package/app/client/src/pages/auth.tsx +13 -0
- package/app/client/src/pages/blog/[slug].tsx +23 -0
- package/app/client/src/pages/counter.tsx +15 -0
- package/app/client/src/pages/form.tsx +13 -0
- package/app/client/src/pages/index.tsx +9 -0
- package/app/client/src/pages/ping-pong.tsx +13 -0
- package/app/client/src/pages/room-chat.tsx +13 -0
- package/app/client/src/pages/shared-counter.tsx +13 -0
- package/app/client/src/pages/sobre.tsx +19 -0
- package/app/server/index.ts +5 -1
- package/app/server/live/rooms/index.ts +14 -0
- package/config/system/plugins.config.ts +12 -2
- package/core/cli/commands/dev.ts +9 -1
- package/core/plugins/built-in/rsc/index.ts +156 -0
- package/core/plugins/built-in/ssr/bun-asset-loader.ts +46 -0
- package/core/plugins/built-in/ssr/index.ts +143 -0
- package/core/plugins/built-in/ssr/registry.ts +38 -0
- package/core/plugins/built-in/vite/index.ts +7 -0
- package/core/server/live/websocket-plugin.ts +29 -10
- package/core/utils/version.ts +6 -6
- package/create-fluxstack.ts +64 -5
- package/package.json +112 -108
- package/playwright.config.ts +6 -2
- package/vite.config.ts +22 -0
- package/app/client/.live-stubs/LiveUpload.js +0 -15
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun asset loader para SSR — alinha imports de asset com o Vite.
|
|
3
|
+
*
|
|
4
|
+
* Problema: no client, o Vite resolve `import logo from '...x.svg'` para a URL
|
|
5
|
+
* `/src/assets/x.svg`. No server, o Bun resolve o MESMO import para o caminho
|
|
6
|
+
* de arquivo absoluto (C:\...\x.svg). Strings diferentes no atributo src/href
|
|
7
|
+
* → hydration mismatch (o React descarta o HTML do server e re-renderiza tudo).
|
|
8
|
+
*
|
|
9
|
+
* Solução: registrar um plugin no Bun que, ao carregar um asset, exporta a
|
|
10
|
+
* MESMA URL que o Vite geraria — relativa ao root do client (app/client).
|
|
11
|
+
* Em dev essa URL é servida pelo Vite; em prod, pelos assets buildados.
|
|
12
|
+
*
|
|
13
|
+
* Registrar via bunfig.toml [run].preload ANTES de qualquer import do app.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { plugin } from 'bun'
|
|
17
|
+
import { sep } from 'path'
|
|
18
|
+
|
|
19
|
+
/** Root do client onde o Vite baseia as URLs (app/client) */
|
|
20
|
+
const CLIENT_ROOT = 'app' + sep + 'client' + sep
|
|
21
|
+
|
|
22
|
+
/** Extensões que o Vite trata como asset-URL (não código) */
|
|
23
|
+
const ASSET_RE = /\.(svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|eot|mp4|webm|mp3|wav)$/i
|
|
24
|
+
|
|
25
|
+
plugin({
|
|
26
|
+
name: 'ssr-asset-url',
|
|
27
|
+
setup(build) {
|
|
28
|
+
build.onLoad({ filter: ASSET_RE }, (args) => {
|
|
29
|
+
// args.path é absoluto. Converter para a URL que o Vite usa:
|
|
30
|
+
// .../app/client/src/assets/x.svg -> /src/assets/x.svg
|
|
31
|
+
const idx = args.path.replace(/\//g, sep).indexOf(CLIENT_ROOT)
|
|
32
|
+
let url: string
|
|
33
|
+
if (idx !== -1) {
|
|
34
|
+
const rel = args.path.slice(idx + CLIENT_ROOT.length).replaceAll(sep, '/')
|
|
35
|
+
url = '/' + rel
|
|
36
|
+
} else {
|
|
37
|
+
// fallback: usa o basename
|
|
38
|
+
url = '/' + args.path.split(/[\\/]/).pop()
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
exports: { default: url },
|
|
42
|
+
loader: 'object',
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack SSR Plugin
|
|
3
|
+
*
|
|
4
|
+
* Renderiza o HTML do app no servidor (renderToString) para rotas de PÁGINA,
|
|
5
|
+
* substituindo o `<div id="root"></div>` vazio do SPA por markup pronto.
|
|
6
|
+
* Os assets (JS/CSS/imagens, /@vite, /src/*) continuam servidos pelo Vite (dev)
|
|
7
|
+
* ou pelo static fallback (prod) — o SSR só intercepta navegação de página.
|
|
8
|
+
*
|
|
9
|
+
* Ordem: prioridade 850 > vite (800), então o onBeforeRoute do SSR roda ANTES
|
|
10
|
+
* do proxy do Vite. Se o SSR tratar a request (handled=true), o Vite não vê.
|
|
11
|
+
*
|
|
12
|
+
* O renderer do app é injetado via registry (registerSsrRenderer) — o core não
|
|
13
|
+
* conhece o React do app. Ver ./registry.ts.
|
|
14
|
+
*
|
|
15
|
+
* LIMITAÇÃO ATUAL (PoC): pipeline de hydration montado para DEV (Vite :5173).
|
|
16
|
+
* Produção (sem Vite dev) precisa apontar para os assets buildados — TODO abaixo.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { FluxStack, PluginContext, RequestContext } from '@core/plugins/types'
|
|
20
|
+
import { FLUXSTACK_VERSION } from '@core/utils/version'
|
|
21
|
+
import { isDevelopment } from '@core/utils/helpers'
|
|
22
|
+
import { clientConfig, pluginsConfig } from '@config'
|
|
23
|
+
import { getSsrRenderer } from './registry'
|
|
24
|
+
|
|
25
|
+
type Plugin = FluxStack.Plugin
|
|
26
|
+
|
|
27
|
+
const PLUGIN_PRIORITY = 850 // > vite (800): intercepta páginas antes do proxy Vite
|
|
28
|
+
const IS_DEV = isDevelopment()
|
|
29
|
+
|
|
30
|
+
/** Extensões de asset — nunca devem ser tratadas como página */
|
|
31
|
+
const ASSET_EXT = /\.(js|mjs|ts|tsx|jsx|css|map|json|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|eot|wasm|txt|xml|webmanifest)$/i
|
|
32
|
+
|
|
33
|
+
/** Prefixos internos (Vite/API/dev) que NUNCA são páginas — passam direto */
|
|
34
|
+
const PASSTHROUGH_PREFIXES = ['/@', '/src/', '/node_modules/', '/api', '/swagger', '/__plugins']
|
|
35
|
+
|
|
36
|
+
/** Decide se um path é uma rota de PÁGINA (candidata a SSR) */
|
|
37
|
+
function isPageRoute(path: string): boolean {
|
|
38
|
+
if (ASSET_EXT.test(path)) return false
|
|
39
|
+
return !PASSTHROUGH_PREFIXES.some((p) => path === p || path.startsWith(p))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Monta o documento HTML completo com o markup SSR + pipeline de hydration */
|
|
43
|
+
function buildDocument(opts: {
|
|
44
|
+
appHtml: string
|
|
45
|
+
bootstrapData?: Record<string, unknown>
|
|
46
|
+
headTags?: string
|
|
47
|
+
}): string {
|
|
48
|
+
const { appHtml, bootstrapData, headTags = '' } = opts
|
|
49
|
+
|
|
50
|
+
const viteHost = (clientConfig.vite.host as string) || 'localhost'
|
|
51
|
+
const vitePort = (clientConfig.vite.port as number) || 5173
|
|
52
|
+
const viteBase = `http://${viteHost}:${vitePort}`
|
|
53
|
+
const entry = (pluginsConfig as Record<string, unknown>).ssrClientEntry as string | undefined
|
|
54
|
+
?? '/src/entry-ssr.tsx'
|
|
55
|
+
|
|
56
|
+
const dataScript = bootstrapData
|
|
57
|
+
? `<script>window.__SSR_DATA__ = ${JSON.stringify(bootstrapData).replace(/</g, '\\u003c')};</script>`
|
|
58
|
+
: ''
|
|
59
|
+
|
|
60
|
+
// DEV: pipeline do Vite (react-refresh preamble + client + entry TSX)
|
|
61
|
+
const devScripts = IS_DEV
|
|
62
|
+
? `<script type="module">
|
|
63
|
+
import RefreshRuntime from "${viteBase}/@react-refresh"
|
|
64
|
+
RefreshRuntime.injectIntoGlobalHook(window)
|
|
65
|
+
window.$RefreshReg$ = () => {}
|
|
66
|
+
window.$RefreshSig$ = () => (type) => type
|
|
67
|
+
window.__vite_plugin_react_preamble_installed__ = true
|
|
68
|
+
</script>
|
|
69
|
+
<script type="module" src="${viteBase}/@vite/client"></script>
|
|
70
|
+
<script type="module" src="${viteBase}${entry}"></script>`
|
|
71
|
+
// PROD: TODO — apontar para o entry buildado (ler do manifest do Vite).
|
|
72
|
+
// Por ora, sem script em prod (HTML estático apenas, sem interatividade).
|
|
73
|
+
: `<!-- TODO(SSR prod): injetar /assets/entry-ssr.[hash].js do build manifest -->`
|
|
74
|
+
|
|
75
|
+
return `<!doctype html>
|
|
76
|
+
<html lang="en">
|
|
77
|
+
<head>
|
|
78
|
+
<meta charset="UTF-8" />
|
|
79
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
80
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
81
|
+
<title>FluxStack</title>
|
|
82
|
+
${headTags}
|
|
83
|
+
${IS_DEV ? devScripts : ''}
|
|
84
|
+
</head>
|
|
85
|
+
<body>
|
|
86
|
+
<div id="root">${appHtml}</div>
|
|
87
|
+
${dataScript}
|
|
88
|
+
${IS_DEV ? '' : devScripts}
|
|
89
|
+
</body>
|
|
90
|
+
</html>`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const ssrPlugin: Plugin = {
|
|
94
|
+
name: 'ssr',
|
|
95
|
+
version: FLUXSTACK_VERSION,
|
|
96
|
+
description: 'Server-side rendering of the React app for page routes',
|
|
97
|
+
author: 'FluxStack Team',
|
|
98
|
+
priority: PLUGIN_PRIORITY,
|
|
99
|
+
category: 'rendering',
|
|
100
|
+
tags: ['ssr', 'react', 'hydration'],
|
|
101
|
+
dependencies: [],
|
|
102
|
+
|
|
103
|
+
setup: async (context: PluginContext) => {
|
|
104
|
+
const enabled = (pluginsConfig as Record<string, unknown>).ssrEnabled
|
|
105
|
+
if (!enabled) {
|
|
106
|
+
context.logger.debug('SSR plugin disabled (set SSR_ENABLED=true)')
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
context.logger.debug(`SSR plugin active (priority ${PLUGIN_PRIORITY})`)
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
onBeforeRoute: async (ctx: RequestContext) => {
|
|
113
|
+
if (!(pluginsConfig as Record<string, unknown>).ssrEnabled) return
|
|
114
|
+
if (ctx.method !== 'GET') return
|
|
115
|
+
if (!isPageRoute(ctx.path)) return
|
|
116
|
+
|
|
117
|
+
const renderer = getSsrRenderer()
|
|
118
|
+
if (!renderer) return // app não registrou renderer → deixa o SPA fallback agir
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await renderer({ url: ctx.path, headers: ctx.headers })
|
|
122
|
+
const html = buildDocument({
|
|
123
|
+
appHtml: result.html,
|
|
124
|
+
bootstrapData: result.bootstrapData,
|
|
125
|
+
headTags: result.headTags,
|
|
126
|
+
})
|
|
127
|
+
ctx.handled = true
|
|
128
|
+
ctx.response = new Response(html, {
|
|
129
|
+
status: result.status ?? 200,
|
|
130
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
131
|
+
})
|
|
132
|
+
} catch (err) {
|
|
133
|
+
// SSR falhou → NÃO trata (handled fica false), deixa o Vite/SPA servir.
|
|
134
|
+
// Falhar para o SPA é melhor que mostrar 500: o client ainda renderiza.
|
|
135
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
136
|
+
;(ctx as unknown as { logger?: { error: (m: string) => void } }).logger?.error?.(
|
|
137
|
+
`SSR render failed for ${ctx.path}, falling back to SPA: ${msg}`
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default ssrPlugin
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack SSR — Renderer Registry
|
|
3
|
+
*
|
|
4
|
+
* Inversão de dependência: o CORE (framework) não conhece o App do usuário.
|
|
5
|
+
* O app registra sua função de render (que importa o AppShell client) aqui,
|
|
6
|
+
* e o plugin SSR a consome. Assim o core fica agnóstico ao React do app.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface SsrRenderInput {
|
|
10
|
+
/** Caminho da rota a renderizar (ex: '/', '/counter') */
|
|
11
|
+
url: string
|
|
12
|
+
/** Headers da request original (útil para cookies/auth no futuro) */
|
|
13
|
+
headers: Record<string, string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SsrRenderOutput {
|
|
17
|
+
/** HTML do corpo do app (vai dentro de <div id="root">) */
|
|
18
|
+
html: string
|
|
19
|
+
/** Status HTTP (default 200; use 404 para rota não encontrada) */
|
|
20
|
+
status?: number
|
|
21
|
+
/** Dados serializáveis injetados como window.__SSR_DATA__ para hydration */
|
|
22
|
+
bootstrapData?: Record<string, unknown>
|
|
23
|
+
/** <head> extra opcional (meta tags, title dinâmico) */
|
|
24
|
+
headTags?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SsrRenderer = (input: SsrRenderInput) => SsrRenderOutput | Promise<SsrRenderOutput>
|
|
28
|
+
|
|
29
|
+
let registeredRenderer: SsrRenderer | null = null
|
|
30
|
+
|
|
31
|
+
/** O app chama isto no boot para registrar seu renderer (AppShell). */
|
|
32
|
+
export function registerSsrRenderer(renderer: SsrRenderer): void {
|
|
33
|
+
registeredRenderer = renderer
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getSsrRenderer(): SsrRenderer | null {
|
|
37
|
+
return registeredRenderer
|
|
38
|
+
}
|
|
@@ -226,6 +226,13 @@ export const vitePlugin: Plugin = {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
if (!IS_DEV) {
|
|
229
|
+
// Em prod com RSC, o rscPlugin serve o HTML (handler buildado) e os assets
|
|
230
|
+
// (app/client/dist/client). O static fallback do vite procuraria dist/client
|
|
231
|
+
// (que não existe nesse layout) e quebraria — então pulamos.
|
|
232
|
+
if ((pluginsConfig as Record<string, unknown>).rscEnabled) {
|
|
233
|
+
context.logger.debug('Production + RSC: static serving handled by rsc plugin')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
229
236
|
context.logger.debug("Production mode: static file serving enabled")
|
|
230
237
|
;(context.app as Record<string, Function>).all('*', createStaticFallback())
|
|
231
238
|
return
|
|
@@ -26,24 +26,43 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
26
26
|
tags: ['websocket', 'real-time', 'live-components'],
|
|
27
27
|
|
|
28
28
|
setup: async (context: PluginContext) => {
|
|
29
|
-
|
|
29
|
+
const isProd = process.env.NODE_ENV === 'production'
|
|
30
30
|
const componentsPath = path.join(process.cwd(), 'app', 'server', 'live')
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
|
|
32
|
+
// Em DEV: (re)gera o auto-generated-components.ts varrendo o disco.
|
|
33
|
+
// Em PROD: o app/server/live não existe no dist — o registro estático já
|
|
34
|
+
// foi gerado no build e bundlado. Pular a geração (evita erro de FS).
|
|
35
|
+
if (!isProd) {
|
|
36
|
+
generateLiveComponentsFile({
|
|
37
|
+
componentsDir: componentsPath,
|
|
38
|
+
outFile: path.join(__dirname, 'auto-generated-components.ts'),
|
|
39
|
+
importPrefix: '@app/server/live',
|
|
40
|
+
})
|
|
41
|
+
}
|
|
36
42
|
const { liveComponentClasses } = await import('./auto-generated-components')
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
// dual-Elysia: FluxStack tem elysia@1.4.7, @fluxstack/live-elysia (monorepo
|
|
45
|
+
// linkado) tem elysia@1.4.28 — tipos de instâncias DIFERENTES, mesma API em
|
|
46
|
+
// runtime. `as never` neutraliza o mismatch de tipo no argumento.
|
|
47
|
+
const transport = new ElysiaTransport(context.app as never)
|
|
39
48
|
|
|
40
|
-
//
|
|
49
|
+
// Rooms: em DEV, auto-descobre varrendo rooms/ (import do disco). Em PROD,
|
|
50
|
+
// usa o registro ESTÁTICO (@app/server/live/rooms) — import do disco em prod
|
|
51
|
+
// carregaria uma instância separada do @fluxstack/live (context null). O
|
|
52
|
+
// registro estático entra no bundle com o context único do LiveServer.
|
|
41
53
|
const roomsPath = path.join(componentsPath, 'rooms')
|
|
42
|
-
const discoveredRooms =
|
|
54
|
+
const discoveredRooms = isProd
|
|
55
|
+
? (await import('@app/server/live/rooms')).liveRoomClasses as LiveRoomClass[]
|
|
56
|
+
: await discoverRoomClasses(roomsPath)
|
|
43
57
|
|
|
44
58
|
liveServer = new LiveServer({
|
|
45
59
|
transport,
|
|
46
|
-
componentsPath
|
|
60
|
+
// SÓ em dev: componentsPath dispara o auto-discover dinâmico (import() do
|
|
61
|
+
// disco). Em PROD isso carrega os componentes de uma instância SEPARADA do
|
|
62
|
+
// @fluxstack/live (source linkado), com _ctx null → "LiveServer.start() must
|
|
63
|
+
// be called". Em prod usamos APENAS o registro estático (components), que
|
|
64
|
+
// está no mesmo bundle onde start() setou o context.
|
|
65
|
+
...(isProd ? {} : { componentsPath }),
|
|
47
66
|
wsPath: '/api/live/ws',
|
|
48
67
|
httpPrefix: '/api/live',
|
|
49
68
|
rooms: [...discoveredRooms, ...pendingRoomClasses],
|
package/core/utils/version.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FluxStack Framework Version
|
|
3
|
-
* Single source of truth for version number
|
|
4
|
-
* Auto-synced with package.json
|
|
5
|
-
*/
|
|
6
|
-
export const FLUXSTACK_VERSION = '1.21.1'
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Framework Version
|
|
3
|
+
* Single source of truth for version number
|
|
4
|
+
* Auto-synced with package.json
|
|
5
|
+
*/
|
|
6
|
+
export const FLUXSTACK_VERSION = '1.21.1'
|
package/create-fluxstack.ts
CHANGED
|
@@ -35,8 +35,12 @@ import { resolve, join, basename } from 'path'
|
|
|
35
35
|
import { existsSync, mkdirSync, cpSync, writeFileSync, readFileSync, readdirSync } from 'fs'
|
|
36
36
|
import chalk from 'chalk'
|
|
37
37
|
import ora from 'ora'
|
|
38
|
+
import prompts from 'prompts'
|
|
38
39
|
import { FLUXSTACK_VERSION } from './core/utils/version'
|
|
39
40
|
|
|
41
|
+
/** Modo de renderização do projeto criado. */
|
|
42
|
+
type RenderMode = 'spa' | 'ssr'
|
|
43
|
+
|
|
40
44
|
const logo = `
|
|
41
45
|
⚡ ███████ ██ ██ ██ ██ ██ ███████ ████████ █████ ██████ ██ ██
|
|
42
46
|
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
@@ -52,20 +56,67 @@ program
|
|
|
52
56
|
.name('create-fluxstack')
|
|
53
57
|
.description('⚡ Create FluxStack apps with zero configuration')
|
|
54
58
|
.version(FLUXSTACK_VERSION)
|
|
55
|
-
.argument('[project-name]', 'Name of the project to create')
|
|
59
|
+
.argument('[project-name]', 'Name of the project to create (use "." for current dir; omit for interactive mode)')
|
|
56
60
|
.option('--no-install', 'Skip dependency installation')
|
|
57
61
|
.option('--no-git', 'Skip git initialization')
|
|
62
|
+
.option('--mode <mode>', 'Render mode: "spa" or "ssr" (skips the prompt)')
|
|
58
63
|
.action(async (projectName, options) => {
|
|
59
64
|
console.clear()
|
|
60
65
|
console.log(chalk.magenta(logo))
|
|
61
66
|
|
|
67
|
+
// ── Modo INTERATIVO: sem project-name → pergunta tudo pelo terminal ──
|
|
68
|
+
// create-fluxstack . → cria na pasta atual
|
|
69
|
+
// create-fluxstack my-app → cria em ./my-app
|
|
70
|
+
// create-fluxstack → menu interativo (pergunta nome + modo)
|
|
71
|
+
let renderMode: RenderMode | undefined =
|
|
72
|
+
options.mode === 'spa' || options.mode === 'ssr' ? options.mode : undefined
|
|
73
|
+
|
|
62
74
|
if (!projectName || projectName.trim().length === 0) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
const answers = await prompts(
|
|
76
|
+
[
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
name: 'projectName',
|
|
80
|
+
message: 'Nome do projeto (use "." para a pasta atual):',
|
|
81
|
+
initial: 'my-fluxstack-app',
|
|
82
|
+
validate: (v: string) => (v && v.trim().length > 0 ? true : 'Informe um nome ou "."'),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: renderMode ? null : 'select',
|
|
86
|
+
name: 'mode',
|
|
87
|
+
message: 'Modo de renderização:',
|
|
88
|
+
choices: [
|
|
89
|
+
{ title: 'SSR (RSC)', description: 'Server-rendered + React Server Components + ilhas Live (padrão)', value: 'ssr' },
|
|
90
|
+
{ title: 'SPA', description: 'Client-side puro (mais simples)', value: 'spa' },
|
|
91
|
+
],
|
|
92
|
+
initial: 0, // SSR é o padrão
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
{ onCancel: () => { console.log(chalk.gray('\nCancelado.')); process.exit(0) } },
|
|
96
|
+
)
|
|
97
|
+
projectName = answers.projectName
|
|
98
|
+
renderMode = renderMode ?? (answers.mode as RenderMode)
|
|
99
|
+
} else if (!renderMode) {
|
|
100
|
+
// Tem nome mas não passou --mode → pergunta só o modo.
|
|
101
|
+
const { mode } = await prompts(
|
|
102
|
+
{
|
|
103
|
+
type: 'select',
|
|
104
|
+
name: 'mode',
|
|
105
|
+
message: 'Modo de renderização:',
|
|
106
|
+
choices: [
|
|
107
|
+
{ title: 'SSR (RSC)', description: 'Server-rendered + React Server Components + ilhas Live (padrão)', value: 'ssr' },
|
|
108
|
+
{ title: 'SPA', description: 'Client-side puro (mais simples)', value: 'spa' },
|
|
109
|
+
],
|
|
110
|
+
initial: 0, // SSR é o padrão
|
|
111
|
+
},
|
|
112
|
+
{ onCancel: () => { console.log(chalk.gray('\nCancelado.')); process.exit(0) } },
|
|
113
|
+
)
|
|
114
|
+
renderMode = (mode as RenderMode) ?? 'ssr'
|
|
67
115
|
}
|
|
68
116
|
|
|
117
|
+
// SSR é o padrão do FluxStack; SPA continua disponível via escolha/--mode.
|
|
118
|
+
renderMode = renderMode ?? 'ssr'
|
|
119
|
+
|
|
69
120
|
const currentDir = import.meta.dir
|
|
70
121
|
|
|
71
122
|
// Normalize path: remove trailing slashes (which may indicate current dir usage like path/.)
|
|
@@ -105,6 +156,7 @@ program
|
|
|
105
156
|
|
|
106
157
|
console.log(chalk.cyan(`\n🚀 Creating FluxStack project: ${chalk.bold(displayName)}`))
|
|
107
158
|
console.log(chalk.gray(`📁 Location: ${projectPath}`))
|
|
159
|
+
console.log(chalk.gray(`🎨 Render mode: ${chalk.bold(renderMode === 'ssr' ? 'SSR (RSC)' : 'SPA')}`))
|
|
108
160
|
|
|
109
161
|
// Create project directory
|
|
110
162
|
const spinner = ora('Creating project structure...').start()
|
|
@@ -427,6 +479,13 @@ bun.lockb
|
|
|
427
479
|
envContent = envContent.replace('NODE_ENV=production', 'NODE_ENV=development')
|
|
428
480
|
// Customize app name to match project name
|
|
429
481
|
envContent = envContent.replace('VITE_APP_NAME=FluxStack', `VITE_APP_NAME=${actualProjectName}`)
|
|
482
|
+
// Render mode escolhido: liga RSC (SSR) ou mantém SPA.
|
|
483
|
+
const rscLine = `\n# Render mode (escolhido na criação): RSC/SSR liga server-rendering\nRSC_ENABLED=${renderMode === 'ssr' ? 'true' : 'false'}\n`
|
|
484
|
+
if (/^RSC_ENABLED=/m.test(envContent)) {
|
|
485
|
+
envContent = envContent.replace(/^RSC_ENABLED=.*$/m, `RSC_ENABLED=${renderMode === 'ssr' ? 'true' : 'false'}`)
|
|
486
|
+
} else {
|
|
487
|
+
envContent += rscLine
|
|
488
|
+
}
|
|
430
489
|
writeFileSync(envPath, envContent)
|
|
431
490
|
}
|
|
432
491
|
|
package/package.json
CHANGED
|
@@ -1,108 +1,112 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-fluxstack",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "⚡ Revolutionary full-stack TypeScript framework with Declarative Config System, Elysia + React + Bun",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"framework",
|
|
7
|
-
"full-stack",
|
|
8
|
-
"typescript",
|
|
9
|
-
"elysia",
|
|
10
|
-
"react",
|
|
11
|
-
"bun",
|
|
12
|
-
"vite"
|
|
13
|
-
],
|
|
14
|
-
"author": "FluxStack Team",
|
|
15
|
-
"license": "MIT",
|
|
16
|
-
"homepage": "https://github.com/MarcosBrendonDePaula/FluxStack",
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/MarcosBrendonDePaula/FluxStack.git"
|
|
20
|
-
},
|
|
21
|
-
"module": "app/server/index.ts",
|
|
22
|
-
"type": "module",
|
|
23
|
-
"bin": {
|
|
24
|
-
"create-fluxstack": "create-fluxstack.ts"
|
|
25
|
-
},
|
|
26
|
-
"scripts": {
|
|
27
|
-
"dev": "bun run core/cli/index.ts dev",
|
|
28
|
-
"dev:frontend": "bun run core/cli/index.ts dev --frontend-only",
|
|
29
|
-
"dev:backend": "bun run core/cli/index.ts dev --backend-only",
|
|
30
|
-
"build": "cross-env NODE_ENV=production bun run core/cli/index.ts build",
|
|
31
|
-
"build:frontend": "cross-env NODE_ENV=production bun run core/cli/index.ts build --frontend-only",
|
|
32
|
-
"build:backend": "cross-env NODE_ENV=production bun run core/cli/index.ts build --backend-only",
|
|
33
|
-
"build:exe": "cross-env NODE_ENV=production && bun run core/cli/index.ts build && bun run core/cli/index.ts build:exe",
|
|
34
|
-
"start": "NODE_ENV=production bun dist/index.js",
|
|
35
|
-
"create": "bun run core/cli/index.ts create",
|
|
36
|
-
"cli": "bun run core/cli/index.ts",
|
|
37
|
-
"make:component": "bun run core/cli/index.ts make:component",
|
|
38
|
-
"sync-version": "bun run core/utils/sync-version.ts",
|
|
39
|
-
"test": "vitest",
|
|
40
|
-
"test:ui": "vitest --ui",
|
|
41
|
-
"test:coverage": "vitest run --coverage",
|
|
42
|
-
"typecheck:api": "tsc --noEmit -p tsconfig.api-strict.json",
|
|
43
|
-
"test:e2e": "playwright test",
|
|
44
|
-
"test:e2e:ui": "playwright test --ui",
|
|
45
|
-
"test:e2e:headed": "playwright test --headed"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@eslint/js": "^9.30.1",
|
|
49
|
-
"@noble/curves": "1.2.0",
|
|
50
|
-
"@noble/hashes": "1.3.2",
|
|
51
|
-
"@playwright/test": "^1.58.2",
|
|
52
|
-
"@tailwindcss/vite": "^4.1.13",
|
|
53
|
-
"@testing-library/jest-dom": "^6.6.4",
|
|
54
|
-
"@testing-library/react": "^16.3.0",
|
|
55
|
-
"@testing-library/user-event": "^14.6.1",
|
|
56
|
-
"@types/bun": "latest",
|
|
57
|
-
"@types/node": "^24.5.2",
|
|
58
|
-
"@types/react": "^19.1.8",
|
|
59
|
-
"@types/react-dom": "^19.1.6",
|
|
60
|
-
"@vitest/coverage-v8": "^3.2.4",
|
|
61
|
-
"@vitest/ui": "^3.2.4",
|
|
62
|
-
"baseline-browser-mapping": "^2.10.7",
|
|
63
|
-
"cross-env": "^10.1.0",
|
|
64
|
-
"eslint": "^9.30.1",
|
|
65
|
-
"eslint-plugin-react-hooks": "^5.2.0",
|
|
66
|
-
"eslint-plugin-react-refresh": "^0.4.20",
|
|
67
|
-
"globals": "^16.3.0",
|
|
68
|
-
"jsdom": "^26.1.0",
|
|
69
|
-
"rollup": "4.20.0",
|
|
70
|
-
"tailwindcss": "^4.1.13",
|
|
71
|
-
"typescript": "^5.8.3",
|
|
72
|
-
"typescript-eslint": "^8.35.1",
|
|
73
|
-
"vite-plugin-checker": "^0.12.0",
|
|
74
|
-
"vite-tsconfig-paths": "^6.0.5",
|
|
75
|
-
"vitest": "^3.2.4"
|
|
76
|
-
},
|
|
77
|
-
"dependencies": {
|
|
78
|
-
"@elysiajs/eden": "^1.3.2",
|
|
79
|
-
"@elysiajs/swagger": "^1.3.1",
|
|
80
|
-
"@fluxstack/config": "^1.0.0",
|
|
81
|
-
"@fluxstack/live": "^
|
|
82
|
-
"@fluxstack/live-client": "^0.
|
|
83
|
-
"@fluxstack/live-elysia": "^0.
|
|
84
|
-
"@fluxstack/live-react": "^0.
|
|
85
|
-
"@fluxstack/plugin-
|
|
86
|
-
"@fluxstack/plugin-
|
|
87
|
-
"@fluxstack/plugin-
|
|
88
|
-
"@
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"react
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "create-fluxstack",
|
|
3
|
+
"version": "1.22.0",
|
|
4
|
+
"description": "⚡ Revolutionary full-stack TypeScript framework with Declarative Config System, Elysia + React + Bun",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"framework",
|
|
7
|
+
"full-stack",
|
|
8
|
+
"typescript",
|
|
9
|
+
"elysia",
|
|
10
|
+
"react",
|
|
11
|
+
"bun",
|
|
12
|
+
"vite"
|
|
13
|
+
],
|
|
14
|
+
"author": "FluxStack Team",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://github.com/MarcosBrendonDePaula/FluxStack",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/MarcosBrendonDePaula/FluxStack.git"
|
|
20
|
+
},
|
|
21
|
+
"module": "app/server/index.ts",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"create-fluxstack": "create-fluxstack.ts"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "bun run core/cli/index.ts dev",
|
|
28
|
+
"dev:frontend": "bun run core/cli/index.ts dev --frontend-only",
|
|
29
|
+
"dev:backend": "bun run core/cli/index.ts dev --backend-only",
|
|
30
|
+
"build": "cross-env NODE_ENV=production bun run core/cli/index.ts build",
|
|
31
|
+
"build:frontend": "cross-env NODE_ENV=production bun run core/cli/index.ts build --frontend-only",
|
|
32
|
+
"build:backend": "cross-env NODE_ENV=production bun run core/cli/index.ts build --backend-only",
|
|
33
|
+
"build:exe": "cross-env NODE_ENV=production && bun run core/cli/index.ts build && bun run core/cli/index.ts build:exe",
|
|
34
|
+
"start": "NODE_ENV=production bun dist/index.js",
|
|
35
|
+
"create": "bun run core/cli/index.ts create",
|
|
36
|
+
"cli": "bun run core/cli/index.ts",
|
|
37
|
+
"make:component": "bun run core/cli/index.ts make:component",
|
|
38
|
+
"sync-version": "bun run core/utils/sync-version.ts",
|
|
39
|
+
"test": "vitest",
|
|
40
|
+
"test:ui": "vitest --ui",
|
|
41
|
+
"test:coverage": "vitest run --coverage",
|
|
42
|
+
"typecheck:api": "tsc --noEmit -p tsconfig.api-strict.json",
|
|
43
|
+
"test:e2e": "playwright test",
|
|
44
|
+
"test:e2e:ui": "playwright test --ui",
|
|
45
|
+
"test:e2e:headed": "playwright test --headed"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^9.30.1",
|
|
49
|
+
"@noble/curves": "1.2.0",
|
|
50
|
+
"@noble/hashes": "1.3.2",
|
|
51
|
+
"@playwright/test": "^1.58.2",
|
|
52
|
+
"@tailwindcss/vite": "^4.1.13",
|
|
53
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
54
|
+
"@testing-library/react": "^16.3.0",
|
|
55
|
+
"@testing-library/user-event": "^14.6.1",
|
|
56
|
+
"@types/bun": "latest",
|
|
57
|
+
"@types/node": "^24.5.2",
|
|
58
|
+
"@types/react": "^19.1.8",
|
|
59
|
+
"@types/react-dom": "^19.1.6",
|
|
60
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
61
|
+
"@vitest/ui": "^3.2.4",
|
|
62
|
+
"baseline-browser-mapping": "^2.10.7",
|
|
63
|
+
"cross-env": "^10.1.0",
|
|
64
|
+
"eslint": "^9.30.1",
|
|
65
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
66
|
+
"eslint-plugin-react-refresh": "^0.4.20",
|
|
67
|
+
"globals": "^16.3.0",
|
|
68
|
+
"jsdom": "^26.1.0",
|
|
69
|
+
"rollup": "4.20.0",
|
|
70
|
+
"tailwindcss": "^4.1.13",
|
|
71
|
+
"typescript": "^5.8.3",
|
|
72
|
+
"typescript-eslint": "^8.35.1",
|
|
73
|
+
"vite-plugin-checker": "^0.12.0",
|
|
74
|
+
"vite-tsconfig-paths": "^6.0.5",
|
|
75
|
+
"vitest": "^3.2.4"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@elysiajs/eden": "^1.3.2",
|
|
79
|
+
"@elysiajs/swagger": "^1.3.1",
|
|
80
|
+
"@fluxstack/config": "^1.0.0",
|
|
81
|
+
"@fluxstack/live": "^0.10.0",
|
|
82
|
+
"@fluxstack/live-client": "^0.10.0",
|
|
83
|
+
"@fluxstack/live-elysia": "^0.10.0",
|
|
84
|
+
"@fluxstack/live-react": "^0.10.0",
|
|
85
|
+
"@fluxstack/plugin-crypto-auth": "^1.0.0",
|
|
86
|
+
"@fluxstack/plugin-csrf-protection": "^1.2.0",
|
|
87
|
+
"@fluxstack/plugin-kit": "^0.4.0",
|
|
88
|
+
"@types/prompts": "^2.4.9",
|
|
89
|
+
"@vitejs/plugin-react": "^4.6.0",
|
|
90
|
+
"@vitejs/plugin-rsc": "^0.5.26",
|
|
91
|
+
"chalk": "^5.3.0",
|
|
92
|
+
"commander": "^12.1.0",
|
|
93
|
+
"elysia": "^1.4.6",
|
|
94
|
+
"lightningcss": "^1.30.1",
|
|
95
|
+
"ora": "^8.1.0",
|
|
96
|
+
"prompts": "^2.4.2",
|
|
97
|
+
"react": "^19.1.0",
|
|
98
|
+
"react-dom": "^19.1.0",
|
|
99
|
+
"react-icons": "^5.5.0",
|
|
100
|
+
"react-router": "^7.9.3",
|
|
101
|
+
"react-server-dom-webpack": "^19.2.6",
|
|
102
|
+
"uuid": "^13.0.0",
|
|
103
|
+
"vite": "^7.1.7",
|
|
104
|
+
"winston": "^3.18.3",
|
|
105
|
+
"winston-daily-rotate-file": "^5.0.0",
|
|
106
|
+
"zustand": "^5.0.8"
|
|
107
|
+
},
|
|
108
|
+
"engines": {
|
|
109
|
+
"bun": ">=1.2.0"
|
|
110
|
+
},
|
|
111
|
+
"preferredPackageManager": "bun"
|
|
112
|
+
}
|