fiboui 2.6.3 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +16 -6
  2. package/dist/cli/bin.js +3 -4
  3. package/dist/cli/commands/generate/component.js +21 -33
  4. package/dist/cli/commands/generate/index.js +3 -3
  5. package/dist/cli/commands/generate/module.js +21 -24
  6. package/dist/cli/commands/generate/plugin.js +18 -34
  7. package/dist/cli/commands/init.js +5 -25
  8. package/dist/cli/config.js +118 -0
  9. package/dist/cli/names.js +35 -0
  10. package/dist/cli/templates.js +76 -0
  11. package/dist/client-CGjHCx8F.js +17621 -0
  12. package/dist/content.d.ts +1 -0
  13. package/dist/content.js +14 -14
  14. package/dist/core.d.ts +2 -0
  15. package/dist/core.js +65 -59
  16. package/dist/effects-BxvduC-G.js +14 -0
  17. package/dist/icons-GEqYd2M9.js +383 -0
  18. package/dist/index-7bieEGAP.js +944 -0
  19. package/dist/index-C4qvvr03.js +1474 -0
  20. package/dist/index-D-ljLuf8.js +839 -0
  21. package/dist/index-DVApKYWt.js +217 -0
  22. package/dist/index-Db8lVjta.js +150 -0
  23. package/dist/index.d.ts +73156 -505
  24. package/dist/index.js +4419 -2237
  25. package/dist/interactive.d.ts +2 -0
  26. package/dist/interactive.js +22 -9
  27. package/dist/layout.d.ts +1 -0
  28. package/dist/layout.js +14 -13
  29. package/dist/metadata-3YYH1uiS.js +147 -0
  30. package/dist/navigation.d.ts +1 -0
  31. package/dist/navigation.js +15 -9
  32. package/dist/page-CYqQOLXe.js +266 -0
  33. package/dist/server.d.ts +2 -0
  34. package/dist/server.js +5001 -1777
  35. package/dist/structure.d.ts +1 -0
  36. package/dist/structure.js +13 -9
  37. package/dist/style.css +1 -1
  38. package/dist/theme.d.ts +2 -0
  39. package/dist/theme.js +8 -8
  40. package/package.json +9 -2
  41. package/dist/Link-l2KUUUg4.js +0 -50
  42. package/dist/PluginManager-BRjgOLVa.js +0 -110
  43. package/dist/client-BtHxgLa_.js +0 -10602
  44. package/dist/effects-D16XdB_d.js +0 -10
  45. package/dist/index-Bhq7-OrE.js +0 -602
  46. package/dist/index-CZJqskaC.js +0 -1028
  47. package/dist/index-D50CTdE9.js +0 -455
  48. package/dist/index-D5Ru-8kF.js +0 -608
  49. package/dist/index-DjwLjHe1.js +0 -119
  50. package/dist/mount-yYCj33dL.js +0 -135
package/README.md CHANGED
@@ -26,12 +26,12 @@ There is no JSX — the function return **is** the view.
26
26
 
27
27
  ### Live product
28
28
 
29
- | Marketing site | Store demo |
29
+ | Marketing site | Store (in-app `/shop`) |
30
30
  | :---: | :---: |
