vite-plugin-ferry 0.1.2 → 0.1.4

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.
@@ -1,221 +1,53 @@
1
+ import ts from 'typescript';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { join, parse } from 'node:path';
3
4
  import { getPhpFiles, readFileSafe, writeFileEnsureDir } from '../utils/file.js';
4
- import { extractDocblockArrayShape, extractReturnArrayBlock, getModelCasts, parseEnumFile, } from '../utils/php-parser.js';
5
- import { 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
- }
5
+ import { extractDocblockArrayShape, parseResourceFieldsAst, } from '../utils/php-parser.js';
6
+ import { mapDocTypeToTs } from '../utils/type-mapper.js';
7
+ import { printNode, createTypeAlias, createImportType, parseTypeString, createTypeLiteral, } from '../utils/ts-generator.js';
174
8
  /**
175
9
  * Generate TypeScript type declarations for resources.
176
10
  */
177
11
  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('');
12
+ const nodes = [];
182
13
  // Import referenced enums from @app/enums
183
14
  if (referencedEnums.size > 0) {
184
- const enumImports = Array.from(referencedEnums).sort().join(', ');
185
- lines.push(`import type { ${enumImports} } from '@app/enums';`);
186
- lines.push('');
15
+ const enumImports = Array.from(referencedEnums).sort();
16
+ nodes.push(createImportType(enumImports, '@app/enums'));
187
17
  }
188
18
  // Generate resource types
189
19
  for (const className of Object.keys(resources)) {
190
20
  const fields = resources[className];
191
21
  if (fallbacks.includes(className)) {
192
- lines.push(`export type ${className} = Record<string, any>;`);
193
- lines.push('');
22
+ // Fallback type: Record<string, any>
23
+ const recordType = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [
24
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
25
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
26
+ ]);
27
+ nodes.push(createTypeAlias(className, recordType));
194
28
  continue;
195
29
  }
196
- lines.push(`export type ${className} = {`);
197
- for (const key of Object.keys(fields)) {
30
+ // Create type literal with all fields
31
+ const properties = Object.keys(fields).map((key) => {
198
32
  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('');
33
+ return {
34
+ name: key,
35
+ type: parseTypeString(info.type || 'any'),
36
+ optional: info.optional,
37
+ };
38
+ });
39
+ nodes.push(createTypeAlias(className, createTypeLiteral(properties)));
205
40
  }
206
- return lines.join('\n');
41
+ if (nodes.length === 0)
42
+ return '';
43
+ return nodes.map(printNode).join('\n\n') + '\n';
207
44
  }
208
45
  /**
209
46
  * Generate runtime JavaScript for resources.
210
47
  * Resources are type-only, so this just exports an empty object.
211
48
  */
212
49
  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');
50
+ return 'export default {};';
219
51
  }
220
52
  /**
221
53
  * Generate resource type files (TypeScript declarations and runtime JavaScript).
@@ -236,13 +68,19 @@ export function generateResources(options) {
236
68
  const content = readFileSafe(filePath) || '';
237
69
  const className = parse(file).name;
238
70
  const docShape = extractDocblockArrayShape(content);
239
- const arrayBlock = extractReturnArrayBlock(content);
240
- if (!arrayBlock) {
71
+ const mappedDocShape = docShape ? mapDocTypeToTsForShape(docShape) : null;
72
+ const fields = parseResourceFieldsAst(content, {
73
+ resourcesDir,
74
+ modelsDir,
75
+ enumsDir,
76
+ docShape: mappedDocShape,
77
+ collectedEnums,
78
+ });
79
+ if (!fields) {
241
80
  fallbacks.push(className);
242
81
  resources[className] = {};
243
82
  }
244
83
  else {
245
- const fields = parseFieldsFromArrayBlock(arrayBlock, className, docShape ? mapDocTypeToTsForShape(docShape) : null, resourcesDir, modelsDir, enumsDir, collectedEnums);
246
84
  resources[className] = fields;
247
85
  }
248
86
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Plugin } from 'vite';
2
2
  export type ResourceTypesPluginOptions = {
3
3
  cwd?: string;
4
+ prettyPrint?: boolean;
4
5
  };
5
6
  /**
6
7
  * Vite plugin for generating TypeScript types from Laravel PHP files.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAMnC,MAAM,MAAM,0BAA0B,GAAG;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,UAAU,KAAK,CAC3B,OAAO,GAAE,0BAER,GACA,MAAM,CAmFR"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAMnC,MAAM,MAAM,0BAA0B,GAAG;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,OAAO,GAAE,0BAA+B,GAAG,MAAM,CAyF9E"}
package/dist/index.js CHANGED
@@ -11,18 +11,19 @@ import { setupResourceWatcher } from './watchers/resources.js';
11
11
  * - @app/resources - Laravel JsonResource types
12
12
  * - @app/schemas - (future) Zod schemas from FormRequests
13
13
  */
