@lytjs/cli 6.4.0 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,6 +5,26 @@ var fs = require('fs');
5
5
  var path = require('path');
6
6
  var child_process = require('child_process');
7
7
 
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var path__namespace = /*#__PURE__*/_interopNamespace(path);
27
+
8
28
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
29
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
30
  }) : x)(function(x) {
@@ -62,8 +82,8 @@ function writeFile(filePath, content) {
62
82
  function readFile(filePath) {
63
83
  return fs.readFileSync(filePath, "utf-8");
64
84
  }
65
- function exists(path) {
66
- return fs.existsSync(path);
85
+ function exists(path2) {
86
+ return fs.existsSync(path2);
67
87
  }
68
88
  function isEmptyDir(dir) {
69
89
  if (!fs.existsSync(dir)) return true;
@@ -170,7 +190,7 @@ function generateProjectFiles(targetDir, projectName, template) {
170
190
  },
171
191
  devDependencies: {
172
192
  "@lytjs/plugin-vite": "^6.0.0",
173
- "vite": "^5.0.0"
193
+ vite: "^5.0.0"
174
194
  }
175
195
  };
176
196
  if (!isMinimal) {
@@ -443,12 +463,19 @@ export const useCounterStore = defineStore('counter', () => {
443
463
  writeFile(path.join(targetDir, "src", "stores", "counter.ts"), counterStore);
444
464
  }
445
465
  if (isSsr) {
446
- const entryServer = `import { createSSRApp } from '@lytjs/core';
466
+ const entryServer = `import { createSSRApp, h } from '@lytjs/core';
467
+ import { renderToString } from '@lytjs/ssr';
447
468
  import App from './App.lyt';
448
469
 
449
470
  export async function render(url: string) {
450
- const app = createSSRApp(App);
451
- return app;
471
+ const app = createSSRApp({
472
+ render() {
473
+ return h(App);
474
+ }
475
+ });
476
+
477
+ const html = await renderToString(app);
478
+ return html;
452
479
  }
453
480
  `;
454
481
  writeFile(path.join(targetDir, "src/entry-server.ts"), entryServer);
@@ -462,36 +489,114 @@ app.mount('#app');
462
489
  const serverTs = `/**
463
490
  * LytJS SSR Server
464
491
  *
465
- * A minimal SSR server for development and production.
492
+ * Complete SSR server with Vite dev server and production build support.
493
+ * Supports streaming SSR, route prefetching, and static file serving.
466
494
  */
467
495
 
468
496
  import fs from 'fs';
469
497
  import path from 'path';
470
498
  import { fileURLToPath } from 'url';
499
+ import http from 'http';
471
500
 
472
501
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
473
502
  const isProduction = process.env.NODE_ENV === 'production';
503
+ const DIST_DIR = path.join(__dirname, 'dist');
504
+ const PORT = parseInt(process.env.PORT || '3000', 10);
505
+
506
+ interface RenderOptions {
507
+ url: string;
508
+ template: string;
509
+ manifest?: Record<string, string[]>;
510
+ }
511
+
512
+ async function renderPage({ url, template, manifest }: RenderOptions): Promise<string> {
513
+ let app: any;
514
+ let vite: any;
515
+
516
+ if (!isProduction) {
517
+ const { createServer: createViteServer } = await import('vite');
518
+ vite = await createViteServer({
519
+ server: { middlewareMode: true },
520
+ appType: 'custom',
521
+ });
522
+ app = (await vite.ssrLoadModule(path.join(__dirname, 'src/entry-server.ts'))).default;
523
+ } else {
524
+ app = (await import(path.join(DIST_DIR, 'server/entry-server.js'))).default;
525
+ }
526
+
527
+ const html = await app.render(url);
528
+ return template.replace('<!--app-html-->', html);
529
+ }
474
530
 
475
531
  async function createServer() {
476
- let resolve: any;
477
532
  let vite: any;
478
533
 
534
+ // Load index.html template
535
+ let template: string;
536
+
479
537
  if (!isProduction) {
480
538
  const { createServer: createViteServer } = await import('vite');
481
539
  vite = await createViteServer({
482
540
  server: { middlewareMode: true },
483
541
  appType: 'custom',
484
542
  });
485
- resolve = (id: string) => vite.resolveUrl(id);
543
+ template = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf-8');
486
544
  } else {
487
- resolve = (id: string) => id;
545
+ template = fs.readFileSync(path.join(DIST_DIR, 'client/index.html'), 'utf-8');
488
546
  }
489
547
 
490
- // TODO: Set up express/polka server and SSR rendering
491
- console.log('LytJS SSR server starting...');
548
+ const server = http.createServer(async (req, res) => {
549
+ const url = req.url || '/';
550
+
551
+ try {
552
+ if (!isProduction && url.startsWith('/@')) {
553
+ // Vite dev server requests
554
+ return;
555
+ }
556
+
557
+ // Static assets
558
+ if (url.startsWith('/assets/') || url.endsWith('.js') || url.endsWith('.css')) {
559
+ const filePath = isProduction
560
+ ? path.join(DIST_DIR, 'client', url)
561
+ : path.join(__dirname, url);
562
+
563
+ if (fs.existsSync(filePath)) {
564
+ const ext = path.extname(filePath);
565
+ const contentType = ext === '.css' ? 'text/css' : 'application/javascript';
566
+ res.writeHead(200, { 'Content-Type': contentType });
567
+ res.end(fs.readFileSync(filePath));
568
+ return;
569
+ }
570
+ }
571
+
572
+ // SSR rendering
573
+ const html = await renderPage({
574
+ url,
575
+ template,
576
+ manifest: isProduction
577
+ ? JSON.parse(fs.readFileSync(path.join(DIST_DIR, 'client/ssr-manifest.json'), 'utf-8'))
578
+ : undefined
579
+ });
580
+
581
+ res.writeHead(200, { 'Content-Type': 'text/html' });
582
+ res.end(html);
583
+ } catch (err: any) {
584
+ if (!isProduction && vite) {
585
+ vite.ssrFixStacktrace(err);
586
+ }
587
+ console.error('SSR Error:', err);
588
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
589
+ res.end('Internal Server Error');
590
+ }
591
+ });
592
+
593
+ server.listen(PORT, () => {
594
+ console.log(\`LytJS SSR server running at http://localhost:\${PORT}\`);
595
+ console.log(\`Mode: \${isProduction ? 'Production' : 'Development'}\`);
596
+ });
492
597
  }
493
598
 
494
- createServer();
599
+ createServer().catch(console.error);
495
600
  `;
496
601
  writeFile(path.join(targetDir, "server.ts"), serverTs);
497
602
  }
@@ -641,9 +746,10 @@ async function test(options = {}) {
641
746
  var TEMPLATES2 = {
642
747
  component(name, basePath) {
643
748
  const filePath = path.join(basePath, `${name}.lyt`);
644
- return [{
645
- filePath,
646
- content: `<template>
749
+ return [
750
+ {
751
+ filePath,
752
+ content: `<template>
647
753
  <div class="${name}">
648
754
  <slot />
649
755
  </div>
@@ -665,15 +771,18 @@ defineEmits<{
665
771
  }
666
772
  </style>
667
773
  `
668
- }];
774
+ }
775
+ ];
669
776
  },
670
777
  page(name, basePath) {
778
+ const pascalName = toPascalCase(name);
671
779
  const filePath = path.join(basePath, `${name}.lyt`);
672
- return [{
673
- filePath,
674
- content: `<template>
780
+ return [
781
+ {
782
+ filePath,
783
+ content: `<template>
675
784
  <div class="page-${name}">
676
- <h1>${toPascalCase(name)}</h1>
785
+ <h1>${pascalName}</h1>
677
786
  </div>
678
787
  </template>
679
788
 
@@ -687,13 +796,15 @@ defineEmits<{
687
796
  }
688
797
  </style>
689
798
  `
690
- }];
799
+ }
800
+ ];
691
801
  },
692
802
  store(name, basePath) {
693
803
  const filePath = path.join(basePath, `${name}.ts`);
694
- return [{
695
- filePath,
696
- content: `import { defineStore } from '@lytjs/store';
804
+ return [
805
+ {
806
+ filePath,
807
+ content: `import { defineStore } from '@lytjs/store';
697
808
  import { signal, computed } from '@lytjs/reactivity';
698
809
 
699
810
  export const use${toPascalCase(name)}Store = defineStore('${name}', () => {
@@ -725,12 +836,178 @@ export const use${toPascalCase(name)}Store = defineStore('${name}', () => {
725
836
  };
726
837
  });
727
838
  `
728
- }];
839
+ }
840
+ ];
841
+ },
842
+ directive(name, basePath) {
843
+ const filePath = path.join(basePath, `${name}.ts`);
844
+ const camelCaseName = toCamelCase(name);
845
+ return [
846
+ {
847
+ filePath,
848
+ content: `import type { Directive } from '@lytjs/core';
849
+
850
+ /**
851
+ * ${toPascalCase(name)} Directive
852
+ *
853
+ * @example
854
+ * \`\`\`vue
855
+ * <div v-${camelCaseName} />
856
+ * \`\`\`
857
+ */
858
+ export const v${toPascalCase(name)}: Directive = {
859
+ mounted(el, binding) {
860
+ // Directive mounted
861
+ },
862
+
863
+ updated(el, binding) {
864
+ // Directive updated
865
+ },
866
+
867
+ unmounted(el) {
868
+ // Directive unmounted
869
+ },
870
+ };
871
+ `
872
+ }
873
+ ];
874
+ },
875
+ composable(name, basePath) {
876
+ const filePath = path.join(basePath, `use${toPascalCase(name)}.ts`);
877
+ return [
878
+ {
879
+ filePath,
880
+ content: `import { signal, computed } from '@lytjs/reactivity';
881
+
882
+ /**
883
+ * ${toPascalCase(name)} Composable
884
+ *
885
+ * @example
886
+ * \`\`\`typescript
887
+ * const { state, actions } = use${toPascalCase(name)}();
888
+ * \`\`\`
889
+ */
890
+ export function use${toPascalCase(name)}() {
891
+ // State
892
+ const isLoading = signal(false);
893
+ const error = signal<Error | null>(null);
894
+ const data = signal<any>(null);
895
+
896
+ // Computed
897
+ const hasData = computed(() => data.value !== null);
898
+
899
+ // Actions
900
+ async function fetch() {
901
+ isLoading.value = true;
902
+ error.value = null;
903
+ try {
904
+ // TODO: Fetch logic here
905
+ // data.value = await someApi();
906
+ } catch (e) {
907
+ error.value = e as Error;
908
+ } finally {
909
+ isLoading.value = false;
910
+ }
911
+ }
912
+
913
+ function reset() {
914
+ isLoading.value = false;
915
+ error.value = null;
916
+ data.value = null;
917
+ }
918
+
919
+ return {
920
+ isLoading,
921
+ error,
922
+ data,
923
+ hasData,
924
+ fetch,
925
+ reset,
926
+ };
927
+ }
928
+ `
929
+ }
930
+ ];
931
+ },
932
+ hook(name, basePath) {
933
+ const filePath = path.join(basePath, `use${toPascalCase(name)}.ts`);
934
+ return [
935
+ {
936
+ filePath,
937
+ content: `import { signal, onMounted, onUnmounted } from '@lytjs/core';
938
+
939
+ /**
940
+ * ${toPascalCase(name)} Hook
941
+ */
942
+ export function use${toPascalCase(name)}() {
943
+ const state = signal(null);
944
+
945
+ onMounted(() => {
946
+ // Setup code on mount
947
+ });
948
+
949
+ onUnmounted(() => {
950
+ // Cleanup on unmount
951
+ });
952
+
953
+ return {
954
+ state,
955
+ };
956
+ }
957
+ `
958
+ }
959
+ ];
960
+ },
961
+ util(name, basePath) {
962
+ const filePath = path.join(basePath, `${name}.ts`);
963
+ return [
964
+ {
965
+ filePath,
966
+ content: `/**
967
+ * ${toPascalCase(name)} Utility Functions
968
+ */
969
+
970
+ /**
971
+ * ${toPascalCase(name)} function
972
+ *
973
+ * @param input - The input value
974
+ * @returns The processed result
975
+ */
976
+ export function ${toCamelCase(name)}(input: any) {
977
+ // TODO: Implement function
978
+ return input;
979
+ }
980
+ `
981
+ }
982
+ ];
983
+ },
984
+ middleware(name, basePath) {
985
+ const filePath = path.join(basePath, `${name}.ts`);
986
+ return [
987
+ {
988
+ filePath,
989
+ content: `import type { NavigationGuard } from '@lytjs/router';
990
+
991
+ /**
992
+ * ${toPascalCase(name)} Middleware
993
+ */
994
+ export const ${toCamelCase(name)}Middleware: NavigationGuard = (to, from, next) => {
995
+ // Middleware logic
996
+ console.log('Middleware:', to.path);
997
+ next();
998
+ };
999
+ `
1000
+ }
1001
+ ];
729
1002
  }
730
1003
  };
731
1004
  function toPascalCase(str) {
732
1005
  return str.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
733
1006
  }
1007
+ function toCamelCase(str) {
1008
+ const pascalCase = toPascalCase(str);
1009
+ return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
1010
+ }
734
1011
  function resolveTargetDir(type) {
735
1012
  const cwd = process.cwd();
736
1013
  switch (type) {
@@ -740,6 +1017,16 @@ function resolveTargetDir(type) {
740
1017
  return path.join(cwd, "src", "pages");
741
1018
  case "store":
742
1019
  return path.join(cwd, "src", "stores");
1020
+ case "directive":
1021
+ return path.join(cwd, "src", "directives");
1022
+ case "composable":
1023
+ return path.join(cwd, "src", "composables");
1024
+ case "util":
1025
+ return path.join(cwd, "src", "utils");
1026
+ case "middleware":
1027
+ return path.join(cwd, "src", "middleware");
1028
+ case "hook":
1029
+ return path.join(cwd, "src", "hooks");
743
1030
  }
744
1031
  }
745
1032
  async function add(type, name, options = {}) {
@@ -766,6 +1053,546 @@ async function add(type, name, options = {}) {
766
1053
  logger.success(`Created ${type}: ${file.filePath}`);
767
1054
  }
768
1055
  }
1056
+ var TEMPLATES3 = {
1057
+ component: (data, withStyles, withTest, template, lang) => {
1058
+ const styleImport = withStyles ? `
1059
+ import './${data.kebabName}.styles.css';` : "";
1060
+ const tsOnly = lang === "ts";
1061
+ if (template === "sfc") {
1062
+ return `<template>
1063
+ <div class="${data.kebabName}">
1064
+ <slot>
1065
+ ${data.pascalName} Component
1066
+ </slot>
1067
+ </div>
1068
+ </template>
1069
+
1070
+ <script setup${tsOnly ? ' lang="ts"' : ""}>
1071
+ ${tsOnly ? `import { ref } from '@lytjs/reactivity';
1072
+ ` : ""}
1073
+ ${tsOnly ? `
1074
+ export interface ${data.pascalName}Props {
1075
+ className?: string;
1076
+ }
1077
+
1078
+ const props = defineProps<${data.pascalName}Props>();
1079
+ ` : ""}
1080
+
1081
+ const title = ref('${data.pascalName}');
1082
+ </script>
1083
+
1084
+ <style scoped>
1085
+ .${data.kebabName} {
1086
+ /* Component styles */
1087
+ }
1088
+ </style>
1089
+ `;
1090
+ }
1091
+ const testImport = withTest ? `
1092
+ import { describe, it, expect } from 'vitest';
1093
+ import { ${data.pascalName} } from './${data.kebabName}';
1094
+
1095
+ describe('${data.pascalName}', () => {
1096
+ it('should render', () => {
1097
+ // Add test here
1098
+ expect(true).toBe(true);
1099
+ });
1100
+ });` : "";
1101
+ const propsDecl = tsOnly ? `['className', 'children']` : `[]`;
1102
+ return `/**
1103
+ * ${data.pascalName} \u7EC4\u4EF6
1104
+ *
1105
+ * @description ${data.description}
1106
+ * @created ${data.date}
1107
+ */
1108
+
1109
+ import { h, defineComponent } from '@lytjs/core';${styleImport}
1110
+
1111
+ ${tsOnly ? `export interface ${data.pascalName}Props {
1112
+ className?: string;
1113
+ children?: any;
1114
+ }
1115
+
1116
+ ` : ""}${template === "functional" ? `export function ${data.pascalName}(${tsOnly ? `props: ${data.pascalName}Props` : "props"}) {
1117
+ const { className = '', children } = props;
1118
+
1119
+ return (
1120
+ <div className={\`${data.kebabName} \${className}\`}>
1121
+ {children || '${data.pascalName} Component'}
1122
+ </div>
1123
+ );
1124
+ }` : `export const ${data.pascalName} = defineComponent({
1125
+ name: '${data.pascalName}',
1126
+ props: ${propsDecl},
1127
+ setup(props) {
1128
+ const { className = '', children } = props;
1129
+
1130
+ return () => (
1131
+ <div className={\`${data.kebabName} \${className}\`}>
1132
+ {children || '${data.pascalName} Component'}
1133
+ </div>
1134
+ );
1135
+ },
1136
+ });`}
1137
+
1138
+ export default ${data.pascalName};${testImport}
1139
+ `;
1140
+ },
1141
+ page: (data, withStyles, withTest, template, lang) => {
1142
+ const styleImport = withStyles ? `
1143
+ import './${data.kebabName}.styles.css';` : "";
1144
+ const tsOnly = lang === "ts";
1145
+ if (template === "sfc") {
1146
+ return `<template>
1147
+ <div class="${data.kebabName}-page">
1148
+ <h1>{title}</h1>
1149
+ <p>Page content for ${data.pascalName}</p>
1150
+ <slot />
1151
+ </div>
1152
+ </template>
1153
+
1154
+ <script setup${tsOnly ? ' lang="ts"' : ""}>
1155
+ ${tsOnly ? `import { ref } from '@lytjs/reactivity';
1156
+ ` : ""}
1157
+ ${tsOnly ? `
1158
+ export interface ${data.pascalName}PageProps {
1159
+ title?: string;
1160
+ }
1161
+
1162
+ const props = defineProps<${data.pascalName}PageProps>();
1163
+ ` : ""}
1164
+
1165
+ const title = ref(props.title || '${data.pascalName}');
1166
+ </script>
1167
+
1168
+ <style scoped>
1169
+ .${data.kebabName}-page {
1170
+ padding: 2rem;
1171
+ }
1172
+ </style>
1173
+ `;
1174
+ }
1175
+ const testImport = withTest ? `
1176
+ import { describe, it, expect } from 'vitest';
1177
+ import { ${data.pascalName}Page } from './${data.kebabName}';
1178
+
1179
+ describe('${data.pascalName}Page', () => {
1180
+ it('should render', () => {
1181
+ // Add test here
1182
+ expect(true).toBe(true);
1183
+ });
1184
+ });` : "";
1185
+ return `/**
1186
+ * ${data.pascalName} \u9875\u9762
1187
+ *
1188
+ * @description ${data.description}
1189
+ * @created ${data.date}
1190
+ */
1191
+
1192
+ import { h, ${tsOnly ? "signal" : "signal"} } from '@lytjs/core';
1193
+ ${styleImport}
1194
+
1195
+ ${tsOnly ? `export interface ${data.pascalName}PageProps {
1196
+ title?: string;
1197
+ }
1198
+
1199
+ ` : ""}export function ${data.pascalName}Page(${tsOnly ? `props: ${data.pascalName}PageProps` : "props"}) {
1200
+ const { title = '${data.pascalName}' } = props;
1201
+
1202
+ return (
1203
+ <div className="${data.kebabName}-page">
1204
+ <h1>{title}</h1>
1205
+ <p>Page content for ${data.pascalName}</p>
1206
+ </div>
1207
+ );
1208
+ }
1209
+
1210
+ export default ${data.pascalName}Page;${testImport}
1211
+ `;
1212
+ },
1213
+ service: (data, _withStyles, _withTest, _template, lang) => {
1214
+ const tsOnly = lang === "ts";
1215
+ return `/**
1216
+ * ${data.pascalName} \u670D\u52A1
1217
+ *
1218
+ * @description ${data.description}
1219
+ * @created ${data.date}
1220
+ */
1221
+
1222
+ ${tsOnly ? `export interface ${data.pascalName}ServiceOptions {
1223
+ baseUrl?: string;
1224
+ timeout?: number;
1225
+ }
1226
+
1227
+ ` : ""}${tsOnly ? `export class ${data.pascalName}Service {
1228
+ private baseUrl: string;
1229
+ private timeout: number;
1230
+ ` : `export class ${data.pascalName}Service {
1231
+ `}
1232
+ constructor(${tsOnly ? `options: ${data.pascalName}ServiceOptions = {}` : "options = {}"}) {
1233
+ this.baseUrl = options.baseUrl || '/api';
1234
+ this.timeout = options.timeout || 30000;
1235
+ }
1236
+
1237
+ async getAll()${tsOnly ? ": Promise<any[]>" : ""} {
1238
+ const response = await fetch(\`\${this.baseUrl}/${data.kebabName}s\`, {
1239
+ method: 'GET',
1240
+ headers: { 'Content-Type': 'application/json' },
1241
+ signal: AbortSignal.timeout(this.timeout),
1242
+ });
1243
+ return response.json();
1244
+ }
1245
+
1246
+ async getById(id)${tsOnly ? ": Promise<any>" : ""} {
1247
+ const response = await fetch(\`\${this.baseUrl}/${data.kebabName}s/\${id}\`, {
1248
+ method: 'GET',
1249
+ headers: { 'Content-Type': 'application/json' },
1250
+ signal: AbortSignal.timeout(this.timeout),
1251
+ });
1252
+ return response.json();
1253
+ }
1254
+
1255
+ async create(${tsOnly ? "data: any" : "data"})${tsOnly ? ": Promise<any>" : ""} {
1256
+ const response = await fetch(\`\${this.baseUrl}/${data.kebabName}s\`, {
1257
+ method: 'POST',
1258
+ headers: { 'Content-Type': 'application/json' },
1259
+ body: JSON.stringify(data),
1260
+ signal: AbortSignal.timeout(this.timeout),
1261
+ });
1262
+ return response.json();
1263
+ }
1264
+
1265
+ async update(id)${tsOnly ? ": Promise<any>" : ""} {
1266
+ const response = await fetch(\`\${this.baseUrl}/${data.kebabName}s/\${id}\`, {
1267
+ method: 'PUT',
1268
+ headers: { 'Content-Type': 'application/json' },
1269
+ body: JSON.stringify(data),
1270
+ signal: AbortSignal.timeout(this.timeout),
1271
+ });
1272
+ return response.json();
1273
+ }
1274
+
1275
+ async delete(id)${tsOnly ? ": Promise<void>" : ""} {
1276
+ await fetch(\`\${this.baseUrl}/${data.kebabName}s/\${id}\`, {
1277
+ method: 'DELETE',
1278
+ signal: AbortSignal.timeout(this.timeout),
1279
+ });
1280
+ }
1281
+ }
1282
+
1283
+ export default ${data.pascalName}Service;
1284
+ `;
1285
+ },
1286
+ hook: (data, _withStyles, _withTest, _template, lang) => {
1287
+ const tsOnly = lang === "ts";
1288
+ return `/**
1289
+ * ${data.pascalName} Hook
1290
+ *
1291
+ * @description ${data.description}
1292
+ * @created ${data.date}
1293
+ */
1294
+
1295
+ import { signal, effect } from '@lytjs/reactivity';
1296
+
1297
+ ${tsOnly ? `export interface ${data.pascalName}Options {
1298
+ immediate?: boolean;
1299
+ }
1300
+
1301
+ export interface ${data.pascalName}Return {
1302
+ data: ReturnType<typeof signal>;
1303
+ loading: ReturnType<typeof signal>;
1304
+ error: ReturnType<typeof signal>;
1305
+ execute: () => Promise<void>;
1306
+ reset: () => void;
1307
+ }
1308
+
1309
+ ` : ""}export function use${data.pascalName}(${tsOnly ? `options: ${data.pascalName}Options = {}` : "options = {}"})${tsOnly ? `: ${data.pascalName}Return` : ""} {
1310
+ const { immediate = false } = options;
1311
+
1312
+ const data = signal<any>(null);
1313
+ const loading = signal(false);
1314
+ const error = signal<Error | null>(null);
1315
+
1316
+ async function execute() {
1317
+ loading.value = true;
1318
+ error.value = null;
1319
+
1320
+ try {
1321
+ const result = await new Promise(resolve => setTimeout(() => resolve(null), 100));
1322
+ data.value = result;
1323
+ } catch (e) {
1324
+ error.value = e as Error;
1325
+ } finally {
1326
+ loading.value = false;
1327
+ }
1328
+ }
1329
+
1330
+ function reset() {
1331
+ data.value = null;
1332
+ loading.value = false;
1333
+ error.value = null;
1334
+ }
1335
+
1336
+ if (immediate) {
1337
+ execute();
1338
+ }
1339
+
1340
+ return {
1341
+ data,
1342
+ loading,
1343
+ error,
1344
+ execute,
1345
+ reset,
1346
+ };
1347
+ }
1348
+
1349
+ export default use${data.pascalName};
1350
+ `;
1351
+ },
1352
+ store: (data, _withStyles, _withTest, _template, lang) => {
1353
+ const tsOnly = lang === "ts";
1354
+ return `/**
1355
+ * ${data.pascalName} Store
1356
+ *
1357
+ * @description ${data.description}
1358
+ * @created ${data.date}
1359
+ */
1360
+
1361
+ import { signal, computed } from '@lytjs/reactivity';
1362
+
1363
+ ${tsOnly ? `export interface ${data.pascalName}State {
1364
+ items: any[];
1365
+ selectedId: string | null;
1366
+ loading: boolean;
1367
+ error: Error | null;
1368
+ }
1369
+
1370
+ ` : ""}export function create${data.pascalName}Store() {
1371
+ const state = signal${tsOnly ? `<${data.pascalName}State>` : ""}({
1372
+ items: [],
1373
+ selectedId: null,
1374
+ loading: false,
1375
+ error: null,
1376
+ });
1377
+
1378
+ const selectedItem = computed(() => {
1379
+ const currentState = state.value;
1380
+ return currentState.items.find(item => item.id === currentState.selectedId);
1381
+ });
1382
+
1383
+ const itemCount = computed(() => state.value.items.length);
1384
+
1385
+ function setItems(items) {
1386
+ state.value = { ...state.value, items };
1387
+ }
1388
+
1389
+ function selectItem(id) {
1390
+ state.value = { ...state.value, selectedId: id };
1391
+ }
1392
+
1393
+ function addItem(item) {
1394
+ state.value = {
1395
+ ...state.value,
1396
+ items: [...state.value.items, item],
1397
+ };
1398
+ }
1399
+
1400
+ function updateItem(id, updates) {
1401
+ state.value = {
1402
+ ...state.value,
1403
+ items: state.value.items.map(item =>
1404
+ item.id === id ? { ...item, ...updates } : item
1405
+ ),
1406
+ };
1407
+ }
1408
+
1409
+ function removeItem(id) {
1410
+ state.value = {
1411
+ ...state.value,
1412
+ items: state.value.items.filter(item => item.id !== id),
1413
+ selectedId: state.value.selectedId === id ? null : state.value.selectedId,
1414
+ };
1415
+ }
1416
+
1417
+ function setLoading(loading) {
1418
+ state.value = { ...state.value, loading };
1419
+ }
1420
+
1421
+ function setError(error) {
1422
+ state.value = { ...state.value, error };
1423
+ }
1424
+
1425
+ function reset() {
1426
+ state.value = {
1427
+ items: [],
1428
+ selectedId: null,
1429
+ loading: false,
1430
+ error: null,
1431
+ };
1432
+ }
1433
+
1434
+ return {
1435
+ state,
1436
+ selectedItem,
1437
+ itemCount,
1438
+ setItems,
1439
+ selectItem,
1440
+ addItem,
1441
+ updateItem,
1442
+ removeItem,
1443
+ setLoading,
1444
+ setError,
1445
+ reset,
1446
+ };
1447
+ }
1448
+
1449
+ ${tsOnly ? `export type ${data.pascalName}Store = ReturnType<typeof create${data.pascalName}Store>;
1450
+ ` : ""}export default create${data.pascalName}Store;
1451
+ `;
1452
+ },
1453
+ layout: (data, withStyles, _withTest, _template, lang) => {
1454
+ const tsOnly = lang === "ts";
1455
+ const styleImport = withStyles ? `
1456
+ import './${data.kebabName}.styles.css';` : "";
1457
+ return `/**
1458
+ * ${data.pascalName} \u5E03\u5C40
1459
+ *
1460
+ * @description ${data.description}
1461
+ * @created ${data.date}
1462
+ */
1463
+
1464
+ import { h, defineComponent } from '@lytjs/core';${styleImport}
1465
+
1466
+ ${tsOnly ? `export interface ${data.pascalName}LayoutProps {
1467
+ children?: any;
1468
+ }
1469
+
1470
+ ` : ""}export const ${data.pascalName}Layout = defineComponent({
1471
+ name: '${data.pascalName}Layout',
1472
+ setup(props) {
1473
+ return () => (
1474
+ <div className="${data.kebabName}-layout">
1475
+ <header className="${data.kebabName}-header">
1476
+ <slot name="header">
1477
+ <h1>${data.pascalName}</h1>
1478
+ </slot>
1479
+ </header>
1480
+ <main className="${data.kebabName}-main">
1481
+ <slot />
1482
+ </main>
1483
+ <footer className="${data.kebabName}-footer">
1484
+ <slot name="footer" />
1485
+ </footer>
1486
+ </div>
1487
+ );
1488
+ },
1489
+ });
1490
+
1491
+ export default ${data.pascalName}Layout;
1492
+ `;
1493
+ },
1494
+ middleware: (data, _withStyles, _withTest, _template, lang) => {
1495
+ const tsOnly = lang === "ts";
1496
+ return `/**
1497
+ * ${data.pascalName} \u4E2D\u95F4\u4EF6
1498
+ *
1499
+ * @description ${data.description}
1500
+ * @created ${data.date}
1501
+ */
1502
+
1503
+ ${tsOnly ? `import type { Request, Response, NextFunction } from 'express';
1504
+ ` : ""}
1505
+ export function ${data.camelName}Middleware(${tsOnly ? `req: Request, res: Response, next: NextFunction` : "req, res, next"}) {
1506
+ try {
1507
+ console.log('[${data.pascalName}] Middleware executed');
1508
+ next();
1509
+ } catch (error) {
1510
+ next(error);
1511
+ }
1512
+ }
1513
+
1514
+ export default ${data.camelName}Middleware;
1515
+ `;
1516
+ }
1517
+ };
1518
+ function toPascalCase2(name) {
1519
+ return name.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1520
+ }
1521
+ function toCamelCase2(name) {
1522
+ const pascal = toPascalCase2(name);
1523
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
1524
+ }
1525
+ function toKebabCase(name) {
1526
+ return name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
1527
+ }
1528
+ async function generate(options) {
1529
+ const {
1530
+ type,
1531
+ name,
1532
+ path: basePath = "./src",
1533
+ withStyles = false,
1534
+ withTest = false,
1535
+ description = "",
1536
+ template = "default",
1537
+ language = "ts"
1538
+ } = options;
1539
+ const templateData = {
1540
+ name,
1541
+ pascalName: toPascalCase2(name),
1542
+ kebabName: toKebabCase(name),
1543
+ camelName: toCamelCase2(name),
1544
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1545
+ description: description || `${toPascalCase2(name)} ${type}`
1546
+ };
1547
+ const typeDirs = {
1548
+ component: "components",
1549
+ page: "pages",
1550
+ service: "services",
1551
+ hook: "hooks",
1552
+ store: "stores",
1553
+ layout: "layouts",
1554
+ middleware: "middleware"
1555
+ };
1556
+ const targetDir = path__namespace.join(process.cwd(), basePath, typeDirs[type] || "components");
1557
+ await ensureDir(targetDir);
1558
+ const templateFn = TEMPLATES3[type];
1559
+ if (!templateFn) {
1560
+ logger.error(`Unknown type: ${type}`);
1561
+ logger.info("Available types: component, page, service, hook, store, layout, middleware");
1562
+ process.exit(1);
1563
+ }
1564
+ const extension = template === "sfc" ? "lyt" : language;
1565
+ const filename = `${templateData.kebabName}${type === "page" ? ".page" : ""}.${extension}`;
1566
+ const filePath = path__namespace.join(targetDir, filename);
1567
+ const content = templateFn(templateData, withStyles, withTest, template, language);
1568
+ await writeFile(filePath, content);
1569
+ logger.success(`Generated ${type}: ${filePath}`);
1570
+ if (withStyles && template !== "sfc") {
1571
+ const styleContent = `/**
1572
+ * ${templateData.pascalName} Styles
1573
+ */
1574
+
1575
+ .${templateData.kebabName} {
1576
+ /* Component styles */
1577
+ }
1578
+ `;
1579
+ const stylePath = path__namespace.join(targetDir, `${templateData.kebabName}.styles.css`);
1580
+ await writeFile(stylePath, styleContent);
1581
+ logger.success(`Generated styles: ${stylePath}`);
1582
+ }
1583
+ if (withTest && template !== "sfc") {
1584
+ logger.info("Test file included in generated component");
1585
+ }
1586
+ logger.info("\nNext steps:");
1587
+ logger.info(` cd ${targetDir}`);
1588
+ logger.info(
1589
+ ` Import your ${type}: import { ${template === "sfc" ? "default" : templateData.pascalName} } from './${templateData.kebabName}'`
1590
+ );
1591
+ logger.info("\nAvailable options:");
1592
+ logger.info(" --template=sfc : Single File Component (.lyt)");
1593
+ logger.info(" --template=functional : Functional component");
1594
+ logger.info(" --language=js : JavaScript output");
1595
+ }
769
1596
  var PLUGIN_TEMPLATES = {
770
1597
  default: "Default plugin template with TypeScript",
771
1598
  minimal: "Minimal plugin without extra dependencies",
@@ -774,45 +1601,53 @@ var PLUGIN_TEMPLATES = {
774
1601
  function getTemplateContent(template, pluginName) {
775
1602
  const packageName = `@lytjs/plugin-${pluginName}`;
776
1603
  const files = {};
777
- files["package.json"] = JSON.stringify({
778
- name: packageName,
779
- version: "0.1.0",
780
- description: `LytJS plugin: ${pluginName}`,
781
- main: "dist/index.cjs",
782
- module: "dist/index.mjs",
783
- types: "dist/index.d.ts",
784
- exports: {
785
- ".": {
786
- import: "./dist/index.mjs",
787
- require: "./dist/index.cjs",
788
- types: "./dist/index.d.ts"
1604
+ files["package.json"] = JSON.stringify(
1605
+ {
1606
+ name: packageName,
1607
+ version: "0.1.0",
1608
+ description: `LytJS plugin: ${pluginName}`,
1609
+ main: "dist/index.cjs",
1610
+ module: "dist/index.mjs",
1611
+ types: "dist/index.d.ts",
1612
+ exports: {
1613
+ ".": {
1614
+ import: "./dist/index.mjs",
1615
+ require: "./dist/index.cjs",
1616
+ types: "./dist/index.d.ts"
1617
+ }
1618
+ },
1619
+ files: ["dist"],
1620
+ scripts: {
1621
+ build: "tsup",
1622
+ dev: "tsup --watch",
1623
+ test: "vitest",
1624
+ lint: "eslint src",
1625
+ prepublishOnly: "npm run build"
1626
+ },
1627
+ keywords: ["lytjs", "plugin"],
1628
+ license: "MIT",
1629
+ peerDependencies: {
1630
+ "@lytjs/core": ">=6.0.0"
789
1631
  }
790
1632
  },
791
- files: ["dist"],
792
- scripts: {
793
- build: "tsup",
794
- dev: "tsup --watch",
795
- test: "vitest",
796
- lint: "eslint src",
797
- "prepublishOnly": "npm run build"
1633
+ null,
1634
+ 2
1635
+ );
1636
+ files["tsconfig.json"] = JSON.stringify(
1637
+ {
1638
+ extends: "@lytjs/core/tsconfig.json",
1639
+ compilerOptions: {
1640
+ outDir: "./dist",
1641
+ rootDir: "./src",
1642
+ declaration: true,
1643
+ declarationMap: true
1644
+ },
1645
+ include: ["src"],
1646
+ exclude: ["node_modules", "dist", "tests"]
798
1647
  },
799
- keywords: ["lytjs", "plugin"],
800
- license: "MIT",
801
- peerDependencies: {
802
- "@lytjs/core": ">=6.0.0"
803
- }
804
- }, null, 2);
805
- files["tsconfig.json"] = JSON.stringify({
806
- extends: "@lytjs/core/tsconfig.json",
807
- compilerOptions: {
808
- outDir: "./dist",
809
- rootDir: "./src",
810
- declaration: true,
811
- declarationMap: true
812
- },
813
- include: ["src"],
814
- exclude: ["node_modules", "dist", "tests"]
815
- }, null, 2);
1648
+ null,
1649
+ 2
1650
+ );
816
1651
  files["tsup.config.ts"] = `import { defineConfig } from 'tsup';
817
1652
 
818
1653
  export default defineConfig({
@@ -857,8 +1692,6 @@ const optionsSchema: ConfigSchema<${pluginName.replace(/-/g, "")}Options> = {
857
1692
  additionalProperties: false,
858
1693
  };
859
1694
 
860
- ${pluginName.replace(/-/g, "")}Options\`;
861
-
862
1695
  export interface ${pluginName.replace(/-/g, "")}Options {
863
1696
  debug?: boolean;
864
1697
  option1?: string;
@@ -1026,7 +1859,16 @@ async function buildPlugin(options = {}) {
1026
1859
  logger.info("Building plugin...");
1027
1860
  try {
1028
1861
  const buildCmd = "tsup";
1029
- const args = ["--entry", "src/index.ts", "--outDir", outDir, "--format", "esm,cjs", "--dts", "--sourcemap"];
1862
+ const args = [
1863
+ "--entry",
1864
+ "src/index.ts",
1865
+ "--outDir",
1866
+ outDir,
1867
+ "--format",
1868
+ "esm,cjs",
1869
+ "--dts",
1870
+ "--sourcemap"
1871
+ ];
1030
1872
  if (options.minify) {
1031
1873
  args.push("--minify");
1032
1874
  }
@@ -1092,8 +1934,8 @@ async function validatePlugin(options = {}) {
1092
1934
  if (!fs.existsSync(tsupConfigPath)) {
1093
1935
  warnings.push("tsup.config.ts not found (recommended for builds)");
1094
1936
  }
1095
- let hasErrors = errors.length > 0;
1096
- let hasWarnings = warnings.length > 0;
1937
+ const hasErrors = errors.length > 0;
1938
+ const hasWarnings = warnings.length > 0;
1097
1939
  if (hasErrors) {
1098
1940
  logger.error("Validation failed with errors:");
1099
1941
  for (const err of errors) {
@@ -1122,7 +1964,7 @@ async function validatePlugin(options = {}) {
1122
1964
  }
1123
1965
  function isEmptyDir2(dir) {
1124
1966
  if (!fs.existsSync(dir)) return true;
1125
- const files = __require("fs").readdirSync(dir);
1967
+ const files = readdirSync(dir);
1126
1968
  return files.length === 0;
1127
1969
  }
1128
1970
  function listPluginTemplates() {
@@ -1141,7 +1983,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
1141
1983
  return;
1142
1984
  }
1143
1985
  if (options.version || command === "version" || command === "-v" || command === "--version") {
1144
- console.log(`LytJS CLI v${VERSION}`);
1986
+ console.warn(`LytJS CLI v${VERSION}`);
1145
1987
  return;
1146
1988
  }
1147
1989
  switch (command) {
@@ -1175,9 +2017,20 @@ async function runCli(rawArgs = process.argv.slice(2)) {
1175
2017
  grep: options.grep
1176
2018
  });
1177
2019
  break;
1178
- case "add":
1179
- if (!args[0] || !["component", "page", "store"].includes(args[0])) {
1180
- logger.error("Usage: lyt add <component|page|store> <name>");
2020
+ case "add": {
2021
+ const addTypes = [
2022
+ "component",
2023
+ "page",
2024
+ "store",
2025
+ "directive",
2026
+ "composable",
2027
+ "util",
2028
+ "middleware",
2029
+ "hook"
2030
+ ];
2031
+ if (!args[0] || !addTypes.includes(args[0])) {
2032
+ logger.error("Usage: lyt add <type> <name>");
2033
+ logger.info("Types: component, page, store, directive, composable, util, middleware, hook");
1181
2034
  logger.info("Example: lyt add component Button");
1182
2035
  process.exit(1);
1183
2036
  }
@@ -1185,7 +2038,27 @@ async function runCli(rawArgs = process.argv.slice(2)) {
1185
2038
  force: options.force
1186
2039
  });
1187
2040
  break;
1188
- case "plugin":
2041
+ }
2042
+ case "generate":
2043
+ case "g": {
2044
+ const genTypes = ["component", "page", "service", "hook", "store"];
2045
+ if (!args[0] || !genTypes.includes(args[0])) {
2046
+ logger.error("Usage: lyt generate <type> <name>");
2047
+ logger.info("Types: component, page, service, hook, store");
2048
+ logger.info("Example: lyt generate component Button");
2049
+ process.exit(1);
2050
+ }
2051
+ await generate({
2052
+ type: args[0],
2053
+ name: args[1] || "Unnamed",
2054
+ path: options.path,
2055
+ withStyles: options.styles,
2056
+ withTest: options.test,
2057
+ withStorybook: options.storybook
2058
+ });
2059
+ break;
2060
+ }
2061
+ case "plugin": {
1189
2062
  if (!args[0]) {
1190
2063
  logger.error("Usage: lyt plugin <create|build|validate|templates>");
1191
2064
  logger.info("Example: lyt plugin create my-plugin");
@@ -1222,6 +2095,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
1222
2095
  process.exit(1);
1223
2096
  }
1224
2097
  break;
2098
+ }
1225
2099
  default:
1226
2100
  if (command) {
1227
2101
  logger.error(`Unknown command: ${command}`);
@@ -1268,7 +2142,7 @@ function parseArgs(args) {
1268
2142
  return { command, args: positional, options };
1269
2143
  }
1270
2144
  function showHelp() {
1271
- console.log(`
2145
+ console.warn(`
1272
2146
  ${logger.bold("LytJS CLI")} v${VERSION}
1273
2147
 
1274
2148
  ${logger.bold("Usage:")}
@@ -1280,10 +2154,11 @@ ${logger.bold("Commands:")}
1280
2154
  dev Start development server
1281
2155
  build Build for production
1282
2156
  test Run tests
1283
- add <type> <name> Generate a component, page, or store
2157
+ add <type> <name> Generate a component, page, store, directive, composable, etc.
2158
+ generate, g Advanced code generation (component, page, service, hook, store)
1284
2159
  plugin <subcmd> Plugin development commands
1285
2160
  help Show this help message
1286
-
2161
+
1287
2162
  ${logger.bold("Options:")}
1288
2163
  --version, -v Show version number
1289
2164
  --help Show help
@@ -1307,6 +2182,12 @@ ${logger.bold("Test Options:")}
1307
2182
  --coverage Generate coverage report
1308
2183
  --grep <pattern> Filter tests by pattern
1309
2184
 
2185
+ ${logger.bold("Generate Options:")}
2186
+ --path <dir> Output directory (default: ./src)
2187
+ --styles Generate CSS styles file
2188
+ --test Generate test file
2189
+ --storybook Generate Storybook story file
2190
+
1310
2191
  ${logger.bold("Plugin Options:")}
1311
2192
  --template <name> Use a specific plugin template (default, minimal, withConfig)
1312
2193
  --force Overwrite existing directory
@@ -1327,6 +2208,16 @@ ${logger.bold("Examples:")}
1327
2208
  lyt add component Button
1328
2209
  lyt add page About
1329
2210
  lyt add store user
2211
+ lyt directive click-outside
2212
+ lyt add composable fetch-data
2213
+ lyt add util format
2214
+ lyt add hook window-size
2215
+ lyt generate component Button --styles --test
2216
+ lyt generate page Dashboard --path ./src/pages
2217
+ lyt generate service User --path ./src/services
2218
+ lyt generate hook useCounter
2219
+ lyt generate store Auth --path ./src/stores
2220
+ lyt g page Login
1330
2221
  lyt plugin create my-plugin
1331
2222
  lyt plugin create my-plugin --template withConfig
1332
2223
  lyt plugin build
@@ -1348,6 +2239,7 @@ exports.detectPackageManager = detectPackageManager;
1348
2239
  exports.dev = dev;
1349
2240
  exports.ensureDir = ensureDir;
1350
2241
  exports.exists = exists;
2242
+ exports.generate = generate;
1351
2243
  exports.getAddCommand = getAddCommand;
1352
2244
  exports.getInstallCommand = getInstallCommand;
1353
2245
  exports.getRunCommand = getRunCommand;