@maxisoft/instantcms-mcp 1.2.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/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/__tests__/artifact-tool.test.js +196 -0
- package/dist/__tests__/db-tool.test.js +152 -0
- package/dist/__tests__/define-tool.test.js +106 -0
- package/dist/__tests__/find-tool.test.js +185 -0
- package/dist/__tests__/get-component-api.test.js +144 -0
- package/dist/__tests__/hardening.test.js +73 -0
- package/dist/__tests__/hooks-tool.test.js +173 -0
- package/dist/__tests__/mcp-integration-extra.test.js +166 -0
- package/dist/__tests__/mcp-integration.test.js +53 -0
- package/dist/__tests__/pagination.test.js +114 -0
- package/dist/__tests__/performance-baseline.test.js +56 -0
- package/dist/__tests__/project-patch.test.js +144 -0
- package/dist/__tests__/project-source.test.js +44 -0
- package/dist/__tests__/project-workflows-extra.test.js +165 -0
- package/dist/__tests__/project-workflows.test.js +51 -0
- package/dist/__tests__/scaffold-addon-roundtrip.test.js +287 -0
- package/dist/__tests__/serialization.test.js +170 -0
- package/dist/__tests__/source-knowledge-parsers.test.js +42 -0
- package/dist/__tests__/template-development.test.js +78 -0
- package/dist/__tests__/template-productivity.test.js +87 -0
- package/dist/__tests__/tools.test.js +2143 -0
- package/dist/data/components.js +2818 -0
- package/dist/data/controllers-map.js +7571 -0
- package/dist/data/core-api.js +9356 -0
- package/dist/data/database-schema.js +4555 -0
- package/dist/data/events-map.js +1339 -0
- package/dist/data/fields-map.js +3692 -0
- package/dist/data/hooks.js +2257 -0
- package/dist/data/js-api.js +123 -0
- package/dist/data/libs-api.js +264 -0
- package/dist/data/routes-map.js +203 -0
- package/dist/data/schemas-validation.js +255 -0
- package/dist/data/schemas.js +1404 -0
- package/dist/data/traits-map.js +1004 -0
- package/dist/data/version-profiles.js +41 -0
- package/dist/data/widgets-map.js +131 -0
- package/dist/data/wysiwyg-map.js +486 -0
- package/dist/generated/components-source.js +14748 -0
- package/dist/generated/hooks-source.js +3399 -0
- package/dist/generated/knowledge-meta.js +101 -0
- package/dist/index.js +13 -0
- package/dist/registry/database-tools.js +112 -0
- package/dist/registry/extension-tools.js +473 -0
- package/dist/registry/generator-tools.js +506 -0
- package/dist/registry/knowledge-tools.js +441 -0
- package/dist/registry/language-tools.js +99 -0
- package/dist/registry/meta-tools.js +152 -0
- package/dist/registry/project-tools.js +48 -0
- package/dist/registry/resources.js +98 -0
- package/dist/registry/source-tools.js +211 -0
- package/dist/registry/template-development-tools.js +79 -0
- package/dist/server.js +32 -0
- package/dist/tools/addon-tool.js +1437 -0
- package/dist/tools/admin-partial-tool.js +498 -0
- package/dist/tools/api-tool.js +294 -0
- package/dist/tools/artifact-tool.js +95 -0
- package/dist/tools/cache-tool.js +360 -0
- package/dist/tools/component-tool.js +415 -0
- package/dist/tools/controllers-tool.js +125 -0
- package/dist/tools/cron-tool.js +197 -0
- package/dist/tools/crud-tool.js +659 -0
- package/dist/tools/db-tool.js +198 -0
- package/dist/tools/email-tool.js +222 -0
- package/dist/tools/external-api-tool.js +596 -0
- package/dist/tools/filter-tool.js +341 -0
- package/dist/tools/form-tool.js +275 -0
- package/dist/tools/grid-tool.js +189 -0
- package/dist/tools/hooks-tool.js +104 -0
- package/dist/tools/import-export-tool.js +548 -0
- package/dist/tools/lang-tool.js +251 -0
- package/dist/tools/layout-override-tool.js +125 -0
- package/dist/tools/layout-tool.js +548 -0
- package/dist/tools/maria-tool.js +127 -0
- package/dist/tools/mariadb.js +201 -0
- package/dist/tools/migration-tool.js +329 -0
- package/dist/tools/oauth-tool.js +520 -0
- package/dist/tools/parser/components-parser.js +76 -0
- package/dist/tools/parser/controllers-parser.js +313 -0
- package/dist/tools/parser/core-parser.js +294 -0
- package/dist/tools/parser/coverage-generator.js +424 -0
- package/dist/tools/parser/events-parser.js +127 -0
- package/dist/tools/parser/fields-parser.js +382 -0
- package/dist/tools/parser/hooks-parser.js +106 -0
- package/dist/tools/parser/sql-parser.js +197 -0
- package/dist/tools/parser/traits-parser.js +161 -0
- package/dist/tools/parser/widgets-parser.js +150 -0
- package/dist/tools/permission-tool.js +348 -0
- package/dist/tools/project-patch-tool.js +73 -0
- package/dist/tools/project-source-tool.js +188 -0
- package/dist/tools/project-workflow-tool.js +186 -0
- package/dist/tools/requirement-tool.js +212 -0
- package/dist/tools/scaffold-tool.js +815 -0
- package/dist/tools/seo-tool.js +412 -0
- package/dist/tools/source-tool.js +155 -0
- package/dist/tools/template-development-tool.js +271 -0
- package/dist/tools/template-overrides-tool.js +294 -0
- package/dist/tools/template-productivity-tool.js +319 -0
- package/dist/tools/template-tool.js +665 -0
- package/dist/tools/test-tool.js +183 -0
- package/dist/tools/webhook-tool.js +427 -0
- package/dist/tools/widget-tool.js +331 -0
- package/dist/tools/wysiwyg-tool.js +137 -0
- package/dist/types/scaffold.js +68 -0
- package/dist/utils/define-tool.js +37 -0
- package/dist/utils/find-tool.js +97 -0
- package/dist/utils/mcp-result.js +18 -0
- package/dist/utils/pagination.js +27 -0
- package/dist/utils/serialization.js +27 -0
- package/package.json +94 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeTemplateOverrides = mergeTemplateOverrides;
|
|
4
|
+
exports.auditTemplateFrontend = auditTemplateFrontend;
|
|
5
|
+
exports.extractTemplateDesignTokens = extractTemplateDesignTokens;
|
|
6
|
+
exports.auditTemplateWidgetPositions = auditTemplateWidgetPositions;
|
|
7
|
+
exports.scaffoldTemplateE2eEnvironment = scaffoldTemplateE2eEnvironment;
|
|
8
|
+
exports.indexUpstreamTemplateSources = indexUpstreamTemplateSources;
|
|
9
|
+
exports.scaffoldTemplatePhpQuality = scaffoldTemplatePhpQuality;
|
|
10
|
+
const yaml_1 = require("yaml");
|
|
11
|
+
const project_patch_tool_js_1 = require("./project-patch-tool.js");
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
function normalizeFiles(files) {
|
|
14
|
+
return Object.fromEntries(Object.entries(files).map(([path, content]) => [
|
|
15
|
+
path.replace(/\\/g, '/').replace(/^\.\//, ''),
|
|
16
|
+
content,
|
|
17
|
+
]));
|
|
18
|
+
}
|
|
19
|
+
function overridePaths(files) {
|
|
20
|
+
return Object.keys(files).filter(path => /^controllers\/[^/]+\/(?:backend\/)?[^/]+\.tpl\.php$/.test(path));
|
|
21
|
+
}
|
|
22
|
+
function findSource(files, override) {
|
|
23
|
+
return Object.keys(files).find(path => path === override || path.endsWith(`/${override}`));
|
|
24
|
+
}
|
|
25
|
+
function applySingleUpstreamDelta(base, current, incoming) {
|
|
26
|
+
let prefix = 0;
|
|
27
|
+
while (prefix < base.length && prefix < incoming.length && base[prefix] === incoming[prefix])
|
|
28
|
+
prefix += 1;
|
|
29
|
+
let suffix = 0;
|
|
30
|
+
while (suffix < base.length - prefix &&
|
|
31
|
+
suffix < incoming.length - prefix &&
|
|
32
|
+
base[base.length - 1 - suffix] === incoming[incoming.length - 1 - suffix])
|
|
33
|
+
suffix += 1;
|
|
34
|
+
const removed = base.slice(prefix, base.length - suffix);
|
|
35
|
+
const added = incoming.slice(prefix, incoming.length - suffix);
|
|
36
|
+
if (!removed)
|
|
37
|
+
return null;
|
|
38
|
+
const first = current.indexOf(removed);
|
|
39
|
+
if (first < 0 || current.indexOf(removed, first + removed.length) >= 0)
|
|
40
|
+
return null;
|
|
41
|
+
return `${current.slice(0, first)}${added}${current.slice(first + removed.length)}`;
|
|
42
|
+
}
|
|
43
|
+
function mergeTemplateOverrides(themeFilesInput, upstreamBeforeInput, upstreamAfterInput) {
|
|
44
|
+
const themeFiles = normalizeFiles(themeFilesInput);
|
|
45
|
+
const upstreamBefore = normalizeFiles(upstreamBeforeInput);
|
|
46
|
+
const upstreamAfter = normalizeFiles(upstreamAfterInput);
|
|
47
|
+
const mergedFiles = { ...themeFiles };
|
|
48
|
+
const results = overridePaths(themeFiles).map(path => {
|
|
49
|
+
const beforePath = findSource(upstreamBefore, path);
|
|
50
|
+
const afterPath = findSource(upstreamAfter, path);
|
|
51
|
+
if (!beforePath || !afterPath) {
|
|
52
|
+
return { path, status: !afterPath ? 'missing_upstream' : 'missing_base', auto_merged: false };
|
|
53
|
+
}
|
|
54
|
+
const base = upstreamBefore[beforePath];
|
|
55
|
+
const current = themeFiles[path];
|
|
56
|
+
const incoming = upstreamAfter[afterPath];
|
|
57
|
+
if (current === base) {
|
|
58
|
+
mergedFiles[path] = incoming;
|
|
59
|
+
return { path, status: 'updated_from_upstream', auto_merged: true };
|
|
60
|
+
}
|
|
61
|
+
if (incoming === base || current === incoming) {
|
|
62
|
+
return { path, status: 'already_compatible', auto_merged: true };
|
|
63
|
+
}
|
|
64
|
+
const deltaMerged = applySingleUpstreamDelta(base, current, incoming);
|
|
65
|
+
if (deltaMerged !== null) {
|
|
66
|
+
mergedFiles[path] = deltaMerged;
|
|
67
|
+
return { path, status: 'upstream_delta_applied', auto_merged: true };
|
|
68
|
+
}
|
|
69
|
+
return { path, status: 'conflict', auto_merged: false };
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
files: mergedFiles,
|
|
73
|
+
patch: (0, project_patch_tool_js_1.createProjectPatch)(themeFiles, mergedFiles),
|
|
74
|
+
summary: {
|
|
75
|
+
checked: results.length,
|
|
76
|
+
auto_merged: results.filter(item => item.auto_merged).length,
|
|
77
|
+
conflicts: results.filter(item => item.status === 'conflict').length,
|
|
78
|
+
missing_upstream: results.filter(item => item.status === 'missing_upstream').length,
|
|
79
|
+
},
|
|
80
|
+
results,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function auditTemplateFrontend(filesInput) {
|
|
84
|
+
const files = normalizeFiles(filesInput);
|
|
85
|
+
const diagnostics = [];
|
|
86
|
+
for (const [path, content] of Object.entries(files)) {
|
|
87
|
+
if (path.endsWith('.tpl.php') || path.endsWith('.html')) {
|
|
88
|
+
for (const match of content.matchAll(/<img\b([^>]*)>/gi)) {
|
|
89
|
+
if (!/\balt\s*=/.test(match[1]))
|
|
90
|
+
diagnostics.push({
|
|
91
|
+
code: 'IMAGE_MISSING_ALT',
|
|
92
|
+
severity: 'warning',
|
|
93
|
+
path,
|
|
94
|
+
message: 'Image is missing an alt attribute.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
for (const match of content.matchAll(/<a\b([^>]*)>/gi)) {
|
|
98
|
+
if (/\btarget\s*=\s*['"]_blank['"]/i.test(match[1]) &&
|
|
99
|
+
!/\brel\s*=\s*['"][^'"]*(?:noopener|noreferrer)/i.test(match[1]))
|
|
100
|
+
diagnostics.push({
|
|
101
|
+
code: 'UNSAFE_BLANK_TARGET',
|
|
102
|
+
severity: 'warning',
|
|
103
|
+
path,
|
|
104
|
+
message: 'target="_blank" should use rel="noopener".',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const ids = [...content.matchAll(/\bid\s*=\s*['"]([^'"?]+)['"]/gi)].map(match => match[1]);
|
|
108
|
+
for (const id of [...new Set(ids.filter((item, index) => ids.indexOf(item) !== index))])
|
|
109
|
+
diagnostics.push({
|
|
110
|
+
code: 'DUPLICATE_HTML_ID',
|
|
111
|
+
severity: 'error',
|
|
112
|
+
path,
|
|
113
|
+
message: `Duplicate HTML id: ${id}`,
|
|
114
|
+
});
|
|
115
|
+
const headings = [...content.matchAll(/<h([1-6])\b/gi)].map(match => Number(match[1]));
|
|
116
|
+
for (let index = 1; index < headings.length; index += 1)
|
|
117
|
+
if (headings[index] - headings[index - 1] > 1)
|
|
118
|
+
diagnostics.push({
|
|
119
|
+
code: 'SKIPPED_HEADING_LEVEL',
|
|
120
|
+
severity: 'warning',
|
|
121
|
+
path,
|
|
122
|
+
message: `Heading level jumps from h${headings[index - 1]} to h${headings[index]}.`,
|
|
123
|
+
});
|
|
124
|
+
if (/\becho\s+\$(?!this\b)[a-z_][a-z0-9_]*(?:\[[^\]]+\])?\s*;/i.test(content))
|
|
125
|
+
diagnostics.push({
|
|
126
|
+
code: 'POSSIBLE_UNESCAPED_OUTPUT',
|
|
127
|
+
severity: 'warning',
|
|
128
|
+
path,
|
|
129
|
+
message: 'Variable output may require context-aware escaping.',
|
|
130
|
+
});
|
|
131
|
+
if (/<html\b(?![^>]*\blang=)/i.test(content))
|
|
132
|
+
diagnostics.push({
|
|
133
|
+
code: 'HTML_LANG_MISSING',
|
|
134
|
+
severity: 'warning',
|
|
135
|
+
path,
|
|
136
|
+
message: 'The html element should declare a language.',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (path.endsWith('.css')) {
|
|
140
|
+
const hardcoded = content.match(/#[0-9a-f]{3,8}\b|rgba?\([^)]*\)/gi) ?? [];
|
|
141
|
+
if (hardcoded.length > 12)
|
|
142
|
+
diagnostics.push({
|
|
143
|
+
code: 'MANY_HARDCODED_COLORS',
|
|
144
|
+
severity: 'info',
|
|
145
|
+
path,
|
|
146
|
+
message: `${hardcoded.length} hardcoded colors found; consider design tokens.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
is_valid: !diagnostics.some(item => item.severity === 'error'),
|
|
152
|
+
summary: {
|
|
153
|
+
errors: diagnostics.filter(item => item.severity === 'error').length,
|
|
154
|
+
warnings: diagnostics.filter(item => item.severity === 'warning').length,
|
|
155
|
+
info: diagnostics.filter(item => item.severity === 'info').length,
|
|
156
|
+
},
|
|
157
|
+
diagnostics,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function extractTemplateDesignTokens(filesInput) {
|
|
161
|
+
const files = normalizeFiles(filesInput);
|
|
162
|
+
const customProperties = {};
|
|
163
|
+
const colors = new Map();
|
|
164
|
+
const spacing = new Map();
|
|
165
|
+
for (const [path, content] of Object.entries(files)) {
|
|
166
|
+
if (!/\.(?:css|scss)$/.test(path))
|
|
167
|
+
continue;
|
|
168
|
+
for (const match of content.matchAll(/(--[a-z0-9_-]+)\s*:\s*([^;}]+)[;}]/gi))
|
|
169
|
+
customProperties[match[1]] = match[2].trim();
|
|
170
|
+
for (const match of content.matchAll(/#[0-9a-f]{3,8}\b|rgba?\([^)]*\)/gi))
|
|
171
|
+
colors.set(match[0].toLowerCase(), (colors.get(match[0].toLowerCase()) ?? 0) + 1);
|
|
172
|
+
for (const match of content.matchAll(/(?:margin|padding|gap)(?:-[a-z]+)?\s*:\s*([0-9.]+(?:px|rem|em))/gi))
|
|
173
|
+
spacing.set(match[1], (spacing.get(match[1]) ?? 0) + 1);
|
|
174
|
+
}
|
|
175
|
+
const suggestedColors = [...colors].sort((a, b) => b[1] - a[1]).slice(0, 12);
|
|
176
|
+
const suggestedSpacing = [...spacing].sort((a, b) => b[1] - a[1]).slice(0, 12);
|
|
177
|
+
const css = [
|
|
178
|
+
':root {',
|
|
179
|
+
...suggestedColors.map(([value], index) => ` --color-${index + 1}: ${value};`),
|
|
180
|
+
...suggestedSpacing.map(([value], index) => ` --space-${index + 1}: ${value};`),
|
|
181
|
+
'}',
|
|
182
|
+
].join('\n');
|
|
183
|
+
return {
|
|
184
|
+
existing_custom_properties: customProperties,
|
|
185
|
+
colors: suggestedColors.map(([value, uses]) => ({ value, uses })),
|
|
186
|
+
spacing: suggestedSpacing.map(([value, uses]) => ({ value, uses })),
|
|
187
|
+
suggested_tokens_css: `${css}\n`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function positionsFromLayout(content) {
|
|
191
|
+
try {
|
|
192
|
+
const document = (0, yaml_1.parse)(content);
|
|
193
|
+
const found = [];
|
|
194
|
+
const visit = (value) => {
|
|
195
|
+
if (Array.isArray(value))
|
|
196
|
+
return value.forEach(visit);
|
|
197
|
+
if (!value || typeof value !== 'object')
|
|
198
|
+
return;
|
|
199
|
+
for (const [key, child] of Object.entries(value)) {
|
|
200
|
+
if (key === 'position' && typeof child === 'string')
|
|
201
|
+
found.push(child);
|
|
202
|
+
visit(child);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
visit(document);
|
|
206
|
+
return found;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function auditTemplateWidgetPositions(filesInput) {
|
|
213
|
+
const files = normalizeFiles(filesInput);
|
|
214
|
+
const rendered = [
|
|
215
|
+
...new Set(Object.values(files).flatMap(content => [...content.matchAll(/\bwidgets\(\s*['"]([^'"]+)['"]/g)].map(match => match[1]))),
|
|
216
|
+
].sort();
|
|
217
|
+
const checked = [
|
|
218
|
+
...new Set(Object.values(files).flatMap(content => [...content.matchAll(/\bhasWidgetsOn\(\s*['"]([^'"]+)['"]/g)].map(match => match[1]))),
|
|
219
|
+
].sort();
|
|
220
|
+
const layout = [
|
|
221
|
+
...new Set(Object.entries(files)
|
|
222
|
+
.filter(([path]) => /\.ya?ml$/i.test(path))
|
|
223
|
+
.flatMap(([, content]) => positionsFromLayout(content))),
|
|
224
|
+
].sort();
|
|
225
|
+
return {
|
|
226
|
+
rendered_positions: rendered,
|
|
227
|
+
guarded_positions: checked,
|
|
228
|
+
layout_positions: layout,
|
|
229
|
+
layout_not_rendered: layout.filter(position => !rendered.includes(position)),
|
|
230
|
+
rendered_not_in_layout: rendered.filter(position => !layout.includes(position)),
|
|
231
|
+
unguarded_positions: rendered.filter(position => !checked.includes(position)),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function scaffoldTemplateE2eEnvironment(options) {
|
|
235
|
+
if (!/^[a-z][a-z0-9_]{1,63}$/.test(options.theme))
|
|
236
|
+
throw new Error('Invalid theme name');
|
|
237
|
+
const baseUrl = options.base_url ?? 'http://web';
|
|
238
|
+
return {
|
|
239
|
+
files: {
|
|
240
|
+
'template-e2e/compose.yaml': `services:\n web:\n build: ./php\n ports: ["8080:80"]\n volumes:\n - \${INSTANTCMS_SOURCE:-../instantcms}:/var/www/html\n db:\n image: mysql:8.0\n environment:\n MYSQL_DATABASE: instantcms\n MYSQL_USER: instantcms\n MYSQL_PASSWORD: instantcms\n MYSQL_ROOT_PASSWORD: root\n visual:\n image: mcr.microsoft.com/playwright:v1.52.0-noble\n working_dir: /tests\n volumes:\n - ./:/tests\n environment:\n BASE_URL: ${baseUrl}\n command: ["npx", "playwright", "test"]\n depends_on: [web]\n`,
|
|
241
|
+
'template-e2e/php/Dockerfile': `FROM php:8.2-apache\nRUN apt-get update && apt-get install -y libicu-dev libpng-dev libzip-dev unzip && docker-php-ext-install intl mysqli gd zip && rm -rf /var/lib/apt/lists/*\nRUN a2enmod rewrite\n`,
|
|
242
|
+
'template-e2e/package.json': JSON.stringify({
|
|
243
|
+
private: true,
|
|
244
|
+
devDependencies: { '@playwright/test': '1.52.0' },
|
|
245
|
+
scripts: { test: 'playwright test', update: 'playwright test --update-snapshots' },
|
|
246
|
+
}, null, 2),
|
|
247
|
+
'template-e2e/playwright.config.mjs': `import { defineConfig, devices } from '@playwright/test';\nexport default defineConfig({\n testDir: './tests',\n outputDir: './artifacts',\n use: { baseURL: process.env.BASE_URL || 'http://localhost:8080', screenshot: 'only-on-failure' },\n projects: [\n { name: 'desktop', use: { ...devices['Desktop Chrome'] } },\n { name: 'mobile', use: { ...devices['Pixel 7'] } }\n ]\n});\n`,
|
|
248
|
+
'template-e2e/tests/template.spec.mjs': `import { test, expect } from '@playwright/test';\nconst pages = (process.env.TEST_PATHS || '/').split(',');\nfor (const path of pages) test(\`${options.theme}: \${path}\`, async ({ page }) => {\n const errors = [];\n page.on('console', message => message.type() === 'error' && errors.push(message.text()));\n page.on('pageerror', error => errors.push(error.message));\n const response = await page.goto(path, { waitUntil: 'networkidle' });\n expect(response?.ok()).toBeTruthy();\n await expect(page).toHaveScreenshot(\`${options.theme}-\${path.replace(/\\W+/g, '-') || 'home'}.png\`, { fullPage: true });\n expect(errors).toEqual([]);\n});\n`,
|
|
249
|
+
'template-e2e/.env.example': 'INSTANTCMS_SOURCE=../instantcms\nBASE_URL=http://web\nTEST_PATHS=/,/login\n',
|
|
250
|
+
'template-e2e/README.md': `# ${options.theme} visual tests\n\nMount an installed InstantCMS source tree with this theme enabled, then run:\n\n\`\`\`sh\ndocker compose run --rm visual npm install\ndocker compose run --rm visual npx playwright test --update-snapshots\ndocker compose run --rm visual npx playwright test\n\`\`\`\n\nKeep approved snapshots in version control. Configure INSTANTCMS_SOURCE, BASE_URL and TEST_PATHS in .env.\n`,
|
|
251
|
+
},
|
|
252
|
+
notes: [
|
|
253
|
+
'The InstantCMS source must already be configured for the bundled MySQL service or another reachable database.',
|
|
254
|
+
'Commit approved screenshots and review diffs in pull requests.',
|
|
255
|
+
'Run desktop and mobile projects before release.',
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function indexUpstreamTemplateSources(filesInput, source) {
|
|
260
|
+
const files = normalizeFiles(filesInput);
|
|
261
|
+
const templates = Object.entries(files)
|
|
262
|
+
.filter(([path]) => path.endsWith('.tpl.php'))
|
|
263
|
+
.map(([path, content]) => {
|
|
264
|
+
const match = path.match(/(?:^|\/)controllers\/([^/]+)\/(backend\/)?([^/]+)\.tpl\.php$/);
|
|
265
|
+
return {
|
|
266
|
+
path,
|
|
267
|
+
sha256: (0, node_crypto_1.createHash)('sha256').update(content).digest('hex'),
|
|
268
|
+
bytes: Buffer.byteLength(content),
|
|
269
|
+
controller: match?.[1] ?? null,
|
|
270
|
+
action: match?.[3] ?? null,
|
|
271
|
+
backend: Boolean(match?.[2]),
|
|
272
|
+
source_url: `https://github.com/${source.repository}/blob/${encodeURIComponent(source.ref)}/${path}`,
|
|
273
|
+
};
|
|
274
|
+
})
|
|
275
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
276
|
+
return {
|
|
277
|
+
repository: source.repository,
|
|
278
|
+
ref: source.ref,
|
|
279
|
+
files_indexed: templates.length,
|
|
280
|
+
controllers: [...new Set(templates.flatMap(item => item.controller ?? []))].sort(),
|
|
281
|
+
templates,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function scaffoldTemplatePhpQuality(options) {
|
|
285
|
+
if (!/^[a-z][a-z0-9_]{1,63}$/.test(options.theme))
|
|
286
|
+
throw new Error('Invalid theme name');
|
|
287
|
+
const phpMin = options.php_min ?? '7.2';
|
|
288
|
+
if (!/^\d+\.\d+$/.test(phpMin))
|
|
289
|
+
throw new Error('php_min must use major.minor format');
|
|
290
|
+
return {
|
|
291
|
+
files: {
|
|
292
|
+
'composer.quality.json': JSON.stringify({
|
|
293
|
+
require: { php: `>=${phpMin}` },
|
|
294
|
+
'require-dev': {
|
|
295
|
+
'phpstan/phpstan': '^1.12 || ^2.0',
|
|
296
|
+
'squizlabs/php_codesniffer': '^3.10',
|
|
297
|
+
'phpcompatibility/php-compatibility': '^9.3',
|
|
298
|
+
'dealerdirect/phpcodesniffer-composer-installer': '^1.0',
|
|
299
|
+
},
|
|
300
|
+
config: { 'allow-plugins': { 'dealerdirect/phpcodesniffer-composer-installer': true } },
|
|
301
|
+
scripts: {
|
|
302
|
+
'quality:phpstan': 'phpstan analyse -c phpstan.neon',
|
|
303
|
+
'quality:phpcs': 'phpcs --standard=phpcs.xml',
|
|
304
|
+
},
|
|
305
|
+
}, null, 2),
|
|
306
|
+
'phpstan.neon': `parameters:\n level: 1\n paths:\n - templates/${options.theme}\n fileExtensions:\n - php\n reportUnmatchedIgnoredErrors: false\n`,
|
|
307
|
+
'phpcs.xml': `<?xml version="1.0"?>\n<ruleset name="InstantCMS template quality">\n <description>Syntax, style and PHP ${phpMin}+ compatibility for ${options.theme}.</description>\n <file>templates/${options.theme}</file>\n <exclude-pattern>*/vendor/*</exclude-pattern>\n <rule ref="PSR12"/>\n <rule ref="PHPCompatibility">\n <properties><property name="testVersion" value="${phpMin}-"/></properties>\n </rule>\n</ruleset>\n`,
|
|
308
|
+
},
|
|
309
|
+
commands: [
|
|
310
|
+
'COMPOSER=composer.quality.json composer install --no-interaction',
|
|
311
|
+
'vendor/bin/phpstan analyse -c phpstan.neon',
|
|
312
|
+
'vendor/bin/phpcs --standard=phpcs.xml',
|
|
313
|
+
],
|
|
314
|
+
notes: [
|
|
315
|
+
'Merge require-dev entries into the project composer.json instead of replacing existing dependencies.',
|
|
316
|
+
'Review framework-dependent PHPStan findings before suppressing them.',
|
|
317
|
+
],
|
|
318
|
+
};
|
|
319
|
+
}
|