vite-plugin-ferry 0.1.2 → 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 +76 -2
- package/dist/generators/enums.d.ts +3 -1
- package/dist/generators/enums.d.ts.map +1 -1
- package/dist/generators/enums.js +45 -52
- package/dist/generators/resources.d.ts +7 -2
- package/dist/generators/resources.d.ts.map +1 -1
- package/dist/generators/resources.js +44 -39
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -8
- package/dist/utils/php-parser.d.ts +11 -11
- package/dist/utils/php-parser.d.ts.map +1 -1
- package/dist/utils/php-parser.js +241 -67
- package/dist/utils/ts-generator.d.ts +67 -0
- package/dist/utils/ts-generator.d.ts.map +1 -0
- package/dist/utils/ts-generator.js +179 -0
- package/package.json +11 -3
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
|
|
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":"
|
|
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"}
|
package/dist/generators/enums.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
29
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
const val = String(c.value).replace(/'/g, "\\'");
|
|
44
|
+
const properties = enumDef.cases.map((c) => {
|
|
45
|
+
let value;
|
|
63
46
|
if (hasLabels) {
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
56
|
+
value = createStringLiteral(String(c.value));
|
|
69
57
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
return { key: c.key, value };
|
|
59
|
+
});
|
|
60
|
+
nodes.push(createConstObject(enumDef.name, properties));
|
|
73
61
|
}
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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":"
|
|
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,
|
|
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
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
75
|
-
if (
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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()
|
|
185
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
203
|
+
// Create type literal with all fields
|
|
204
|
+
const properties = Object.keys(fields).map((key) => {
|
|
198
205
|
const info = fields[key];
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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(
|
|
21
|
-
const resourcesDir = join(
|
|
22
|
-
const modelsDir = join(
|
|
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(
|
|
25
|
-
const resourcesOutputDir = join(
|
|
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 {
|
|
@@ -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
|
|
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
|
|
15
|
+
export declare function parseEnumContent(phpContent: string): EnumDefinition | null;
|
|
15
16
|
/**
|
|
16
|
-
* Parse
|
|
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
|
|
20
|
+
export declare function parseModelCasts(phpContent: string): Record<string, string>;
|
|
19
21
|
/**
|
|
20
|
-
* Extract
|
|
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
|
|
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":"
|
|
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"}
|
package/dist/utils/php-parser.js
CHANGED
|
@@ -1,96 +1,267 @@
|
|
|
1
|
-
|
|
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
|
|
15
|
+
* Parse PHP content and return the AST.
|
|
16
|
+
* Uses parseEval which doesn't require <?php tags or filenames.
|
|
4
17
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 =
|
|
14
|
-
const backing =
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
*
|
|
187
|
+
* Extract key-value pairs from a PHP array node.
|
|
47
188
|
*/
|
|
48
|
-
|
|
189
|
+
function extractArrayPairs(arrayNode) {
|
|
49
190
|
const pairs = {};
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
*
|
|
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
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
226
|
+
export function parseModelCasts(phpContent) {
|
|
227
|
+
const ast = parsePhp(phpContent);
|
|
228
|
+
if (!ast)
|
|
70
229
|
return {};
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
260
|
+
return {};
|
|
91
261
|
}
|
|
92
262
|
/**
|
|
93
|
-
* Extract docblock array shape from PHP
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|