vite-plugin-ferry 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -10,10 +10,14 @@ in sync with your backend.
10
10
  - **Enums** — Generates types and runtime constants from `app/Enums/`
11
11
  - **Resources** — Generates response types from `app/Http/Resources/`
12
12
 
13
+ ## Requirements
14
+
15
+ - **TypeScript ^5.0** — Required as a peer dependency for code generation
16
+
13
17
  ## Installation
14
18
 
15
19
  ```bash
16
- npm install vite-plugin-ferry --save-dev
20
+ npm install vite-plugin-ferry typescript@^5 --save-dev
17
21
  ```
18
22
 
19
23
  Add it to your `vite.config.ts`:
@@ -33,7 +37,77 @@ Import your backend types directly in your frontend code:
33
37
 
34
38
  ```ts
35
39
  import { OrderStatus } from '@ferry/enums';
36
- import { UserResource } from '@ferry/resources';
40
+ import type { UserResource } from '@ferry/resources';
41
+ ```
42
+
43
+ ## Examples
44
+
45
+ ### Enums
46
+
47
+ A PHP enum with labels:
48
+
49
+ ```php
50
+ // app/Enums/OrderStatus.php
51
+ enum OrderStatus: string
52
+ {
53
+ case Pending = 'pending';
54
+ case Shipped = 'shipped';
55
+ case Delivered = 'delivered';
56
+
57
+ public function label(): string
58
+ {
59
+ return match ($this) {
60
+ self::Pending => 'Pending Order',
61
+ self::Shipped => 'Shipped',
62
+ self::Delivered => 'Delivered',
63
+ };
64
+ }
65
+ }
66
+ ```
67
+
68
+ Generates:
69
+
70
+ ```ts
71
+ // @ferry/enums
72
+ export declare const OrderStatus: {
73
+ Pending: { value: 'pending'; label: 'Pending Order' };
74
+ Shipped: { value: 'shipped'; label: 'Shipped' };
75
+ Delivered: { value: 'delivered'; label: 'Delivered' };
76
+ };
77
+ ```
78
+
79
+ ### Resources
80
+
81
+ A Laravel JsonResource:
82
+
83
+ ```php
84
+ // app/Http/Resources/UserResource.php
85
+ class UserResource extends JsonResource
86
+ {
87
+ public function toArray(Request $request): array
88
+ {
89
+ return [
90
+ 'id' => $this->resource->id,
91
+ 'name' => $this->resource->name,
92
+ 'email' => $this->resource->email,
93
+ 'created_at' => $this->resource->created_at,
94
+ 'posts' => PostResource::collection($this->whenLoaded('posts')),
95
+ ];
96
+ }
97
+ }
98
+ ```
99
+
100
+ Generates:
101
+
102
+ ```ts
103
+ // @ferry/resources
104
+ export type UserResource = {
105
+ id: string;
106
+ name: string;
107
+ email: string;
108
+ created_at: string;
109
+ posts?: PostResource[];
110
+ };
37
111
  ```
38
112
 
39
113
  ## License
@@ -3,6 +3,7 @@ export type EnumGeneratorOptions = {
3
3
  enumsDir: string;
4
4
  outputDir: string;
5
5
  packageName: string;
6
+ prettyPrint?: boolean;
6
7
  };
7
8
  /**
8
9
  * Generate TypeScript type declarations for enums.
@@ -11,9 +12,10 @@ export declare function generateEnumTypeScript(enums: Record<string, EnumDefinit
11
12
  /**
12
13
  * Generate runtime JavaScript for enums.
13
14
  */
14
- export declare function generateEnumRuntime(enums: Record<string, EnumDefinition>): string;
15
+ export declare function generateEnumRuntime(enums: Record<string, EnumDefinition>, prettyPrint?: boolean): string;
15
16
  /**
16
17
  * Collect all enum definitions from the enums directory.
18
+ * This is a plugin-level function that handles file I/O.
17
19
  */
18
20
  export declare function collectEnums(enumsDir: string): Record<string, EnumDefinition>;
19
21
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"enums.d.ts","sourceRoot":"","sources":["../../src/generators/enums.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE5E,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,MAAM,CAyCpF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,MAAM,CAyBjF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAuB7E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CA6BjE"}
1
+ {"version":3,"file":"enums.d.ts","sourceRoot":"","sources":["../../src/generators/enums.ts"],"names":[],"mappings":"AAIA,OAAO,EAAoB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAc/E,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,MAAM,CA4BpF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,WAAW,UAAO,GAAG,MAAM,CAgCrG;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0B7E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CA6BjE"}
@@ -1,81 +1,71 @@
1
+ import ts from 'typescript';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
- import { getPhpFiles, writeFileEnsureDir } from '../utils/file.js';
4
- import { parseEnumFile } from '../utils/php-parser.js';
4
+ import { getPhpFiles, readFileSafe, writeFileEnsureDir } from '../utils/file.js';
5
+ import { parseEnumContent } from '../utils/php-parser.js';
6
+ import { printNodes, createEnum, createConstObject, createObjectLiteral, createDeclareConstWithType, createTypeLiteral, createStringLiteral, createNumericLiteral, createExportDefault, printNode, } from '../utils/ts-generator.js';
5
7
  /**
6
8
  * Generate TypeScript type declarations for enums.
7
9
  */