14
- export default function ferry(options = {
15
- cwd: process.cwd(),
16
- }) {
14
+ export default function ferry(options = {}) {
17
15
  const namespace = '@ferry';
18
16
  const name = 'vite-plugin-ferry';
17
+ // Apply defaults
18
+ const cwd = options.cwd ?? process.cwd();
19
+ const prettyPrint = options.prettyPrint ?? true;
19
20
  // Directory paths
20
- const enumsDir = join(options.cwd, 'app/Enums');
21
- const resourcesDir = join(options.cwd, 'app/Http/Resources');
22
- const modelsDir = join(options.cwd, 'app/Models');
21
+ const enumsDir = join(cwd, 'app/Enums');
22
+ const resourcesDir = join(cwd, 'app/Http/Resources');
23
+ const modelsDir = join(cwd, 'app/Models');
23
24
  // Output directories for each package
24
- const enumsOutputDir = join(options.cwd, 'node_modules', ...namespace.split('/'), 'enums');
25
- const resourcesOutputDir = join(options.cwd, 'node_modules', ...namespace.split('/'), 'resources');
25
+ const enumsOutputDir = join(cwd, 'node_modules', ...namespace.split('/'), 'enums');
26
+ const resourcesOutputDir = join(cwd, 'node_modules', ...namespace.split('/'), 'resources');
26
27
  /**
27
28
  * Generate all packages.
28
29
  */
@@ -32,6 +33,7 @@ export default function ferry(options = {
32
33
  enumsDir,
33
34
  outputDir: enumsOutputDir,
34
35
  packageName: `${namespace}/enums`,
36
+ prettyPrint,
35
37
  });
36
38
  // Generate @app/resources package
37
39
  generateResources({
@@ -40,6 +42,7 @@ export default function ferry(options = {
40
42
  modelsDir,
41
43
  outputDir: resourcesOutputDir,
42
44
  packageName: `${namespace}/resources`,
45
+ prettyPrint,
43
46
  });
44
47
  }
45
48
  return {
@@ -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,40 @@ export type EnumDefinition = {
9
9
  cases: EnumCase[];
10
10
  };
11
11
  /**
12
- * Parse a PHP enum file and extract its definition.
12
+ * Parse PHP enum content and extract its definition.
13
+ * This is a pure function that takes PHP source code as input.
13
14
  */
14
- export declare function parseEnumFile(enumPath: string): EnumDefinition | null;
15
+ export declare function parseEnumContent(phpContent: string): EnumDefinition | null;
15
16
  /**
16
- * Parse PHP array pairs from a string like "'key' => 'value'".
17
+ * Parse model casts from PHP model content.
18
+ * This is a pure function that takes PHP source code as input.
17
19
  */
18
- export declare function parsePhpArrayPairs(inside: string): Record<string, string>;
20
+ export declare function parseModelCasts(phpContent: string): Record<string, string>;
19
21
  /**
20
- * Extract model casts from a PHP model file.
21
- */
22
- export declare function getModelCasts(modelPath: string): Record<string, string>;
23
- /**
24
- * Extract docblock array shape from PHP file content.
22
+ * Extract docblock array shape from PHP content.
23
+ * This is a pure function that takes PHP source code as input.
25
24
  */
26
25
  export declare function extractDocblockArrayShape(phpContent: string): Record<string, string> | null;
26
+ export type ResourceFieldInfo = {
27
+ type: string;
28
+ optional: boolean;
29
+ };
30
+ export type ResourceArrayEntry = {
31
+ key: string;
32
+ fieldInfo: ResourceFieldInfo;
33
+ nested?: Record<string, ResourceArrayEntry>;
34
+ };
35
+ export type ParseResourceOptions = {
36
+ resourcesDir?: string;
37
+ modelsDir?: string;
38
+ enumsDir?: string;
39
+ docShape?: Record<string, string> | null;
40
+ collectedEnums?: Record<string, EnumDefinition>;
41
+ resourceClass?: string;
42
+ };
27
43
  /**
28
- * Extract the return array block from a toArray() method in a PHP resource.
44
+ * Parse resource fields from PHP content using AST.
45
+ * Returns null if parsing fails or no toArray method is found.
29
46
  */
30
- export declare function extractReturnArrayBlock(phpContent: string): string | null;
47
+ export declare function parseResourceFieldsAst(phpContent: string, options?: Omit<ParseResourceOptions, 'resourceClass'>): Record<string, ResourceFieldInfo> | null;
31
48
  //# sourceMappingURL=php-parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"php-parser.d.ts","sourceRoot":"","sources":["../../src/utils/php-parser.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CA8CrE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBzE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4BvE;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAqE3F;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzE"}
1
+ {"version":3,"file":"php-parser.d.ts","sourceRoot":"","sources":["../../src/utils/php-parser.ts"],"names":[],"mappings":"AAqBA,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,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,iBAAiB,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AA8RF;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,eAAe,CAAM,GACxD,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,IAAI,CAqC1C"}