create-prisma-php-app 2.1.3 → 2.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.
|
@@ -20,17 +20,12 @@ export const SRC_DIR = path.join(PROJECT_ROOT, "src");
|
|
|
20
20
|
const IMPORTS_FILE = path.join(PROJECT_ROOT, "settings/class-imports.json");
|
|
21
21
|
const CLASS_LOG_FILE = path.join(PROJECT_ROOT, "settings/class-log.json");
|
|
22
22
|
|
|
23
|
-
async function loadImportsData(): Promise<Record<string, string>> {
|
|
24
|
-
try {
|
|
25
|
-
const content = await fs.readFile(IMPORTS_FILE, "utf-8");
|
|
26
|
-
return JSON.parse(content);
|
|
27
|
-
} catch {
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
23
|
|
|
32
24
|
async function saveImportsData(
|
|
33
|
-
data: Record<
|
|
25
|
+
data: Record<
|
|
26
|
+
string,
|
|
27
|
+
Array<{ className: string; filePath: string; importer: string }>
|
|
28
|
+
>
|
|
34
29
|
) {
|
|
35
30
|
await fs.writeFile(IMPORTS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
36
31
|
}
|
|
@@ -86,16 +81,14 @@ export async function analyzeImportsInFile(
|
|
|
86
81
|
} else {
|
|
87
82
|
// Handle grouped `use` statements
|
|
88
83
|
if (node.kind === "usegroup" && node.name) {
|
|
89
|
-
baseNamespace = node.name.name || node.name;
|
|
84
|
+
baseNamespace = node.name.name || node.name;
|
|
90
85
|
for (const useItem of node.items || []) {
|
|
91
86
|
if (useItem.kind === "useitem" && useItem.name) {
|
|
92
|
-
const subNamespace = useItem.name.name || useItem.name;
|
|
93
|
-
const fqn = combineNamespaces(baseNamespace, subNamespace);
|
|
94
|
-
const alias = useItem.alias ? useItem.alias.name : subNamespace;
|
|
87
|
+
const subNamespace = useItem.name.name || useItem.name;
|
|
88
|
+
const fqn = combineNamespaces(baseNamespace, subNamespace);
|
|
89
|
+
const alias = useItem.alias ? useItem.alias.name : subNamespace;
|
|
95
90
|
if (!imports[alias]) {
|
|
96
|
-
|
|
97
|
-
imports[alias] = fqn; // Map alias to FQN
|
|
98
|
-
// console.log(`🚀 Adding import: ${alias} -> ${fqn}`);
|
|
91
|
+
imports[alias] = fqn;
|
|
99
92
|
}
|
|
100
93
|
}
|
|
101
94
|
}
|
|
@@ -103,12 +96,11 @@ export async function analyzeImportsInFile(
|
|
|
103
96
|
|
|
104
97
|
// Handle non-grouped `use` statements
|
|
105
98
|
if (node.kind === "useitem" && node.name) {
|
|
106
|
-
const fqn = node.name.name || node.name;
|
|
99
|
+
const fqn = node.name.name || node.name;
|
|
107
100
|
const alias = node.alias
|
|
108
101
|
? node.alias.name
|
|
109
102
|
: path.basename(fqn.replace(/\\/g, "/"));
|
|
110
103
|
if (!imports[alias]) {
|
|
111
|
-
// Prevent overwriting
|
|
112
104
|
imports[alias] = fqn;
|
|
113
105
|
}
|
|
114
106
|
}
|
|
@@ -129,38 +121,56 @@ export async function analyzeImportsInFile(
|
|
|
129
121
|
}
|
|
130
122
|
|
|
131
123
|
export async function updateComponentImports() {
|
|
132
|
-
// Load existing imports (if any)
|
|
133
|
-
const allImports = await loadImportsData();
|
|
134
|
-
|
|
135
124
|
// Analyze all PHP files for use statements
|
|
136
125
|
const phpFiles = await getAllPhpFiles(SRC_DIR);
|
|
126
|
+
// Build a mapping: alias -> array of { fqn, importer }
|
|
127
|
+
const allImports: Record<
|
|
128
|
+
string,
|
|
129
|
+
Array<{ fqn: string; importer: string }>
|
|
130
|
+
> = {};
|
|
131
|
+
|
|
137
132
|
for (const file of phpFiles) {
|
|
138
133
|
const fileImports = await analyzeImportsInFile(file);
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
for (const [alias, fqn] of Object.entries(fileImports)) {
|
|
135
|
+
if (allImports[alias]) {
|
|
136
|
+
// Check both fqn and importer to avoid duplicates
|
|
137
|
+
if (
|
|
138
|
+
!allImports[alias].some(
|
|
139
|
+
(entry) => entry.fqn === fqn && entry.importer === file
|
|
140
|
+
)
|
|
141
|
+
) {
|
|
142
|
+
allImports[alias].push({ fqn, importer: file });
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
allImports[alias] = [{ fqn, importer: file }];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
141
148
|
}
|
|
142
149
|
|
|
143
|
-
//
|
|
150
|
+
// Load the class log to filter valid imports
|
|
144
151
|
const classLog = await loadClassLogData();
|
|
145
|
-
|
|
146
152
|
const filteredImports: Record<
|
|
147
153
|
string,
|
|
148
|
-
{ className: string; filePath: string }
|
|
154
|
+
Array<{ className: string; filePath: string; importer: string }>
|
|
149
155
|
> = {};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
|
|
157
|
+
for (const [alias, entries] of Object.entries(allImports)) {
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (classLog[entry.fqn]) {
|
|
160
|
+
const importEntry = {
|
|
161
|
+
className: entry.fqn,
|
|
162
|
+
filePath: classLog[entry.fqn].filePath,
|
|
163
|
+
importer: entry.importer,
|
|
164
|
+
};
|
|
165
|
+
if (filteredImports[alias]) {
|
|
166
|
+
filteredImports[alias].push(importEntry);
|
|
167
|
+
} else {
|
|
168
|
+
filteredImports[alias] = [importEntry];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
159
171
|
}
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
await saveImportsData(filteredImports);
|
|
163
|
-
// console.log(
|
|
164
|
-
// "component_imports.json updated with IPHPX/PHPX components only."
|
|
165
|
-
// );
|
|
175
|
+
// console.log("component_imports.json updated with importer file path included.");
|
|
166
176
|
}
|
|
@@ -26,12 +26,49 @@ function findComponentsInFile(code: string): string[] {
|
|
|
26
26
|
|
|
27
27
|
export async function checkComponentImports(
|
|
28
28
|
filePath: string,
|
|
29
|
-
fileImports: Record<
|
|
29
|
+
fileImports: Record<
|
|
30
|
+
string,
|
|
31
|
+
| Array<{ className: string; filePath: string; importer?: string }>
|
|
32
|
+
| { className: string; filePath: string; importer?: string }
|
|
33
|
+
>
|
|
30
34
|
) {
|
|
31
35
|
const code = await fs.readFile(filePath, "utf-8");
|
|
32
36
|
const usedComponents = findComponentsInFile(code);
|
|
37
|
+
// Normalize the current file path: replace backslashes, trim, remove trailing slash, and lower-case.
|
|
38
|
+
const normalizedFilePath = filePath
|
|
39
|
+
.replace(/\\/g, "/")
|
|
40
|
+
.trim()
|
|
41
|
+
.replace(/\/+$/, "")
|
|
42
|
+
.toLowerCase();
|
|
43
|
+
|
|
33
44
|
usedComponents.forEach((component) => {
|
|
34
|
-
|
|
45
|
+
const rawMapping = fileImports[component];
|
|
46
|
+
// Normalize rawMapping to an array
|
|
47
|
+
let mappings: Array<{
|
|
48
|
+
className: string;
|
|
49
|
+
filePath: string;
|
|
50
|
+
importer?: string;
|
|
51
|
+
}> = [];
|
|
52
|
+
if (Array.isArray(rawMapping)) {
|
|
53
|
+
mappings = rawMapping;
|
|
54
|
+
} else if (rawMapping) {
|
|
55
|
+
mappings = [rawMapping];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if any mapping's importer matches the current file.
|
|
59
|
+
const found = mappings.some((mapping) => {
|
|
60
|
+
const normalizedImporter = (mapping.importer || "")
|
|
61
|
+
.replace(/\\/g, "/")
|
|
62
|
+
.trim()
|
|
63
|
+
.replace(/\/+$/, "")
|
|
64
|
+
.toLowerCase();
|
|
65
|
+
// Either exact match or the current file path ends with the importer.
|
|
66
|
+
return (
|
|
67
|
+
normalizedFilePath === normalizedImporter ||
|
|
68
|
+
normalizedFilePath.endsWith(normalizedImporter)
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
if (!found) {
|
|
35
72
|
console.warn(
|
|
36
73
|
chalk.yellow("Warning: ") +
|
|
37
74
|
chalk.white("Component ") +
|
|
@@ -11,6 +11,7 @@ use DOMElement;
|
|
|
11
11
|
use DOMComment;
|
|
12
12
|
use DOMNode;
|
|
13
13
|
use RuntimeException;
|
|
14
|
+
use Bootstrap;
|
|
14
15
|
|
|
15
16
|
class TemplateCompiler
|
|
16
17
|
{
|
|
@@ -40,6 +41,7 @@ class TemplateCompiler
|
|
|
40
41
|
self::initializeClassMappings();
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
|
|
43
45
|
$templateContent = self::preprocessBindings($templateContent);
|
|
44
46
|
|
|
45
47
|
$dom = self::convertToXml($templateContent);
|
|
@@ -71,7 +73,6 @@ class TemplateCompiler
|
|
|
71
73
|
$expression = $matches[3];
|
|
72
74
|
$afterBinding = $matches[4];
|
|
73
75
|
|
|
74
|
-
// Escape the binding expression for attributes.
|
|
75
76
|
$escapedExpression = htmlspecialchars($expression, ENT_QUOTES, 'UTF-8');
|
|
76
77
|
$placeholder = '%%ATTR_BIND_' . count($attributePlaceholders) . '%%';
|
|
77
78
|
|
|
@@ -81,17 +82,14 @@ class TemplateCompiler
|
|
|
81
82
|
$templateContent
|
|
82
83
|
);
|
|
83
84
|
|
|
84
|
-
// Process text {{ ... }} bindings, but skip ones inside attribute values.
|
|
85
85
|
$textBindingPattern = '/(?:"[^"]*"|\'[^\']*\')(*SKIP)(*FAIL)|{{\s*(.+?)\s*}}/u';
|
|
86
86
|
$templateContent = preg_replace_callback(
|
|
87
87
|
$textBindingPattern,
|
|
88
88
|
function ($matches) {
|
|
89
89
|
$expr = $matches[1];
|
|
90
|
-
// If the expression is a simple word/dot path, use a simple binding.
|
|
91
90
|
if (preg_match('/^[\w.]+$/u', $expr)) {
|
|
92
91
|
return "<span pp-bind=\"{$expr}\"></span>";
|
|
93
92
|
} else {
|
|
94
|
-
// Otherwise, encode the expression and use an expression binding.
|
|
95
93
|
$encodedExpr = htmlspecialchars($expr, ENT_QUOTES, 'UTF-8');
|
|
96
94
|
return "<span pp-bind-expr=\"{$encodedExpr}\"></span>";
|
|
97
95
|
}
|
|
@@ -219,56 +217,97 @@ class TemplateCompiler
|
|
|
219
217
|
if ($node instanceof DOMElement) {
|
|
220
218
|
$componentName = $node->nodeName;
|
|
221
219
|
$attributes = [];
|
|
222
|
-
|
|
223
220
|
foreach ($node->attributes as $attr) {
|
|
224
221
|
$attributes[$attr->name] = $attr->value;
|
|
225
222
|
}
|
|
226
223
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
$childOutput[] = self::processNode($child);
|
|
230
|
-
}
|
|
231
|
-
$innerContent = implode('', $childOutput);
|
|
224
|
+
if (isset(self::$classMappings[$componentName])) {
|
|
225
|
+
$componentInstance = self::initializeComponentInstance($componentName, $attributes);
|
|
232
226
|
|
|
233
|
-
|
|
227
|
+
$childOutput = [];
|
|
228
|
+
foreach ($node->childNodes as $child) {
|
|
229
|
+
$childOutput[] = self::processNode($child);
|
|
230
|
+
}
|
|
231
|
+
$componentInstance->children = implode('', $childOutput);
|
|
234
232
|
|
|
235
|
-
|
|
233
|
+
$renderedContent = $componentInstance->render();
|
|
234
|
+
if (self::hasComponentTag($renderedContent)) {
|
|
235
|
+
return self::compile($renderedContent);
|
|
236
|
+
}
|
|
237
|
+
return $renderedContent;
|
|
238
|
+
} else {
|
|
239
|
+
$childOutput = [];
|
|
240
|
+
foreach ($node->childNodes as $child) {
|
|
241
|
+
$childOutput[] = self::processNode($child);
|
|
242
|
+
}
|
|
243
|
+
$attributes['children'] = implode('', $childOutput);
|
|
244
|
+
return self::renderAsHtml($componentName, $attributes);
|
|
245
|
+
}
|
|
236
246
|
} elseif ($node instanceof DOMComment) {
|
|
237
247
|
return "<!--{$node->textContent}-->";
|
|
238
248
|
}
|
|
239
|
-
|
|
240
249
|
return $node->textContent;
|
|
241
250
|
}
|
|
242
251
|
|
|
243
|
-
protected static function
|
|
252
|
+
protected static function initializeComponentInstance(string $componentName, array $attributes)
|
|
244
253
|
{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
}
|
|
254
|
+
$importerFile = Bootstrap::$contentToInclude;
|
|
255
|
+
$normalizedImporterFile = str_replace('\\', '/', $importerFile);
|
|
249
256
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (isset(self::$classMappings[$componentName])) {
|
|
253
|
-
$className = self::$classMappings[$componentName]['className'];
|
|
254
|
-
$filePath = self::$classMappings[$componentName]['filePath'];
|
|
257
|
+
$srcPathNormalized = str_replace('\\', '/', SRC_PATH);
|
|
258
|
+
$relativeImporterFile = str_replace($srcPathNormalized . '/', '', $normalizedImporterFile);
|
|
255
259
|
|
|
256
|
-
|
|
260
|
+
if (!isset(self::$classMappings[$componentName])) {
|
|
261
|
+
throw new RuntimeException("Component {$componentName} is not registered.");
|
|
262
|
+
}
|
|
257
263
|
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
$mappings = self::$classMappings[$componentName];
|
|
265
|
+
$selectedMapping = null;
|
|
266
|
+
|
|
267
|
+
if (is_array($mappings)) {
|
|
268
|
+
if (isset($mappings[0]) && is_array($mappings[0])) {
|
|
269
|
+
foreach ($mappings as $entry) {
|
|
270
|
+
$entryImporter = isset($entry['importer']) ? str_replace('\\', '/', $entry['importer']) : '';
|
|
271
|
+
$relativeEntryImporter = str_replace($srcPathNormalized . '/', '', $entryImporter);
|
|
272
|
+
if ($relativeEntryImporter === $relativeImporterFile) {
|
|
273
|
+
$selectedMapping = $entry;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if ($selectedMapping === null) {
|
|
278
|
+
$selectedMapping = $mappings[0];
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
$selectedMapping = $mappings;
|
|
260
282
|
}
|
|
283
|
+
}
|
|
261
284
|
|
|
262
|
-
|
|
263
|
-
|
|
285
|
+
if (!isset($selectedMapping['className']) || !isset($selectedMapping['filePath'])) {
|
|
286
|
+
throw new RuntimeException("Invalid component mapping for {$componentName}.");
|
|
287
|
+
}
|
|
264
288
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
289
|
+
$className = $selectedMapping['className'];
|
|
290
|
+
$filePath = $selectedMapping['filePath'];
|
|
291
|
+
|
|
292
|
+
require_once str_replace('\\', '/', SRC_PATH . '/' . $filePath);
|
|
293
|
+
|
|
294
|
+
if (!class_exists($className)) {
|
|
295
|
+
throw new RuntimeException("Class {$className} does not exist.");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return new $className($attributes);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
protected static function initializeClassMappings(): void
|
|
302
|
+
{
|
|
303
|
+
foreach (PrismaPHPSettings::$classLogFiles as $tagName => $fullyQualifiedClassName) {
|
|
304
|
+
self::$classMappings[$tagName] = $fullyQualifiedClassName;
|
|
269
305
|
}
|
|
306
|
+
}
|
|
270
307
|
|
|
271
|
-
|
|
308
|
+
protected static function hasComponentTag(string $templateContent): bool
|
|
309
|
+
{
|
|
310
|
+
return preg_match('/<\/*[A-Z][\w-]*/u', $templateContent) === 1;
|
|
272
311
|
}
|
|
273
312
|
|
|
274
313
|
protected static function renderAsHtml(string $tagName, array $attributes): string
|