8
10
  export function generateEnumTypeScript(enums) {
9
- const lines = [];
10
- lines.push('// This file is auto-generated by the primcloud Vite plugin.');
11
- lines.push('// Do not edit directly.');
12
- lines.push('');
11
+ const nodes = [];
13
12
  for (const enumName of Object.keys(enums)) {
14
13
  const enumDef = enums[enumName];
15
14
  const hasLabels = enumDef.cases.some((c) => c.label);
16
15
  if (hasLabels) {
17
- // Generate a const object with typed properties for enums with labels
18
- lines.push(`export declare const ${enumDef.name}: {`);
19
- for (const c of enumDef.cases) {
20
- const val = String(c.value).replace(/'/g, "\\'");
21
- const label = c.label ? String(c.label).replace(/'/g, "\\'") : val;
22
- lines.push(` ${c.key}: { value: '${val}'; label: '${label}' };`);
23
- }
24
- lines.push('};');
25
- lines.push('');
16
+ // Generate a declare const with typed properties for enums with labels
17
+ const properties = enumDef.cases.map((c) => ({
18
+ name: c.key,
19
+ type: createTypeLiteral([
20
+ { name: 'value', type: ts.factory.createLiteralTypeNode(createStringLiteral(String(c.value))) },
21
+ {
22
+ name: 'label',
23
+ type: ts.factory.createLiteralTypeNode(createStringLiteral(c.label || String(c.value))),
24
+ },
25
+ ]),
26
+ }));
27
+ nodes.push(createDeclareConstWithType(enumDef.name, createTypeLiteral(properties)));
26
28
  }
27
29
  else {
28
- // Generate a traditional enum for enums without labels
29
- lines.push(`export enum ${enumDef.name} {`);
30
- for (const c of enumDef.cases) {
31
- const val = c.value;
32
- if (enumDef.backing === 'int' || enumDef.backing === 'integer') {
33
- if (!isNaN(Number(val))) {
34
- lines.push(` ${c.key} = ${val},`);
35
- }
36
- else {
37
- lines.push(` ${c.key} = '${val}',`);
38
- }
39
- }
40
- else {
41
- lines.push(` ${c.key} = '${String(val).replace(/'/g, "\\'")}',`);
42
- }
43
- }
44
- lines.push('}');
45
- lines.push('');
30
+ // Generate a traditional enum
31
+ nodes.push(createEnum(enumDef.name, enumDef.cases.map((c) => ({ key: c.key, value: c.value }))));
46
32
  }
47
33
  }
48
- return lines.join('\n');
34
+ return nodes.length > 0 ? printNodes(nodes) + '\n' : '';
49
35
  }
50
36
  /**
51
37
  * Generate runtime JavaScript for enums.
52
38
  */
53
- export function generateEnumRuntime(enums) {
54
- const lines = [];
55
- lines.push('// Auto-generated by primcloud Vite plugin');
56
- lines.push('');
39
+ export function generateEnumRuntime(enums, prettyPrint = true) {
40
+ const nodes = [];
57
41
  for (const enumName of Object.keys(enums)) {
58
42
  const enumDef = enums[enumName];
59
43
  const hasLabels = enumDef.cases.some((c) => c.label);
60
- lines.push(`export const ${enumDef.name} = {`);
61
- for (const c of enumDef.cases) {
62
- const val = String(c.value).replace(/'/g, "\\'");
44
+ const properties = enumDef.cases.map((c) => {
45
+ let value;
63
46
  if (hasLabels) {
64
- const label = c.label ? String(c.label).replace(/'/g, "\\'") : val;
65
- lines.push(` ${c.key}: { value: '${val}', label: '${label}' },`);
47
+ value = createObjectLiteral([
48
+ { key: 'value', value: createStringLiteral(String(c.value)) },
49
+ { key: 'label', value: createStringLiteral(c.label || String(c.value)) },
50
+ ], prettyPrint);
51
+ }
52
+ else if (typeof c.value === 'number') {
53
+ value = createNumericLiteral(c.value);
66
54
  }
67
55
  else {
68
- lines.push(` ${c.key}: '${val}',`);
56
+ value = createStringLiteral(String(c.value));
69
57
  }
70
- }
71
- lines.push('};');
72
- lines.push('');
58
+ return { key: c.key, value };
59
+ });
60
+ nodes.push(createConstObject(enumDef.name, properties));
73
61
  }
74
- lines.push('export default {};');
75
- return lines.join('\n');
62
+ // Add export default {}
63
+ nodes.push(createExportDefault(ts.factory.createObjectLiteralExpression([])));
64
+ return nodes.map(printNode).join('\n\n') + '\n';
76
65
  }
77
66
  /**
78
67
  * Collect all enum definitions from the enums directory.
68
+ * This is a plugin-level function that handles file I/O.
79
69
  */
80
70
  export function collectEnums(enumsDir) {
81
71
  const enums = {};
@@ -86,7 +76,10 @@ export function collectEnums(enumsDir) {
86
76
  for (const file of enumFiles) {
87
77
  try {
88
78
  const enumPath = join(enumsDir, file);
89
- const def = parseEnumFile(enumPath);
79
+ const content = readFileSafe(enumPath);
80
+ if (!content)
81
+ continue;
82
+ const def = parseEnumContent(content);
90
83
  if (def) {
91
84
  enums[def.name] = def;
92
85
  }
@@ -102,7 +95,7 @@ export function collectEnums(enumsDir) {
102
95
  * Generate enum files (TypeScript declarations and runtime JavaScript).
103
96
  */
104
97
  export function generateEnums(options) {
105
- const { enumsDir, outputDir, packageName } = options;
98
+ const { enumsDir, outputDir, packageName, prettyPrint = true } = options;
106
99
  // Collect all enums
107
100
  const enums = collectEnums(enumsDir);
108
101
  // Generate TypeScript declarations
@@ -110,7 +103,7 @@ export function generateEnums(options) {
110
103
  const dtsPath = join(outputDir, 'index.d.ts');
111
104
  writeFileEnsureDir(dtsPath, dtsContent);
112
105
  // Generate runtime JavaScript
113
- const jsContent = generateEnumRuntime(enums);
106
+ const jsContent = generateEnumRuntime(enums, prettyPrint);
114
107
  const jsPath = join(outputDir, 'index.js');
115
108
  writeFileEnsureDir(jsPath, jsContent);
116
109
  // Generate package.json
@@ -1,14 +1,20 @@
1
+ import { type EnumDefinition } from '../utils/php-parser.js';
1
2
  export type ResourceGeneratorOptions = {
2
3
  resourcesDir: string;
3
4
  enumsDir: string;
4
5
  modelsDir: string;
5
6
  outputDir: string;
6
7
  packageName: string;
8
+ prettyPrint?: boolean;
7
9
  };
8
- type FieldInfo = {
10
+ export type FieldInfo = {
9
11
  type: string;
10
12
  optional: boolean;
11
13
  };
14
+ /**
15
+ * Parse fields from a PHP array block (from toArray() method).
16
+ */
17
+ export declare function parseFieldsFromArrayBlock(block: string, resourceClass: string, docShape: Record<string, string> | null, resourcesDir: string, modelsDir: string, enumsDir: string, collectedEnums: Record<string, EnumDefinition>): Record<string, FieldInfo>;
12
18
  /**
13
19
  * Generate TypeScript type declarations for resources.
14
20
  */
@@ -22,5 +28,4 @@ export declare function generateResourceRuntime(): string;
22
28
  * Generate resource type files (TypeScript declarations and runtime JavaScript).
23
29
  */
24
30
  export declare function generateResources(options: ResourceGeneratorOptions): void;
25
- export {};
26
31
  //# sourceMappingURL=resources.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../src/generators/resources.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,wBAAwB,GAAG;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAiOF;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,EACpD,SAAS,EAAE,MAAM,EAAE,EACnB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAC3B,MAAM,CAmCR;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAOhD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAqEzE"}
1
+ {"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../src/generators/resources.ts"],"names":[],"mappings":"AAIA,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAUhC,MAAM,MAAM,wBAAwB,GAAG;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAgIF;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,EACvC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GAC7C,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAyF3B;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,EACpD,SAAS,EAAE,MAAM,EAAE,EACnB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAC3B,MAAM,CAuCR;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAqEzE"}
@@ -1,23 +1,27 @@
1
+ import ts from 'typescript';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { join, parse } from 'node:path';
3
4
  import { getPhpFiles, readFileSafe, writeFileEnsureDir } from '../utils/file.js';
4
- import { extractDocblockArrayShape, extractReturnArrayBlock, getModelCasts, parseEnumFile, } from '../utils/php-parser.js';
5
+ import { extractDocblockArrayShape, extractReturnArrayBlock, parseEnumContent, parseModelCasts, } from '../utils/php-parser.js';
5
6
  import { mapDocTypeToTs, mapPhpTypeToTs, parseTsObjectStringToPairs } from '../utils/type-mapper.js';
7
+ import { printNode, createTypeAlias, createImportType, parseTypeString, createTypeLiteral, } from '../utils/ts-generator.js';
6
8
  /**
7
9
  * Map a PHP cast to a TypeScript type, potentially collecting enum references.
8
10
  */
9
11
  function mapCastToType(cast, enumsDir, collectedEnums) {
10
12
  const original = cast;
11
- const lower = cast.toLowerCase();
12
13
  // Try to find enum in app/Enums
13
14
  const match = original.match(/([A-Za-z0-9_\\]+)$/);
14
15
  const short = match ? match[1].replace(/^\\+/, '') : original;
15
16
  const enumPath = join(enumsDir, `${short}.php`);
16
17
  if (existsSync(enumPath)) {
17
- const def = parseEnumFile(enumPath);
18
- if (def) {
19
- collectedEnums[def.name] = def;
20
- return def.name;
18
+ const content = readFileSafe(enumPath);
19
+ if (content) {
20
+ const def = parseEnumContent(content);
21
+ if (def) {
22
+ collectedEnums[def.name] = def;
23
+ return def.name;
24
+ }
21
25
  }
22
26
  }
23
27
  return mapPhpTypeToTs(cast);
@@ -71,14 +75,17 @@ function inferTypeFromValue(value, key, resourceClass, resourcesDir, modelsDir,
71
75
  const modelCandidate = resourceClass.replace(/Resource$/, '');
72
76
  const modelPath = join(modelsDir, `${modelCandidate}.php`);
73
77
  if (existsSync(modelPath)) {
74
- const casts = getModelCasts(modelPath);
75
- if (casts[prop]) {
76
- const cast = casts[prop];
77
- const trim = cast.trim();
78
- const tsType = trim.startsWith('{') || trim.includes(':') || /array\s*\{/.test(trim)
79
- ? trim
80
- : mapCastToType(cast, enumsDir, collectedEnums);
81
- return { type: tsType, optional: false };
78
+ const modelContent = readFileSafe(modelPath);
79
+ if (modelContent) {
80
+ const casts = parseModelCasts(modelContent);
81
+ if (casts[prop]) {
82
+ const cast = casts[prop];
83
+ const trim = cast.trim();
84
+ const tsType = trim.startsWith('{') || trim.includes(':') || /array\s*\{/.test(trim)
85
+ ? trim
86
+ : mapCastToType(cast, enumsDir, collectedEnums);
87
+ return { type: tsType, optional: false };
88
+ }
82
89
  }
83
90
  }
84
91
  // Number heuristics
@@ -100,7 +107,7 @@ function inferTypeFromValue(value, key, resourceClass, resourcesDir, modelsDir,
100
107
  /**
101
108
  * Parse fields from a PHP array block (from toArray() method).
102
109
  */
103
- function parseFieldsFromArrayBlock(block, resourceClass, docShape, resourcesDir, modelsDir, enumsDir, collectedEnums) {
110
+ export function parseFieldsFromArrayBlock(block, resourceClass, docShape, resourcesDir, modelsDir, enumsDir, collectedEnums) {
104
111
  const lines = block.split(/\r?\n/);
105
112
  const fields = {};
106
113
  for (let i = 0; i < lines.length; i++) {
@@ -175,47 +182,45 @@ function parseFieldsFromArrayBlock(block, resourceClass, docShape, resourcesDir,
175
182
  * Generate TypeScript type declarations for resources.
176
183
  */
177
184
  export function generateResourceTypeScript(resources, fallbacks, referencedEnums) {
178
- const lines = [];
179
- lines.push('// This file is auto-generated by the primcloud Vite plugin.');
180
- lines.push('// Do not edit directly.');
181
- lines.push('');
185
+ const nodes = [];
182
186
  // Import referenced enums from @app/enums
183
187
  if (referencedEnums.size > 0) {
184
- const enumImports = Array.from(referencedEnums).sort().join(', ');
185
- lines.push(`import type { ${enumImports} } from '@app/enums';`);
186
- lines.push('');
188
+ const enumImports = Array.from(referencedEnums).sort();
189
+ nodes.push(createImportType(enumImports, '@app/enums'));
187
190
  }
188
191
  // Generate resource types
189
192
  for (const className of Object.keys(resources)) {
190
193
  const fields = resources[className];
191
194
  if (fallbacks.includes(className)) {
192
- lines.push(`export type ${className} = Record<string, any>;`);
193
- lines.push('');
195
+ // Fallback type: Record<string, any>
196
+ const recordType = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [
197
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
198
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
199
+ ]);
200
+ nodes.push(createTypeAlias(className, recordType));
194
201
  continue;
195
202
  }
196
- lines.push(`export type ${className} = {`);
197
- for (const key of Object.keys(fields)) {
203
+ // Create type literal with all fields
204
+ const properties = Object.keys(fields).map((key) => {
198
205
  const info = fields[key];
199
- const type = info.type || 'any';
200
- const optional = info.optional ? '?' : '';
201
- lines.push(` ${key}${optional}: ${type};`);
202
- }
203
- lines.push('};');
204
- lines.push('');
206
+ return {
207
+ name: key,
208
+ type: parseTypeString(info.type || 'any'),
209
+ optional: info.optional,
210
+ };
211
+ });
212
+ nodes.push(createTypeAlias(className, createTypeLiteral(properties)));
205
213
  }
206
- return lines.join('\n');
214
+ if (nodes.length === 0)
215
+ return '';
216
+ return nodes.map(printNode).join('\n\n') + '\n';
207
217
  }
208
218
  /**
209
219
  * Generate runtime JavaScript for resources.
210
220
  * Resources are type-only, so this just exports an empty object.
211
221
  */
212
222
  export function generateResourceRuntime() {
213
- const lines = [];
214
- lines.push('// Auto-generated by primcloud Vite plugin');
215
- lines.push('// Resources are type-only exports');
216
- lines.push('');
217
- lines.push('export default {};');
218
- return lines.join('\n');
223
+ return 'export default {};';
219
224
  }
220
225
  /**
221
226
  * Generate resource type files (TypeScript declarations and runtime JavaScript).
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Plugin } from 'vite';
2
2
  export type ResourceTypesPluginOptions = {
3
3
  cwd?: string;
4
+ prettyPrint?: boolean;
4
5
  };
5
6
  /**
6
7
  * Vite plugin for generating TypeScript types from Laravel PHP files.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAMnC,MAAM,MAAM,0BAA0B,GAAG;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,UAAU,KAAK,CAC3B,OAAO,GAAE,0BAER,GACA,MAAM,CA8ER"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAMnC,MAAM,MAAM,0BAA0B,GAAG;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,OAAO,GAAE,0BAA+B,GAAG,MAAM,CAyF9E"}
package/dist/index.js CHANGED
@@ -11,18 +11,19 @@ import { setupResourceWatcher } from './watchers/resources.js';
11
11
  * - @app/resources - Laravel JsonResource types
12
12
  * - @app/schemas - (future) Zod schemas from FormRequests
13
13
  */
14
- export default function ferry(options = {
15
- cwd: process.cwd(),
16
- }) {
14
+ export default function ferry(options = {}) {
17
15
  const namespace = '@ferry';
18
16
  const name = 'vite-plugin-ferry';
17
+ // Apply defaults
18
+ const cwd = options.cwd ?? process.cwd();
19
+ const prettyPrint = options.prettyPrint ?? true;
19
20
  // Directory paths
20
- const enumsDir = join(options.cwd, 'app/Enums');
21
- const resourcesDir = join(options.cwd, 'app/Http/Resources');
22
- const modelsDir = join(options.cwd, 'app/Models');
21
+ const enumsDir = join(cwd, 'app/Enums');
22
+ const resourcesDir = join(cwd, 'app/Http/Resources');
23
+ const modelsDir = join(cwd, 'app/Models');
23
24
  // Output directories for each package
24
- const enumsOutputDir = join(options.cwd, 'node_modules', ...namespace.split('/'), 'enums');
25
- const resourcesOutputDir = join(options.cwd, 'node_modules', ...namespace.split('/'), 'resources');
25
+ const enumsOutputDir = join(cwd, 'node_modules', ...namespace.split('/'), 'enums');
26
+ const resourcesOutputDir = join(cwd, 'node_modules', ...namespace.split('/'), 'resources');
26
27
  /**
27
28
  * Generate all packages.
28
29
  */
@@ -32,6 +33,7 @@ export default function ferry(options = {
32
33
  enumsDir,
33
34
  outputDir: enumsOutputDir,
34
35
  packageName: `${namespace}/enums`,
36
+ prettyPrint,
35
37
  });
36
38
  // Generate @app/resources package
37
39
  generateResources({
@@ -40,6 +42,7 @@ export default function ferry(options = {
40
42
  modelsDir,
41
43
  outputDir: resourcesOutputDir,
42
44
  packageName: `${namespace}/resources`,
45
+ prettyPrint,
43
46
  });
44
47
  }
45
48
  return {
@@ -53,7 +56,11 @@ export default function ferry(options = {
53
56
  catch (e) {
54
57
  console.error(`[${name}] Error generating types during config():`, e);
55
58
  }
56
- return null;
59
+ return {
60
+ optimizeDeps: {
61
+ exclude: [`${namespace}/enums`, `${namespace}/resources`],
62
+ },
63
+ };
57
64
  },
58
65
  // Run generation when build starts
59
66
  buildStart() {
@@ -1,6 +1,6 @@
1
1
  export type EnumCase = {
2
2
  key: string;
3
- value: string;
3
+ value: string | number;
4
4
  label?: string;
5
5
  };
6
6
  export type EnumDefinition = {
@@ -9,23 +9,23 @@ export type EnumDefinition = {
9
9
  cases: EnumCase[];
10
10
  };
11
11
  /**
12
- * Parse a PHP enum file and extract its definition.
12
+ * Parse PHP enum content and extract its definition.
13
+ * This is a pure function that takes PHP source code as input.
13
14
  */
14
- export declare function parseEnumFile(enumPath: string): EnumDefinition | null;
15
+ export declare function parseEnumContent(phpContent: string): EnumDefinition | null;
15
16
  /**
16
- * Parse PHP array pairs from a string like "'key' => 'value'".
17
+ * Parse model casts from PHP model content.
18
+ * This is a pure function that takes PHP source code as input.
17
19
  */
18
- export declare function parsePhpArrayPairs(inside: string): Record<string, string>;
20
+ export declare function parseModelCasts(phpContent: string): Record<string, string>;
19
21
  /**
20
- * Extract model casts from a PHP model file.
21
- */
22
- export declare function getModelCasts(modelPath: string): Record<string, string>;
23
- /**
24
- * Extract docblock array shape from PHP file content.
22
+ * Extract docblock array shape from PHP content.
23
+ * This is a pure function that takes PHP source code as input.
25
24
  */
26
25
  export declare function extractDocblockArrayShape(phpContent: string): Record<string, string> | null;
27
26
  /**
28
- * Extract the return array block from a toArray() method in a PHP resource.
27
+ * Extract the return array block from a toArray() method in PHP content.
28
+ * This is a pure function that takes PHP source code as input.
29
29
  */
30
30
  export declare function extractReturnArrayBlock(phpContent: string): string | null;
31
31
  //# sourceMappingURL=php-parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"php-parser.d.ts","sourceRoot":"","sources":["../../src/utils/php-parser.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CA8CrE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBzE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4BvE;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAqE3F;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzE"}
1
+ {"version":3,"file":"php-parser.d.ts","sourceRoot":"","sources":["../../src/utils/php-parser.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AA6FF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAsF1E;AA4CD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwC1E;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAwE3F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzE"}
@@ -1,96 +1,267 @@
1
- import { readFileSafe } from './file.js';
1
+ // Import php-parser (CommonJS module with constructor)
2
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
3
+ const PhpParser = require('php-parser');
4
+ // Initialize the PHP parser (PHP 8+ only)
5
+ const parser = new PhpParser({
6
+ parser: {
7
+ extractDoc: true,
8
+ php8: true,
9
+ },
10
+ ast: {
11
+ withPositions: false,
12
+ },
13
+ });
2
14
  /**
3
- * Parse a PHP enum file and extract its definition.
15
+ * Parse PHP content and return the AST.
16
+ * Uses parseEval which doesn't require <?php tags or filenames.
4
17
  */
5
- export function parseEnumFile(enumPath) {
6
- const content = readFileSafe(enumPath);
7
- if (!content)
18
+ function parsePhp(content) {
19
+ try {
20
+ // Strip <?php tag if present (parseEval expects raw PHP code)
21
+ let code = content.trimStart();
22
+ if (code.startsWith('<?php')) {
23
+ code = code.slice(5);
24
+ }
25
+ else if (code.startsWith('<?')) {
26
+ code = code.slice(2);
27
+ }
28
+ return parser.parseEval(code);
29
+ }
30
+ catch {
8
31
  return null;
9
- // Extract enum name and backing type
10
- const enumMatch = content.match(/enum\s+([A-Za-z0-9_]+)\s*(?:\:\s*([A-Za-z0-9_]+))?/);
11
- if (!enumMatch)
32
+ }
33
+ }
34
+ /**
35
+ * Walk all child nodes in an AST node.
36
+ */
37
+ function walkChildren(node, callback) {
38
+ const obj = node;
39
+ for (const key of Object.keys(obj)) {
40
+ const val = obj[key];
41
+ if (val && typeof val === 'object' && val.kind) {
42
+ if (callback(val))
43
+ return true;
44
+ }
45
+ else if (Array.isArray(val)) {
46
+ for (const item of val) {
47
+ if (item && typeof item === 'object' && item.kind) {
48
+ if (callback(item))
49
+ return true;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+ /**
57
+ * Find a node by kind in the AST.
58
+ */
59
+ function findNodeByKind(ast, kind) {
60
+ if (ast.kind === kind)
61
+ return ast;
62
+ let result = null;
63
+ walkChildren(ast, (child) => {
64
+ const found = findNodeByKind(child, kind);
65
+ if (found) {
66
+ result = found;
67
+ return true;
68
+ }
69
+ return false;
70
+ });
71
+ return result;
72
+ }
73
+ /**
74
+ * Find all nodes of a specific kind in the AST.
75
+ */
76
+ function findAllNodesByKind(ast, kind) {
77
+ const results = [];
78
+ function walk(node) {
79
+ if (node.kind === kind) {
80
+ results.push(node);
81
+ }
82
+ walkChildren(node, (child) => {
83
+ walk(child);
84
+ return false;
85
+ });
86
+ }
87
+ walk(ast);
88
+ return results;
89
+ }
90
+ /**
91
+ * Extract string value from a PHP literal node.
92
+ */
93
+ function getStringValue(node) {
94
+ if (node.kind === 'string') {
95
+ return node.value;
96
+ }
97
+ if (node.kind === 'number') {
98
+ return String(node.value);
99
+ }
100
+ return null;
101
+ }
102
+ /**
103
+ * Parse PHP enum content and extract its definition.
104
+ * This is a pure function that takes PHP source code as input.
105
+ */
106
+ export function parseEnumContent(phpContent) {
107
+ const ast = parsePhp(phpContent);
108
+ if (!ast)
109
+ return null;
110
+ // Find the enum declaration
111
+ const enumNode = findNodeByKind(ast, 'enum');
112
+ if (!enumNode)
12
113
  return null;
13
- const name = enumMatch[1];
14
- const backing = enumMatch[2] ? enumMatch[2].toLowerCase() : null;
114
+ const name = typeof enumNode.name === 'string' ? enumNode.name : enumNode.name.name;
115
+ const backing = enumNode.valueType ? enumNode.valueType.name.toLowerCase() : null;
15
116
  // Extract enum cases
16
117
  const cases = [];
17
- const explicitCases = [...content.matchAll(/case\s+([A-Za-z0-9_]+)\s*=\s*'([^']*)'\s*;/g)];
18
- if (explicitCases.length) {
19
- for (const match of explicitCases) {
20
- cases.push({ key: match[1], value: match[2] });
118
+ const enumCases = findAllNodesByKind(enumNode, 'enumcase');
119
+ for (const enumCase of enumCases) {
120
+ // Name can be an Identifier or string
121
+ const key = typeof enumCase.name === 'string'
122
+ ? enumCase.name
123
+ : enumCase.name.name;
124
+ let value;
125
+ if (enumCase.value !== null && enumCase.value !== undefined) {
126
+ // Value is a String or Number node (types say string|number but runtime is Node)
127
+ const valueNode = enumCase.value;
128
+ if (typeof valueNode === 'object' && valueNode.kind) {
129
+ if (valueNode.kind === 'number') {
130
+ // php-parser returns number values as strings, convert to actual number
131
+ value = Number(valueNode.value);
132
+ }
133
+ else {
134
+ const extracted = getStringValue(valueNode);
135
+ value = extracted !== null ? extracted : key;
136
+ }
137
+ }
138
+ else {
139
+ value = String(enumCase.value);
140
+ }
21
141
  }
22
- }
23
- else {
24
- const implicitCases = [...content.matchAll(/case\s+([A-Za-z0-9_]+)\s*;/g)];
25
- for (const match of implicitCases) {
26
- cases.push({ key: match[1], value: match[1] });
142
+ else {
143
+ value = key;
27
144
  }
145
+ cases.push({ key, value });
28
146
  }
29
- // Parse getLabel() method if it exists
30
- const labelMethodMatch = content.match(/function\s+getLabel\s*\(\s*\)\s*:\s*string\s*\{[\s\S]*?return\s+match\s*\(\s*\$this\s*\)\s*\{([\s\S]*?)\};/);
31
- if (labelMethodMatch) {
32
- const matchBody = labelMethodMatch[1];
33
- const labelMatches = [...matchBody.matchAll(/self::([A-Za-z0-9_]+)\s*=>\s*'([^']*)'/g)];
34
- for (const labelMatch of labelMatches) {
35
- const caseKey = labelMatch[1];
36
- const labelValue = labelMatch[2];
37
- const enumCase = cases.find((c) => c.key === caseKey);
38
- if (enumCase) {
39
- enumCase.label = labelValue;
147
+ // Parse label() method if it exists
148
+ const methods = findAllNodesByKind(enumNode, 'method');
149
+ const labelMethod = methods.find((m) => {
150
+ const methodName = typeof m.name === 'string' ? m.name : m.name.name;
151
+ return methodName === 'label';
152
+ });
153
+ if (labelMethod && labelMethod.body) {
154
+ // Find match expression in the method
155
+ const matchNode = findNodeByKind(labelMethod.body, 'match');
156
+ if (matchNode && matchNode.arms) {
157
+ for (const arm of matchNode.arms) {
158
+ if (arm.conds) {
159
+ for (const cond of arm.conds) {
160
+ // Handle self::CASE_NAME
161
+ if (cond.kind === 'staticlookup') {
162
+ const lookup = cond;
163
+ const offset = lookup.offset;
164
+ const caseName = typeof offset === 'string'
165
+ ? offset
166
+ : offset.kind === 'identifier'
167
+ ? offset.name
168
+ : null;
169
+ if (caseName) {
170
+ const labelValue = getStringValue(arm.body);
171
+ if (labelValue !== null) {
172
+ const enumCase = cases.find((c) => c.key === caseName);
173
+ if (enumCase) {
174
+ enumCase.label = labelValue;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
40
181
  }
41
182
  }
42
183
  }
43
184
  return { name, backing, cases };
44
185
  }
45
186
  /**
46
- * Parse PHP array pairs from a string like "'key' => 'value'".
187
+ * Extract key-value pairs from a PHP array node.
47
188
  */
48
- export function parsePhpArrayPairs(inside) {
189
+ function extractArrayPairs(arrayNode) {
49
190
  const pairs = {};
50
- const re = /["'](?<key>[A-Za-z0-9_]+)["']\s*=>\s*(?<val>[^,\n]+)/g;
51
- for (const m of inside.matchAll(re)) {
52
- let val = m.groups.val.trim();
53
- val = val.replace(/[,\s]*$/g, '');
54
- if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
55
- val = val.slice(1, -1);
56
- }
57
- if (val.endsWith('::class')) {
58
- val = val.slice(0, -7);
191
+ for (const item of arrayNode.items) {
192
+ if (item.kind === 'entry') {
193
+ const entry = item;
194
+ const key = entry.key ? getStringValue(entry.key) : null;
195
+ if (!key)
196
+ continue;
197
+ const value = entry.value;
198
+ let strValue = null;
199
+ if (value.kind === 'string' || value.kind === 'number') {
200
+ strValue = getStringValue(value);
201
+ }
202
+ else if (value.kind === 'staticlookup') {
203
+ // Handle Foo::class
204
+ const lookup = value;
205
+ const offset = lookup.offset;
206
+ if (offset &&
207
+ offset.kind === 'identifier' &&
208
+ offset.name === 'class') {
209
+ const what = lookup.what;
210
+ if (what.kind === 'name') {
211
+ strValue = what.name.replace(/^\\+/, '');
212
+ }
213
+ }
214
+ }
215
+ if (strValue !== null) {
216
+ pairs[key] = strValue;
217
+ }
59
218
  }
60
- pairs[m.groups.key] = val;
61
219
  }
62
220
  return pairs;
63
221
  }
64
222
  /**
65
- * Extract model casts from a PHP model file.
223
+ * Parse model casts from PHP model content.
224
+ * This is a pure function that takes PHP source code as input.
66
225
  */
67
- export function getModelCasts(modelPath) {
68
- const content = readFileSafe(modelPath);
69
- if (!content)
226
+ export function parseModelCasts(phpContent) {
227
+ const ast = parsePhp(phpContent);
228
+ if (!ast)
70
229
  return {};
71
- // Try protected $casts property
72
- const castsMatch = content.match(/protected\s+\$casts\s*=\s*\[([^\]]*)\]/s);
73
- if (castsMatch) {
74
- return parsePhpArrayPairs(castsMatch[1]);
75
- }
76
- // Try casts() method
77
- const castsMethodMatch = content.match(/function\s+casts\s*\([^)]*\)\s*\{[^}]*return\s*\[([^\]]*)\]/s);
78
- if (castsMethodMatch) {
79
- return parsePhpArrayPairs(castsMethodMatch[1]);
230
+ // Find the class
231
+ const classNode = findNodeByKind(ast, 'class');
232
+ if (!classNode)
233
+ return {};
234
+ // Look for protected $casts property
235
+ const propertyStatements = findAllNodesByKind(classNode, 'propertystatement');
236
+ for (const propStmt of propertyStatements) {
237
+ for (const prop of propStmt.properties) {
238
+ // prop.name can be a string or Identifier
239
+ const propName = typeof prop.name === 'string'
240
+ ? prop.name
241
+ : prop.name.name;
242
+ if (propName === 'casts' && prop.value && prop.value.kind === 'array') {
243
+ return extractArrayPairs(prop.value);
244
+ }
245
+ }
80
246
  }
81
- // Try class-based casts
82
- const matches = [...content.matchAll(/["'](?<key>[A-Za-z0-9_]+)["']\s*=>\s*(?<class>[A-Za-z0-9_\\]+)::class/g)];
83
- const res = {};
84
- for (const m of matches) {
85
- const k = m.groups.key;
86
- let v = m.groups.class;
87
- v = v.replace(/^\\+/, '');
88
- res[k] = v;
247
+ // Look for casts() method
248
+ const methods = findAllNodesByKind(classNode, 'method');
249
+ const castsMethod = methods.find((m) => {
250
+ const methodName = typeof m.name === 'string' ? m.name : m.name.name;
251
+ return methodName === 'casts';
252
+ });
253
+ if (castsMethod && castsMethod.body) {
254
+ // Find return statement with array
255
+ const returnNode = findNodeByKind(castsMethod.body, 'return');
256
+ if (returnNode && returnNode.expr && returnNode.expr.kind === 'array') {
257
+ return extractArrayPairs(returnNode.expr);
258
+ }
89
259
  }
90
- return res;
260
+ return {};
91
261
  }
92
262
  /**
93
- * Extract docblock array shape from PHP file content.
263
+ * Extract docblock array shape from PHP content.
264
+ * This is a pure function that takes PHP source code as input.
94
265
  */
95
266
  export function extractDocblockArrayShape(phpContent) {
96
267
  const match = phpContent.match(/@return\s+array\s*\{/s);
@@ -119,7 +290,9 @@ export function extractDocblockArrayShape(phpContent) {
119
290
  }
120
291
  if (endPos === null)
121
292
  return null;
122
- const inside = phpContent.slice(openBracePos + 1, endPos);
293
+ // Extract content and strip docblock asterisks from multiline format
294
+ let inside = phpContent.slice(openBracePos + 1, endPos);
295
+ inside = inside.replace(/^\s*\*\s?/gm, '');
123
296
  const pairs = {};
124
297
  let i = 0;
125
298
  while (i < inside.length) {
@@ -166,7 +339,8 @@ export function extractDocblockArrayShape(phpContent) {
166
339
  return pairs;
167
340
  }
168
341
  /**
169
- * Extract the return array block from a toArray() method in a PHP resource.
342
+ * Extract the return array block from a toArray() method in PHP content.
343
+ * This is a pure function that takes PHP source code as input.
170
344
  */
171
345
  export function extractReturnArrayBlock(phpContent) {
172
346
  const match = phpContent.match(/function\s+toArray\s*\([^)]*\)\s*:\s*array\s*\{([\s\S]*?)\n\s*\}/);
@@ -0,0 +1,67 @@
1
+ import ts from 'typescript';
2
+ /**
3
+ * Print a TypeScript node to a string.
4
+ */
5
+ export declare function printNode(node: ts.Node): string;
6
+ /**
7
+ * Print multiple TypeScript nodes to a string with blank lines between them.
8
+ */
9
+ export declare function printNodes(nodes: ts.Node[]): string;
10
+ /**
11
+ * Create a string literal type node.
12
+ */
13
+ export declare function createStringLiteral(value: string): ts.StringLiteral;
14
+ /**
15
+ * Create a numeric literal node.
16
+ */
17
+ export declare function createNumericLiteral(value: number): ts.NumericLiteral;
18
+ /**
19
+ * Create an enum declaration.
20
+ */
21
+ export declare function createEnum(name: string, members: Array<{
22
+ key: string;
23
+ value: string | number;
24
+ }>): ts.EnumDeclaration;
25
+ /**
26
+ * Create an object literal expression.
27
+ */
28
+ export declare function createObjectLiteral(properties: Array<{
29
+ key: string;
30
+ value: ts.Expression;
31
+ }>, multiLine?: boolean): ts.ObjectLiteralExpression;
32
+ /**
33
+ * Create a const declaration with an object literal.
34
+ */
35
+ export declare function createConstObject(name: string, properties: Array<{
36
+ key: string;
37
+ value: ts.Expression;
38
+ }>, multiLine?: boolean): ts.VariableStatement;
39
+ /**
40
+ * Create a declare const statement with a typed object.
41
+ */
42
+ export declare function createDeclareConstWithType(name: string, type: ts.TypeNode): ts.VariableStatement;
43
+ /**
44
+ * Create a type alias declaration.
45
+ */
46
+ export declare function createTypeAlias(name: string, type: ts.TypeNode): ts.TypeAliasDeclaration;
47
+ /**
48
+ * Create a type literal with property signatures.
49
+ */
50
+ export declare function createTypeLiteral(properties: Array<{
51
+ name: string;
52
+ type: ts.TypeNode;
53
+ optional?: boolean;
54
+ }>): ts.TypeLiteralNode;
55
+ /**
56
+ * Create an import type declaration.
57
+ */
58
+ export declare function createImportType(names: string[], from: string): ts.ImportDeclaration;
59
+ /**
60
+ * Parse a type string into a TypeNode.
61
+ */
62
+ export declare function parseTypeString(typeStr: string): ts.TypeNode;
63
+ /**
64
+ * Create an export default statement.
65
+ */
66
+ export declare function createExportDefault(expression: ts.Expression): ts.ExportAssignment;
67
+ //# sourceMappingURL=ts-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ts-generator.d.ts","sourceRoot":"","sources":["../../src/utils/ts-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAI5B;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,MAAM,CAG/C;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,MAAM,CAEnD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,EAAE,CAAC,aAAa,CAEnE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,EAAE,CAAC,cAAc,CAErE;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC,GACtD,EAAE,CAAC,eAAe,CAWpB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAA;CAAE,CAAC,EACxD,SAAS,UAAO,GACf,EAAE,CAAC,uBAAuB,CAK5B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAA;CAAE,CAAC,EACxD,SAAS,UAAO,GACf,EAAE,CAAC,iBAAiB,CAUtB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,QAAQ,GAAG,EAAE,CAAC,iBAAiB,CAQhG;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,QAAQ,GAAG,EAAE,CAAC,oBAAoB,CAOxF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,GACzE,EAAE,CAAC,eAAe,CAUpB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,EAAE,CAAC,iBAAiB,CAcpF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,EAAE,CAAC,QAAQ,CAiD5D;AAkDD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,gBAAgB,CAElF"}
@@ -0,0 +1,179 @@
1
+ import ts from 'typescript';
2
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
3
+ /**
4
+ * Print a TypeScript node to a string.
5
+ */
6
+ export function printNode(node) {
7
+ const sourceFile = ts.createSourceFile('output.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
8
+ return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
9
+ }
10
+ /**
11
+ * Print multiple TypeScript nodes to a string with blank lines between them.
12
+ */
13
+ export function printNodes(nodes) {
14
+ return nodes.map(printNode).join('\n\n');
15
+ }
16
+ /**
17
+ * Create a string literal type node.
18
+ */
19
+ export function createStringLiteral(value) {
20
+ return ts.factory.createStringLiteral(value);
21
+ }
22
+ /**
23
+ * Create a numeric literal node.
24
+ */
25
+ export function createNumericLiteral(value) {
26
+ return ts.factory.createNumericLiteral(value);
27
+ }
28
+ /**
29
+ * Create an enum declaration.
30
+ */
31
+ export function createEnum(name, members) {
32
+ const enumMembers = members.map((m) => {
33
+ const initializer = typeof m.value === 'number' ? createNumericLiteral(m.value) : createStringLiteral(m.value);
34
+ return ts.factory.createEnumMember(ts.factory.createIdentifier(m.key), initializer);
35
+ });
36
+ return ts.factory.createEnumDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(name), enumMembers);
37
+ }
38
+ /**
39
+ * Create an object literal expression.
40
+ */
41
+ export function createObjectLiteral(properties, multiLine = true) {
42
+ const objectProperties = properties.map((p) => ts.factory.createPropertyAssignment(ts.factory.createIdentifier(p.key), p.value));
43
+ return ts.factory.createObjectLiteralExpression(objectProperties, multiLine);
44
+ }
45
+ /**
46
+ * Create a const declaration with an object literal.
47
+ */
48
+ export function createConstObject(name, properties, multiLine = true) {
49
+ const objectLiteral = createObjectLiteral(properties, multiLine);
50
+ return ts.factory.createVariableStatement([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createIdentifier(name), undefined, undefined, objectLiteral)], ts.NodeFlags.Const));
51
+ }
52
+ /**
53
+ * Create a declare const statement with a typed object.
54
+ */
55
+ export function createDeclareConstWithType(name, type) {
56
+ return ts.factory.createVariableStatement([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword), ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)], ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createIdentifier(name), undefined, type, undefined)], ts.NodeFlags.Const));
57
+ }
58
+ /**
59
+ * Create a type alias declaration.
60
+ */
61
+ export function createTypeAlias(name, type) {
62
+ return ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(name), undefined, type);
63
+ }
64
+ /**
65
+ * Create a type literal with property signatures.
66
+ */
67
+ export function createTypeLiteral(properties) {
68
+ const members = properties.map((p) => ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(p.name), p.optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, p.type));
69
+ return ts.factory.createTypeLiteralNode(members);
70
+ }
71
+ /**
72
+ * Create an import type declaration.
73
+ */
74
+ export function createImportType(names, from) {
75
+ const importSpecifiers = names.map((name) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(name)));
76
+ return ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(true, // isTypeOnly
77
+ undefined, ts.factory.createNamedImports(importSpecifiers)), ts.factory.createStringLiteral(from));
78
+ }
79
+ /**
80
+ * Parse a type string into a TypeNode.
81
+ */
82
+ export function parseTypeString(typeStr) {
83
+ // Handle common types
84
+ switch (typeStr) {
85
+ case 'string':
86
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
87
+ case 'number':
88
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
89
+ case 'boolean':
90
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
91
+ case 'any':
92
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
93
+ case 'null':
94
+ return ts.factory.createLiteralTypeNode(ts.factory.createNull());
95
+ case 'undefined':
96
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword);
97
+ }
98
+ // Handle array types like "string[]" or "User[]"
99
+ if (typeStr.endsWith('[]')) {
100
+ const elementType = parseTypeString(typeStr.slice(0, -2));
101
+ return ts.factory.createArrayTypeNode(elementType);
102
+ }
103
+ // Handle union types like "string | null"
104
+ if (typeStr.includes(' | ')) {
105
+ const types = typeStr.split(' | ').map((t) => parseTypeString(t.trim()));
106
+ return ts.factory.createUnionTypeNode(types);
107
+ }
108
+ // Handle Record<K, V>
109
+ const recordMatch = typeStr.match(/^Record<([^,]+),\s*([^>]+)>$/);
110
+ if (recordMatch) {
111
+ const keyType = parseTypeString(recordMatch[1].trim());
112
+ const valueType = parseTypeString(recordMatch[2].trim());
113
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [keyType, valueType]);
114
+ }
115
+ // Handle inline object types like "{ key: type; ... }"
116
+ if (typeStr.startsWith('{') && typeStr.endsWith('}')) {
117
+ const inner = typeStr.slice(1, -1).trim();
118
+ if (!inner) {
119
+ return ts.factory.createTypeLiteralNode([]);
120
+ }
121
+ const properties = parseObjectTypeProperties(inner);
122
+ return createTypeLiteral(properties);
123
+ }
124
+ // Default to type reference (custom types like "UserResource")
125
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(typeStr), undefined);
126
+ }
127
+ /**
128
+ * Parse object type properties from a string like "key: type; key2: type2"
129
+ */
130
+ function parseObjectTypeProperties(inner) {
131
+ const properties = [];
132
+ let depth = 0;
133
+ let current = '';
134
+ let i = 0;
135
+ while (i < inner.length) {
136
+ const ch = inner[i];
137
+ if (ch === '{' || ch === '<' || ch === '(')
138
+ depth++;
139
+ else if (ch === '}' || ch === '>' || ch === ')')
140
+ depth--;
141
+ if ((ch === ';' || ch === ',') && depth === 0) {
142
+ const prop = parsePropertyString(current.trim());
143
+ if (prop)
144
+ properties.push(prop);
145
+ current = '';
146
+ }
147
+ else {
148
+ current += ch;
149
+ }
150
+ i++;
151
+ }
152
+ // Handle last property
153
+ const lastProp = parsePropertyString(current.trim());
154
+ if (lastProp)
155
+ properties.push(lastProp);
156
+ return properties;
157
+ }
158
+ /**
159
+ * Parse a single property string like "key: type" or "key?: type"
160
+ */
161
+ function parsePropertyString(propStr) {
162
+ if (!propStr)
163
+ return null;
164
+ const match = propStr.match(/^([A-Za-z0-9_]+)(\?)?:\s*(.+)$/);
165
+ if (!match)
166
+ return null;
167
+ const [, name, optional, typeStr] = match;
168
+ return {
169
+ name,
170
+ type: parseTypeString(typeStr.trim()),
171
+ optional: !!optional,
172
+ };
173
+ }
174
+ /**
175
+ * Create an export default statement.
176
+ */
177
+ export function createExportDefault(expression) {
178
+ return ts.factory.createExportAssignment(undefined, false, expression);
179
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "prettier": "@aniftyco/prettier",
4
4
  "description": "Ferries Laravel types to your TypeScript frontend",
5
5
  "type": "module",
6
- "version": "0.1.1",
6
+ "version": "0.1.3",
7
7
  "repository": "https://github.com/aniftyco/vite-plugin-ferry",
8
8
  "main": "dist/index.js",
9
9
  "types": "dist/index.d.ts",
@@ -13,13 +13,21 @@
13
13
  "scripts": {
14
14
  "start": "tsc --watch",
15
15
  "build": "rm -rf dist/ && tsc",
16
- "prepublishOnly": "rm -rf dist/ && tsc"
16
+ "test": "vitest run",
17
+ "test:watch": "vitest"
18
+ },
19
+ "dependencies": {
20
+ "php-parser": "^3.2.5"
17
21
  },
18
22
  "devDependencies": {
19
23
  "@aniftyco/prettier": "^1.3.0",
20
24
  "@types/node": "^24",
21
25
  "prettier": "^3.6.2",
22
26
  "typescript": "^5.9.2",
23
- "vite": "^7.3.0"
27
+ "vite": "^7.3.0",
28
+ "vitest": "^4.0.16"
29
+ },
30
+ "peerDependencies": {
31
+ "typescript": "^5.0.0"
24
32
  }
25
33
  }