vite-plugin-ferry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/generators/enums.d.ts +23 -0
- package/dist/generators/enums.d.ts.map +1 -0
- package/dist/generators/enums.js +125 -0
- package/dist/generators/resources.d.ts +26 -0
- package/dist/generators/resources.d.ts.map +1 -0
- package/dist/generators/resources.js +282 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +97 -0
- package/dist/utils/banner.d.ts +20 -0
- package/dist/utils/banner.d.ts.map +1 -0
- package/dist/utils/banner.js +40 -0
- package/dist/utils/file.d.ts +13 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +30 -0
- package/dist/utils/php-parser.d.ts +31 -0
- package/dist/utils/php-parser.d.ts.map +1 -0
- package/dist/utils/php-parser.js +180 -0
- package/dist/utils/type-mapper.d.ts +13 -0
- package/dist/utils/type-mapper.d.ts.map +1 -0
- package/dist/utils/type-mapper.js +158 -0
- package/dist/watchers/enums.d.ts +10 -0
- package/dist/watchers/enums.d.ts.map +1 -0
- package/dist/watchers/enums.js +30 -0
- package/dist/watchers/resources.d.ts +10 -0
- package/dist/watchers/resources.d.ts.map +1 -0
- package/dist/watchers/resources.js +36 -0
- package/package.json +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NiftyCo, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# vite-plugin-ferry
|
|
2
|
+
|
|
3
|
+
> A Vite plugin that ferries your Laravel backend types to the frontend as fully-typed TypeScript.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Ferry watches your Laravel application and automatically generates TypeScript definitions so your frontend always stays
|
|
8
|
+
in sync with your backend.
|
|
9
|
+
|
|
10
|
+
- **Enums** — Generates types and runtime constants from `app/Enums/`
|
|
11
|
+
- **Resources** — Generates response types from `app/Http/Resources/`
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install vite-plugin-ferry --save-dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Add it to your `vite.config.ts`:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { defineConfig } from 'vite';
|
|
23
|
+
import ferry from 'vite-plugin-ferry';
|
|
24
|
+
|
|
25
|
+
export default defineConfig({
|
|
26
|
+
plugins: [ferry()],
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Import your backend types directly in your frontend code:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { OrderStatus } from '@ferry/enums';
|
|
36
|
+
import { UserResource } from '@ferry/resources';
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type EnumDefinition } from '../utils/php-parser.js';
|
|
2
|
+
export type EnumGeneratorOptions = {
|
|
3
|
+
enumsDir: string;
|
|
4
|
+
outputDir: string;
|
|
5
|
+
packageName: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Generate TypeScript type declarations for enums.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateEnumTypeScript(enums: Record<string, EnumDefinition>): string;
|
|
11
|
+
/**
|
|
12
|
+
* Generate runtime JavaScript for enums.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateEnumRuntime(enums: Record<string, EnumDefinition>): string;
|
|
15
|
+
/**
|
|
16
|
+
* Collect all enum definitions from the enums directory.
|
|
17
|
+
*/
|
|
18
|
+
export declare function collectEnums(enumsDir: string): Record<string, EnumDefinition>;
|
|
19
|
+
/**
|
|
20
|
+
* Generate enum files (TypeScript declarations and runtime JavaScript).
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateEnums(options: EnumGeneratorOptions): void;
|
|
23
|
+
//# sourceMappingURL=enums.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getPhpFiles, writeFileEnsureDir } from '../utils/file.js';
|
|
4
|
+
import { parseEnumFile } from '../utils/php-parser.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generate TypeScript type declarations for enums.
|
|
7
|
+
*/
|
|
8
|
+
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('');
|
|
13
|
+
for (const enumName of Object.keys(enums)) {
|
|
14
|
+
const enumDef = enums[enumName];
|
|
15
|
+
const hasLabels = enumDef.cases.some((c) => c.label);
|
|
16
|
+
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('');
|
|
26
|
+
}
|
|
27
|
+
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('');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Generate runtime JavaScript for enums.
|
|
52
|
+
*/
|
|
53
|
+
export function generateEnumRuntime(enums) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
lines.push('// Auto-generated by primcloud Vite plugin');
|
|
56
|
+
lines.push('');
|
|
57
|
+
for (const enumName of Object.keys(enums)) {
|
|
58
|
+
const enumDef = enums[enumName];
|
|
59
|
+
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, "\\'");
|
|
63
|
+
if (hasLabels) {
|
|
64
|
+
const label = c.label ? String(c.label).replace(/'/g, "\\'") : val;
|
|
65
|
+
lines.push(` ${c.key}: { value: '${val}', label: '${label}' },`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lines.push(` ${c.key}: '${val}',`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
lines.push('};');
|
|
72
|
+
lines.push('');
|
|
73
|
+
}
|
|
74
|
+
lines.push('export default {};');
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Collect all enum definitions from the enums directory.
|
|
79
|
+
*/
|
|
80
|
+
export function collectEnums(enumsDir) {
|
|
81
|
+
const enums = {};
|
|
82
|
+
if (!existsSync(enumsDir)) {
|
|
83
|
+
return enums;
|
|
84
|
+
}
|
|
85
|
+
const enumFiles = getPhpFiles(enumsDir);
|
|
86
|
+
for (const file of enumFiles) {
|
|
87
|
+
try {
|
|
88
|
+
const enumPath = join(enumsDir, file);
|
|
89
|
+
const def = parseEnumFile(enumPath);
|
|
90
|
+
if (def) {
|
|
91
|
+
enums[def.name] = def;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
// Ignore parse errors
|
|
96
|
+
console.warn(`Failed to parse enum file: ${file}`, e);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return enums;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generate enum files (TypeScript declarations and runtime JavaScript).
|
|
103
|
+
*/
|
|
104
|
+
export function generateEnums(options) {
|
|
105
|
+
const { enumsDir, outputDir, packageName } = options;
|
|
106
|
+
// Collect all enums
|
|
107
|
+
const enums = collectEnums(enumsDir);
|
|
108
|
+
// Generate TypeScript declarations
|
|
109
|
+
const dtsContent = generateEnumTypeScript(enums);
|
|
110
|
+
const dtsPath = join(outputDir, 'index.d.ts');
|
|
111
|
+
writeFileEnsureDir(dtsPath, dtsContent);
|
|
112
|
+
// Generate runtime JavaScript
|
|
113
|
+
const jsContent = generateEnumRuntime(enums);
|
|
114
|
+
const jsPath = join(outputDir, 'index.js');
|
|
115
|
+
writeFileEnsureDir(jsPath, jsContent);
|
|
116
|
+
// Generate package.json
|
|
117
|
+
const pkgJson = JSON.stringify({
|
|
118
|
+
name: packageName,
|
|
119
|
+
version: '0.0.0',
|
|
120
|
+
main: 'index.js',
|
|
121
|
+
types: 'index.d.ts',
|
|
122
|
+
}, null, 2);
|
|
123
|
+
const pkgPath = join(outputDir, 'package.json');
|
|
124
|
+
writeFileEnsureDir(pkgPath, pkgJson);
|
|
125
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ResourceGeneratorOptions = {
|
|
2
|
+
resourcesDir: string;
|
|
3
|
+
enumsDir: string;
|
|
4
|
+
modelsDir: string;
|
|
5
|
+
outputDir: string;
|
|
6
|
+
packageName: string;
|
|
7
|
+
};
|
|
8
|
+
type FieldInfo = {
|
|
9
|
+
type: string;
|
|
10
|
+
optional: boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Generate TypeScript type declarations for resources.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateResourceTypeScript(resources: Record<string, Record<string, FieldInfo>>, fallbacks: string[], referencedEnums: Set<string>): string;
|
|
16
|
+
/**
|
|
17
|
+
* Generate runtime JavaScript for resources.
|
|
18
|
+
* Resources are type-only, so this just exports an empty object.
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateResourceRuntime(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Generate resource type files (TypeScript declarations and runtime JavaScript).
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateResources(options: ResourceGeneratorOptions): void;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=resources.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, parse } from 'node:path';
|
|
3
|
+
import { getPhpFiles, readFileSafe, writeFileEnsureDir } from '../utils/file.js';
|
|
4
|
+
import { extractDocblockArrayShape, extractReturnArrayBlock, getModelCasts, parseEnumFile, } from '../utils/php-parser.js';
|
|
5
|
+
import { mapDocTypeToTs, mapPhpTypeToTs, parseTsObjectStringToPairs } from '../utils/type-mapper.js';
|
|
6
|
+
/**
|
|
7
|
+
* Map a PHP cast to a TypeScript type, potentially collecting enum references.
|
|
8
|
+
*/
|
|
9
|
+
function mapCastToType(cast, enumsDir, collectedEnums) {
|
|
10
|
+
const original = cast;
|
|
11
|
+
const lower = cast.toLowerCase();
|
|
12
|
+
// Try to find enum in app/Enums
|
|
13
|
+
const match = original.match(/([A-Za-z0-9_\\]+)$/);
|
|
14
|
+
const short = match ? match[1].replace(/^\\+/, '') : original;
|
|
15
|
+
const enumPath = join(enumsDir, `${short}.php`);
|
|
16
|
+
if (existsSync(enumPath)) {
|
|
17
|
+
const def = parseEnumFile(enumPath);
|
|
18
|
+
if (def) {
|
|
19
|
+
collectedEnums[def.name] = def;
|
|
20
|
+
return def.name;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return mapPhpTypeToTs(cast);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Infer TypeScript type from a PHP resource value expression.
|
|
27
|
+
*/
|
|
28
|
+
function inferTypeFromValue(value, key, resourceClass, resourcesDir, modelsDir, enumsDir, collectedEnums) {
|
|
29
|
+
let optional = false;
|
|
30
|
+
// Resource::collection
|
|
31
|
+
const collMatch = value.match(/([A-Za-z0-9_]+)::collection\s*\(\s*(.*?)\s*\)/);
|
|
32
|
+
if (collMatch) {
|
|
33
|
+
const res = collMatch[1];
|
|
34
|
+
const inside = collMatch[2];
|
|
35
|
+
if (inside.includes('whenLoaded('))
|
|
36
|
+
optional = true;
|
|
37
|
+
return { type: `${res}[]`, optional };
|
|
38
|
+
}
|
|
39
|
+
// whenLoaded
|
|
40
|
+
const whenLoadedMatch = value.match(/whenLoaded\(\s*["']([A-Za-z0-9_]+)["']\s*\)/);
|
|
41
|
+
if (whenLoadedMatch) {
|
|
42
|
+
const name = whenLoadedMatch[1];
|
|
43
|
+
optional = true;
|
|
44
|
+
const candidate = `${name[0].toUpperCase()}${name.slice(1)}Resource`;
|
|
45
|
+
const resPath = join(resourcesDir, `${candidate}.php`);
|
|
46
|
+
if (existsSync(resPath)) {
|
|
47
|
+
return { type: candidate, optional };
|
|
48
|
+
}
|
|
49
|
+
return { type: 'Record<string, any>', optional };
|
|
50
|
+
}
|
|
51
|
+
// $this->resource->property
|
|
52
|
+
const propMatch = value.match(/\$this->resource->([A-Za-z0-9_]+)/);
|
|
53
|
+
if (propMatch) {
|
|
54
|
+
const prop = propMatch[1];
|
|
55
|
+
// Boolean checks
|
|
56
|
+
if (/\?\s*true\s*:\s*false|===\s*(true|false)|==\s*(true|false)/i.test(value)) {
|
|
57
|
+
return { type: 'boolean', optional: false };
|
|
58
|
+
}
|
|
59
|
+
if (/\$this->resource->(is|has)[A-Za-z0-9_]*\s*\(/i.test(value)) {
|
|
60
|
+
return { type: 'boolean', optional: false };
|
|
61
|
+
}
|
|
62
|
+
const lower = prop.toLowerCase();
|
|
63
|
+
if (lower.startsWith('is_') || lower.startsWith('has_') || /^(is|has)[A-Z]/.test(prop)) {
|
|
64
|
+
return { type: 'boolean', optional: false };
|
|
65
|
+
}
|
|
66
|
+
// IDs and UUIDs
|
|
67
|
+
if (prop === 'id' || prop.endsWith('_id') || lower === 'uuid') {
|
|
68
|
+
return { type: 'string', optional: false };
|
|
69
|
+
}
|
|
70
|
+
// Check model casts
|
|
71
|
+
const modelCandidate = resourceClass.replace(/Resource$/, '');
|
|
72
|
+
const modelPath = join(modelsDir, `${modelCandidate}.php`);
|
|
73
|
+
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 };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Number heuristics
|
|
85
|
+
if (['last4', 'count', 'total'].includes(prop) || /\d$/.test(prop)) {
|
|
86
|
+
return { type: 'number', optional: false };
|
|
87
|
+
}
|
|
88
|
+
// String heuristics
|
|
89
|
+
if (['id', 'uuid', 'slug', 'name', 'repository', 'region', 'email'].includes(prop)) {
|
|
90
|
+
return { type: 'string', optional: false };
|
|
91
|
+
}
|
|
92
|
+
// Timestamps
|
|
93
|
+
if (prop.endsWith('_at') || ['created_at', 'updated_at', 'lastActive'].includes(prop)) {
|
|
94
|
+
return { type: 'string', optional: false };
|
|
95
|
+
}
|
|
96
|
+
return { type: 'string', optional: false };
|
|
97
|
+
}
|
|
98
|
+
return { type: 'any', optional: false };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parse fields from a PHP array block (from toArray() method).
|
|
102
|
+
*/
|
|
103
|
+
function parseFieldsFromArrayBlock(block, resourceClass, docShape, resourcesDir, modelsDir, enumsDir, collectedEnums) {
|
|
104
|
+
const lines = block.split(/\r?\n/);
|
|
105
|
+
const fields = {};
|
|
106
|
+
for (let i = 0; i < lines.length; i++) {
|
|
107
|
+
const line = lines[i].trim();
|
|
108
|
+
if (!line || line.startsWith('//'))
|
|
109
|
+
continue;
|
|
110
|
+
const match = line.match(/["'](?<key>[A-Za-z0-9_]+)["']\s*=>\s*(?<value>.*?)(?:,\s*$|$)/);
|
|
111
|
+
if (!match || !match.groups)
|
|
112
|
+
continue;
|
|
113
|
+
const key = match.groups.key;
|
|
114
|
+
let value = match.groups.value.trim();
|
|
115
|
+
// Boolean heuristic
|
|
116
|
+
const lowerKey = key.toLowerCase();
|
|
117
|
+
if (lowerKey.startsWith('is_') || lowerKey.startsWith('has_') || /^(is|has)[A-Z]/.test(key)) {
|
|
118
|
+
fields[key] = { type: 'boolean', optional: false };
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Handle nested arrays
|
|
122
|
+
if (value.startsWith('[')) {
|
|
123
|
+
let bracketDepth = (value.match(/\[/g) || []).length - (value.match(/\]/g) || []).length;
|
|
124
|
+
const innerLines = [];
|
|
125
|
+
const rest = value.replace(/^\[\s*/, '');
|
|
126
|
+
if (rest)
|
|
127
|
+
innerLines.push(rest);
|
|
128
|
+
let j = i + 1;
|
|
129
|
+
while (j < lines.length && bracketDepth > 0) {
|
|
130
|
+
const l = lines[j];
|
|
131
|
+
bracketDepth += (l.match(/\[/g) || []).length - (l.match(/\]/g) || []).length;
|
|
132
|
+
innerLines.push(l.trim());
|
|
133
|
+
j++;
|
|
134
|
+
}
|
|
135
|
+
i = j - 1;
|
|
136
|
+
const innerBlock = innerLines.join('\n');
|
|
137
|
+
const nested = parseFieldsFromArrayBlock(innerBlock, resourceClass, docShape, resourcesDir, modelsDir, enumsDir, collectedEnums);
|
|
138
|
+
// Apply docblock shape if available
|
|
139
|
+
if (docShape && docShape[key]) {
|
|
140
|
+
const docType = docShape[key].trim();
|
|
141
|
+
if (docType.startsWith('{')) {
|
|
142
|
+
const docInner = parseTsObjectStringToPairs(docType);
|
|
143
|
+
for (const dk of Object.keys(docInner)) {
|
|
144
|
+
nested[dk] = { type: docInner[dk], optional: false };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const props = [];
|
|
149
|
+
for (const nkey of Object.keys(nested)) {
|
|
150
|
+
const ninfo = nested[nkey];
|
|
151
|
+
const ntype = ninfo.type || 'any';
|
|
152
|
+
const nopt = ninfo.optional ? '?' : '';
|
|
153
|
+
props.push(`${nkey}${nopt}: ${ntype}`);
|
|
154
|
+
}
|
|
155
|
+
const inline = `{ ${props.join('; ')} }`;
|
|
156
|
+
fields[key] = { type: inline, optional: false };
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Use docblock type if available
|
|
160
|
+
if (docShape && docShape[key]) {
|
|
161
|
+
fields[key] = { type: docShape[key], optional: false };
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// Infer type from value
|
|
165
|
+
const info = inferTypeFromValue(value, key, resourceClass, resourcesDir, modelsDir, enumsDir, collectedEnums);
|
|
166
|
+
if (docShape && docShape[key] && (!info.type || info.type === 'any')) {
|
|
167
|
+
info.type = docShape[key];
|
|
168
|
+
info.optional = info.optional ?? false;
|
|
169
|
+
}
|
|
170
|
+
fields[key] = info;
|
|
171
|
+
}
|
|
172
|
+
return fields;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Generate TypeScript type declarations for resources.
|
|
176
|
+
*/
|
|
177
|
+
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('');
|
|
182
|
+
// Import referenced enums from @app/enums
|
|
183
|
+
if (referencedEnums.size > 0) {
|
|
184
|
+
const enumImports = Array.from(referencedEnums).sort().join(', ');
|
|
185
|
+
lines.push(`import type { ${enumImports} } from '@app/enums';`);
|
|
186
|
+
lines.push('');
|
|
187
|
+
}
|
|
188
|
+
// Generate resource types
|
|
189
|
+
for (const className of Object.keys(resources)) {
|
|
190
|
+
const fields = resources[className];
|
|
191
|
+
if (fallbacks.includes(className)) {
|
|
192
|
+
lines.push(`export type ${className} = Record<string, any>;`);
|
|
193
|
+
lines.push('');
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
lines.push(`export type ${className} = {`);
|
|
197
|
+
for (const key of Object.keys(fields)) {
|
|
198
|
+
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('');
|
|
205
|
+
}
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Generate runtime JavaScript for resources.
|
|
210
|
+
* Resources are type-only, so this just exports an empty object.
|
|
211
|
+
*/
|
|
212
|
+
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');
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Generate resource type files (TypeScript declarations and runtime JavaScript).
|
|
222
|
+
*/
|
|
223
|
+
export function generateResources(options) {
|
|
224
|
+
const { resourcesDir, enumsDir, modelsDir, outputDir, packageName } = options;
|
|
225
|
+
const collectedEnums = {};
|
|
226
|
+
const resources = {};
|
|
227
|
+
const fallbacks = [];
|
|
228
|
+
if (!existsSync(resourcesDir)) {
|
|
229
|
+
console.warn(`Resources directory not found: ${resourcesDir}`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const files = getPhpFiles(resourcesDir);
|
|
233
|
+
for (const file of files) {
|
|
234
|
+
try {
|
|
235
|
+
const filePath = join(resourcesDir, file);
|
|
236
|
+
const content = readFileSafe(filePath) || '';
|
|
237
|
+
const className = parse(file).name;
|
|
238
|
+
const docShape = extractDocblockArrayShape(content);
|
|
239
|
+
const arrayBlock = extractReturnArrayBlock(content);
|
|
240
|
+
if (!arrayBlock) {
|
|
241
|
+
fallbacks.push(className);
|
|
242
|
+
resources[className] = {};
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const fields = parseFieldsFromArrayBlock(arrayBlock, className, docShape ? mapDocTypeToTsForShape(docShape) : null, resourcesDir, modelsDir, enumsDir, collectedEnums);
|
|
246
|
+
resources[className] = fields;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
console.warn(`Failed to parse resource file: ${file}`, e);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Track which enums are actually referenced
|
|
254
|
+
const referencedEnums = new Set(Object.keys(collectedEnums));
|
|
255
|
+
// Generate TypeScript declarations
|
|
256
|
+
const dtsContent = generateResourceTypeScript(resources, fallbacks, referencedEnums);
|
|
257
|
+
const dtsPath = join(outputDir, 'index.d.ts');
|
|
258
|
+
writeFileEnsureDir(dtsPath, dtsContent);
|
|
259
|
+
// Generate runtime JavaScript
|
|
260
|
+
const jsContent = generateResourceRuntime();
|
|
261
|
+
const jsPath = join(outputDir, 'index.js');
|
|
262
|
+
writeFileEnsureDir(jsPath, jsContent);
|
|
263
|
+
// Generate package.json
|
|
264
|
+
const pkgJson = JSON.stringify({
|
|
265
|
+
name: packageName,
|
|
266
|
+
version: '0.0.0',
|
|
267
|
+
main: 'index.js',
|
|
268
|
+
types: 'index.d.ts',
|
|
269
|
+
}, null, 2);
|
|
270
|
+
const pkgPath = join(outputDir, 'package.json');
|
|
271
|
+
writeFileEnsureDir(pkgPath, pkgJson);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Map docblock types to TypeScript for each field in a shape.
|
|
275
|
+
*/
|
|
276
|
+
function mapDocTypeToTsForShape(docShape) {
|
|
277
|
+
const result = {};
|
|
278
|
+
for (const [key, type] of Object.entries(docShape)) {
|
|
279
|
+
result[key] = mapDocTypeToTs(type);
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
export type ResourceTypesPluginOptions = {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Vite plugin for generating TypeScript types from Laravel PHP files.
|
|
7
|
+
*
|
|
8
|
+
* This plugin generates separate packages for each type:
|
|
9
|
+
* - @app/enums - PHP enums with labels
|
|
10
|
+
* - @app/resources - Laravel JsonResource types
|
|
11
|
+
* - @app/schemas - (future) Zod schemas from FormRequests
|
|
12
|
+
*/
|
|
13
|
+
export default function ferry(options?: ResourceTypesPluginOptions): Plugin;
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAOnC,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,CAwFR"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { generateEnums } from './generators/enums.js';
|
|
3
|
+
import { generateResources } from './generators/resources.js';
|
|
4
|
+
import { displayBanner } from './utils/banner.js';
|
|
5
|
+
import { setupEnumWatcher } from './watchers/enums.js';
|
|
6
|
+
import { setupResourceWatcher } from './watchers/resources.js';
|
|
7
|
+
/**
|
|
8
|
+
* Vite plugin for generating TypeScript types from Laravel PHP files.
|
|
9
|
+
*
|
|
10
|
+
* This plugin generates separate packages for each type:
|
|
11
|
+
* - @app/enums - PHP enums with labels
|
|
12
|
+
* - @app/resources - Laravel JsonResource types
|
|
13
|
+
* - @app/schemas - (future) Zod schemas from FormRequests
|
|
14
|
+
*/
|
|
15
|
+
export default function ferry(options = {
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
}) {
|
|
18
|
+
const namespace = '@ferry';
|
|
19
|
+
const name = 'vite-plugin-ferry';
|
|
20
|
+
// Directory paths
|
|
21
|
+
const enumsDir = join(options.cwd, 'app/Enums');
|
|
22
|
+
const resourcesDir = join(options.cwd, 'app/Http/Resources');
|
|
23
|
+
const modelsDir = join(options.cwd, 'app/Models');
|
|
24
|
+
// Output directories for each package
|
|
25
|
+
const enumsOutputDir = join(options.cwd, 'node_modules', ...namespace.split('/'), 'enums');
|
|
26
|
+
const resourcesOutputDir = join(options.cwd, 'node_modules', ...namespace.split('/'), 'resources');
|
|
27
|
+
/**
|
|
28
|
+
* Generate all packages.
|
|
29
|
+
*/
|
|
30
|
+
function generateAll() {
|
|
31
|
+
// Generate @app/enums package
|
|
32
|
+
generateEnums({
|
|
33
|
+
enumsDir,
|
|
34
|
+
outputDir: enumsOutputDir,
|
|
35
|
+
packageName: `${namespace}/enums`,
|
|
36
|
+
});
|
|
37
|
+
// Generate @app/resources package
|
|
38
|
+
generateResources({
|
|
39
|
+
resourcesDir,
|
|
40
|
+
enumsDir,
|
|
41
|
+
modelsDir,
|
|
42
|
+
outputDir: resourcesOutputDir,
|
|
43
|
+
packageName: `${namespace}/resources`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
enforce: 'pre',
|
|
49
|
+
// Run generation during config resolution so files exist before other plugins need them
|
|
50
|
+
config() {
|
|
51
|
+
try {
|
|
52
|
+
generateAll();
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
console.error(`[${name}] Error generating types during config():`, e);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
},
|
|
59
|
+
// Run generation when build starts
|
|
60
|
+
buildStart() {
|
|
61
|
+
try {
|
|
62
|
+
generateAll();
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
console.error(`[${name}] Error generating types during buildStart():`, e);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
// Set up watchers for dev server
|
|
69
|
+
configureServer(server) {
|
|
70
|
+
// Display startup banner
|
|
71
|
+
server.httpServer?.once('listening', () => {
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
displayBanner({
|
|
74
|
+
packages: [`${namespace}/enums`, `${namespace}/resources`],
|
|
75
|
+
version: '1.0.0',
|
|
76
|
+
});
|
|
77
|
+
}, 200);
|
|
78
|
+
});
|
|
79
|
+
// Set up enum watcher
|
|
80
|
+
setupEnumWatcher({
|
|
81
|
+
enumsDir,
|
|
82
|
+
outputDir: enumsOutputDir,
|
|
83
|
+
packageName: `${namespace}/enums`,
|
|
84
|
+
server,
|
|
85
|
+
});
|
|
86
|
+
// Set up resource watcher
|
|
87
|
+
setupResourceWatcher({
|
|
88
|
+
resourcesDir,
|
|
89
|
+
enumsDir,
|
|
90
|
+
modelsDir,
|
|
91
|
+
outputDir: resourcesOutputDir,
|
|
92
|
+
packageName: `${namespace}/resources`,
|
|
93
|
+
server,
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display a startup banner for the plugin.
|
|
3
|
+
*/
|
|
4
|
+
export declare function displayBanner(options: {
|
|
5
|
+
packages: string[];
|
|
6
|
+
version?: string;
|
|
7
|
+
}): void;
|
|
8
|
+
/**
|
|
9
|
+
* Log a file change event.
|
|
10
|
+
*/
|
|
11
|
+
export declare function logFileChange(packageName: string, fileName: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Log a regeneration event.
|
|
14
|
+
*/
|
|
15
|
+
export declare function logRegeneration(packageName: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Log an error.
|
|
18
|
+
*/
|
|
19
|
+
export declare function logError(packageName: string, message: string, error?: any): void;
|
|
20
|
+
//# sourceMappingURL=banner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../../src/utils/banner.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAYrF;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIzE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAIzD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,GAAG,IAAI,CAMhF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
/**
|
|
3
|
+
* Display a startup banner for the plugin.
|
|
4
|
+
*/
|
|
5
|
+
export function displayBanner(options) {
|
|
6
|
+
const { packages, version = '1.0.0' } = options;
|
|
7
|
+
console.log('');
|
|
8
|
+
console.log(pc.cyan(' PRIMCLOUD') + pc.dim(` resource-types ${pc.bold(`v${version}`)}`));
|
|
9
|
+
console.log('');
|
|
10
|
+
for (const pkg of packages) {
|
|
11
|
+
console.log(pc.green(' ➜') + ' ' + pc.bold(pkg));
|
|
12
|
+
}
|
|
13
|
+
console.log('');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Log a file change event.
|
|
17
|
+
*/
|
|
18
|
+
export function logFileChange(packageName, fileName) {
|
|
19
|
+
const pkgLabel = pc.cyan(`[${packageName}]`);
|
|
20
|
+
const fileLabel = pc.dim(fileName);
|
|
21
|
+
console.log(`${pkgLabel} File changed: ${fileLabel}`);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Log a regeneration event.
|
|
25
|
+
*/
|
|
26
|
+
export function logRegeneration(packageName) {
|
|
27
|
+
const pkgLabel = pc.cyan(`[${packageName}]`);
|
|
28
|
+
const message = pc.green('✓') + ' Regenerated types';
|
|
29
|
+
console.log(`${pkgLabel} ${message}`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Log an error.
|
|
33
|
+
*/
|
|
34
|
+
export function logError(packageName, message, error) {
|
|
35
|
+
const pkgLabel = pc.red(`[${packageName}]`);
|
|
36
|
+
console.error(`${pkgLabel} ${pc.red('✗')} ${message}`);
|
|
37
|
+
if (error) {
|
|
38
|
+
console.error(pc.dim(error.stack || error.message || String(error)));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely read a file, returning null if it doesn't exist or can't be read.
|
|
3
|
+
*/
|
|
4
|
+
export declare function readFileSafe(filePath: string): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Write a file, ensuring the directory exists.
|
|
7
|
+
*/
|
|
8
|
+
export declare function writeFileEnsureDir(filePath: string, content: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Get all PHP files from a directory.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getPhpFiles(dir: string): string[];
|
|
13
|
+
//# sourceMappingURL=file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/utils/file.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM5D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAI1E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAKjD"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Safely read a file, returning null if it doesn't exist or can't be read.
|
|
5
|
+
*/
|
|
6
|
+
export function readFileSafe(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
return readFileSync(filePath, 'utf8');
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Write a file, ensuring the directory exists.
|
|
16
|
+
*/
|
|
17
|
+
export function writeFileEnsureDir(filePath, content) {
|
|
18
|
+
const dir = dirname(filePath);
|
|
19
|
+
mkdirSync(dir, { recursive: true });
|
|
20
|
+
writeFileSync(filePath, content, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get all PHP files from a directory.
|
|
24
|
+
*/
|
|
25
|
+
export function getPhpFiles(dir) {
|
|
26
|
+
if (!existsSync(dir)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return readdirSync(dir).filter((f) => f.endsWith('.php'));
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type EnumCase = {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
};
|
|
6
|
+
export type EnumDefinition = {
|
|
7
|
+
name: string;
|
|
8
|
+
backing: string | null;
|
|
9
|
+
cases: EnumCase[];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Parse a PHP enum file and extract its definition.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseEnumFile(enumPath: string): EnumDefinition | null;
|
|
15
|
+
/**
|
|
16
|
+
* Parse PHP array pairs from a string like "'key' => 'value'".
|
|
17
|
+
*/
|
|
18
|
+
export declare function parsePhpArrayPairs(inside: string): Record<string, string>;
|
|
19
|
+
/**
|
|
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.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractDocblockArrayShape(phpContent: string): Record<string, string> | null;
|
|
27
|
+
/**
|
|
28
|
+
* Extract the return array block from a toArray() method in a PHP resource.
|
|
29
|
+
*/
|
|
30
|
+
export declare function extractReturnArrayBlock(phpContent: string): string | null;
|
|
31
|
+
//# sourceMappingURL=php-parser.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { readFileSafe } from './file.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a PHP enum file and extract its definition.
|
|
4
|
+
*/
|
|
5
|
+
export function parseEnumFile(enumPath) {
|
|
6
|
+
const content = readFileSafe(enumPath);
|
|
7
|
+
if (!content)
|
|
8
|
+
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)
|
|
12
|
+
return null;
|
|
13
|
+
const name = enumMatch[1];
|
|
14
|
+
const backing = enumMatch[2] ? enumMatch[2].toLowerCase() : null;
|
|
15
|
+
// Extract enum cases
|
|
16
|
+
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] });
|
|
21
|
+
}
|
|
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] });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
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;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { name, backing, cases };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse PHP array pairs from a string like "'key' => 'value'".
|
|
47
|
+
*/
|
|
48
|
+
export function parsePhpArrayPairs(inside) {
|
|
49
|
+
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);
|
|
59
|
+
}
|
|
60
|
+
pairs[m.groups.key] = val;
|
|
61
|
+
}
|
|
62
|
+
return pairs;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Extract model casts from a PHP model file.
|
|
66
|
+
*/
|
|
67
|
+
export function getModelCasts(modelPath) {
|
|
68
|
+
const content = readFileSafe(modelPath);
|
|
69
|
+
if (!content)
|
|
70
|
+
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]);
|
|
80
|
+
}
|
|
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;
|
|
89
|
+
}
|
|
90
|
+
return res;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Extract docblock array shape from PHP file content.
|
|
94
|
+
*/
|
|
95
|
+
export function extractDocblockArrayShape(phpContent) {
|
|
96
|
+
const match = phpContent.match(/@return\s+array\s*\{/s);
|
|
97
|
+
if (!match)
|
|
98
|
+
return null;
|
|
99
|
+
const startPos = match.index;
|
|
100
|
+
const openBracePos = phpContent.indexOf('{', startPos);
|
|
101
|
+
if (openBracePos === -1)
|
|
102
|
+
return null;
|
|
103
|
+
// Find matching closing brace
|
|
104
|
+
let depth = 0;
|
|
105
|
+
let pos = openBracePos;
|
|
106
|
+
let endPos = null;
|
|
107
|
+
while (pos < phpContent.length) {
|
|
108
|
+
const ch = phpContent[pos];
|
|
109
|
+
if (ch === '{')
|
|
110
|
+
depth++;
|
|
111
|
+
else if (ch === '}') {
|
|
112
|
+
depth--;
|
|
113
|
+
if (depth === 0) {
|
|
114
|
+
endPos = pos;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
pos++;
|
|
119
|
+
}
|
|
120
|
+
if (endPos === null)
|
|
121
|
+
return null;
|
|
122
|
+
const inside = phpContent.slice(openBracePos + 1, endPos);
|
|
123
|
+
const pairs = {};
|
|
124
|
+
let i = 0;
|
|
125
|
+
while (i < inside.length) {
|
|
126
|
+
// Skip whitespace and commas
|
|
127
|
+
while (i < inside.length && (inside[i].match(/\s/) || inside[i] === ','))
|
|
128
|
+
i++;
|
|
129
|
+
if (i >= inside.length)
|
|
130
|
+
break;
|
|
131
|
+
// Extract key
|
|
132
|
+
const keyMatch = inside.slice(i).match(/^[A-Za-z0-9_]+/);
|
|
133
|
+
if (!keyMatch)
|
|
134
|
+
break;
|
|
135
|
+
const key = keyMatch[0];
|
|
136
|
+
i += key.length;
|
|
137
|
+
// Skip to colon
|
|
138
|
+
while (i < inside.length && /\s/.test(inside[i]))
|
|
139
|
+
i++;
|
|
140
|
+
if (i >= inside.length || inside[i] !== ':')
|
|
141
|
+
break;
|
|
142
|
+
i++;
|
|
143
|
+
// Extract type
|
|
144
|
+
while (i < inside.length && /\s/.test(inside[i]))
|
|
145
|
+
i++;
|
|
146
|
+
const typeStart = i;
|
|
147
|
+
let depthCur = 0;
|
|
148
|
+
while (i < inside.length) {
|
|
149
|
+
const ch = inside[i];
|
|
150
|
+
if (ch === '{' || ch === '<' || ch === '(')
|
|
151
|
+
depthCur++;
|
|
152
|
+
else if (ch === '}' || ch === '>' || ch === ')') {
|
|
153
|
+
if (depthCur > 0)
|
|
154
|
+
depthCur--;
|
|
155
|
+
}
|
|
156
|
+
else if (ch === ',' && depthCur === 0)
|
|
157
|
+
break;
|
|
158
|
+
i++;
|
|
159
|
+
}
|
|
160
|
+
const type = inside.slice(typeStart, i).trim();
|
|
161
|
+
if (type)
|
|
162
|
+
pairs[key] = type;
|
|
163
|
+
if (i < inside.length && inside[i] === ',')
|
|
164
|
+
i++;
|
|
165
|
+
}
|
|
166
|
+
return pairs;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Extract the return array block from a toArray() method in a PHP resource.
|
|
170
|
+
*/
|
|
171
|
+
export function extractReturnArrayBlock(phpContent) {
|
|
172
|
+
const match = phpContent.match(/function\s+toArray\s*\([^)]*\)\s*:\s*array\s*\{([\s\S]*?)\n\s*\}/);
|
|
173
|
+
if (!match)
|
|
174
|
+
return null;
|
|
175
|
+
const body = match[1];
|
|
176
|
+
const returnMatch = body.match(/return\s*\[\s*([\s\S]*?)\s*\];/);
|
|
177
|
+
if (!returnMatch)
|
|
178
|
+
return null;
|
|
179
|
+
return returnMatch[1];
|
|
180
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map PHP types to TypeScript types.
|
|
3
|
+
*/
|
|
4
|
+
export declare function mapPhpTypeToTs(phpType: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Map docblock types to TypeScript types.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mapDocTypeToTs(docType: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Parse TypeScript object string to key-value pairs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseTsObjectStringToPairs(tsObj: string): Record<string, string>;
|
|
13
|
+
//# sourceMappingURL=type-mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type-mapper.d.ts","sourceRoot":"","sources":["../../src/utils/type-mapper.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWtD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAiGtD;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA8ChF"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map PHP types to TypeScript types.
|
|
3
|
+
*/
|
|
4
|
+
export function mapPhpTypeToTs(phpType) {
|
|
5
|
+
const lower = phpType.toLowerCase();
|
|
6
|
+
if (['int', 'integer'].includes(lower))
|
|
7
|
+
return 'number';
|
|
8
|
+
if (['real', 'float', 'double', 'decimal'].includes(lower))
|
|
9
|
+
return 'number';
|
|
10
|
+
if (lower === 'string')
|
|
11
|
+
return 'string';
|
|
12
|
+
if (['bool', 'boolean'].includes(lower))
|
|
13
|
+
return 'boolean';
|
|
14
|
+
if (['array', 'json'].includes(lower))
|
|
15
|
+
return 'any[]';
|
|
16
|
+
if (['datetime', 'date', 'immutable_datetime', 'immutable_date'].includes(lower))
|
|
17
|
+
return 'string';
|
|
18
|
+
return 'any';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Map docblock types to TypeScript types.
|
|
22
|
+
*/
|
|
23
|
+
export function mapDocTypeToTs(docType) {
|
|
24
|
+
let type = docType.trim();
|
|
25
|
+
let nullable = false;
|
|
26
|
+
if (type.startsWith('?')) {
|
|
27
|
+
nullable = true;
|
|
28
|
+
type = type.slice(1);
|
|
29
|
+
}
|
|
30
|
+
// Handle array shapes like "array {key: type, ...}"
|
|
31
|
+
const arrShape = type.match(/^array\s*\{(.+)\}$/s);
|
|
32
|
+
if (arrShape) {
|
|
33
|
+
const inside = arrShape[1];
|
|
34
|
+
const parts = [];
|
|
35
|
+
const innerRe = /(?<key>[A-Za-z0-9_]+)\s*:\s*(?<type>[^,\n}]+)/g;
|
|
36
|
+
for (const mm of inside.matchAll(innerRe)) {
|
|
37
|
+
const k = mm.groups.key;
|
|
38
|
+
const t = mm.groups.type.trim();
|
|
39
|
+
parts.push(`${k}: ${mapDocTypeToTs(t)}`);
|
|
40
|
+
}
|
|
41
|
+
const obj = `{ ${parts.join('; ')} }`;
|
|
42
|
+
return nullable ? `${obj} | null` : obj;
|
|
43
|
+
}
|
|
44
|
+
// Handle union types
|
|
45
|
+
const parts = type
|
|
46
|
+
.split('|')
|
|
47
|
+
.map((p) => p.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
const mapped = [];
|
|
50
|
+
for (const p of parts) {
|
|
51
|
+
const low = p.toLowerCase();
|
|
52
|
+
if (low === 'null') {
|
|
53
|
+
mapped.push('null');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (low === 'mixed') {
|
|
57
|
+
mapped.push('any');
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (low === 'array') {
|
|
61
|
+
mapped.push('any[]');
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (['int', 'integer', 'float', 'double', 'number', 'decimal'].includes(low)) {
|
|
65
|
+
mapped.push('number');
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (['bool', 'boolean'].includes(low)) {
|
|
69
|
+
mapped.push('boolean');
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (low.startsWith('string')) {
|
|
73
|
+
mapped.push('string');
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (low === 'object' || low === 'stdclass') {
|
|
77
|
+
mapped.push('Record<string, any>');
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Handle array notation like "Foo[]"
|
|
81
|
+
const arrMatch = p.match(/^(?<inner>[A-Za-z0-9_\\]+)\[\]$/);
|
|
82
|
+
if (arrMatch) {
|
|
83
|
+
const inner = arrMatch.groups.inner.replace(/\\\\/g, '');
|
|
84
|
+
mapped.push(`${inner}[]`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Handle generic array like "array<Foo>"
|
|
88
|
+
const genMatch = p.match(/array\s*<\s*([^,>\s]+)\s*>/i);
|
|
89
|
+
if (genMatch) {
|
|
90
|
+
const inner = genMatch[1].replace(/[^A-Za-z0-9_]/g, '');
|
|
91
|
+
mapped.push(`${inner}[]`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Handle Record types
|
|
95
|
+
if (/record\s*<\s*[^>]+>/i.test(p) || p.includes('Record')) {
|
|
96
|
+
mapped.push(p.replace('mixed', 'any'));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Default: sanitize and use as-is
|
|
100
|
+
const san = p.replace(/[^A-Za-z0-9_\\[\]]/g, '').replace(/\\/g, '');
|
|
101
|
+
mapped.push(san === '' ? 'any' : san);
|
|
102
|
+
}
|
|
103
|
+
if (nullable && !mapped.includes('null')) {
|
|
104
|
+
mapped.push('null');
|
|
105
|
+
}
|
|
106
|
+
return Array.from(new Set(mapped)).join(' | ');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Parse TypeScript object string to key-value pairs.
|
|
110
|
+
*/
|
|
111
|
+
export function parseTsObjectStringToPairs(tsObj) {
|
|
112
|
+
const pairs = {};
|
|
113
|
+
let inside = tsObj.trim();
|
|
114
|
+
if (!inside.startsWith('{') || !inside.endsWith('}'))
|
|
115
|
+
return pairs;
|
|
116
|
+
inside = inside.slice(1, -1);
|
|
117
|
+
let i = 0;
|
|
118
|
+
while (i < inside.length) {
|
|
119
|
+
// Skip whitespace and separators
|
|
120
|
+
while (i < inside.length && (/\s/.test(inside[i]) || inside[i] === ';' || inside[i] === ','))
|
|
121
|
+
i++;
|
|
122
|
+
// Extract key
|
|
123
|
+
const keyMatch = inside.slice(i).match(/^[A-Za-z0-9_]+\??/);
|
|
124
|
+
if (!keyMatch)
|
|
125
|
+
break;
|
|
126
|
+
const keyRaw = keyMatch[0];
|
|
127
|
+
i += keyRaw.length;
|
|
128
|
+
const key = keyRaw.endsWith('?') ? keyRaw.slice(0, -1) : keyRaw;
|
|
129
|
+
// Skip to colon
|
|
130
|
+
while (i < inside.length && /\s/.test(inside[i]))
|
|
131
|
+
i++;
|
|
132
|
+
if (i >= inside.length || inside[i] !== ':')
|
|
133
|
+
break;
|
|
134
|
+
i++;
|
|
135
|
+
// Extract type
|
|
136
|
+
while (i < inside.length && /\s/.test(inside[i]))
|
|
137
|
+
i++;
|
|
138
|
+
const typeStart = i;
|
|
139
|
+
let depth = 0;
|
|
140
|
+
while (i < inside.length) {
|
|
141
|
+
const ch = inside[i];
|
|
142
|
+
if (ch === '{' || ch === '(' || ch === '<')
|
|
143
|
+
depth++;
|
|
144
|
+
else if (ch === '}' || ch === ')' || ch === '>') {
|
|
145
|
+
if (depth > 0)
|
|
146
|
+
depth--;
|
|
147
|
+
}
|
|
148
|
+
else if ((ch === ';' || ch === ',') && depth === 0)
|
|
149
|
+
break;
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
const type = inside.slice(typeStart, i).trim();
|
|
153
|
+
pairs[key] = type === '' ? 'any' : type;
|
|
154
|
+
if (i < inside.length && (inside[i] === ';' || inside[i] === ','))
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
return pairs;
|
|
158
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ViteDevServer } from 'vite';
|
|
2
|
+
import { type EnumGeneratorOptions } from '../generators/enums.js';
|
|
3
|
+
export type EnumWatcherOptions = EnumGeneratorOptions & {
|
|
4
|
+
server: ViteDevServer;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Set up a watcher for enum files.
|
|
8
|
+
*/
|
|
9
|
+
export declare function setupEnumWatcher(options: EnumWatcherOptions): void;
|
|
10
|
+
//# sourceMappingURL=enums.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enums.d.ts","sourceRoot":"","sources":["../../src/watchers/enums.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAiB,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGlF,MAAM,MAAM,kBAAkB,GAAG,oBAAoB,GAAG;IACtD,MAAM,EAAE,aAAa,CAAC;CACvB,CAAC;AAEF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CA6BlE"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { join, basename } from 'node:path';
|
|
2
|
+
import { generateEnums } from '../generators/enums.js';
|
|
3
|
+
import { logError, logFileChange, logRegeneration } from '../utils/banner.js';
|
|
4
|
+
/**
|
|
5
|
+
* Set up a watcher for enum files.
|
|
6
|
+
*/
|
|
7
|
+
export function setupEnumWatcher(options) {
|
|
8
|
+
const { enumsDir, outputDir, packageName, server } = options;
|
|
9
|
+
const enumPattern = join(enumsDir, '*.php');
|
|
10
|
+
const generatedJsPath = join(outputDir, 'index.js');
|
|
11
|
+
// Watch PHP enum files
|
|
12
|
+
server.watcher.add(enumPattern);
|
|
13
|
+
// Also watch the generated JS file (for HMR)
|
|
14
|
+
server.watcher.add(generatedJsPath);
|
|
15
|
+
server.watcher.on('change', (filePath) => {
|
|
16
|
+
if (filePath.startsWith(enumsDir)) {
|
|
17
|
+
try {
|
|
18
|
+
logFileChange('enums', basename(filePath));
|
|
19
|
+
// Regenerate enum files
|
|
20
|
+
generateEnums({ enumsDir, outputDir, packageName });
|
|
21
|
+
// Tell Vite the generated file changed (triggers normal HMR)
|
|
22
|
+
server.watcher.emit('change', generatedJsPath);
|
|
23
|
+
logRegeneration('enums');
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
logError('enums', 'Error regenerating enum types', e);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ViteDevServer } from 'vite';
|
|
2
|
+
import { type ResourceGeneratorOptions } from '../generators/resources.js';
|
|
3
|
+
export type ResourceWatcherOptions = ResourceGeneratorOptions & {
|
|
4
|
+
server: ViteDevServer;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Set up a watcher for resource and model files.
|
|
8
|
+
*/
|
|
9
|
+
export declare function setupResourceWatcher(options: ResourceWatcherOptions): void;
|
|
10
|
+
//# sourceMappingURL=resources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../src/watchers/resources.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAqB,KAAK,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAG9F,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,GAAG;IAC9D,MAAM,EAAE,aAAa,CAAC;CACvB,CAAC;AAEF;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAqC1E"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { join, basename } from 'node:path';
|
|
2
|
+
import { generateResources } from '../generators/resources.js';
|
|
3
|
+
import { logError, logFileChange, logRegeneration } from '../utils/banner.js';
|
|
4
|
+
/**
|
|
5
|
+
* Set up a watcher for resource and model files.
|
|
6
|
+
*/
|
|
7
|
+
export function setupResourceWatcher(options) {
|
|
8
|
+
const { resourcesDir, enumsDir, modelsDir, outputDir, packageName, server } = options;
|
|
9
|
+
const resourcePattern = join(resourcesDir, '*.php');
|
|
10
|
+
const modelPattern = join(modelsDir, '*.php');
|
|
11
|
+
const generatedDtsPath = join(outputDir, 'index.d.ts');
|
|
12
|
+
// Watch PHP resource and model files
|
|
13
|
+
server.watcher.add(resourcePattern);
|
|
14
|
+
server.watcher.add(modelPattern);
|
|
15
|
+
// Also watch the generated .d.ts file
|
|
16
|
+
server.watcher.add(generatedDtsPath);
|
|
17
|
+
const handleChange = (filePath) => {
|
|
18
|
+
if (filePath.startsWith(resourcesDir) || filePath.startsWith(modelsDir)) {
|
|
19
|
+
try {
|
|
20
|
+
const isModel = filePath.startsWith(modelsDir);
|
|
21
|
+
const fileType = isModel ? 'model' : 'resource';
|
|
22
|
+
logFileChange(fileType, basename(filePath));
|
|
23
|
+
// Regenerate resource types
|
|
24
|
+
generateResources({ resourcesDir, enumsDir, modelsDir, outputDir, packageName });
|
|
25
|
+
// Tell Vite the generated type file changed
|
|
26
|
+
// TypeScript will pick up changes automatically
|
|
27
|
+
server.watcher.emit('change', generatedDtsPath);
|
|
28
|
+
logRegeneration('resources');
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
logError('resources', 'Error regenerating resource types', e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
server.watcher.on('change', handleChange);
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-ferry",
|
|
3
|
+
"prettier": "@aniftyco/prettier",
|
|
4
|
+
"description": "Ferries Laravel types to your TypeScript frontend",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"repository": "https://github.com/aniftyco/vite-plugin-ferry",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "tsc --watch",
|
|
15
|
+
"build": "rm -rf dist/ && tsc",
|
|
16
|
+
"prepublishOnly": "rm -rf dist/ && tsc"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@aniftyco/prettier": "^1.3.0",
|
|
20
|
+
"@types/node": "^24",
|
|
21
|
+
"prettier": "^3.6.2",
|
|
22
|
+
"typescript": "^5.9.2",
|
|
23
|
+
"vite": "^7.3.0"
|
|
24
|
+
}
|
|
25
|
+
}
|