31
- | [![FiboUI site](./docs/brand/site-hero.png)](https://fiboui.vercel.app) | [![Fibo Store](./docs/brand/store-hero.png)](https://fiboui-store.vercel.app) |
32
- | [**fiboui.vercel.app**](https://fiboui.vercel.app) | [**fiboui-store.vercel.app**](https://fiboui-store.vercel.app) |
31
+ | [![FiboUI site](./docs/brand/site-hero.png)](https://fiboui.vercel.app) | [![Fibo Store](./docs/brand/store-hero.png)](https://fiboui.vercel.app/shop) |
32
+ | [**fiboui.vercel.app**](https://fiboui.vercel.app) | [**fiboui.vercel.app/shop**](https://fiboui.vercel.app/shop) |
33
33
 
34
- Both ships are built **only** with FiboUI fluent builders — no JSX in the product UI.
34
+ Both ships are built **only** with FiboUI fluent builders — no JSX in the product UI. The store is `/shop` (alias `/store`). The playground is [`/play`](https://fiboui.vercel.app/play) (alias `/playground`). Same Vercel app, not extra deployments.
35
35
 
36
36
  </div>
37
37
 
@@ -71,7 +71,16 @@ function App() {
71
71
  mount('#root', App)
72
72
  ```
73
73
 
74
- ESM-only · React **18 / 19** · Next App Router → `'use client'` on files that call builders.
74
+ Tree-shakeable entries (each has its own `types`): `fiboui/core`, `fiboui/layout`, `fiboui/content`, `fiboui/interactive`, `fiboui/structure`, `fiboui/navigation`, `fiboui/theme`, `fiboui/server`.
75
+
76
+ ```tsx
77
+ import { signal } from 'fiboui/core'
78
+ import { Column } from 'fiboui/layout'
79
+ import { Button } from 'fiboui/interactive'
80
+ import { page } from 'fiboui/server'
81
+ ```
82
+
83
+ ESM-only · React **18 / 19** · Next App Router: static builders + `page()` work in Server Components (`fiboui/server` / the `react-server` export); signals / Router still need `'use client'`.
75
84
 
76
85
  ---
77
86
 
@@ -247,6 +256,7 @@ Column().crossAxisAlignment('stretch')
247
256
 
248
257
  | | |
249
258
  | :--- | :--- |
259
+ | Getting started | [`docs/guide/getting-started.md`](./docs/guide/getting-started.md) |
250
260
  | Migration from 1.x | [MIGRATION.md](./MIGRATION.md) |
251
261
  | Contributing & release | [CONTRIBUTING.md](./CONTRIBUTING.md) |
252
262
  | Animation guide | [`docs/guide/animation.md`](./docs/guide/animation.md) |
@@ -266,6 +276,6 @@ Publishing is automated on **`main`** (GitHub Actions + `NPM_TOKEN`). Prefer not
266
276
 
267
277
  MIT © 2025–2026 FiboUI
268
278
 
269
- [Site](https://fiboui.vercel.app) · [Store](https://fiboui-store.vercel.app) · [npm](https://www.npmjs.com/package/fiboui) · [GitHub](https://github.com/felipeoliveiradev/fiboui)
279
+ [Site](https://fiboui.vercel.app) · [Store](https://fiboui.vercel.app/shop) · [Play](https://fiboui.vercel.app/play) · [npm](https://www.npmjs.com/package/fiboui) · [GitHub](https://github.com/felipeoliveiradev/fiboui)
270
280
 
271
281
  </div>
package/dist/cli/bin.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import { Command } from 'commander';
4
- import { generateCommand } from './commands/generate';
5
- import { initCommand } from './commands/init';
4
+ import { generateCommand } from './commands/generate/index.js';
5
+ import { initCommand } from './commands/init.js';
6
6
  const require = createRequire(import.meta.url);
7
7
  const pkg = require('../../package.json');
8
8
  const version = pkg.version;
@@ -11,10 +11,9 @@ program
11
11
  .name('fibo')
12
12
  .description('FiboUI CLI - Supercharge your development')
13
13
  .version(version);
14
- // ... (version import)
15
14
  program.addCommand(initCommand);
16
15
  program.addCommand(generateCommand);
17
- program.parse(process.argv);
16
+ await program.parseAsync(process.argv);
18
17
  if (!process.argv.slice(2).length) {
19
18
  program.outputHelp();
20
19
  }
@@ -4,64 +4,52 @@ import chalk from 'chalk';
4
4
  import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import ora from 'ora';
7
+ import { joinBasename, loadFiboConfig, resolveGenerateDir } from '../../config.js';
8
+ import { toPascalCase } from '../../names.js';
9
+ import { componentTemplate } from '../../templates.js';
7
10
  export const componentCommand = new Command('component')
8
11
  .alias('c')
9
12
  .description('Generate a new FiboUI functional component')
10
13
  .argument('[name]', 'Name of the component')
11
14
  .action(async (name) => {
12
- let componentName = name;
13
- if (!componentName) {
15
+ let rawName = name;
16
+ if (!rawName) {
14
17
  const response = await prompts({
15
18
  type: 'text',
16
19
  name: 'value',
17
20
  message: 'What is the name of your Component?',
18
- validate: value => value.length < 3 ? 'Component name must be at least 3 characters' : true
21
+ validate: (value) => (value.length < 3 ? 'Component name must be at least 3 characters' : true),
19
22
  });
20
- componentName = response.value;
23
+ rawName = response.value;
21
24
  }
25
+ if (!rawName)
26
+ process.exit(1);
27
+ const componentName = toPascalCase(rawName);
22
28
  if (!componentName)
23
29
  process.exit(1);
24
30
  const spinner = ora(`Scaffolding component ${chalk.bold(componentName)}...`).start();
25
- // Try to guess location: src/components or current dir
26
- const srcComponents = path.resolve(process.cwd(), 'src/components');
27
- const targetDir = (await fs.pathExists(srcComponents)) ? srcComponents : process.cwd();
28
- // Create folder for component? Or just file? Let's do file for simplicity or folder if preferred.
29
- // Standard: src/components/MyComponent/index.tsx
30
- const componentDir = path.join(targetDir, componentName);
31
- const targetFile = path.join(componentDir, 'index.tsx');
32
31
  try {
32
+ const cwd = process.cwd();
33
+ const config = await loadFiboConfig(cwd);
34
+ const srcComponents = path.resolve(cwd, 'src/components');
35
+ const targetDir = resolveGenerateDir('component', cwd, config, undefined, {
36
+ componentsDirExists: await fs.pathExists(srcComponents),
37
+ });
38
+ const componentDir = joinBasename(targetDir, componentName);
39
+ const targetFile = joinBasename(componentDir, 'index.tsx');
33
40
  if (await fs.pathExists(componentDir)) {
34
41
  spinner.fail(`Component ${componentName} already exists at ${componentDir}`);
42
+ process.exitCode = 1;
35
43
  return;
36
44
  }
37
45
  await fs.ensureDir(componentDir);
38
- const content = `import React from 'react';
39
- import { Component, Text, Row, FiboBuilder } from 'fiboui';
40
-
41
- export interface ${componentName}Props {
42
- title?: string;
43
- }
44
-
45
- export const ${componentName} = Component<${componentName}Props>()(
46
- ({ title = '${componentName}' }) => {
47
- return Row()
48
- .padding(16)
49
- .bg('white')
50
- .rounded('lg')
51
- .shadow('sm')
52
- .children([
53
- Text(title).size(18).weight('bold').build()
54
- ])
55
- .build();
56
- }
57
- );
58
- `;
59
- await fs.writeFile(targetFile, content);
46
+ await fs.writeFile(targetFile, componentTemplate(componentName));
60
47
  spinner.succeed(`Component ${chalk.green(componentName)} created successfully!`);
61
48
  console.log(chalk.dim(`\nLocated at: ${targetFile}`));
62
49
  }
63
50
  catch (error) {
64
51
  spinner.fail('Failed to create component');
65
52
  console.error(error);
53
+ process.exitCode = 1;
66
54
  }
67
55
  });
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
- import { moduleCommand } from './module';
3
- import { pluginCommand } from './plugin';
4
- import { componentCommand } from './component';
2
+ import { moduleCommand } from './module.js';
3
+ import { pluginCommand } from './plugin.js';
4
+ import { componentCommand } from './component.js';
5
5
  export const generateCommand = new Command('generate')
6
6
  .alias('g')
7
7
  .description('Generate new architectural components (modules, plugins, etc.)')
@@ -4,55 +4,52 @@ import chalk from 'chalk';
4
4
  import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import ora from 'ora';
7
+ import { loadFiboConfig, resolveGenerateDir, resolveUnderRoot } from '../../config.js';
8
+ import { toPascalCase } from '../../names.js';
9
+ import { moduleFiles } from '../../templates.js';
7
10
  export const moduleCommand = new Command('module')
8
11
  .alias('m')
9
12
  .description('Generate a new clean architecture module')
10
13
  .argument('[name]', 'Name of the module')
11
14
  .action(async (name) => {
12
- let moduleName = name;
13
- if (!moduleName) {
15
+ let rawName = name;
16
+ if (!rawName) {
14
17
  const response = await prompts({
15
18
  type: 'text',
16
19
  name: 'value',
17
20
  message: 'What is the name of your module?',
18
- validate: value => value.length < 3 ? 'Module name must be at least 3 characters' : true
21
+ validate: (value) => (value.length < 3 ? 'Module name must be at least 3 characters' : true),
19
22
  });
20
- moduleName = response.value;
23
+ rawName = response.value;
21
24
  }
25
+ if (!rawName)
26
+ process.exit(1);
27
+ const moduleName = toPascalCase(rawName);
22
28
  if (!moduleName)
23
29
  process.exit(1);
24
30
  const spinner = ora(`Scaffolding module ${chalk.bold(moduleName)}...`).start();
25
- // Normalize path: assume we are in project root or src
26
- const targetDir = path.resolve(process.cwd(), 'src/modules', moduleName);
27
31
  try {
32
+ const cwd = process.cwd();
33
+ const config = await loadFiboConfig(cwd);
34
+ const targetDir = resolveGenerateDir('module', cwd, config, moduleName);
28
35
  if (await fs.pathExists(targetDir)) {
29
36
  spinner.fail(`Module ${moduleName} already exists at ${targetDir}`);
37
+ process.exitCode = 1;
30
38
  return;
31
39
  }
32
40
  await fs.ensureDir(targetDir);
33
- // 1. Domain Layer
34
- await fs.ensureDir(path.join(targetDir, 'domain/entities'));
35
- await fs.ensureDir(path.join(targetDir, 'domain/repositories'));
36
- await fs.writeFile(path.join(targetDir, 'domain/entities', `${moduleName}.entity.ts`), `export interface ${capitalize(moduleName)} {\n id: string;\n // Add properties\n}\n`);
37
- await fs.writeFile(path.join(targetDir, 'domain/repositories', `${moduleName}.repository.ts`), `import { ${capitalize(moduleName)} } from '../entities/${moduleName}.entity';\n\nexport interface I${capitalize(moduleName)}Repository {\n getAll(): Promise<${capitalize(moduleName)}[]>;\n getById(id: string): Promise<${capitalize(moduleName)} | null>;\n}\n`);
38
- // 2. Application Layer (Use Cases)
39
- await fs.ensureDir(path.join(targetDir, 'application/usecases'));
40
- await fs.writeFile(path.join(targetDir, 'application/usecases', `Get${capitalize(moduleName)}.usecase.ts`), `import { I${capitalize(moduleName)}Repository } from '../../domain/repositories/${moduleName}.repository';\n\nexport class Get${capitalize(moduleName)}UseCase {\n constructor(private repo: I${capitalize(moduleName)}Repository) {}\n\n async execute() {\n return this.repo.getAll();\n }\n}\n`);
41
- // 3. Infrastructure Layer
42
- await fs.ensureDir(path.join(targetDir, 'infrastructure/api'));
43
- await fs.writeFile(path.join(targetDir, 'infrastructure/api', `${moduleName}.api.ts`), `import { I${capitalize(moduleName)}Repository } from '../../domain/repositories/${moduleName}.repository';\nimport { ${capitalize(moduleName)} } from '../../domain/entities/${moduleName}.entity';\n\nexport class ${capitalize(moduleName)}ApiRepository implements I${capitalize(moduleName)}Repository {\n async getAll(): Promise<${capitalize(moduleName)}[]> {\n // Implement API call\n return [];\n }\n\n async getById(id: string): Promise<${capitalize(moduleName)} | null> {\n return null;\n }\n}\n`);
44
- // 4. Presentation Layer (UI)
45
- await fs.ensureDir(path.join(targetDir, 'presentation/components'));
46
- await fs.ensureDir(path.join(targetDir, 'presentation/pages'));
47
- await fs.writeFile(path.join(targetDir, 'presentation/pages', `${capitalize(moduleName)}Page.tsx`), `import React from 'react';\nimport { Column, Text, FiboBuilder } from 'fiboui';\n\nexport const ${capitalize(moduleName)}Page = () => \n Column()\n .center()\n .gap(20)\n .children([\n Text('${capitalize(moduleName)} Module').size(24).weight('bold').build()\n ])\n .build();\n`);
41
+ const files = moduleFiles(moduleName);
42
+ for (const [rel, content] of Object.entries(files)) {
43
+ const full = resolveUnderRoot(targetDir, rel);
44
+ await fs.ensureDir(path.dirname(full));
45
+ await fs.writeFile(full, content);
46
+ }
48
47
  spinner.succeed(`Module ${chalk.green(moduleName)} created successfully!`);
49
48
  console.log(chalk.dim(`\nLocated at: ${targetDir}`));
50
49
  }
51
50
  catch (error) {
52
51
  spinner.fail('Failed to create module');
53
52
  console.error(error);
53
+ process.exitCode = 1;
54
54
  }
55
55
  });
56
- function capitalize(str) {
57
- return str.charAt(0).toUpperCase() + str.slice(1);
58
- }
@@ -2,60 +2,43 @@ import { Command } from 'commander';
2
2
  import prompts from 'prompts';
3
3
  import chalk from 'chalk';
4
4
  import fs from 'fs-extra';
5
- import path from 'path';
6
5
  import ora from 'ora';
6
+ import { joinBasename, loadFiboConfig, resolveGenerateDir } from '../../config.js';
7
+ import { pluginNames } from '../../names.js';
8
+ import { pluginTemplate } from '../../templates.js';
7
9
  export const pluginCommand = new Command('plugin')
8
10
  .alias('p')
9
11
  .description('Generate a new FiboUI plugin')
10
12
  .argument('[name]', 'Name of the plugin')
11
13
  .action(async (name) => {
12
- let pluginName = name;
13
- if (!pluginName) {
14
+ let rawName = name;
15
+ if (!rawName) {
14
16
  const response = await prompts({
15
17
  type: 'text',
16
18
  name: 'value',
17
19
  message: 'What is the name of your Plugin?',
18
- validate: value => value.length < 3 ? 'Plugin name must be at least 3 characters' : true
20
+ validate: (value) => (value.length < 3 ? 'Plugin name must be at least 3 characters' : true),
19
21
  });
20
- pluginName = response.value;
22
+ rawName = response.value;
21
23
  }
22
- if (!pluginName)
24
+ if (!rawName)
25
+ process.exit(1);
26
+ const { safeName } = pluginNames(rawName);
27
+ if (!safeName)
23
28
  process.exit(1);
24
- const safeName = pluginName.endsWith('Plugin') ? pluginName : `${pluginName}Plugin`;
25
29
  const spinner = ora(`Scaffolding plugin ${chalk.bold(safeName)}...`).start();
26
- const targetDir = path.resolve(process.cwd(), 'src/plugins');
27
- const targetFile = path.resolve(targetDir, `${safeName}.ts`);
28
30
  try {
31
+ const cwd = process.cwd();
32
+ const config = await loadFiboConfig(cwd);
33
+ const targetDir = resolveGenerateDir('plugin', cwd, config);
34
+ const targetFile = joinBasename(targetDir, `${safeName}.ts`);
29
35
  await fs.ensureDir(targetDir);
30
36
  if (await fs.pathExists(targetFile)) {
31
37
  spinner.fail(`Plugin ${safeName} already exists at ${targetFile}`);
38
+ process.exitCode = 1;
32
39
  return;
33
40
  }
34
- const content = `import { FiboPlugin, FiboContext, Card, Text, Icon } from 'fiboui';
35
- import { Settings } from 'lucide-react';
36
-
37
- export const ${safeName}: FiboPlugin = {
38
- name: '${safeName}',
39
- version: '1.0.0',
40
- setup: (ctx: FiboContext) => {
41
- console.log('Initializing ${safeName}...');
42
-
43
- // Example: Register Sidebar Item
44
- ctx.registerSidebarItem({
45
- label: '${pluginName.replace('Plugin', '')}',
46
- path: '/${pluginName.toLowerCase().replace('plugin', '')}',
47
- icon: Settings, // Example icon
48
- order: 10
49
- });
50
-
51
- // Example: Register an Overlay
52
- // ctx.registerOverlay(() =>
53
- // Card().padding(10).children([ Text('Hello from ${safeName}').build() ]).build()
54
- // );
55
- }
56
- };
57
- `;
58
- await fs.writeFile(targetFile, content);
41
+ await fs.writeFile(targetFile, pluginTemplate(rawName));
59
42
  spinner.succeed(`Plugin ${chalk.green(safeName)} created successfully!`);
60
43
  console.log(chalk.dim(`\nLocated at: ${targetFile}`));
61
44
  console.log(chalk.blue(`\nNext step: Register it in your main.tsx using .plugins([${safeName}])`));
@@ -63,5 +46,6 @@ export const ${safeName}: FiboPlugin = {
63
46
  catch (error) {
64
47
  spinner.fail('Failed to create plugin');
65
48
  console.error(error);
49
+ process.exitCode = 1;
66
50
  }
67
51
  });
@@ -3,49 +3,28 @@ import chalk from 'chalk';
3
3
  import fs from 'fs-extra';
4
4
  import path from 'path';
5
5
  import ora from 'ora';
6
+ import { initConfigSource } from '../templates.js';
6
7
  export const initCommand = new Command('init')
7
8
  .description('Initialize FiboUI configuration')
8
9
  .action(async () => {
9
10
  const spinner = ora('Checking project structure...').start();
10
- const configPath = path.resolve(process.cwd(), 'fibo.config.js'); // Keeping it JS for wider compatibility or TS? User asked for ts.
11
+ const configPath = path.resolve(process.cwd(), 'fibo.config.js');
11
12
  if (await fs.pathExists(configPath)) {
12
13
  spinner.fail('fibo.config.js already exists.');
14
+ process.exitCode = 1;
13
15
  return;
14
16
  }
15
- // Heuristics for standard architecture
16
17
  const hasModules = await fs.pathExists(path.resolve(process.cwd(), 'src/modules'));
17
18
  const hasPlugins = await fs.pathExists(path.resolve(process.cwd(), 'src/plugins'));
18
19
  const hasComponents = await fs.pathExists(path.resolve(process.cwd(), 'src/components'));
19
20
  const isStandard = hasModules && hasPlugins && hasComponents;
20
- let configContent = '';
21
+ const configContent = initConfigSource(isStandard);
21
22
  if (isStandard) {
22
23
  spinner.succeed('Standard FiboUI architecture detected.');
23
- configContent = `/** @type {import('fiboui').FiboConfig} */
24
- module.exports = {
25
- paths: {
26
- modules: 'src/modules',
27
- plugins: 'src/plugins',
28
- components: 'src/components',
29
- core: 'src/core'
30
- },
31
- architecture: 'clean'
32
- };
33
- `;
34
24
  }
35
25
  else {
36
26
  spinner.warn('Standard architecture NOT detected.');
37
27
  console.log(chalk.yellow('Creating configuration with default/empty values. Please update "fibo.config.js" with your project paths.'));
38
- configContent = `/** @type {import('fiboui').FiboConfig} */
39
- module.exports = {
40
- paths: {
41
- modules: 'src/modules', // Update this
42
- plugins: 'src/plugins', // Update this
43
- components: 'src/components', // Update this
44
- core: 'src/core' // Update this
45
- },
46
- architecture: 'custom'
47
- };
48
- `;
49
28
  }
50
29
  try {
51
30
  await fs.writeFile(configPath, configContent);
@@ -54,5 +33,6 @@ module.exports = {
54
33
  }
55
34
  catch (error) {
56
35
  console.error(chalk.red('Failed to write config file.'), error);
36
+ process.exitCode = 1;
57
37
  }
58
38
  });
@@ -0,0 +1,118 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { pathToFileURL } from 'node:url';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ const execFileAsync = promisify(execFile);
7
+ const CONFIG_EXPORT_MARK = '__FIBO_CLI_CONFIG__';
8
+ const CONFIG_FILES = ['fibo.config.js', 'fibo.config.mjs', 'fibo.config.cjs'];
9
+ /** Parse `export default { … }` or `module.exports = { … }` data configs. */
10
+ export function parseFiboConfigSource(source) {
11
+ const trimmed = source.trim();
12
+ const body = trimmed.match(/export\s+default\s+(\{[\s\S]*\})\s*;?\s*$/)?.[1] ??
13
+ trimmed.match(/module\.exports\s*=\s*(\{[\s\S]*\})\s*;?\s*$/)?.[1];
14
+ if (!body)
15
+ return null;
16
+ const jsonish = body
17
+ .replace(/\/\/[^\n]*/g, '')
18
+ .replace(/'([^'\\]*)'/g, '"$1"')
19
+ .replace(/([,{]\s*)([A-Za-z_][\w]*)\s*:/g, '$1"$2":')
20
+ .replace(/,(\s*[}\]])/g, '$1');
21
+ try {
22
+ const cfg = JSON.parse(jsonish);
23
+ if (!cfg || typeof cfg !== 'object')
24
+ return null;
25
+ return { paths: cfg.paths ?? {}, architecture: cfg.architecture };
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ function normalizeFiboConfig(raw) {
32
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
33
+ return null;
34
+ const cfg = raw;
35
+ if (cfg.paths != null && (typeof cfg.paths !== 'object' || Array.isArray(cfg.paths)))
36
+ return null;
37
+ return { paths: cfg.paths ?? {}, architecture: cfg.architecture };
38
+ }
39
+ /** Load `fibo.config.*` as a real Node module (computed ESM/CJS), not a JSON-ish regex. */
40
+ async function importConfigModule(full) {
41
+ const href = pathToFileURL(full).href;
42
+ const script = `import(${JSON.stringify(href)}).then((m)=>{process.stdout.write(${JSON.stringify(CONFIG_EXPORT_MARK)}+JSON.stringify(m.default??null))})`;
43
+ const { stdout } = await execFileAsync(process.execPath, ['--input-type=module', '-e', script], {
44
+ timeout: 8000,
45
+ maxBuffer: 256 * 1024,
46
+ windowsHide: true,
47
+ encoding: 'utf8',
48
+ });
49
+ const idx = stdout.lastIndexOf(CONFIG_EXPORT_MARK);
50
+ if (idx === -1)
51
+ return undefined;
52
+ return JSON.parse(stdout.slice(idx + CONFIG_EXPORT_MARK.length));
53
+ }
54
+ export async function loadFiboConfig(cwd) {
55
+ for (const name of CONFIG_FILES) {
56
+ const full = path.resolve(cwd, name);
57
+ if (!(await fs.pathExists(full)))
58
+ continue;
59
+ let loaded = null;
60
+ try {
61
+ loaded = normalizeFiboConfig(await importConfigModule(full));
62
+ }
63
+ catch {
64
+ loaded = null;
65
+ }
66
+ if (!loaded) {
67
+ const source = await fs.readFile(full, 'utf8');
68
+ loaded = parseFiboConfigSource(source);
69
+ }
70
+ if (!loaded) {
71
+ throw new Error(`Invalid ${name}: could not load FiboUI config`);
72
+ }
73
+ return loaded;
74
+ }
75
+ return null;
76
+ }
77
+ export function isInsideRoot(root, target) {
78
+ const resolvedRoot = path.resolve(root);
79
+ const resolvedTarget = path.resolve(target);
80
+ const rel = path.relative(resolvedRoot, resolvedTarget);
81
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel));
82
+ }
83
+ /** Join `dir` with a single path segment. Rejects `..`, separators, and empty names. */
84
+ export function joinBasename(dir, name) {
85
+ if (typeof name !== 'string' || name.length === 0 || name === '.' || name === '..' || name.includes('\0')) {
86
+ throw new Error(`Invalid path segment: ${name}`);
87
+ }
88
+ if (/[/\\]/.test(name) || path.basename(name) !== name) {
89
+ throw new Error(`Invalid path segment: ${name}`);
90
+ }
91
+ const resolved = path.resolve(dir, name);
92
+ if (!isInsideRoot(dir, resolved)) {
93
+ throw new Error(`Invalid path segment: ${name}`);
94
+ }
95
+ return resolved;
96
+ }
97
+ export function resolveUnderRoot(root, rel) {
98
+ if (typeof rel !== 'string' || rel.trim() === '') {
99
+ throw new Error(`Path escapes project: ${rel}`);
100
+ }
101
+ const resolved = path.resolve(root, rel);
102
+ if (!isInsideRoot(root, resolved)) {
103
+ throw new Error(`Path escapes project: ${rel}`);
104
+ }
105
+ return resolved;
106
+ }
107
+ export function resolveGenerateDir(kind, cwd, config, extra, opts) {
108
+ const paths = config?.paths ?? {};
109
+ if (kind === 'component') {
110
+ const rel = paths.components ?? (opts?.componentsDirExists ? 'src/components' : null);
111
+ return rel ? resolveUnderRoot(cwd, rel) : path.resolve(cwd);
112
+ }
113
+ if (kind === 'plugin') {
114
+ return resolveUnderRoot(cwd, paths.plugins ?? 'src/plugins');
115
+ }
116
+ const modulesRoot = resolveUnderRoot(cwd, paths.modules ?? 'src/modules');
117
+ return extra ? joinBasename(modulesRoot, extra) : modulesRoot;
118
+ }
@@ -0,0 +1,35 @@
1
+ /** Turn `my-card` / `user_profile` / `Hero` into a TS identifier. */
2
+ export function toPascalCase(name) {
3
+ if (name == null)
4
+ return '';
5
+ const parts = String(name)
6
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
7
+ .trim()
8
+ .split(/\s+/)
9
+ .filter(Boolean);
10
+ if (parts.length === 0)
11
+ return '';
12
+ const ident = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('');
13
+ return /^[0-9]/.test(ident) ? `N${ident}` : ident;
14
+ }
15
+ export function toKebabCase(name) {
16
+ const pascal = toPascalCase(name);
17
+ return pascal
18
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
19
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
20
+ .toLowerCase();
21
+ }
22
+ export function pluginNames(raw) {
23
+ const pascal = toPascalCase(raw);
24
+ if (!pascal)
25
+ return { safeName: '', label: '', path: '' };
26
+ // Strip a redundant "Plugin" suffix, but keep the stem when the name is only "Plugin"
27
+ // (otherwise label is empty and the sidebar path becomes "/").
28
+ const stripped = pascal.endsWith('Plugin') ? pascal.slice(0, -'Plugin'.length) : pascal;
29
+ const base = stripped || pascal;
30
+ return {
31
+ safeName: `${stripped}Plugin`,
32
+ label: base,
33
+ path: `/${toKebabCase(base)}`,
34
+ };
35
+ }
@@ -0,0 +1,76 @@
1
+ import { pluginNames, toPascalCase } from './names.js';
2
+ /** 2.0 playground style: builders are React nodes, 2-space indent, no `.build()`. */
3
+ export function componentTemplate(name) {
4
+ const componentName = toPascalCase(name);
5
+ return `import { Row, Text } from 'fiboui';
6
+
7
+ export interface ${componentName}Props {
8
+ title?: string;
9
+ }
10
+
11
+ export function ${componentName}({ title = '${componentName}' }: ${componentName}Props) {
12
+ return Row()
13
+ .padding(16)
14
+ .bg('white')
15
+ .rounded('lg')
16
+ .shadow('sm')
17
+ .children([
18
+ Text(title).fontSize(18).fontWeight('bold'),
19
+ ]);
20
+ }
21
+ `;
22
+ }
23
+ export function pluginTemplate(raw) {
24
+ const { safeName, label, path } = pluginNames(raw);
25
+ return `import type { FiboPlugin, FiboContext } from 'fiboui';
26
+
27
+ export const ${safeName}: FiboPlugin = {
28
+ name: '${safeName}',
29
+ version: '1.0.0',
30
+ setup: (ctx: FiboContext) => {
31
+ ctx.registerSidebarItem({
32
+ label: '${label}',
33
+ path: '${path}',
34
+ order: 10,
35
+ });
36
+ },
37
+ };
38
+ `;
39
+ }
40
+ export function modulePageTemplate(name) {
41
+ const moduleName = toPascalCase(name);
42
+ return `import { Column, Text } from 'fiboui';
43
+
44
+ export function ${moduleName}Page() {
45
+ return Column()
46
+ .center()
47
+ .gap(20)
48
+ .children([
49
+ Text('${moduleName} Module').variant('h2'),
50
+ ]);
51
+ }
52
+ `;
53
+ }
54
+ export function moduleFiles(name) {
55
+ const moduleName = toPascalCase(name);
56
+ return {
57
+ [`domain/entities/${moduleName}.entity.ts`]: `export interface ${moduleName} {\n id: string;\n}\n`,
58
+ [`domain/repositories/${moduleName}.repository.ts`]: `import { ${moduleName} } from '../entities/${moduleName}.entity';\n\nexport interface I${moduleName}Repository {\n getAll(): Promise<${moduleName}[]>;\n getById(id: string): Promise<${moduleName} | null>;\n}\n`,
59
+ [`application/usecases/Get${moduleName}.usecase.ts`]: `import { I${moduleName}Repository } from '../../domain/repositories/${moduleName}.repository';\n\nexport class Get${moduleName}UseCase {\n constructor(private repo: I${moduleName}Repository) {}\n\n async execute() {\n return this.repo.getAll();\n }\n}\n`,
60
+ [`infrastructure/api/${moduleName}.api.ts`]: `import { I${moduleName}Repository } from '../../domain/repositories/${moduleName}.repository';\nimport { ${moduleName} } from '../../domain/entities/${moduleName}.entity';\n\nexport class ${moduleName}ApiRepository implements I${moduleName}Repository {\n async getAll(): Promise<${moduleName}[]> {\n return [];\n }\n\n async getById(id: string): Promise<${moduleName} | null> {\n return null;\n }\n}\n`,
61
+ [`presentation/pages/${moduleName}Page.tsx`]: modulePageTemplate(moduleName),
62
+ };
63
+ }
64
+ export function initConfigSource(isStandard) {
65
+ const note = isStandard ? '' : ' // Update this';
66
+ return `export default {
67
+ paths: {
68
+ modules: 'src/modules',${note}
69
+ plugins: 'src/plugins',${note}
70
+ components: 'src/components',${note}
71
+ core: 'src/core',${note}
72
+ },
73
+ architecture: '${isStandard ? 'clean' : 'custom'}',
74
+ };
75
+ `;
76
+ }