@fluixi/core 1.0.0-alpha.53
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/LICENSE +21 -0
- package/README.md +611 -0
- package/dist/cdn/core.global.js +3 -0
- package/dist/cli/generate.cjs +1814 -0
- package/dist/cli/generate.d.ts +7 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +2580 -0
- package/dist/cli/generate.mjs +1814 -0
- package/dist/cli/index.cjs +1814 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2580 -0
- package/dist/cli/index.mjs +1814 -0
- package/dist/cli/run.cjs +2 -0
- package/dist/cli/run.d.ts +3 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +62 -0
- package/dist/cli/run.mjs +2 -0
- package/dist/fluixi.d.ts +5 -0
- package/dist/src/cdn/chunk-PFM5O5UE.mjs +1 -0
- package/dist/src/cdn/core.cjs +1 -0
- package/dist/src/cdn/core.d.ts +16 -0
- package/dist/src/cdn/core.d.ts.map +1 -0
- package/dist/src/cdn/core.js +15 -0
- package/dist/src/cdn/core.mjs +1 -0
- package/dist/src/cdn/router-next-WNWCFVC2.mjs +1 -0
- package/dist/src/index.cjs +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +26 -0
- package/dist/src/index.mjs +1 -0
- package/dist/src/jsx-dev-runtime.cjs +1 -0
- package/dist/src/jsx-dev-runtime.d.ts +2 -0
- package/dist/src/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/src/jsx-dev-runtime.js +1 -0
- package/dist/src/jsx-dev-runtime.mjs +1 -0
- package/dist/src/jsx-runtime.cjs +1 -0
- package/dist/src/jsx-runtime.d.ts +7 -0
- package/dist/src/jsx-runtime.d.ts.map +1 -0
- package/dist/src/jsx-runtime.js +17 -0
- package/dist/src/jsx-runtime.mjs +1 -0
- package/dist/src/lib/client/chunk-D3VTWVNJ.mjs +1 -0
- package/dist/src/lib/client/index.cjs +1 -0
- package/dist/src/lib/client/index.d.ts +54 -0
- package/dist/src/lib/client/index.d.ts.map +1 -0
- package/dist/src/lib/client/index.js +142 -0
- package/dist/src/lib/client/index.mjs +1 -0
- package/dist/src/lib/client/router-next-UYIYMRJL.mjs +1 -0
- package/dist/src/lib/context/context.cjs +1 -0
- package/dist/src/lib/context/context.d.ts +337 -0
- package/dist/src/lib/context/context.d.ts.map +1 -0
- package/dist/src/lib/context/context.js +462 -0
- package/dist/src/lib/context/context.mjs +1 -0
- package/dist/src/lib/context/index.cjs +1 -0
- package/dist/src/lib/context/index.d.ts +3 -0
- package/dist/src/lib/context/index.d.ts.map +1 -0
- package/dist/src/lib/context/index.js +5 -0
- package/dist/src/lib/context/index.mjs +1 -0
- package/dist/src/lib/control-flow.cjs +1 -0
- package/dist/src/lib/control-flow.d.ts +4 -0
- package/dist/src/lib/control-flow.d.ts.map +1 -0
- package/dist/src/lib/control-flow.js +1 -0
- package/dist/src/lib/control-flow.mjs +1 -0
- package/dist/src/lib/core.cjs +1 -0
- package/dist/src/lib/core.d.ts +90 -0
- package/dist/src/lib/core.d.ts.map +1 -0
- package/dist/src/lib/core.js +181 -0
- package/dist/src/lib/core.mjs +1 -0
- package/dist/src/lib/directives/lit.cjs +1 -0
- package/dist/src/lib/directives/lit.d.ts +2 -0
- package/dist/src/lib/directives/lit.d.ts.map +1 -0
- package/dist/src/lib/directives/lit.js +31 -0
- package/dist/src/lib/directives/lit.mjs +0 -0
- package/dist/src/lib/env/index.cjs +1 -0
- package/dist/src/lib/env/index.d.ts +39 -0
- package/dist/src/lib/env/index.d.ts.map +1 -0
- package/dist/src/lib/env/index.js +50 -0
- package/dist/src/lib/env/index.mjs +1 -0
- package/dist/src/lib/i18n/index.cjs +1 -0
- package/dist/src/lib/i18n/index.d.ts +79 -0
- package/dist/src/lib/i18n/index.d.ts.map +1 -0
- package/dist/src/lib/i18n/index.js +127 -0
- package/dist/src/lib/i18n/index.mjs +1 -0
- package/dist/src/lib/index.cjs +1 -0
- package/dist/src/lib/index.d.ts +83 -0
- package/dist/src/lib/index.d.ts.map +1 -0
- package/dist/src/lib/index.js +147 -0
- package/dist/src/lib/index.mjs +1 -0
- package/dist/src/lib/isomorphic.cjs +1 -0
- package/dist/src/lib/isomorphic.d.ts +69 -0
- package/dist/src/lib/isomorphic.d.ts.map +1 -0
- package/dist/src/lib/isomorphic.js +92 -0
- package/dist/src/lib/isomorphic.mjs +1 -0
- package/dist/src/lib/jsx-runtime/index.cjs +1 -0
- package/dist/src/lib/jsx-runtime/index.d.ts +13 -0
- package/dist/src/lib/jsx-runtime/index.d.ts.map +1 -0
- package/dist/src/lib/jsx-runtime/index.js +23 -0
- package/dist/src/lib/jsx-runtime/index.mjs +1 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.cjs +1 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.d.ts +2 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.js +1 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.mjs +1 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.cjs +13 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.d.ts +21 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.d.ts.map +1 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.js +102 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.mjs +13 -0
- package/dist/src/lib/plugins/index.cjs +13 -0
- package/dist/src/lib/plugins/index.d.ts +4 -0
- package/dist/src/lib/plugins/index.d.ts.map +1 -0
- package/dist/src/lib/plugins/index.js +3 -0
- package/dist/src/lib/plugins/index.mjs +13 -0
- package/dist/src/lib/plugins/route-codegen.cjs +8 -0
- package/dist/src/lib/plugins/route-codegen.d.ts +13 -0
- package/dist/src/lib/plugins/route-codegen.d.ts.map +1 -0
- package/dist/src/lib/plugins/route-codegen.js +81 -0
- package/dist/src/lib/plugins/route-codegen.mjs +8 -0
- package/dist/src/lib/plugins/route-scanner.cjs +1 -0
- package/dist/src/lib/plugins/route-scanner.d.ts +39 -0
- package/dist/src/lib/plugins/route-scanner.d.ts.map +1 -0
- package/dist/src/lib/plugins/route-scanner.js +216 -0
- package/dist/src/lib/plugins/route-scanner.mjs +1 -0
- package/dist/src/lib/plugins/vite.cjs +1 -0
- package/dist/src/lib/plugins/vite.d.ts +3 -0
- package/dist/src/lib/plugins/vite.d.ts.map +1 -0
- package/dist/src/lib/plugins/vite.js +1 -0
- package/dist/src/lib/plugins/vite.mjs +1 -0
- package/dist/src/lib/render/component.cjs +1 -0
- package/dist/src/lib/render/component.d.ts +328 -0
- package/dist/src/lib/render/component.d.ts.map +1 -0
- package/dist/src/lib/render/component.js +520 -0
- package/dist/src/lib/render/component.mjs +1 -0
- package/dist/src/lib/render/index.cjs +1 -0
- package/dist/src/lib/render/index.d.ts +113 -0
- package/dist/src/lib/render/index.d.ts.map +1 -0
- package/dist/src/lib/render/index.js +201 -0
- package/dist/src/lib/render/index.mjs +1 -0
- package/dist/src/lib/render/lazy.cjs +1 -0
- package/dist/src/lib/render/lazy.d.ts +28 -0
- package/dist/src/lib/render/lazy.d.ts.map +1 -0
- package/dist/src/lib/render/lazy.js +62 -0
- package/dist/src/lib/render/lazy.mjs +1 -0
- package/dist/src/lib/render/suspense.cjs +1 -0
- package/dist/src/lib/render/suspense.d.ts +25 -0
- package/dist/src/lib/render/suspense.d.ts.map +1 -0
- package/dist/src/lib/render/suspense.js +139 -0
- package/dist/src/lib/render/suspense.mjs +1 -0
- package/dist/src/lib/router-next/data.cjs +1 -0
- package/dist/src/lib/router-next/data.d.ts +101 -0
- package/dist/src/lib/router-next/data.d.ts.map +1 -0
- package/dist/src/lib/router-next/data.js +179 -0
- package/dist/src/lib/router-next/data.mjs +1 -0
- package/dist/src/lib/router-next/index.cjs +1 -0
- package/dist/src/lib/router-next/index.d.ts +156 -0
- package/dist/src/lib/router-next/index.d.ts.map +1 -0
- package/dist/src/lib/router-next/index.js +464 -0
- package/dist/src/lib/router-next/index.mjs +1 -0
- package/dist/src/lib/server/hydration.cjs +1 -0
- package/dist/src/lib/server/hydration.d.ts +238 -0
- package/dist/src/lib/server/hydration.d.ts.map +1 -0
- package/dist/src/lib/server/hydration.js +384 -0
- package/dist/src/lib/server/hydration.mjs +1 -0
- package/dist/src/lib/server/index.cjs +16 -0
- package/dist/src/lib/server/index.d.ts +91 -0
- package/dist/src/lib/server/index.d.ts.map +1 -0
- package/dist/src/lib/server/index.js +141 -0
- package/dist/src/lib/server/index.mjs +16 -0
- package/dist/src/lib/server/reactive.cjs +1 -0
- package/dist/src/lib/server/reactive.d.ts +298 -0
- package/dist/src/lib/server/reactive.d.ts.map +1 -0
- package/dist/src/lib/server/reactive.js +472 -0
- package/dist/src/lib/server/reactive.mjs +1 -0
- package/dist/src/lib/theme/index.cjs +1 -0
- package/dist/src/lib/theme/index.d.ts +51 -0
- package/dist/src/lib/theme/index.d.ts.map +1 -0
- package/dist/src/lib/theme/index.js +79 -0
- package/dist/src/lib/theme/index.mjs +1 -0
- package/dist/src/lib/utils/index.cjs +1 -0
- package/dist/src/lib/utils/index.d.ts +379 -0
- package/dist/src/lib/utils/index.d.ts.map +1 -0
- package/dist/src/lib/utils/index.js +527 -0
- package/dist/src/lib/utils/index.mjs +1 -0
- package/dist/src/rx.cjs +1 -0
- package/dist/src/rx.d.ts +2 -0
- package/dist/src/rx.d.ts.map +1 -0
- package/dist/src/rx.js +101 -0
- package/dist/src/rx.mjs +0 -0
- package/dist/src/server.cjs +16 -0
- package/dist/src/server.d.ts +64 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +63 -0
- package/dist/src/server.mjs +16 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +128 -0
- package/src/index.ts +31 -0
- package/types/fluixi.d.ts +5 -0
- package/types/vinxi.d.ts +50 -0
|
@@ -0,0 +1,2580 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Fluixi CLI - Command-line interface for project and component generation
|
|
4
|
+
* @module @fluixi/core/cli
|
|
5
|
+
*/
|
|
6
|
+
import { program } from 'commander';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
// const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
// const __dirname = path.dirname(__filename);
|
|
13
|
+
/**
|
|
14
|
+
* CLI version
|
|
15
|
+
*/
|
|
16
|
+
const VERSION = '0.0.1';
|
|
17
|
+
/**
|
|
18
|
+
* Generates a basic component template
|
|
19
|
+
* @param {string} name - Component name
|
|
20
|
+
* @returns {string} Component code
|
|
21
|
+
*/
|
|
22
|
+
function generateBasicComponent(name, pattern = 'functional') {
|
|
23
|
+
if (pattern === 'fluixilement') {
|
|
24
|
+
return `/**
|
|
25
|
+
* @fileoverview ${name} component
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { html } from 'lit';
|
|
29
|
+
import { customElement } from 'lit/decorators.js';
|
|
30
|
+
import { FluixiElement } from '@fluixi/core';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ${name} component using FluixiElement pattern
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* \`\`\`html
|
|
37
|
+
* <${name.toLowerCase()}></${name.toLowerCase()}>
|
|
38
|
+
* \`\`\`
|
|
39
|
+
*/
|
|
40
|
+
@customElement('${name.toLowerCase()}')
|
|
41
|
+
export class ${name} extends FluixiElement {
|
|
42
|
+
view() {
|
|
43
|
+
return html\`
|
|
44
|
+
<div class="${name.toLowerCase()}">
|
|
45
|
+
<h2>${name}</h2>
|
|
46
|
+
<p>Component created with Fluixi CLI (FluixiElement pattern)</p>
|
|
47
|
+
</div>
|
|
48
|
+
\`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default ${name};
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
if (pattern === 'withsignals') {
|
|
56
|
+
return `/**
|
|
57
|
+
* @fileoverview ${name} component
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import { html, LitElement } from 'lit';
|
|
61
|
+
import { customElement } from 'lit/decorators.js';
|
|
62
|
+
import { WithSignals } from '@fluixi/core';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* ${name} component using WithSignals mixin
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* \`\`\`html
|
|
69
|
+
* <${name.toLowerCase()}></${name.toLowerCase()}>
|
|
70
|
+
* \`\`\`
|
|
71
|
+
*/
|
|
72
|
+
@customElement('${name.toLowerCase()}')
|
|
73
|
+
export class ${name} extends WithSignals(LitElement) {
|
|
74
|
+
render() {
|
|
75
|
+
return html\`
|
|
76
|
+
<div class="${name.toLowerCase()}">
|
|
77
|
+
<h2>${name}</h2>
|
|
78
|
+
<p>Component created with Fluixi CLI (WithSignals pattern)</p>
|
|
79
|
+
</div>
|
|
80
|
+
\`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default ${name};
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
if (pattern === 'controller') {
|
|
88
|
+
return `/**
|
|
89
|
+
* @fileoverview ${name} component
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
import { html, LitElement } from 'lit';
|
|
93
|
+
import { customElement } from 'lit/decorators.js';
|
|
94
|
+
import { SignalController } from '@fluixi/core';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* ${name} component using manual controller pattern
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* \`\`\`html
|
|
101
|
+
* <${name.toLowerCase()}></${name.toLowerCase()}>
|
|
102
|
+
* \`\`\`
|
|
103
|
+
*/
|
|
104
|
+
@customElement('${name.toLowerCase()}')
|
|
105
|
+
export class ${name} extends LitElement {
|
|
106
|
+
private signals = new SignalController(this);
|
|
107
|
+
|
|
108
|
+
render() {
|
|
109
|
+
return html\`
|
|
110
|
+
<div class="${name.toLowerCase()}">
|
|
111
|
+
<h2>${name}</h2>
|
|
112
|
+
<p>Component created with Fluixi CLI (Controller pattern)</p>
|
|
113
|
+
</div>
|
|
114
|
+
\`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default ${name};
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
if (pattern === 'litelement') {
|
|
122
|
+
return `/**
|
|
123
|
+
* @fileoverview ${name} component
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
import { html, css, LitElement } from 'lit';
|
|
127
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* ${name} component using plain LitElement
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* \`\`\`html
|
|
134
|
+
* <${name.toLowerCase()}></${name.toLowerCase()}>
|
|
135
|
+
* \`\`\`
|
|
136
|
+
*/
|
|
137
|
+
@customElement('${name.toLowerCase()}')
|
|
138
|
+
export class ${name} extends LitElement {
|
|
139
|
+
static styles = css\`
|
|
140
|
+
:host {
|
|
141
|
+
display: block;
|
|
142
|
+
padding: 1rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.${name.toLowerCase()} {
|
|
146
|
+
font-family: sans-serif;
|
|
147
|
+
}
|
|
148
|
+
\`;
|
|
149
|
+
|
|
150
|
+
render() {
|
|
151
|
+
return html\`
|
|
152
|
+
<div class="${name.toLowerCase()}">
|
|
153
|
+
<h2>${name}</h2>
|
|
154
|
+
<p>Component created with Fluixi CLI (LitElement pattern)</p>
|
|
155
|
+
</div>
|
|
156
|
+
\`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default ${name};
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
// Functional pattern (default)
|
|
164
|
+
return `/**
|
|
165
|
+
* @fileoverview ${name} component
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
import { html, Component, ComponentProps } from '@fluixi/core';
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Props for ${name} component
|
|
172
|
+
*/
|
|
173
|
+
export interface ${name}Props extends ComponentProps {
|
|
174
|
+
// Add your props here
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* ${name} component (functional pattern)
|
|
179
|
+
* @param {${name}Props} props - Component props
|
|
180
|
+
* @returns {TemplateResult} Rendered template
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* \`\`\`typescript
|
|
184
|
+
* const MyPage = () => {
|
|
185
|
+
* return html\`
|
|
186
|
+
* <div>
|
|
187
|
+
* \${${name}({})}
|
|
188
|
+
* </div>
|
|
189
|
+
* \`;
|
|
190
|
+
* };
|
|
191
|
+
* \`\`\`
|
|
192
|
+
*/
|
|
193
|
+
export const ${name}: Component<${name}Props> = (props) => {
|
|
194
|
+
return html\`
|
|
195
|
+
<div class="${name.toLowerCase()}">
|
|
196
|
+
<h2>${name}</h2>
|
|
197
|
+
<p>Component created with Fluixi CLI</p>
|
|
198
|
+
</div>
|
|
199
|
+
\`;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export default ${name};
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Generates a component with state
|
|
207
|
+
* @param {string} name - Component name
|
|
208
|
+
* @param {ComponentPattern} pattern - Component pattern to use
|
|
209
|
+
* @returns {string} Component code
|
|
210
|
+
*/
|
|
211
|
+
function generateComponentWithState(name, pattern = 'functional') {
|
|
212
|
+
if (pattern === 'litelement') {
|
|
213
|
+
return `/**
|
|
214
|
+
* @fileoverview ${name} component with state
|
|
215
|
+
*/
|
|
216
|
+
|
|
217
|
+
import { html, css, LitElement } from 'lit';
|
|
218
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* ${name} component with state using plain LitElement
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* \`\`\`html
|
|
225
|
+
* <${name.toLowerCase()}></${name.toLowerCase()}>
|
|
226
|
+
* \`\`\`
|
|
227
|
+
*/
|
|
228
|
+
@customElement('${name.toLowerCase()}')
|
|
229
|
+
export class ${name} extends LitElement {
|
|
230
|
+
static styles = css\`
|
|
231
|
+
:host {
|
|
232
|
+
display: block;
|
|
233
|
+
padding: 1rem;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.${name.toLowerCase()} {
|
|
237
|
+
font-family: sans-serif;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.counter {
|
|
241
|
+
display: flex;
|
|
242
|
+
gap: 1rem;
|
|
243
|
+
align-items: center;
|
|
244
|
+
margin: 1rem 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
button {
|
|
248
|
+
padding: 0.5rem 1rem;
|
|
249
|
+
font-size: 1rem;
|
|
250
|
+
cursor: pointer;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.count {
|
|
254
|
+
font-size: 2rem;
|
|
255
|
+
font-weight: bold;
|
|
256
|
+
min-width: 3rem;
|
|
257
|
+
text-align: center;
|
|
258
|
+
}
|
|
259
|
+
\`;
|
|
260
|
+
|
|
261
|
+
@state()
|
|
262
|
+
private count = 0;
|
|
263
|
+
|
|
264
|
+
@state()
|
|
265
|
+
private message = '';
|
|
266
|
+
|
|
267
|
+
connectedCallback() {
|
|
268
|
+
super.connectedCallback();
|
|
269
|
+
this.updateMessage();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private updateMessage() {
|
|
273
|
+
this.message = \`Count is \${this.count}\`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private increment() {
|
|
277
|
+
this.count++;
|
|
278
|
+
this.updateMessage();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private decrement() {
|
|
282
|
+
this.count--;
|
|
283
|
+
this.updateMessage();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private reset() {
|
|
287
|
+
this.count = 0;
|
|
288
|
+
this.updateMessage();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
render() {
|
|
292
|
+
return html\`
|
|
293
|
+
<div class="${name.toLowerCase()}">
|
|
294
|
+
<h2>${name} Counter</h2>
|
|
295
|
+
<p>\${this.message}</p>
|
|
296
|
+
<div class="counter">
|
|
297
|
+
<button @click=\${this.decrement}>-</button>
|
|
298
|
+
<span class="count">\${this.count}</span>
|
|
299
|
+
<button @click=\${this.increment}>+</button>
|
|
300
|
+
</div>
|
|
301
|
+
<button @click=\${this.reset}>Reset</button>
|
|
302
|
+
</div>
|
|
303
|
+
\`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export default ${name};
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
if (pattern === 'fluixilement') {
|
|
311
|
+
return `/**
|
|
312
|
+
* @fileoverview ${name} component with reactive state
|
|
313
|
+
*/
|
|
314
|
+
|
|
315
|
+
import { html } from 'lit';
|
|
316
|
+
import { customElement } from 'lit/decorators.js';
|
|
317
|
+
import { FluixiElement } from '@fluixi/core';
|
|
318
|
+
import { createSignal, createEffect } from '@fluixi/reactive';
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* ${name} component with reactive state using FluixiElement
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* \`\`\`html
|
|
325
|
+
* <${name.toLowerCase()} initial-value="10"></${name.toLowerCase()}>
|
|
326
|
+
* \`\`\`
|
|
327
|
+
*/
|
|
328
|
+
@customElement('${name.toLowerCase()}')
|
|
329
|
+
export class ${name} extends FluixiElement {
|
|
330
|
+
private count = createSignal(0);
|
|
331
|
+
private message = createSignal('');
|
|
332
|
+
|
|
333
|
+
connectedCallback() {
|
|
334
|
+
super.connectedCallback();
|
|
335
|
+
|
|
336
|
+
// Effect to update message when count changes
|
|
337
|
+
this.runInScope(() => {
|
|
338
|
+
createEffect(() => {
|
|
339
|
+
const current = this.count[0]();
|
|
340
|
+
this.message[1](\`Count is \${current}\`);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private increment = () => this.count[1](c => c + 1);
|
|
346
|
+
private decrement = () => this.count[1](c => c - 1);
|
|
347
|
+
private reset = () => this.count[1](0);
|
|
348
|
+
|
|
349
|
+
view() {
|
|
350
|
+
return html\`
|
|
351
|
+
<div class="${name.toLowerCase()}">
|
|
352
|
+
<h2>${name}</h2>
|
|
353
|
+
<div class="counter">
|
|
354
|
+
<p class="count">\${this.count[0]()}</p>
|
|
355
|
+
<p class="message">\${this.message[0]()}</p>
|
|
356
|
+
<div class="controls">
|
|
357
|
+
<button @click=\${this.decrement}>-</button>
|
|
358
|
+
<button @click=\${this.reset}>Reset</button>
|
|
359
|
+
<button @click=\${this.increment}>+</button>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
\`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export default ${name};
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
370
|
+
if (pattern === 'withsignals') {
|
|
371
|
+
return `/**
|
|
372
|
+
* @fileoverview ${name} component with reactive state
|
|
373
|
+
*/
|
|
374
|
+
|
|
375
|
+
import { html, LitElement } from 'lit';
|
|
376
|
+
import { customElement } from 'lit/decorators.js';
|
|
377
|
+
import { WithSignals } from '@fluixi/core';
|
|
378
|
+
import { createSignal, createEffect } from '@fluixi/reactive';
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* ${name} component with reactive state using WithSignals mixin
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* \`\`\`html
|
|
385
|
+
* <${name.toLowerCase()}></${name.toLowerCase()}>
|
|
386
|
+
* \`\`\`
|
|
387
|
+
*/
|
|
388
|
+
@customElement('${name.toLowerCase()}')
|
|
389
|
+
export class ${name} extends WithSignals(LitElement) {
|
|
390
|
+
private count = createSignal(0);
|
|
391
|
+
private message = createSignal('');
|
|
392
|
+
|
|
393
|
+
connectedCallback() {
|
|
394
|
+
super.connectedCallback();
|
|
395
|
+
|
|
396
|
+
// Effect to update message when count changes
|
|
397
|
+
this.runInReactiveScope(() => {
|
|
398
|
+
createEffect(() => {
|
|
399
|
+
const current = this.count[0]();
|
|
400
|
+
this.message[1](\`Count is \${current}\`);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private increment = () => this.count[1](c => c + 1);
|
|
406
|
+
private decrement = () => this.count[1](c => c - 1);
|
|
407
|
+
private reset = () => this.count[1](0);
|
|
408
|
+
|
|
409
|
+
render() {
|
|
410
|
+
return html\`
|
|
411
|
+
<div class="${name.toLowerCase()}">
|
|
412
|
+
<h2>${name}</h2>
|
|
413
|
+
<div class="counter">
|
|
414
|
+
<p class="count">\${this.count[0]()}</p>
|
|
415
|
+
<p class="message">\${this.message[0]()}</p>
|
|
416
|
+
<div class="controls">
|
|
417
|
+
<button @click=\${this.decrement}>-</button>
|
|
418
|
+
<button @click=\${this.reset}>Reset</button>
|
|
419
|
+
<button @click=\${this.increment}>+</button>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
\`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export default ${name};
|
|
428
|
+
`;
|
|
429
|
+
}
|
|
430
|
+
if (pattern === 'controller') {
|
|
431
|
+
return `/**
|
|
432
|
+
* @fileoverview ${name} component with reactive state
|
|
433
|
+
*/
|
|
434
|
+
|
|
435
|
+
import { html, LitElement } from 'lit';
|
|
436
|
+
import { customElement } from 'lit/decorators.js';
|
|
437
|
+
import { SignalController } from '@fluixi/core';
|
|
438
|
+
import { createSignal, createEffect } from '@fluixi/reactive';
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* ${name} component with reactive state using manual controller
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* \`\`\`html
|
|
445
|
+
* <${name.toLowerCase()}></${name.toLowerCase()}>
|
|
446
|
+
* \`\`\`
|
|
447
|
+
*/
|
|
448
|
+
@customElement('${name.toLowerCase()}')
|
|
449
|
+
export class ${name} extends LitElement {
|
|
450
|
+
private signals = new SignalController(this);
|
|
451
|
+
private count = createSignal(0);
|
|
452
|
+
private message = createSignal('');
|
|
453
|
+
|
|
454
|
+
connectedCallback() {
|
|
455
|
+
super.connectedCallback();
|
|
456
|
+
|
|
457
|
+
// Effect to update message when count changes
|
|
458
|
+
this.signals.runInScope(() => {
|
|
459
|
+
createEffect(() => {
|
|
460
|
+
const current = this.count[0]();
|
|
461
|
+
this.message[1](\`Count is \${current}\`);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private increment = () => this.count[1](c => c + 1);
|
|
467
|
+
private decrement = () => this.count[1](c => c - 1);
|
|
468
|
+
private reset = () => this.count[1](0);
|
|
469
|
+
|
|
470
|
+
render() {
|
|
471
|
+
return html\`
|
|
472
|
+
<div class="${name.toLowerCase()}">
|
|
473
|
+
<h2>${name}</h2>
|
|
474
|
+
<div class="counter">
|
|
475
|
+
<p class="count">\${this.count[0]()}</p>
|
|
476
|
+
<p class="message">\${this.message[0]()}</p>
|
|
477
|
+
<div class="controls">
|
|
478
|
+
<button @click=\${this.decrement}>-</button>
|
|
479
|
+
<button @click=\${this.reset}>Reset</button>
|
|
480
|
+
<button @click=\${this.increment}>+</button>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
\`;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export default ${name};
|
|
489
|
+
`;
|
|
490
|
+
}
|
|
491
|
+
// Functional pattern (default)
|
|
492
|
+
return `/**
|
|
493
|
+
* @fileoverview ${name} component with reactive state
|
|
494
|
+
*/
|
|
495
|
+
|
|
496
|
+
import { html, Component, ComponentProps } from '@fluixi/core';
|
|
497
|
+
import { createSignal, createEffect } from '@fluixi/reactive';
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Props for ${name} component
|
|
501
|
+
*/
|
|
502
|
+
export interface ${name}Props extends ComponentProps {
|
|
503
|
+
initialValue?: number;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* ${name} component with reactive state management
|
|
508
|
+
* @param {${name}Props} props - Component props
|
|
509
|
+
* @returns {TemplateResult} Rendered template
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* \`\`\`typescript
|
|
513
|
+
* const MyPage = () => {
|
|
514
|
+
* return html\`
|
|
515
|
+
* <div>
|
|
516
|
+
* \${${name}({ initialValue: 10 })}
|
|
517
|
+
* </div>
|
|
518
|
+
* \`;
|
|
519
|
+
* };
|
|
520
|
+
* \`\`\`
|
|
521
|
+
*/
|
|
522
|
+
export const ${name}: Component<${name}Props> = (props) => {
|
|
523
|
+
const [count, setCount] = createSignal(props.initialValue ?? 0);
|
|
524
|
+
const [message, setMessage] = createSignal('');
|
|
525
|
+
|
|
526
|
+
// Effect to update message when count changes
|
|
527
|
+
createEffect(() => {
|
|
528
|
+
const current = count();
|
|
529
|
+
setMessage(\`Count is \${current}\`);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const increment = () => setCount(count() + 1);
|
|
533
|
+
const decrement = () => setCount(count() - 1);
|
|
534
|
+
const reset = () => setCount(props.initialValue ?? 0);
|
|
535
|
+
|
|
536
|
+
return html\`
|
|
537
|
+
<div class="${name.toLowerCase()}">
|
|
538
|
+
<h2>${name}</h2>
|
|
539
|
+
<div class="counter">
|
|
540
|
+
<p class="count">\${count()}</p>
|
|
541
|
+
<p class="message">\${message()}</p>
|
|
542
|
+
<div class="controls">
|
|
543
|
+
<button @click=\${decrement}>-</button>
|
|
544
|
+
<button @click=\${reset}>Reset</button>
|
|
545
|
+
<button @click=\${increment}>+</button>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
\`;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
export default ${name};
|
|
553
|
+
`;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Generates a component with async resource fetching
|
|
557
|
+
* @param {string} name - Component name
|
|
558
|
+
* @param {ComponentPattern} pattern - Component pattern to use
|
|
559
|
+
* @returns {string} Component code
|
|
560
|
+
*/
|
|
561
|
+
function generateComponentWithResource(name, pattern = 'functional') {
|
|
562
|
+
if (pattern === 'litelement') {
|
|
563
|
+
return `/**
|
|
564
|
+
* @fileoverview ${name} component with async resource fetching
|
|
565
|
+
*/
|
|
566
|
+
|
|
567
|
+
import { html, css, LitElement } from 'lit';
|
|
568
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
569
|
+
|
|
570
|
+
interface ${name}Data {
|
|
571
|
+
id: string;
|
|
572
|
+
title: string;
|
|
573
|
+
description: string;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* ${name} component with resource fetching using LitElement
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* \`\`\`html
|
|
581
|
+
* <${name.toLowerCase()} resource-id="123"></${name.toLowerCase()}>
|
|
582
|
+
* \`\`\`
|
|
583
|
+
*/
|
|
584
|
+
@customElement('${name.toLowerCase()}')
|
|
585
|
+
export class ${name} extends LitElement {
|
|
586
|
+
static styles = css\`
|
|
587
|
+
:host {
|
|
588
|
+
display: block;
|
|
589
|
+
padding: 1rem;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.loading, .error {
|
|
593
|
+
padding: 1rem;
|
|
594
|
+
margin: 1rem 0;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.error {
|
|
598
|
+
color: #dc3545;
|
|
599
|
+
background: #f8d7da;
|
|
600
|
+
border: 1px solid #f5c6cb;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
button {
|
|
604
|
+
padding: 0.5rem 1rem;
|
|
605
|
+
cursor: pointer;
|
|
606
|
+
}
|
|
607
|
+
\`;
|
|
608
|
+
|
|
609
|
+
@state()
|
|
610
|
+
private data: ${name}Data | null = null;
|
|
611
|
+
|
|
612
|
+
@state()
|
|
613
|
+
private loading = false;
|
|
614
|
+
|
|
615
|
+
@state()
|
|
616
|
+
private error: Error | null = null;
|
|
617
|
+
|
|
618
|
+
connectedCallback() {
|
|
619
|
+
super.connectedCallback();
|
|
620
|
+
this.fetchData();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private async fetchData() {
|
|
624
|
+
this.loading = true;
|
|
625
|
+
this.error = null;
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
|
|
629
|
+
if (!response.ok) throw new Error('Failed to fetch');
|
|
630
|
+
const json = await response.json();
|
|
631
|
+
this.data = {
|
|
632
|
+
id: json.id,
|
|
633
|
+
title: json.title,
|
|
634
|
+
description: json.body,
|
|
635
|
+
};
|
|
636
|
+
} catch (err) {
|
|
637
|
+
this.error = err as Error;
|
|
638
|
+
} finally {
|
|
639
|
+
this.loading = false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
render() {
|
|
644
|
+
return html\`
|
|
645
|
+
<div class="${name.toLowerCase()}">
|
|
646
|
+
<h2>${name}</h2>
|
|
647
|
+
|
|
648
|
+
\${this.loading ? html\`<div class="loading">Loading...</div>\` : ''}
|
|
649
|
+
|
|
650
|
+
\${this.error ? html\`
|
|
651
|
+
<div class="error">
|
|
652
|
+
<p>Error: \${this.error.message}</p>
|
|
653
|
+
<button @click=\${this.fetchData}>Retry</button>
|
|
654
|
+
</div>
|
|
655
|
+
\` : ''}
|
|
656
|
+
|
|
657
|
+
\${this.data && !this.loading && !this.error ? html\`
|
|
658
|
+
<div class="content">
|
|
659
|
+
<h3>\${this.data.title}</h3>
|
|
660
|
+
<p>\${this.data.description}</p>
|
|
661
|
+
<button @click=\${this.fetchData}>Refresh</button>
|
|
662
|
+
</div>
|
|
663
|
+
\` : ''}
|
|
664
|
+
</div>
|
|
665
|
+
\`;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export default ${name};
|
|
670
|
+
`;
|
|
671
|
+
}
|
|
672
|
+
return `/**
|
|
673
|
+
* @fileoverview ${name} component with async resource fetching
|
|
674
|
+
*/
|
|
675
|
+
|
|
676
|
+
import { html, Component, ComponentProps, createResource, Show } from '@fluixi/core';
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Props for ${name} component
|
|
680
|
+
*/
|
|
681
|
+
export interface ${name}Props extends ComponentProps {
|
|
682
|
+
resourceId?: string | number;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Data type returned by the resource
|
|
687
|
+
*/
|
|
688
|
+
interface ${name}Data {
|
|
689
|
+
id: string | number;
|
|
690
|
+
title: string;
|
|
691
|
+
description: string;
|
|
692
|
+
// Add more fields as needed
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Fetches data for the component
|
|
697
|
+
* @param {string | number} id - Resource ID
|
|
698
|
+
* @returns {Promise<${name}Data>} Fetched data
|
|
699
|
+
*/
|
|
700
|
+
async function fetch${name}Data(id: string | number): Promise<${name}Data> {
|
|
701
|
+
// Replace with your actual API call
|
|
702
|
+
const response = await fetch(\`/api/${name.toLowerCase()}/\${id}\`);
|
|
703
|
+
|
|
704
|
+
if (!response.ok) {
|
|
705
|
+
throw new Error('Failed to fetch data');
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return response.json();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* ${name} component with async resource fetching
|
|
713
|
+
* @param {${name}Props} props - Component props
|
|
714
|
+
* @returns {TemplateResult} Rendered template
|
|
715
|
+
*
|
|
716
|
+
* @example
|
|
717
|
+
* \`\`\`typescript
|
|
718
|
+
* const MyPage = () => {
|
|
719
|
+
* return html\`
|
|
720
|
+
* <div>
|
|
721
|
+
* \${${name}({ resourceId: '123' })}
|
|
722
|
+
* </div>
|
|
723
|
+
* \`;
|
|
724
|
+
* };
|
|
725
|
+
* \`\`\`
|
|
726
|
+
*/
|
|
727
|
+
export const ${name}: Component<${name}Props> = (props) => {
|
|
728
|
+
const [data, { refetch }] = createResource(
|
|
729
|
+
() => props.resourceId ?? '1',
|
|
730
|
+
fetch${name}Data,
|
|
731
|
+
{
|
|
732
|
+
onError: (error) => console.error('Failed to load ${name}:', error),
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
return html\`
|
|
737
|
+
<div class="${name.toLowerCase()}">
|
|
738
|
+
<h2>${name}</h2>
|
|
739
|
+
|
|
740
|
+
\${Show({
|
|
741
|
+
when: data.loading,
|
|
742
|
+
children: html\`<div class="loading">Loading...</div>\`,
|
|
743
|
+
})}
|
|
744
|
+
|
|
745
|
+
\${Show({
|
|
746
|
+
when: data.error,
|
|
747
|
+
children: html\`
|
|
748
|
+
<div class="error">
|
|
749
|
+
<p>Error: \${data.error?.message}</p>
|
|
750
|
+
<button @click=\${refetch}>Retry</button>
|
|
751
|
+
</div>
|
|
752
|
+
\`,
|
|
753
|
+
})}
|
|
754
|
+
|
|
755
|
+
\${Show({
|
|
756
|
+
when: () => data() && !data.loading && !data.error,
|
|
757
|
+
children: () => {
|
|
758
|
+
const resource = data()!;
|
|
759
|
+
return html\`
|
|
760
|
+
<div class="content">
|
|
761
|
+
<h3>\${resource.title}</h3>
|
|
762
|
+
<p>\${resource.description}</p>
|
|
763
|
+
<button @click=\${refetch}>Refresh</button>
|
|
764
|
+
</div>
|
|
765
|
+
\`;
|
|
766
|
+
},
|
|
767
|
+
})}
|
|
768
|
+
</div>
|
|
769
|
+
\`;
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
export default ${name};
|
|
773
|
+
`;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Generates a component with router integration
|
|
777
|
+
* @param {string} name - Component name
|
|
778
|
+
* @param {ComponentPattern} pattern - Component pattern to use
|
|
779
|
+
* @returns {string} Component code
|
|
780
|
+
*/
|
|
781
|
+
function generateComponentWithRouter(name, pattern = 'functional') {
|
|
782
|
+
if (pattern === 'litelement') {
|
|
783
|
+
return `/**
|
|
784
|
+
* @fileoverview ${name} component with router integration
|
|
785
|
+
*/
|
|
786
|
+
|
|
787
|
+
import { html, css, LitElement } from 'lit';
|
|
788
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* ${name} component with router integration using LitElement
|
|
792
|
+
*
|
|
793
|
+
* @example
|
|
794
|
+
* \`\`\`html
|
|
795
|
+
* <${name.toLowerCase()}></${name.toLowerCase()}>
|
|
796
|
+
* \`\`\`
|
|
797
|
+
*/
|
|
798
|
+
@customElement('${name.toLowerCase()}')
|
|
799
|
+
export class ${name} extends LitElement {
|
|
800
|
+
static styles = css\`
|
|
801
|
+
:host {
|
|
802
|
+
display: block;
|
|
803
|
+
padding: 1rem;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
nav {
|
|
807
|
+
display: flex;
|
|
808
|
+
gap: 1rem;
|
|
809
|
+
margin: 1rem 0;
|
|
810
|
+
padding: 1rem;
|
|
811
|
+
background: #f5f5f5;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
nav a {
|
|
815
|
+
padding: 0.5rem 1rem;
|
|
816
|
+
text-decoration: none;
|
|
817
|
+
color: #333;
|
|
818
|
+
background: white;
|
|
819
|
+
border-radius: 4px;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
nav a:hover {
|
|
823
|
+
background: #007bff;
|
|
824
|
+
color: white;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.route-info {
|
|
828
|
+
margin: 1rem 0;
|
|
829
|
+
padding: 1rem;
|
|
830
|
+
background: #e9ecef;
|
|
831
|
+
}
|
|
832
|
+
\`;
|
|
833
|
+
|
|
834
|
+
@state()
|
|
835
|
+
private currentPath = window.location.pathname;
|
|
836
|
+
|
|
837
|
+
connectedCallback() {
|
|
838
|
+
super.connectedCallback();
|
|
839
|
+
window.addEventListener('popstate', this.handlePopState);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
disconnectedCallback() {
|
|
843
|
+
super.disconnectedCallback();
|
|
844
|
+
window.removeEventListener('popstate', this.handlePopState);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private handlePopState = () => {
|
|
848
|
+
this.currentPath = window.location.pathname;
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
private navigate(path: string) {
|
|
852
|
+
window.history.pushState({}, '', path);
|
|
853
|
+
this.currentPath = path;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
render() {
|
|
857
|
+
return html\`
|
|
858
|
+
<div class="${name.toLowerCase()}">
|
|
859
|
+
<h2>${name} - Router Component</h2>
|
|
860
|
+
|
|
861
|
+
<nav>
|
|
862
|
+
<a href="/" @click=\${(e: Event) => {
|
|
863
|
+
e.preventDefault();
|
|
864
|
+
this.navigate('/');
|
|
865
|
+
}}>Home</a>
|
|
866
|
+
<a href="/about" @click=\${(e: Event) => {
|
|
867
|
+
e.preventDefault();
|
|
868
|
+
this.navigate('/about');
|
|
869
|
+
}}>About</a>
|
|
870
|
+
<a href="/contact" @click=\${(e: Event) => {
|
|
871
|
+
e.preventDefault();
|
|
872
|
+
this.navigate('/contact');
|
|
873
|
+
}}>Contact</a>
|
|
874
|
+
</nav>
|
|
875
|
+
|
|
876
|
+
<div class="route-info">
|
|
877
|
+
<p><strong>Current Route:</strong> \${this.currentPath}</p>
|
|
878
|
+
<p>Router integration active</p>
|
|
879
|
+
</div>
|
|
880
|
+
|
|
881
|
+
<div class="content">
|
|
882
|
+
\${this.renderRoute()}
|
|
883
|
+
</div>
|
|
884
|
+
</div>
|
|
885
|
+
\`;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
private renderRoute() {
|
|
889
|
+
switch (this.currentPath) {
|
|
890
|
+
case '/':
|
|
891
|
+
return html\`<h3>Home Page</h3><p>Welcome to the home page!</p>\`;
|
|
892
|
+
case '/about':
|
|
893
|
+
return html\`<h3>About Page</h3><p>Learn more about us.</p>\`;
|
|
894
|
+
case '/contact':
|
|
895
|
+
return html\`<h3>Contact Page</h3><p>Get in touch with us.</p>\`;
|
|
896
|
+
default:
|
|
897
|
+
return html\`<h3>404 Not Found</h3><p>Page not found.</p>\`;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export default ${name};
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
return `/**
|
|
906
|
+
* @fileoverview ${name} component with router integration
|
|
907
|
+
*/
|
|
908
|
+
|
|
909
|
+
import {
|
|
910
|
+
html,
|
|
911
|
+
Component,
|
|
912
|
+
ComponentProps,
|
|
913
|
+
useParams,
|
|
914
|
+
useNavigate,
|
|
915
|
+
useLocation,
|
|
916
|
+
createSignal,
|
|
917
|
+
} from '@fluixi/core';
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Props for ${name} component
|
|
921
|
+
*/
|
|
922
|
+
export interface ${name}Props extends ComponentProps {
|
|
923
|
+
// Add your props here
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* ${name} component with router integration
|
|
928
|
+
* @param {${name}Props} props - Component props
|
|
929
|
+
* @returns {TemplateResult} Rendered template
|
|
930
|
+
*
|
|
931
|
+
* @example
|
|
932
|
+
* \`\`\`typescript
|
|
933
|
+
* // In your routes configuration:
|
|
934
|
+
* const routes = [
|
|
935
|
+
* {
|
|
936
|
+
* path: '/${name.toLowerCase()}/:id',
|
|
937
|
+
* component: ${name}
|
|
938
|
+
* }
|
|
939
|
+
* ];
|
|
940
|
+
* \`\`\`
|
|
941
|
+
*/
|
|
942
|
+
export const ${name}: Component<${name}Props> = (props) => {
|
|
943
|
+
const params = useParams();
|
|
944
|
+
const navigate = useNavigate();
|
|
945
|
+
const location = useLocation();
|
|
946
|
+
|
|
947
|
+
const [selectedTab, setSelectedTab] = createSignal('overview');
|
|
948
|
+
|
|
949
|
+
const goBack = () => {
|
|
950
|
+
navigate(-1);
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const navigateToTab = (tab: string) => {
|
|
954
|
+
setSelectedTab(tab);
|
|
955
|
+
navigate(\`\${location().pathname}?tab=\${tab}\`);
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
return html\`
|
|
959
|
+
<div class="${name.toLowerCase()}">
|
|
960
|
+
<div class="header">
|
|
961
|
+
<button @click=\${goBack}>← Back</button>
|
|
962
|
+
<h2>${name}</h2>
|
|
963
|
+
<p>ID: \${params.id || 'N/A'}</p>
|
|
964
|
+
</div>
|
|
965
|
+
|
|
966
|
+
<div class="tabs">
|
|
967
|
+
<button
|
|
968
|
+
class=\${selectedTab() === 'overview' ? 'active' : ''}
|
|
969
|
+
@click=\${() => navigateToTab('overview')}
|
|
970
|
+
>
|
|
971
|
+
Overview
|
|
972
|
+
</button>
|
|
973
|
+
<button
|
|
974
|
+
class=\${selectedTab() === 'details' ? 'active' : ''}
|
|
975
|
+
@click=\${() => navigateToTab('details')}
|
|
976
|
+
>
|
|
977
|
+
Details
|
|
978
|
+
</button>
|
|
979
|
+
<button
|
|
980
|
+
class=\${selectedTab() === 'settings' ? 'active' : ''}
|
|
981
|
+
@click=\${() => navigateToTab('settings')}
|
|
982
|
+
>
|
|
983
|
+
Settings
|
|
984
|
+
</button>
|
|
985
|
+
</div>
|
|
986
|
+
|
|
987
|
+
<div class="content">
|
|
988
|
+
<p>Current tab: \${selectedTab()}</p>
|
|
989
|
+
<p>Full path: \${location().pathname}</p>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
\`;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
export default ${name};
|
|
996
|
+
`;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Generates a full-featured component
|
|
1000
|
+
* @param {string} name - Component name
|
|
1001
|
+
* @returns {string} Component code
|
|
1002
|
+
*/
|
|
1003
|
+
function generateFullComponent(name) {
|
|
1004
|
+
return `/**
|
|
1005
|
+
* @fileoverview ${name} - Full-featured component with state, resources, and router
|
|
1006
|
+
*/
|
|
1007
|
+
|
|
1008
|
+
import {
|
|
1009
|
+
html,
|
|
1010
|
+
Component,
|
|
1011
|
+
ComponentProps,
|
|
1012
|
+
createSignal,
|
|
1013
|
+
createEffect,
|
|
1014
|
+
createResource,
|
|
1015
|
+
createContext,
|
|
1016
|
+
useContext,
|
|
1017
|
+
useParams,
|
|
1018
|
+
useNavigate,
|
|
1019
|
+
Show,
|
|
1020
|
+
For,
|
|
1021
|
+
onMount,
|
|
1022
|
+
onUnmount,
|
|
1023
|
+
} from '@fluixi/core';
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Props for ${name} component
|
|
1027
|
+
*/
|
|
1028
|
+
export interface ${name}Props extends ComponentProps {
|
|
1029
|
+
initialData?: ${name}Data[];
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Data interface for ${name}
|
|
1034
|
+
*/
|
|
1035
|
+
export interface ${name}Data {
|
|
1036
|
+
id: string | number;
|
|
1037
|
+
title: string;
|
|
1038
|
+
description: string;
|
|
1039
|
+
createdAt: string;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* ${name} context type
|
|
1044
|
+
*/
|
|
1045
|
+
interface ${name}Context {
|
|
1046
|
+
selectedId: () => string | null;
|
|
1047
|
+
selectItem: (id: string) => void;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Context for ${name} component
|
|
1052
|
+
*/
|
|
1053
|
+
const ${name}Context = createContext<${name}Context | null>(null, '${name}Context');
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Fetches items for ${name}
|
|
1057
|
+
* @returns {Promise<${name}Data[]>} Fetched items
|
|
1058
|
+
*/
|
|
1059
|
+
async function fetch${name}Items(): Promise<${name}Data[]> {
|
|
1060
|
+
// Replace with your actual API call
|
|
1061
|
+
const response = await fetch('/api/${name.toLowerCase()}');
|
|
1062
|
+
|
|
1063
|
+
if (!response.ok) {
|
|
1064
|
+
throw new Error('Failed to fetch items');
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return response.json();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* ${name} component - Full-featured implementation
|
|
1072
|
+
* @param {${name}Props} props - Component props
|
|
1073
|
+
* @returns {TemplateResult} Rendered template
|
|
1074
|
+
*
|
|
1075
|
+
* @example
|
|
1076
|
+
* \`\`\`typescript
|
|
1077
|
+
* const App = () => {
|
|
1078
|
+
* return html\`
|
|
1079
|
+
* <div>
|
|
1080
|
+
* \${${name}({ initialData: [] })}
|
|
1081
|
+
* </div>
|
|
1082
|
+
* \`;
|
|
1083
|
+
* };
|
|
1084
|
+
* \`\`\`
|
|
1085
|
+
*/
|
|
1086
|
+
export const ${name}: Component<${name}Props> = (props) => {
|
|
1087
|
+
const params = useParams();
|
|
1088
|
+
const navigate = useNavigate();
|
|
1089
|
+
|
|
1090
|
+
// State
|
|
1091
|
+
const [selectedId, setSelectedId] = createSignal<string | null>(params.id || null);
|
|
1092
|
+
const [filter, setFilter] = createSignal('');
|
|
1093
|
+
const [sortBy, setSortBy] = createSignal<'title' | 'date'>('title');
|
|
1094
|
+
|
|
1095
|
+
// Resource
|
|
1096
|
+
const [items, { refetch, mutate }] = createResource(
|
|
1097
|
+
fetch${name}Items,
|
|
1098
|
+
{
|
|
1099
|
+
initialValue: props.initialData || [],
|
|
1100
|
+
onError: (error) => console.error('Failed to load items:', error),
|
|
1101
|
+
}
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
// Computed filtered and sorted items
|
|
1105
|
+
const filteredItems = () => {
|
|
1106
|
+
const all = items() || [];
|
|
1107
|
+
const filterText = filter().toLowerCase();
|
|
1108
|
+
|
|
1109
|
+
let filtered = all.filter(item =>
|
|
1110
|
+
item.title.toLowerCase().includes(filterText) ||
|
|
1111
|
+
item.description.toLowerCase().includes(filterText)
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
if (sortBy() === 'title') {
|
|
1115
|
+
filtered.sort((a, b) => a.title.localeCompare(b.title));
|
|
1116
|
+
} else {
|
|
1117
|
+
filtered.sort((a, b) =>
|
|
1118
|
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return filtered;
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
// Effects
|
|
1126
|
+
createEffect(() => {
|
|
1127
|
+
if (params.id) {
|
|
1128
|
+
setSelectedId(params.id);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
// Lifecycle
|
|
1133
|
+
onMount(() => {
|
|
1134
|
+
console.log('${name} mounted');
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
onUnmount(() => {
|
|
1138
|
+
console.log('${name} unmounted');
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
// Handlers
|
|
1142
|
+
const selectItem = (id: string) => {
|
|
1143
|
+
setSelectedId(id);
|
|
1144
|
+
navigate(\`/${name.toLowerCase()}/\${id}\`);
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const addNewItem = async () => {
|
|
1148
|
+
// Optimistic update
|
|
1149
|
+
const newItem: ${name}Data = {
|
|
1150
|
+
id: Date.now(),
|
|
1151
|
+
title: 'New Item',
|
|
1152
|
+
description: 'New item description',
|
|
1153
|
+
createdAt: new Date().toISOString(),
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
mutate(prev => [...(prev || []), newItem]);
|
|
1157
|
+
|
|
1158
|
+
// Sync with server
|
|
1159
|
+
try {
|
|
1160
|
+
await fetch('/api/${name.toLowerCase()}', {
|
|
1161
|
+
method: 'POST',
|
|
1162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1163
|
+
body: JSON.stringify(newItem),
|
|
1164
|
+
});
|
|
1165
|
+
refetch();
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
console.error('Failed to add item:', error);
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
const deleteItem = async (id: string | number) => {
|
|
1172
|
+
// Optimistic update
|
|
1173
|
+
mutate(prev => prev?.filter(item => item.id !== id));
|
|
1174
|
+
|
|
1175
|
+
// Sync with server
|
|
1176
|
+
try {
|
|
1177
|
+
await fetch(\`/api/${name.toLowerCase()}/\${id}\`, {
|
|
1178
|
+
method: 'DELETE',
|
|
1179
|
+
});
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
console.error('Failed to delete item:', error);
|
|
1182
|
+
refetch();
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
// Context value
|
|
1187
|
+
const contextValue: ${name}Context = {
|
|
1188
|
+
selectedId,
|
|
1189
|
+
selectItem,
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
return html\`
|
|
1193
|
+
<div class="${name.toLowerCase()}">
|
|
1194
|
+
<div class="header">
|
|
1195
|
+
<h2>${name}</h2>
|
|
1196
|
+
<button @click=\${refetch}>Refresh</button>
|
|
1197
|
+
<button @click=\${addNewItem}>Add New</button>
|
|
1198
|
+
</div>
|
|
1199
|
+
|
|
1200
|
+
<div class="controls">
|
|
1201
|
+
<input
|
|
1202
|
+
type="text"
|
|
1203
|
+
placeholder="Filter items..."
|
|
1204
|
+
.value=\${filter()}
|
|
1205
|
+
@input=\${(e: Event) => setFilter((e.target as HTMLInputElement).value)}
|
|
1206
|
+
/>
|
|
1207
|
+
<select
|
|
1208
|
+
.value=\${sortBy()}
|
|
1209
|
+
@change=\${(e: Event) => setSortBy((e.target as HTMLSelectElement).value as any)}
|
|
1210
|
+
>
|
|
1211
|
+
<option value="title">Sort by Title</option>
|
|
1212
|
+
<option value="date">Sort by Date</option>
|
|
1213
|
+
</select>
|
|
1214
|
+
</div>
|
|
1215
|
+
|
|
1216
|
+
\${Show({
|
|
1217
|
+
when: items.loading,
|
|
1218
|
+
children: html\`<div class="loading">Loading...</div>\`,
|
|
1219
|
+
})}
|
|
1220
|
+
|
|
1221
|
+
\${Show({
|
|
1222
|
+
when: items.error,
|
|
1223
|
+
children: html\`
|
|
1224
|
+
<div class="error">
|
|
1225
|
+
<p>Error: \${items.error?.message}</p>
|
|
1226
|
+
<button @click=\${refetch}>Retry</button>
|
|
1227
|
+
</div>
|
|
1228
|
+
\`,
|
|
1229
|
+
})}
|
|
1230
|
+
|
|
1231
|
+
\${Show({
|
|
1232
|
+
when: () => !items.loading && !items.error,
|
|
1233
|
+
children: html\`
|
|
1234
|
+
<div class="items">
|
|
1235
|
+
\${For({
|
|
1236
|
+
each: filteredItems,
|
|
1237
|
+
children: (item, index) => html\`
|
|
1238
|
+
<div
|
|
1239
|
+
class=\${selectedId() === String(item.id) ? 'item selected' : 'item'}
|
|
1240
|
+
@click=\${() => selectItem(String(item.id))}
|
|
1241
|
+
>
|
|
1242
|
+
<h3>\${item.title}</h3>
|
|
1243
|
+
<p>\${item.description}</p>
|
|
1244
|
+
<span class="date">\${new Date(item.createdAt).toLocaleDateString()}</span>
|
|
1245
|
+
<button
|
|
1246
|
+
@click=\${(e: Event) => {
|
|
1247
|
+
e.stopPropagation();
|
|
1248
|
+
deleteItem(item.id);
|
|
1249
|
+
}}
|
|
1250
|
+
>
|
|
1251
|
+
Delete
|
|
1252
|
+
</button>
|
|
1253
|
+
</div>
|
|
1254
|
+
\`,
|
|
1255
|
+
})}
|
|
1256
|
+
</div>
|
|
1257
|
+
\`,
|
|
1258
|
+
})}
|
|
1259
|
+
|
|
1260
|
+
\${Show({
|
|
1261
|
+
when: () => filteredItems().length === 0 && !items.loading,
|
|
1262
|
+
children: html\`<p class="empty">No items found</p>\`,
|
|
1263
|
+
})}
|
|
1264
|
+
</div>
|
|
1265
|
+
\`;
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
export default ${name};
|
|
1269
|
+
`;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Generates a JSX component
|
|
1273
|
+
*/
|
|
1274
|
+
function generateJSXComponent(name, type = 'basic') {
|
|
1275
|
+
const kebabName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
1276
|
+
if (type === 'basic') {
|
|
1277
|
+
return `/** @jsx h */
|
|
1278
|
+
import { h, Fragment } from '@fluixi/core/jsx-runtime';
|
|
1279
|
+
import { createSignal } from '@fluixi/reactive/signal';
|
|
1280
|
+
import './${kebabName}.css';
|
|
1281
|
+
|
|
1282
|
+
export interface ${name}Props {
|
|
1283
|
+
title?: string;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
export function ${name}({ title = 'Hello' }: ${name}Props) {
|
|
1287
|
+
return (
|
|
1288
|
+
<div class="${kebabName}">
|
|
1289
|
+
<h1>{title}</h1>
|
|
1290
|
+
<p>This is a Fluixi JSX component</p>
|
|
1291
|
+
</div>
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
export default ${name};
|
|
1296
|
+
`;
|
|
1297
|
+
}
|
|
1298
|
+
else if (type === 'with-state') {
|
|
1299
|
+
return `/** @jsx h */
|
|
1300
|
+
import { h, Fragment } from '@fluixi/core/jsx-runtime';
|
|
1301
|
+
import { createSignal } from '@fluixi/reactive/signal';
|
|
1302
|
+
import './${kebabName}.css';
|
|
1303
|
+
|
|
1304
|
+
export interface ${name}Props {
|
|
1305
|
+
initialCount?: number;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
export function ${name}({ initialCount = 0 }: ${name}Props) {
|
|
1309
|
+
const [count, setCount] = createSignal(initialCount);
|
|
1310
|
+
|
|
1311
|
+
const increment = () => setCount(count() + 1);
|
|
1312
|
+
const decrement = () => setCount(count() - 1);
|
|
1313
|
+
const reset = () => setCount(initialCount);
|
|
1314
|
+
|
|
1315
|
+
return (
|
|
1316
|
+
<div class="${kebabName}">
|
|
1317
|
+
<h2>Counter Component</h2>
|
|
1318
|
+
<div class="counter">
|
|
1319
|
+
<button onClick={decrement}>-</button>
|
|
1320
|
+
<span class="count">{count()}</span>
|
|
1321
|
+
<button onClick={increment}>+</button>
|
|
1322
|
+
</div>
|
|
1323
|
+
<button onClick={reset}>Reset</button>
|
|
1324
|
+
</div>
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
export default ${name};
|
|
1329
|
+
`;
|
|
1330
|
+
}
|
|
1331
|
+
return generateJSXComponent(name, 'basic');
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Generates a React Bridge component
|
|
1335
|
+
*/
|
|
1336
|
+
function generateReactBridgeComponent(name, type = 'basic') {
|
|
1337
|
+
const kebabName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
1338
|
+
if (type === 'basic') {
|
|
1339
|
+
return `import React from 'react';
|
|
1340
|
+
import { useSignal } from '@fluixi/core/react-bridge';
|
|
1341
|
+
import './${kebabName}.css';
|
|
1342
|
+
|
|
1343
|
+
export interface ${name}Props {
|
|
1344
|
+
title?: string;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export function ${name}({ title = 'Hello' }: ${name}Props) {
|
|
1348
|
+
return (
|
|
1349
|
+
<div className="${kebabName}">
|
|
1350
|
+
<h1>{title}</h1>
|
|
1351
|
+
<p>This is a Fluixi React Bridge component</p>
|
|
1352
|
+
</div>
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
export default ${name};
|
|
1357
|
+
`;
|
|
1358
|
+
}
|
|
1359
|
+
else if (type === 'with-state') {
|
|
1360
|
+
return `import React from 'react';
|
|
1361
|
+
import { useSignal, useFluixiEffect } from '@fluixi/core/react-bridge';
|
|
1362
|
+
import './${kebabName}.css';
|
|
1363
|
+
|
|
1364
|
+
export interface ${name}Props {
|
|
1365
|
+
initialCount?: number;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
export function ${name}({ initialCount = 0 }: ${name}Props) {
|
|
1369
|
+
const [count, setCount] = useSignal(initialCount);
|
|
1370
|
+
|
|
1371
|
+
useFluixiEffect(() => {
|
|
1372
|
+
console.log('Count changed:', count());
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
const increment = () => setCount(count() + 1);
|
|
1376
|
+
const decrement = () => setCount(count() - 1);
|
|
1377
|
+
const reset = () => setCount(initialCount);
|
|
1378
|
+
|
|
1379
|
+
return (
|
|
1380
|
+
<div className="${kebabName}">
|
|
1381
|
+
<h2>Counter Component</h2>
|
|
1382
|
+
<div className="counter">
|
|
1383
|
+
<button onClick={decrement}>-</button>
|
|
1384
|
+
<span className="count">{count()}</span>
|
|
1385
|
+
<button onClick={increment}>+</button>
|
|
1386
|
+
</div>
|
|
1387
|
+
<button onClick={reset}>Reset</button>
|
|
1388
|
+
</div>
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
export default ${name};
|
|
1393
|
+
`;
|
|
1394
|
+
}
|
|
1395
|
+
return generateReactBridgeComponent(name, 'basic');
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Creates a component file
|
|
1399
|
+
* @param {string} componentPath - Path to create the component
|
|
1400
|
+
* @param {string} name - Component name
|
|
1401
|
+
* @param {ComponentType} type - Component type
|
|
1402
|
+
*/
|
|
1403
|
+
async function createComponent(componentPath, name, type = 'basic', pattern = 'functional', mode = 'lit-html') {
|
|
1404
|
+
const componentDir = path.dirname(componentPath);
|
|
1405
|
+
// Ensure component directory exists
|
|
1406
|
+
await fs.mkdir(componentDir, { recursive: true });
|
|
1407
|
+
let componentCode;
|
|
1408
|
+
// Generate based on mode
|
|
1409
|
+
if (mode === 'jsx') {
|
|
1410
|
+
componentCode = generateJSXComponent(name, type);
|
|
1411
|
+
}
|
|
1412
|
+
else if (mode === 'react-bridge') {
|
|
1413
|
+
componentCode = generateReactBridgeComponent(name, type);
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
// lit-html mode (original generators)
|
|
1417
|
+
switch (type) {
|
|
1418
|
+
case 'basic':
|
|
1419
|
+
componentCode = generateBasicComponent(name, pattern);
|
|
1420
|
+
break;
|
|
1421
|
+
case 'with-state':
|
|
1422
|
+
componentCode = generateComponentWithState(name, pattern);
|
|
1423
|
+
break;
|
|
1424
|
+
case 'with-resource':
|
|
1425
|
+
componentCode = generateComponentWithResource(name, pattern);
|
|
1426
|
+
break;
|
|
1427
|
+
case 'with-router':
|
|
1428
|
+
componentCode = generateComponentWithRouter(name, pattern);
|
|
1429
|
+
break;
|
|
1430
|
+
case 'full':
|
|
1431
|
+
componentCode = generateFullComponent(name);
|
|
1432
|
+
break;
|
|
1433
|
+
default:
|
|
1434
|
+
componentCode = generateBasicComponent(name, pattern);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// Write component file
|
|
1438
|
+
await fs.writeFile(componentPath, componentCode, 'utf-8');
|
|
1439
|
+
// Create test file
|
|
1440
|
+
const testPath = componentPath.replace(/\.tsx?$/, '.spec.ts');
|
|
1441
|
+
const testCode = generateTestFile(name);
|
|
1442
|
+
await fs.writeFile(testPath, testCode, 'utf-8');
|
|
1443
|
+
// Create style file
|
|
1444
|
+
const stylePath = componentPath.replace(/\.tsx?$/, '.css');
|
|
1445
|
+
const styleCode = generateStyleFile(name);
|
|
1446
|
+
await fs.writeFile(stylePath, styleCode, 'utf-8');
|
|
1447
|
+
console.log(chalk.green(`✓ Created component: ${name}`));
|
|
1448
|
+
console.log(chalk.gray(` - ${componentPath}`));
|
|
1449
|
+
console.log(chalk.gray(` - ${testPath}`));
|
|
1450
|
+
console.log(chalk.gray(` - ${stylePath}`));
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Generates a test file for a component
|
|
1454
|
+
* @param {string} name - Component name
|
|
1455
|
+
* @returns {string} Test code
|
|
1456
|
+
*/
|
|
1457
|
+
function generateTestFile(name) {
|
|
1458
|
+
return `/**
|
|
1459
|
+
* @fileoverview Tests for ${name} component
|
|
1460
|
+
*/
|
|
1461
|
+
|
|
1462
|
+
import { describe, it, expect } from 'vitest';
|
|
1463
|
+
import { render } from '@fluixi/core';
|
|
1464
|
+
import { ${name} } from './${name}';
|
|
1465
|
+
|
|
1466
|
+
describe('${name}', () => {
|
|
1467
|
+
it('should render successfully', () => {
|
|
1468
|
+
const container = document.createElement('div');
|
|
1469
|
+
const dispose = render(() => ${name}({}), container);
|
|
1470
|
+
|
|
1471
|
+
expect(container.querySelector('.${name.toLowerCase()}')).toBeTruthy();
|
|
1472
|
+
|
|
1473
|
+
dispose();
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it('should display component title', () => {
|
|
1477
|
+
const container = document.createElement('div');
|
|
1478
|
+
const dispose = render(() => ${name}({}), container);
|
|
1479
|
+
|
|
1480
|
+
const heading = container.querySelector('h2');
|
|
1481
|
+
expect(heading?.textContent).toBe('${name}');
|
|
1482
|
+
|
|
1483
|
+
dispose();
|
|
1484
|
+
});
|
|
1485
|
+
});
|
|
1486
|
+
`;
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Generates a style file for a component
|
|
1490
|
+
* @param {string} name - Component name
|
|
1491
|
+
* @returns {string} CSS code
|
|
1492
|
+
*/
|
|
1493
|
+
function generateStyleFile(name) {
|
|
1494
|
+
return `.${name.toLowerCase()} {
|
|
1495
|
+
padding: 1rem;
|
|
1496
|
+
border: 1px solid #e0e0e0;
|
|
1497
|
+
border-radius: 8px;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
.${name.toLowerCase()} h2 {
|
|
1501
|
+
margin: 0 0 1rem 0;
|
|
1502
|
+
color: #333;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
.${name.toLowerCase()} p {
|
|
1506
|
+
margin: 0.5rem 0;
|
|
1507
|
+
color: #666;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
.${name.toLowerCase()} button {
|
|
1511
|
+
padding: 0.5rem 1rem;
|
|
1512
|
+
margin: 0.25rem;
|
|
1513
|
+
border: none;
|
|
1514
|
+
border-radius: 4px;
|
|
1515
|
+
background: #007bff;
|
|
1516
|
+
color: white;
|
|
1517
|
+
cursor: pointer;
|
|
1518
|
+
font-size: 0.875rem;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.${name.toLowerCase()} button:hover {
|
|
1522
|
+
background: #0056b3;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
.${name.toLowerCase()} button:active {
|
|
1526
|
+
transform: scale(0.98);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
.${name.toLowerCase()} .loading,
|
|
1530
|
+
.${name.toLowerCase()} .error,
|
|
1531
|
+
.${name.toLowerCase()} .empty {
|
|
1532
|
+
padding: 2rem;
|
|
1533
|
+
text-align: center;
|
|
1534
|
+
color: #666;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.${name.toLowerCase()} .error {
|
|
1538
|
+
color: #dc3545;
|
|
1539
|
+
}
|
|
1540
|
+
`;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Interactive component generation
|
|
1544
|
+
*/
|
|
1545
|
+
async function interactiveComponentGeneration() {
|
|
1546
|
+
const answers = await inquirer.prompt([
|
|
1547
|
+
{
|
|
1548
|
+
type: 'input',
|
|
1549
|
+
name: 'name',
|
|
1550
|
+
message: 'Component name:',
|
|
1551
|
+
validate: (input) => {
|
|
1552
|
+
if (!input)
|
|
1553
|
+
return 'Component name is required';
|
|
1554
|
+
if (!/^[A-Z][a-zA-Z0-9]*$/.test(input)) {
|
|
1555
|
+
return 'Component name must start with uppercase letter';
|
|
1556
|
+
}
|
|
1557
|
+
return true;
|
|
1558
|
+
},
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
type: 'list',
|
|
1562
|
+
name: 'mode',
|
|
1563
|
+
message: 'Component mode:',
|
|
1564
|
+
choices: [
|
|
1565
|
+
{
|
|
1566
|
+
name: 'Lit-HTML (Recommended - native Fluixi with lit-html)',
|
|
1567
|
+
value: 'lit-html',
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
name: 'JSX (Native JSX runtime for Fluixi)',
|
|
1571
|
+
value: 'jsx',
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
name: 'React Bridge (React compatibility with Fluixi signals)',
|
|
1575
|
+
value: 'react-bridge',
|
|
1576
|
+
},
|
|
1577
|
+
],
|
|
1578
|
+
default: 'lit-html',
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
type: 'list',
|
|
1582
|
+
name: 'pattern',
|
|
1583
|
+
message: 'Component pattern:',
|
|
1584
|
+
choices: [
|
|
1585
|
+
{
|
|
1586
|
+
name: 'FluixiElement (Recommended - extends FluixiElement base class)',
|
|
1587
|
+
value: 'fluixilement',
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
name: 'LitElement (Plain LitElement class component)',
|
|
1591
|
+
value: 'litelement',
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
name: 'WithSignals (Mixin - adds signals to LitElement)',
|
|
1595
|
+
value: 'withsignals',
|
|
1596
|
+
},
|
|
1597
|
+
{
|
|
1598
|
+
name: 'Controller (Manual - uses SignalController)',
|
|
1599
|
+
value: 'controller',
|
|
1600
|
+
},
|
|
1601
|
+
{
|
|
1602
|
+
name: 'Functional (Function-based component)',
|
|
1603
|
+
value: 'functional',
|
|
1604
|
+
},
|
|
1605
|
+
],
|
|
1606
|
+
default: 'fluixilement',
|
|
1607
|
+
when: (answers) => answers.mode === 'lit-html',
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
type: 'list',
|
|
1611
|
+
name: 'type',
|
|
1612
|
+
message: 'Component features:',
|
|
1613
|
+
choices: [
|
|
1614
|
+
{ name: 'Basic (Simple component)', value: 'basic' },
|
|
1615
|
+
{ name: 'With State (Reactive state management)', value: 'with-state' },
|
|
1616
|
+
{ name: 'With Resource (Async data fetching)', value: 'with-resource' },
|
|
1617
|
+
{ name: 'With Router (Router integration)', value: 'with-router' },
|
|
1618
|
+
{ name: 'Full (All features)', value: 'full' },
|
|
1619
|
+
],
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
type: 'input',
|
|
1623
|
+
name: 'path',
|
|
1624
|
+
message: 'Component path (relative to src):',
|
|
1625
|
+
default: (answers) => {
|
|
1626
|
+
const ext = answers.mode === 'lit-html' ? 'ts' : 'tsx';
|
|
1627
|
+
return `components/${answers.name}/${answers.name}.${ext}`;
|
|
1628
|
+
},
|
|
1629
|
+
},
|
|
1630
|
+
]);
|
|
1631
|
+
const componentPath = path.join(process.cwd(), 'src', answers.path);
|
|
1632
|
+
await createComponent(componentPath, answers.name, answers.type, answers.pattern || 'functional', answers.mode || 'lit-html');
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Generates project structure
|
|
1636
|
+
* @param {string} projectName - Project name
|
|
1637
|
+
* @param {ProjectTemplate} template - Template type
|
|
1638
|
+
* @param {ComponentMode} mode - Default component mode
|
|
1639
|
+
* @param {any} config - Advanced configuration options
|
|
1640
|
+
*/
|
|
1641
|
+
async function generateProject(projectName, template, mode = 'lit-html', config = {}) {
|
|
1642
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
1643
|
+
console.log(chalk.blue(`\nCreating Fluixi project: ${projectName}`));
|
|
1644
|
+
console.log(chalk.gray(`Template: ${template}\n`));
|
|
1645
|
+
// Create project directory
|
|
1646
|
+
await fs.ensureDir(projectPath);
|
|
1647
|
+
// Create directory structure
|
|
1648
|
+
await fs.ensureDir(path.join(projectPath, 'src'));
|
|
1649
|
+
await fs.ensureDir(path.join(projectPath, 'src/components'));
|
|
1650
|
+
await fs.ensureDir(path.join(projectPath, 'src/pages'));
|
|
1651
|
+
await fs.ensureDir(path.join(projectPath, 'src/styles'));
|
|
1652
|
+
await fs.ensureDir(path.join(projectPath, 'public'));
|
|
1653
|
+
// Generate package.json with conditional dependencies based on mode
|
|
1654
|
+
const packageJson = {
|
|
1655
|
+
name: projectName,
|
|
1656
|
+
version: '0.0.1',
|
|
1657
|
+
type: 'module',
|
|
1658
|
+
scripts: {
|
|
1659
|
+
dev: 'vite',
|
|
1660
|
+
build: 'tsc && vite build',
|
|
1661
|
+
preview: 'vite preview',
|
|
1662
|
+
test: 'vitest',
|
|
1663
|
+
},
|
|
1664
|
+
dependencies: {
|
|
1665
|
+
'@fluixi/core': 'workspace:*',
|
|
1666
|
+
'@fluixi/reactive': 'workspace:*',
|
|
1667
|
+
lit: '^3.3.1',
|
|
1668
|
+
},
|
|
1669
|
+
devDependencies: {
|
|
1670
|
+
typescript: '^5.0.0',
|
|
1671
|
+
vite: '^5.0.0',
|
|
1672
|
+
vitest: '^1.0.0',
|
|
1673
|
+
},
|
|
1674
|
+
};
|
|
1675
|
+
// Add React dependencies if using react-bridge mode
|
|
1676
|
+
if (mode === 'react-bridge') {
|
|
1677
|
+
packageJson.dependencies.react = '^18.0.0';
|
|
1678
|
+
packageJson.dependencies['react-dom'] = '^18.0.0';
|
|
1679
|
+
packageJson.devDependencies['@types/react'] = '^18.0.0';
|
|
1680
|
+
packageJson.devDependencies['@types/react-dom'] = '^18.0.0';
|
|
1681
|
+
}
|
|
1682
|
+
// Add advanced configuration dependencies
|
|
1683
|
+
if (config.cssPreprocessor === 'scss') {
|
|
1684
|
+
packageJson.devDependencies.sass = '^1.69.0';
|
|
1685
|
+
}
|
|
1686
|
+
else if (config.cssPreprocessor === 'less') {
|
|
1687
|
+
packageJson.devDependencies.less = '^4.2.0';
|
|
1688
|
+
}
|
|
1689
|
+
else if (config.cssPreprocessor === 'postcss') {
|
|
1690
|
+
packageJson.devDependencies.postcss = '^8.4.31';
|
|
1691
|
+
packageJson.devDependencies.autoprefixer = '^10.4.16';
|
|
1692
|
+
}
|
|
1693
|
+
if (config.includeESLint) {
|
|
1694
|
+
packageJson.devDependencies.eslint = '^8.53.0';
|
|
1695
|
+
packageJson.devDependencies['@typescript-eslint/eslint-plugin'] = '^6.10.0';
|
|
1696
|
+
packageJson.devDependencies['@typescript-eslint/parser'] = '^6.10.0';
|
|
1697
|
+
packageJson.scripts.lint = 'eslint src --ext ts,tsx';
|
|
1698
|
+
}
|
|
1699
|
+
if (config.includePrettier) {
|
|
1700
|
+
packageJson.devDependencies.prettier = '^3.1.0';
|
|
1701
|
+
packageJson.scripts.format = 'prettier --write "src/**/*.{ts,tsx,css}"';
|
|
1702
|
+
}
|
|
1703
|
+
if (config.includeSSR) {
|
|
1704
|
+
packageJson.dependencies['@lit-labs/ssr'] = '^3.3.1';
|
|
1705
|
+
packageJson.dependencies['@lit-labs/ssr-client'] = '^1.1.7';
|
|
1706
|
+
}
|
|
1707
|
+
if (config.author) {
|
|
1708
|
+
packageJson.author = config.author;
|
|
1709
|
+
}
|
|
1710
|
+
if (config.description) {
|
|
1711
|
+
packageJson.description = config.description;
|
|
1712
|
+
}
|
|
1713
|
+
if (config.license) {
|
|
1714
|
+
packageJson.license = config.license;
|
|
1715
|
+
}
|
|
1716
|
+
await fs.writeJSON(path.join(projectPath, 'package.json'), packageJson, {
|
|
1717
|
+
spaces: 2,
|
|
1718
|
+
});
|
|
1719
|
+
// Generate tsconfig.json with conditional JSX settings
|
|
1720
|
+
const tsConfig = {
|
|
1721
|
+
compilerOptions: {
|
|
1722
|
+
target: 'ES2020',
|
|
1723
|
+
module: 'ESNext',
|
|
1724
|
+
lib: ['ES2020', 'DOM', 'DOM.Iterable'],
|
|
1725
|
+
moduleResolution: 'bundler',
|
|
1726
|
+
strict: true,
|
|
1727
|
+
skipLibCheck: true,
|
|
1728
|
+
esModuleInterop: true,
|
|
1729
|
+
resolveJsonModule: true,
|
|
1730
|
+
},
|
|
1731
|
+
include: ['src'],
|
|
1732
|
+
};
|
|
1733
|
+
// Configure JSX based on mode
|
|
1734
|
+
if (mode === 'jsx') {
|
|
1735
|
+
tsConfig.compilerOptions.jsx = 'react-jsx';
|
|
1736
|
+
tsConfig.compilerOptions.jsxImportSource = '@fluixi/core/jsx-runtime';
|
|
1737
|
+
}
|
|
1738
|
+
else if (mode === 'react-bridge') {
|
|
1739
|
+
tsConfig.compilerOptions.jsx = 'react-jsx';
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
tsConfig.compilerOptions.jsx = 'preserve';
|
|
1743
|
+
}
|
|
1744
|
+
await fs.writeJSON(path.join(projectPath, 'tsconfig.json'), tsConfig, {
|
|
1745
|
+
spaces: 2,
|
|
1746
|
+
});
|
|
1747
|
+
// Generate vite.config.ts
|
|
1748
|
+
const viteConfig = `import { defineConfig } from 'vite';
|
|
1749
|
+
|
|
1750
|
+
export default defineConfig({
|
|
1751
|
+
server: {
|
|
1752
|
+
port: 3000,
|
|
1753
|
+
},
|
|
1754
|
+
build: {
|
|
1755
|
+
target: 'esnext',
|
|
1756
|
+
},
|
|
1757
|
+
});
|
|
1758
|
+
`;
|
|
1759
|
+
await fs.writeFile(path.join(projectPath, 'vite.config.ts'), viteConfig);
|
|
1760
|
+
// Generate index.html
|
|
1761
|
+
const indexHtml = `<!DOCTYPE html>
|
|
1762
|
+
<html lang="en">
|
|
1763
|
+
<head>
|
|
1764
|
+
<meta charset="UTF-8" />
|
|
1765
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1766
|
+
<title>${projectName}</title>
|
|
1767
|
+
</head>
|
|
1768
|
+
<body>
|
|
1769
|
+
<div id="app"></div>
|
|
1770
|
+
<script type="module" src="/src/main.ts"></script>
|
|
1771
|
+
</body>
|
|
1772
|
+
</html>
|
|
1773
|
+
`;
|
|
1774
|
+
await fs.writeFile(path.join(projectPath, 'index.html'), indexHtml);
|
|
1775
|
+
// Generate main.ts based on mode
|
|
1776
|
+
let mainTs;
|
|
1777
|
+
const appExt = mode === 'lit-html' ? 'ts' : 'tsx';
|
|
1778
|
+
if (mode === 'jsx') {
|
|
1779
|
+
mainTs = `import { render } from '@fluixi/core';
|
|
1780
|
+
import { App } from './App';
|
|
1781
|
+
import './styles/main.css';
|
|
1782
|
+
|
|
1783
|
+
const root = document.getElementById('app');
|
|
1784
|
+
|
|
1785
|
+
if (root) {
|
|
1786
|
+
render(App, root);
|
|
1787
|
+
}
|
|
1788
|
+
`;
|
|
1789
|
+
}
|
|
1790
|
+
else if (mode === 'react-bridge') {
|
|
1791
|
+
mainTs = `import React from 'react';
|
|
1792
|
+
import { createRoot } from 'react-dom/client';
|
|
1793
|
+
import { App } from './App';
|
|
1794
|
+
import './styles/main.css';
|
|
1795
|
+
|
|
1796
|
+
const root = document.getElementById('app');
|
|
1797
|
+
|
|
1798
|
+
if (root) {
|
|
1799
|
+
const reactRoot = createRoot(root);
|
|
1800
|
+
reactRoot.render(<App />);
|
|
1801
|
+
}
|
|
1802
|
+
`;
|
|
1803
|
+
}
|
|
1804
|
+
else {
|
|
1805
|
+
mainTs = `import { render, html } from '@fluixi/core';
|
|
1806
|
+
import { App } from './App';
|
|
1807
|
+
import './styles/main.css';
|
|
1808
|
+
|
|
1809
|
+
const root = document.getElementById('app');
|
|
1810
|
+
|
|
1811
|
+
if (root) {
|
|
1812
|
+
render(App, root);
|
|
1813
|
+
}
|
|
1814
|
+
`;
|
|
1815
|
+
}
|
|
1816
|
+
await fs.writeFile(path.join(projectPath, 'src/main.ts'), mainTs);
|
|
1817
|
+
// Generate App component based on mode
|
|
1818
|
+
const appTs = generateAppComponent(template, mode);
|
|
1819
|
+
await fs.writeFile(path.join(projectPath, `src/App.${appExt}`), appTs);
|
|
1820
|
+
// Generate example components if requested
|
|
1821
|
+
if (config.exampleComponents && config.exampleComponents.length > 0) {
|
|
1822
|
+
console.log(chalk.blue('\nGenerating example components...'));
|
|
1823
|
+
for (const component of config.exampleComponents) {
|
|
1824
|
+
await generateExampleComponent(projectPath, component, mode);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
// Generate example pages if requested
|
|
1828
|
+
if (config.examplePages && config.examplePages.length > 0) {
|
|
1829
|
+
console.log(chalk.blue('\nGenerating example pages...'));
|
|
1830
|
+
for (const page of config.examplePages) {
|
|
1831
|
+
await generateExamplePage(projectPath, page, mode);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
// Generate ESLint config
|
|
1835
|
+
if (config.includeESLint) {
|
|
1836
|
+
const eslintConfig = {
|
|
1837
|
+
parser: '@typescript-eslint/parser',
|
|
1838
|
+
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
|
|
1839
|
+
parserOptions: {
|
|
1840
|
+
ecmaVersion: 2020,
|
|
1841
|
+
sourceType: 'module',
|
|
1842
|
+
},
|
|
1843
|
+
rules: {},
|
|
1844
|
+
};
|
|
1845
|
+
await fs.writeJSON(path.join(projectPath, '.eslintrc.json'), eslintConfig, {
|
|
1846
|
+
spaces: 2,
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
// Generate Prettier config
|
|
1850
|
+
if (config.includePrettier) {
|
|
1851
|
+
const prettierConfig = {
|
|
1852
|
+
semi: true,
|
|
1853
|
+
trailingComma: 'es5',
|
|
1854
|
+
singleQuote: true,
|
|
1855
|
+
printWidth: 80,
|
|
1856
|
+
tabWidth: 2,
|
|
1857
|
+
};
|
|
1858
|
+
await fs.writeJSON(path.join(projectPath, '.prettierrc.json'), prettierConfig, {
|
|
1859
|
+
spaces: 2,
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
// Generate main.css
|
|
1863
|
+
const mainCss = `* {
|
|
1864
|
+
margin: 0;
|
|
1865
|
+
padding: 0;
|
|
1866
|
+
box-sizing: border-box;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
body {
|
|
1870
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
1871
|
+
line-height: 1.6;
|
|
1872
|
+
color: #333;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
#app {
|
|
1876
|
+
max-width: 1200px;
|
|
1877
|
+
margin: 0 auto;
|
|
1878
|
+
padding: 2rem;
|
|
1879
|
+
}
|
|
1880
|
+
`;
|
|
1881
|
+
await fs.writeFile(path.join(projectPath, 'src/styles/main.css'), mainCss);
|
|
1882
|
+
// Generate README
|
|
1883
|
+
const readme = `# ${projectName}
|
|
1884
|
+
|
|
1885
|
+
A Fluixi application.
|
|
1886
|
+
|
|
1887
|
+
## Getting Started
|
|
1888
|
+
|
|
1889
|
+
\`\`\`bash
|
|
1890
|
+
# Install dependencies
|
|
1891
|
+
npm install
|
|
1892
|
+
|
|
1893
|
+
# Start development server
|
|
1894
|
+
npm run dev
|
|
1895
|
+
|
|
1896
|
+
# Build for production
|
|
1897
|
+
npm run build
|
|
1898
|
+
|
|
1899
|
+
# Run tests
|
|
1900
|
+
npm test
|
|
1901
|
+
\`\`\`
|
|
1902
|
+
|
|
1903
|
+
## Project Structure
|
|
1904
|
+
|
|
1905
|
+
\`\`\`
|
|
1906
|
+
${projectName}/
|
|
1907
|
+
├── src/
|
|
1908
|
+
│ ├── components/ # Reusable components
|
|
1909
|
+
│ ├── pages/ # Page components
|
|
1910
|
+
│ ├── styles/ # Global styles
|
|
1911
|
+
│ ├── App.ts # Root component
|
|
1912
|
+
│ └── main.ts # Application entry point
|
|
1913
|
+
├── public/ # Static assets
|
|
1914
|
+
├── index.html # HTML template
|
|
1915
|
+
├── package.json
|
|
1916
|
+
├── tsconfig.json
|
|
1917
|
+
└── vite.config.ts
|
|
1918
|
+
\`\`\`
|
|
1919
|
+
|
|
1920
|
+
## Learn More
|
|
1921
|
+
|
|
1922
|
+
- [Fluixi Documentation](https://github.com/your-org/fluixi)
|
|
1923
|
+
- [Lit Documentation](https://lit.dev)
|
|
1924
|
+
`;
|
|
1925
|
+
await fs.writeFile(path.join(projectPath, 'README.md'), readme);
|
|
1926
|
+
console.log(chalk.green(`\n✓ Project created successfully!\n`));
|
|
1927
|
+
console.log(chalk.blue('Next steps:'));
|
|
1928
|
+
console.log(chalk.gray(` cd ${projectName}`));
|
|
1929
|
+
console.log(chalk.gray(` npm install`));
|
|
1930
|
+
console.log(chalk.gray(` npm run dev\n`));
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Generates the main App component
|
|
1934
|
+
* @param {ProjectTemplate} template - Template type
|
|
1935
|
+
* @param {ComponentMode} mode - Component mode
|
|
1936
|
+
* @returns {string} Component code
|
|
1937
|
+
*/
|
|
1938
|
+
function generateAppComponent(template, mode = 'lit-html') {
|
|
1939
|
+
if (mode === 'jsx') {
|
|
1940
|
+
return generateJSXAppComponent(template);
|
|
1941
|
+
}
|
|
1942
|
+
else if (mode === 'react-bridge') {
|
|
1943
|
+
return generateReactBridgeAppComponent(template);
|
|
1944
|
+
}
|
|
1945
|
+
// Default lit-html mode
|
|
1946
|
+
if (template === 'full') {
|
|
1947
|
+
return `import { html, createRouter, Router, Link, Component } from '@fluixi/core';
|
|
1948
|
+
|
|
1949
|
+
const Home = () => html\`
|
|
1950
|
+
<div>
|
|
1951
|
+
<h1>Home Page</h1>
|
|
1952
|
+
<p>Welcome to your Fluixi application!</p>
|
|
1953
|
+
</div>
|
|
1954
|
+
\`;
|
|
1955
|
+
|
|
1956
|
+
const About = () => html\`
|
|
1957
|
+
<div>
|
|
1958
|
+
<h1>About Page</h1>
|
|
1959
|
+
<p>This is a full-featured Fluixi application.</p>
|
|
1960
|
+
</div>
|
|
1961
|
+
\`;
|
|
1962
|
+
|
|
1963
|
+
const routes = [
|
|
1964
|
+
{ path: '/', component: Home },
|
|
1965
|
+
{ path: '/about', component: About },
|
|
1966
|
+
];
|
|
1967
|
+
|
|
1968
|
+
export const App: Component = () => {
|
|
1969
|
+
const router = createRouter(routes);
|
|
1970
|
+
|
|
1971
|
+
return Router({
|
|
1972
|
+
router,
|
|
1973
|
+
children: html\`
|
|
1974
|
+
<div class="app">
|
|
1975
|
+
<nav>
|
|
1976
|
+
\${Link({ href: '/', children: 'Home' })}
|
|
1977
|
+
\${Link({ href: '/about', children: 'About' })}
|
|
1978
|
+
</nav>
|
|
1979
|
+
<main id="content"></main>
|
|
1980
|
+
</div>
|
|
1981
|
+
\`,
|
|
1982
|
+
});
|
|
1983
|
+
};
|
|
1984
|
+
`;
|
|
1985
|
+
}
|
|
1986
|
+
return `import { html, createSignal, Component } from '@fluixi/core';
|
|
1987
|
+
|
|
1988
|
+
export const App: Component = () => {
|
|
1989
|
+
const [count, setCount] = createSignal(0);
|
|
1990
|
+
|
|
1991
|
+
return html\`
|
|
1992
|
+
<div class="app">
|
|
1993
|
+
<h1>Welcome to Fluixi</h1>
|
|
1994
|
+
<p>A reactive framework for lit-html</p>
|
|
1995
|
+
<div class="counter">
|
|
1996
|
+
<p>Count: \${count()}</p>
|
|
1997
|
+
<button @click=\${() => setCount(count() + 1)}>Increment</button>
|
|
1998
|
+
</div>
|
|
1999
|
+
</div>
|
|
2000
|
+
\`;
|
|
2001
|
+
};
|
|
2002
|
+
export default App;
|
|
2003
|
+
`;
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Generates JSX App component
|
|
2007
|
+
*/
|
|
2008
|
+
function generateJSXAppComponent(template) {
|
|
2009
|
+
return `/** @jsx h */
|
|
2010
|
+
import { h, Fragment } from '@fluixi/core/jsx-runtime';
|
|
2011
|
+
import { createSignal } from '@fluixi/reactive/signal';
|
|
2012
|
+
import './styles/main.css';
|
|
2013
|
+
|
|
2014
|
+
export function App() {
|
|
2015
|
+
const [count, setCount] = createSignal(0);
|
|
2016
|
+
|
|
2017
|
+
return (
|
|
2018
|
+
<div class="app">
|
|
2019
|
+
<header>
|
|
2020
|
+
<h1>Welcome to Fluixi</h1>
|
|
2021
|
+
<p>A reactive framework powered by signals</p>
|
|
2022
|
+
</header>
|
|
2023
|
+
|
|
2024
|
+
<main>
|
|
2025
|
+
<div class="counter">
|
|
2026
|
+
<h2>Counter Example</h2>
|
|
2027
|
+
<div class="controls">
|
|
2028
|
+
<button onClick={() => setCount(count() - 1)}>-</button>
|
|
2029
|
+
<span class="count">{count()}</span>
|
|
2030
|
+
<button onClick={() => setCount(count() + 1)}>+</button>
|
|
2031
|
+
</div>
|
|
2032
|
+
</div>
|
|
2033
|
+
</main>
|
|
2034
|
+
|
|
2035
|
+
<footer>
|
|
2036
|
+
<p>Built with Fluixi JSX</p>
|
|
2037
|
+
</footer>
|
|
2038
|
+
</div>
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
export default App;
|
|
2043
|
+
`;
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Generates React Bridge App component
|
|
2047
|
+
*/
|
|
2048
|
+
function generateReactBridgeAppComponent(template) {
|
|
2049
|
+
return `import React from 'react';
|
|
2050
|
+
import { useSignal, useFluixiEffect } from '@fluixi/core/react-bridge';
|
|
2051
|
+
import './styles/main.css';
|
|
2052
|
+
|
|
2053
|
+
export function App() {
|
|
2054
|
+
const [count, setCount] = useSignal(0);
|
|
2055
|
+
|
|
2056
|
+
useFluixiEffect(() => {
|
|
2057
|
+
console.log('Count changed:', count());
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
return (
|
|
2061
|
+
<div className="app">
|
|
2062
|
+
<header>
|
|
2063
|
+
<h1>Welcome to Fluixi</h1>
|
|
2064
|
+
<p>A reactive framework with React compatibility</p>
|
|
2065
|
+
</header>
|
|
2066
|
+
|
|
2067
|
+
<main>
|
|
2068
|
+
<div className="counter">
|
|
2069
|
+
<h2>Counter Example</h2>
|
|
2070
|
+
<div className="controls">
|
|
2071
|
+
<button onClick={() => setCount(count() - 1)}>-</button>
|
|
2072
|
+
<span className="count">{count()}</span>
|
|
2073
|
+
<button onClick={() => setCount(count() + 1)}>+</button>
|
|
2074
|
+
</div>
|
|
2075
|
+
</div>
|
|
2076
|
+
</main>
|
|
2077
|
+
|
|
2078
|
+
<footer>
|
|
2079
|
+
<p>Built with Fluixi React Bridge</p>
|
|
2080
|
+
</footer>
|
|
2081
|
+
</div>
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
export default App;
|
|
2086
|
+
`;
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Generate example component
|
|
2090
|
+
*/
|
|
2091
|
+
async function generateExampleComponent(projectPath, componentName, mode) {
|
|
2092
|
+
const componentDir = path.join(projectPath, 'src/components', componentName);
|
|
2093
|
+
await fs.ensureDir(componentDir);
|
|
2094
|
+
const ext = mode === 'lit-html' ? 'ts' : 'tsx';
|
|
2095
|
+
const componentPath = path.join(componentDir, `${componentName}.${ext}`);
|
|
2096
|
+
let componentCode = '';
|
|
2097
|
+
if (componentName === 'counter') {
|
|
2098
|
+
componentCode =
|
|
2099
|
+
mode === 'jsx'
|
|
2100
|
+
? generateJSXComponent('Counter', 'with-state')
|
|
2101
|
+
: mode === 'react-bridge'
|
|
2102
|
+
? generateReactBridgeComponent('Counter', 'with-state')
|
|
2103
|
+
: generateComponentWithState('Counter', 'functional');
|
|
2104
|
+
}
|
|
2105
|
+
else if (componentName === 'todolist') {
|
|
2106
|
+
componentCode = generateTodoListComponent(mode);
|
|
2107
|
+
}
|
|
2108
|
+
else if (componentName === 'usercard') {
|
|
2109
|
+
componentCode = generateUserCardComponent(mode);
|
|
2110
|
+
}
|
|
2111
|
+
else if (componentName === 'datafetcher') {
|
|
2112
|
+
componentCode = generateDataFetcherComponent(mode);
|
|
2113
|
+
}
|
|
2114
|
+
await fs.writeFile(componentPath, componentCode);
|
|
2115
|
+
// Generate CSS
|
|
2116
|
+
const cssPath = path.join(componentDir, `${componentName}.css`);
|
|
2117
|
+
await fs.writeFile(cssPath, generateStyleFile(componentName));
|
|
2118
|
+
console.log(chalk.gray(` ✓ ${componentName} component created`));
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Generate example page
|
|
2122
|
+
*/
|
|
2123
|
+
async function generateExamplePage(projectPath, pageName, mode) {
|
|
2124
|
+
const pageDir = path.join(projectPath, 'src/pages', pageName);
|
|
2125
|
+
await fs.ensureDir(pageDir);
|
|
2126
|
+
const ext = mode === 'lit-html' ? 'ts' : 'tsx';
|
|
2127
|
+
const pagePath = path.join(pageDir, `${pageName}.${ext}`);
|
|
2128
|
+
let pageCode = '';
|
|
2129
|
+
if (mode === 'jsx') {
|
|
2130
|
+
pageCode = `/** @jsx h */
|
|
2131
|
+
import { h } from '@fluixi/core/jsx-runtime';
|
|
2132
|
+
import './${pageName}.css';
|
|
2133
|
+
|
|
2134
|
+
export function ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page() {
|
|
2135
|
+
return (
|
|
2136
|
+
<div class="${pageName}-page">
|
|
2137
|
+
<h1>${pageName.charAt(0).toUpperCase() + pageName.slice(1)} Page</h1>
|
|
2138
|
+
<p>Welcome to the ${pageName} page.</p>
|
|
2139
|
+
</div>
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
export default ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page;
|
|
2144
|
+
`;
|
|
2145
|
+
}
|
|
2146
|
+
else if (mode === 'react-bridge') {
|
|
2147
|
+
pageCode = `import React from 'react';
|
|
2148
|
+
import './${pageName}.css';
|
|
2149
|
+
|
|
2150
|
+
export function ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page() {
|
|
2151
|
+
return (
|
|
2152
|
+
<div className="${pageName}-page">
|
|
2153
|
+
<h1>${pageName.charAt(0).toUpperCase() + pageName.slice(1)} Page</h1>
|
|
2154
|
+
<p>Welcome to the ${pageName} page.</p>
|
|
2155
|
+
</div>
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
export default ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page;
|
|
2160
|
+
`;
|
|
2161
|
+
}
|
|
2162
|
+
else {
|
|
2163
|
+
pageCode = `import { html } from 'lit';
|
|
2164
|
+
import './${pageName}.css';
|
|
2165
|
+
|
|
2166
|
+
export const ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page = () => html\`
|
|
2167
|
+
<div class="${pageName}-page">
|
|
2168
|
+
<h1>${pageName.charAt(0).toUpperCase() + pageName.slice(1)} Page</h1>
|
|
2169
|
+
<p>Welcome to the ${pageName} page.</p>
|
|
2170
|
+
</div>
|
|
2171
|
+
\`;
|
|
2172
|
+
|
|
2173
|
+
export default ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page;
|
|
2174
|
+
`;
|
|
2175
|
+
}
|
|
2176
|
+
await fs.writeFile(pagePath, pageCode);
|
|
2177
|
+
// Generate CSS
|
|
2178
|
+
const cssPath = path.join(pageDir, `${pageName}.css`);
|
|
2179
|
+
await fs.writeFile(cssPath, `.${pageName}-page { padding: 2rem; }`);
|
|
2180
|
+
console.log(chalk.gray(` ✓ ${pageName} page created`));
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Generate TodoList component
|
|
2184
|
+
*/
|
|
2185
|
+
function generateTodoListComponent(mode) {
|
|
2186
|
+
if (mode === 'jsx') {
|
|
2187
|
+
return `/** @jsx h */
|
|
2188
|
+
import { h } from '@fluixi/core/jsx-runtime';
|
|
2189
|
+
import { createSignal } from '@fluixi/reactive/signal';
|
|
2190
|
+
import './todolist.css';
|
|
2191
|
+
|
|
2192
|
+
export function TodoList() {
|
|
2193
|
+
const [todos, setTodos] = createSignal<string[]>([]);
|
|
2194
|
+
const [input, setInput] = createSignal('');
|
|
2195
|
+
|
|
2196
|
+
const addTodo = () => {
|
|
2197
|
+
if (input().trim()) {
|
|
2198
|
+
setTodos([...todos(), input()]);
|
|
2199
|
+
setInput('');
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
return (
|
|
2204
|
+
<div class="todolist">
|
|
2205
|
+
<h2>Todo List</h2>
|
|
2206
|
+
<div class="input-group">
|
|
2207
|
+
<input
|
|
2208
|
+
type="text"
|
|
2209
|
+
value={input()}
|
|
2210
|
+
onInput={(e) => setInput((e.target as HTMLInputElement).value)}
|
|
2211
|
+
placeholder="Add a todo..."
|
|
2212
|
+
/>
|
|
2213
|
+
<button onClick={addTodo}>Add</button>
|
|
2214
|
+
</div>
|
|
2215
|
+
<ul>
|
|
2216
|
+
{todos().map((todo, index) => (
|
|
2217
|
+
<li key={index}>{todo}</li>
|
|
2218
|
+
))}
|
|
2219
|
+
</ul>
|
|
2220
|
+
</div>
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
export default TodoList;
|
|
2225
|
+
`;
|
|
2226
|
+
}
|
|
2227
|
+
return `import { html } from 'lit';
|
|
2228
|
+
import { createSignal } from '@fluixi/reactive/signal';
|
|
2229
|
+
import './todolist.css';
|
|
2230
|
+
|
|
2231
|
+
export const TodoList = () => {
|
|
2232
|
+
const [todos, setTodos] = createSignal<string[]>([]);
|
|
2233
|
+
const [input, setInput] = createSignal('');
|
|
2234
|
+
|
|
2235
|
+
const addTodo = () => {
|
|
2236
|
+
if (input().trim()) {
|
|
2237
|
+
setTodos([...todos(), input()]);
|
|
2238
|
+
setInput('');
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
|
|
2242
|
+
return html\`
|
|
2243
|
+
<div class="todolist">
|
|
2244
|
+
<h2>Todo List</h2>
|
|
2245
|
+
<div class="input-group">
|
|
2246
|
+
<input
|
|
2247
|
+
type="text"
|
|
2248
|
+
.value=\${input()}
|
|
2249
|
+
@input=\${(e: Event) => setInput((e.target as HTMLInputElement).value)}
|
|
2250
|
+
placeholder="Add a todo..."
|
|
2251
|
+
/>
|
|
2252
|
+
<button @click=\${addTodo}>Add</button>
|
|
2253
|
+
</div>
|
|
2254
|
+
<ul>
|
|
2255
|
+
\${todos().map((todo) => html\`<li>\${todo}</li>\`)}
|
|
2256
|
+
</ul>
|
|
2257
|
+
</div>
|
|
2258
|
+
\`;
|
|
2259
|
+
};
|
|
2260
|
+
|
|
2261
|
+
export default TodoList;
|
|
2262
|
+
`;
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Generate UserCard component
|
|
2266
|
+
*/
|
|
2267
|
+
function generateUserCardComponent(mode) {
|
|
2268
|
+
if (mode === 'jsx') {
|
|
2269
|
+
return `/** @jsx h */
|
|
2270
|
+
import { h } from '@fluixi/core/jsx-runtime';
|
|
2271
|
+
import './usercard.css';
|
|
2272
|
+
|
|
2273
|
+
export interface UserCardProps {
|
|
2274
|
+
name: string;
|
|
2275
|
+
email: string;
|
|
2276
|
+
avatar?: string;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
export function UserCard({ name, email, avatar }: UserCardProps) {
|
|
2280
|
+
return (
|
|
2281
|
+
<div class="usercard">
|
|
2282
|
+
<img src={avatar || 'https://via.placeholder.com/100'} alt={name} />
|
|
2283
|
+
<h3>{name}</h3>
|
|
2284
|
+
<p>{email}</p>
|
|
2285
|
+
</div>
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
export default UserCard;
|
|
2290
|
+
`;
|
|
2291
|
+
}
|
|
2292
|
+
return `import { html } from 'lit';
|
|
2293
|
+
import './usercard.css';
|
|
2294
|
+
|
|
2295
|
+
export interface UserCardProps {
|
|
2296
|
+
name: string;
|
|
2297
|
+
email: string;
|
|
2298
|
+
avatar?: string;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
export const UserCard = (props: UserCardProps) => html\`
|
|
2302
|
+
<div class="usercard">
|
|
2303
|
+
<img src="\${props.avatar || 'https://via.placeholder.com/100'}" alt="\${props.name}" />
|
|
2304
|
+
<h3>\${props.name}</h3>
|
|
2305
|
+
<p>\${props.email}</p>
|
|
2306
|
+
</div>
|
|
2307
|
+
\`;
|
|
2308
|
+
|
|
2309
|
+
export default UserCard;
|
|
2310
|
+
`;
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Generate DataFetcher component
|
|
2314
|
+
*/
|
|
2315
|
+
function generateDataFetcherComponent(mode) {
|
|
2316
|
+
return `import { html } from 'lit';
|
|
2317
|
+
import { createResource } from '@fluixi/core/resources';
|
|
2318
|
+
import { Show } from '@fluixi/core/control-flow';
|
|
2319
|
+
import './datafetcher.css';
|
|
2320
|
+
|
|
2321
|
+
async function fetchData() {
|
|
2322
|
+
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
|
|
2323
|
+
return response.json();
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
export const DataFetcher = () => {
|
|
2327
|
+
const [data] = createResource(fetchData);
|
|
2328
|
+
|
|
2329
|
+
return html\`
|
|
2330
|
+
<div class="datafetcher">
|
|
2331
|
+
<h2>Data Fetcher</h2>
|
|
2332
|
+
\${Show({
|
|
2333
|
+
when: () => data.loading,
|
|
2334
|
+
children: html\`<p>Loading...</p>\`,
|
|
2335
|
+
})}
|
|
2336
|
+
\${Show({
|
|
2337
|
+
when: () => data.error,
|
|
2338
|
+
children: html\`<p>Error: \${data.error?.message}</p>\`,
|
|
2339
|
+
})}
|
|
2340
|
+
\${Show({
|
|
2341
|
+
when: () => data(),
|
|
2342
|
+
children: html\`
|
|
2343
|
+
<div>
|
|
2344
|
+
<h3>\${data()?.title}</h3>
|
|
2345
|
+
<p>\${data()?.body}</p>
|
|
2346
|
+
</div>
|
|
2347
|
+
\`,
|
|
2348
|
+
})}
|
|
2349
|
+
</div>
|
|
2350
|
+
\`;
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
export default DataFetcher;
|
|
2354
|
+
`;
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Interactive project generation
|
|
2358
|
+
*/
|
|
2359
|
+
async function interactiveProjectGeneration() {
|
|
2360
|
+
const initialAnswers = await inquirer.prompt([
|
|
2361
|
+
{
|
|
2362
|
+
type: 'input',
|
|
2363
|
+
name: 'name',
|
|
2364
|
+
message: 'Project name:',
|
|
2365
|
+
validate: (input) => {
|
|
2366
|
+
if (!input)
|
|
2367
|
+
return 'Project name is required';
|
|
2368
|
+
if (!/^[a-z][a-z0-9-]*$/.test(input)) {
|
|
2369
|
+
return 'Project name must start with lowercase letter and contain only lowercase letters, numbers, and hyphens';
|
|
2370
|
+
}
|
|
2371
|
+
return true;
|
|
2372
|
+
},
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
type: 'list',
|
|
2376
|
+
name: 'template',
|
|
2377
|
+
message: 'Project template:',
|
|
2378
|
+
choices: [
|
|
2379
|
+
{ name: 'Minimal (Basic setup)', value: 'minimal' },
|
|
2380
|
+
{ name: 'Standard (With router and examples)', value: 'standard' },
|
|
2381
|
+
{ name: 'Full (Complete setup with all features)', value: 'full' },
|
|
2382
|
+
],
|
|
2383
|
+
},
|
|
2384
|
+
{
|
|
2385
|
+
type: 'list',
|
|
2386
|
+
name: 'mode',
|
|
2387
|
+
message: 'Default component mode:',
|
|
2388
|
+
choices: [
|
|
2389
|
+
{ name: 'Lit-HTML (Native Fluixi)', value: 'lit-html' },
|
|
2390
|
+
{ name: 'JSX (Native JSX runtime)', value: 'jsx' },
|
|
2391
|
+
{ name: 'React Bridge (React compatibility)', value: 'react-bridge' },
|
|
2392
|
+
],
|
|
2393
|
+
default: 'lit-html',
|
|
2394
|
+
},
|
|
2395
|
+
{
|
|
2396
|
+
type: 'confirm',
|
|
2397
|
+
name: 'advancedConfig',
|
|
2398
|
+
message: 'Configure advanced project settings?',
|
|
2399
|
+
default: false,
|
|
2400
|
+
},
|
|
2401
|
+
]);
|
|
2402
|
+
let finalAnswers = { ...initialAnswers };
|
|
2403
|
+
// Advanced configuration
|
|
2404
|
+
if (initialAnswers.advancedConfig) {
|
|
2405
|
+
const advancedAnswers = await inquirer.prompt([
|
|
2406
|
+
{
|
|
2407
|
+
type: 'confirm',
|
|
2408
|
+
name: 'includeRouter',
|
|
2409
|
+
message: 'Include router setup?',
|
|
2410
|
+
default: initialAnswers.template !== 'minimal',
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
type: 'confirm',
|
|
2414
|
+
name: 'includeSSR',
|
|
2415
|
+
message: 'Include Server-Side Rendering (SSR) support?',
|
|
2416
|
+
default: false,
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
type: 'confirm',
|
|
2420
|
+
name: 'includeStateManagement',
|
|
2421
|
+
message: 'Include state management utilities?',
|
|
2422
|
+
default: initialAnswers.template === 'full',
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
type: 'confirm',
|
|
2426
|
+
name: 'includeTesting',
|
|
2427
|
+
message: 'Setup testing framework (Vitest)?',
|
|
2428
|
+
default: true,
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
type: 'confirm',
|
|
2432
|
+
name: 'includeESLint',
|
|
2433
|
+
message: 'Include ESLint configuration?',
|
|
2434
|
+
default: true,
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
type: 'confirm',
|
|
2438
|
+
name: 'includePrettier',
|
|
2439
|
+
message: 'Include Prettier configuration?',
|
|
2440
|
+
default: true,
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
type: 'list',
|
|
2444
|
+
name: 'cssPreprocessor',
|
|
2445
|
+
message: 'CSS preprocessor:',
|
|
2446
|
+
choices: [
|
|
2447
|
+
{ name: 'None (Plain CSS)', value: 'none' },
|
|
2448
|
+
{ name: 'SCSS/SASS', value: 'scss' },
|
|
2449
|
+
{ name: 'Less', value: 'less' },
|
|
2450
|
+
{ name: 'PostCSS', value: 'postcss' },
|
|
2451
|
+
],
|
|
2452
|
+
default: 'none',
|
|
2453
|
+
},
|
|
2454
|
+
{
|
|
2455
|
+
type: 'checkbox',
|
|
2456
|
+
name: 'exampleComponents',
|
|
2457
|
+
message: 'Generate example components:',
|
|
2458
|
+
choices: [
|
|
2459
|
+
{
|
|
2460
|
+
name: 'Counter (Basic state management)',
|
|
2461
|
+
value: 'counter',
|
|
2462
|
+
checked: true,
|
|
2463
|
+
},
|
|
2464
|
+
{ name: 'TodoList (CRUD operations)', value: 'todolist' },
|
|
2465
|
+
{ name: 'UserCard (Props & styling)', value: 'usercard' },
|
|
2466
|
+
{ name: 'DataFetcher (Async resources)', value: 'datafetcher' },
|
|
2467
|
+
],
|
|
2468
|
+
},
|
|
2469
|
+
{
|
|
2470
|
+
type: 'checkbox',
|
|
2471
|
+
name: 'examplePages',
|
|
2472
|
+
message: 'Generate example pages:',
|
|
2473
|
+
choices: [
|
|
2474
|
+
{ name: 'Home', value: 'home', checked: true },
|
|
2475
|
+
{ name: 'About', value: 'about' },
|
|
2476
|
+
{ name: 'Dashboard', value: 'dashboard' },
|
|
2477
|
+
{ name: 'NotFound (404)', value: 'notfound', checked: true },
|
|
2478
|
+
],
|
|
2479
|
+
},
|
|
2480
|
+
{
|
|
2481
|
+
type: 'input',
|
|
2482
|
+
name: 'author',
|
|
2483
|
+
message: 'Author name (optional):',
|
|
2484
|
+
default: '',
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
type: 'input',
|
|
2488
|
+
name: 'description',
|
|
2489
|
+
message: 'Project description (optional):',
|
|
2490
|
+
default: 'A Fluixi application',
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
type: 'input',
|
|
2494
|
+
name: 'license',
|
|
2495
|
+
message: 'License:',
|
|
2496
|
+
default: 'MIT',
|
|
2497
|
+
},
|
|
2498
|
+
]);
|
|
2499
|
+
finalAnswers = { ...finalAnswers, ...advancedAnswers };
|
|
2500
|
+
}
|
|
2501
|
+
await generateProject(finalAnswers.name, finalAnswers.template, finalAnswers.mode, finalAnswers);
|
|
2502
|
+
console.log(chalk.yellow('\nNote: Component mode preference saved.'));
|
|
2503
|
+
console.log(chalk.gray(`Default mode: ${finalAnswers.mode}`));
|
|
2504
|
+
}
|
|
2505
|
+
// CLI Commands
|
|
2506
|
+
program
|
|
2507
|
+
.name('fluixi')
|
|
2508
|
+
.description('Fluixi CLI - Generate projects and components')
|
|
2509
|
+
.version(VERSION);
|
|
2510
|
+
program
|
|
2511
|
+
.command('new [project-name]')
|
|
2512
|
+
.description('Create a new Fluixi project')
|
|
2513
|
+
.option('-t, --template <template>', 'Project template (minimal|standard|full)', 'standard')
|
|
2514
|
+
.action(async (projectName, options) => {
|
|
2515
|
+
try {
|
|
2516
|
+
if (projectName) {
|
|
2517
|
+
await generateProject(projectName, options?.template || 'standard');
|
|
2518
|
+
}
|
|
2519
|
+
else {
|
|
2520
|
+
await interactiveProjectGeneration();
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
catch (error) {
|
|
2524
|
+
console.error(chalk.red('Error creating project:'), error);
|
|
2525
|
+
process.exit(1);
|
|
2526
|
+
}
|
|
2527
|
+
});
|
|
2528
|
+
program
|
|
2529
|
+
.command('generate <type> [name]')
|
|
2530
|
+
.alias('g')
|
|
2531
|
+
.description('Generate a component or other artifact')
|
|
2532
|
+
.option('-t, --type <type>', 'Component type (basic|with-state|with-resource|with-router|full)')
|
|
2533
|
+
.option('-p, --path <path>', 'Component path')
|
|
2534
|
+
.option('-m, --mode <mode>', 'Component mode (lit-html|jsx|react-bridge)')
|
|
2535
|
+
.action(async (type, name, options) => {
|
|
2536
|
+
try {
|
|
2537
|
+
if (type === 'component' || type === 'c') {
|
|
2538
|
+
if (name && options?.path) {
|
|
2539
|
+
const componentType = options.type || 'basic';
|
|
2540
|
+
const componentMode = options.mode || 'lit-html';
|
|
2541
|
+
const componentPath = path.join(process.cwd(), 'src', options.path);
|
|
2542
|
+
await createComponent(componentPath, name, componentType, 'functional', componentMode);
|
|
2543
|
+
}
|
|
2544
|
+
else {
|
|
2545
|
+
await interactiveComponentGeneration();
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
else {
|
|
2549
|
+
console.error(chalk.red(`Unknown type: ${type}`));
|
|
2550
|
+
console.log(chalk.gray('Available types: component (or c)'));
|
|
2551
|
+
process.exit(1);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
catch (error) {
|
|
2555
|
+
console.error(chalk.red('Error generating component:'), error);
|
|
2556
|
+
process.exit(1);
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
program
|
|
2560
|
+
.command('info')
|
|
2561
|
+
.description('Display Fluixi information')
|
|
2562
|
+
.action(() => {
|
|
2563
|
+
console.log(chalk.blue('\n╔═══════════════════════════════════╗'));
|
|
2564
|
+
console.log(chalk.blue('║ Fluixi CLI v' + VERSION + ' ║'));
|
|
2565
|
+
console.log(chalk.blue('╚═══════════════════════════════════╝\n'));
|
|
2566
|
+
console.log(chalk.gray('A reactive framework for lit-html\n'));
|
|
2567
|
+
console.log('Commands:');
|
|
2568
|
+
console.log(chalk.cyan(' new [project-name] ') + 'Create a new project');
|
|
2569
|
+
console.log(chalk.cyan(' generate <type> [name] ') + 'Generate a component');
|
|
2570
|
+
console.log(chalk.cyan(' info ') + 'Show this information');
|
|
2571
|
+
console.log(chalk.cyan(' help ') + 'Show help\n');
|
|
2572
|
+
console.log('Examples:');
|
|
2573
|
+
console.log(chalk.gray(' fluixi new my-app'));
|
|
2574
|
+
console.log(chalk.gray(' fluixi generate component MyComponent'));
|
|
2575
|
+
console.log(chalk.gray(' fluixi generate component MyComponent --mode jsx'));
|
|
2576
|
+
console.log(chalk.gray(' fluixi generate component MyComponent --mode react-bridge'));
|
|
2577
|
+
console.log(chalk.gray(' fluixi g c MyComponent -t with-state -p components/MyComponent/MyComponent.ts\n'));
|
|
2578
|
+
});
|
|
2579
|
+
// Parse CLI arguments
|
|
2580
|
+
program.parse();
|