@brixies-mcp/server 0.0.0-stage → 1.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/index.js +1239 -0
- package/lib/transforms.js +185 -0
- package/package.json +33 -4
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/* Shared MCP/prepared-page transformations. No network or platform-specific APIs. */
|
|
2
|
+
import postcss from "postcss";
|
|
3
|
+
function generateRandomID() {
|
|
4
|
+
const chars = "abcdefghijklmnopqrstuvwxyz";
|
|
5
|
+
return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function generateUniqueIDs(count, existingIds) {
|
|
9
|
+
const used = new Set(existingIds);
|
|
10
|
+
const result = [];
|
|
11
|
+
while (result.length < count) {
|
|
12
|
+
const id = generateRandomID();
|
|
13
|
+
if (!used.has(id)) { used.add(id); result.push(id); }
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Return null for a valid block name, otherwise a validation message.
|
|
19
|
+
function validateBemName(name) {
|
|
20
|
+
if (!name || !name.trim()) return "Name is empty.";
|
|
21
|
+
if (name.trim() === "grid") return "The word \"grid\" alone is reserved. Use something like \"blog-grid\".";
|
|
22
|
+
if (!/^[a-zA-Z0-9\-_]+$/.test(name.trim())) return "Use only letters, numbers, hyphens and underscores. No spaces or dots.";
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Discover owned CSS definitions without serializing/reformatting the stylesheet.
|
|
27
|
+
function layoutCssTokens(parsed, oldName, newName) {
|
|
28
|
+
const definitions = new Set();
|
|
29
|
+
function collect(value, key = '') {
|
|
30
|
+
if (typeof value === 'string' && ['cssCode', '_cssCustom'].includes(key) && value.trim()) {
|
|
31
|
+
const root = postcss.parse(value);
|
|
32
|
+
root.walkDecls(d => { if (d.prop.startsWith('--')) definitions.add(d.prop); });
|
|
33
|
+
root.walkAtRules(a => {
|
|
34
|
+
if (/^(?:-webkit-)?keyframes$/i.test(a.name) || a.name.toLowerCase() === 'property') {
|
|
35
|
+
const token = a.params.trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
36
|
+
if (/^(?:--)?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(token)) definitions.add(token);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} else if (Array.isArray(value)) value.forEach(v => collect(v));
|
|
40
|
+
else if (value && typeof value === 'object') {
|
|
41
|
+
for (const [k,v] of Object.entries(value)) {
|
|
42
|
+
if (k === 'globalClasses') {
|
|
43
|
+
for (const c of v || []) if (typeof c.name === 'string' && isBlockClass(c.name, oldName)) collect(c);
|
|
44
|
+
} else collect(v,k);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
collect(parsed);
|
|
49
|
+
const mapping = new Map();
|
|
50
|
+
for (const token of definitions) {
|
|
51
|
+
const prefix = token.startsWith('--') ? '--' : '';
|
|
52
|
+
const name = token.slice(prefix.length);
|
|
53
|
+
if (!isBlockClass(name, oldName)) continue;
|
|
54
|
+
const base = name.startsWith('card-' + oldName) ? 'card-' + oldName : oldName;
|
|
55
|
+
const target = prefix + (base === 'card-' + oldName ? 'card-' : '') + newName.trim() + name.slice(base.length);
|
|
56
|
+
if (target !== token && definitions.has(target)) throw new Error(`CSS name collision: ${target} is already defined. Choose another block name.`);
|
|
57
|
+
mapping.set(token,target);
|
|
58
|
+
}
|
|
59
|
+
return mapping;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Preserve comment text and every byte outside literal identifiers. Quoted JS
|
|
63
|
+
// references (setProperty/getPropertyValue/animationName) are included. Dynamic
|
|
64
|
+
// string construction is not evaluated.
|
|
65
|
+
function replaceLayoutTokens(value, mapping, key = '') {
|
|
66
|
+
if (!mapping.size) return value;
|
|
67
|
+
const replace = text => text.replace(/(?<![\w\\\u0080-\uFFFF-])(?:--)?[a-zA-Z_][a-zA-Z0-9_-]*(?![\w\\\u0080-\uFFFF-])/g, token => mapping.get(token) ?? token);
|
|
68
|
+
const parts = ['jsCode', 'code'].includes(key)
|
|
69
|
+
? /\/\*[\s\S]*?\*\/|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|\/\/[^\r\n]*|[^/"']+|[\s\S]/g
|
|
70
|
+
: /\/\*[\s\S]*?\*\/|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|[^/"']+|[\s\S]/g;
|
|
71
|
+
return value.replace(parts,
|
|
72
|
+
part => part.startsWith('/*') || (['jsCode', 'code'].includes(key) && part.startsWith('//')) ? part : replace(part));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Assign new IDs to owned classes and rename their BEM references; preserve shared classes.
|
|
76
|
+
function applyBemRename(jsonText, oldName, newName) {
|
|
77
|
+
const err = validateBemName(newName);
|
|
78
|
+
if (err) return { ok: false, error: err };
|
|
79
|
+
if (!oldName || oldName === 'unknown') return { ok: false, error: 'Missing source BEM block name; cannot rename reliably.' };
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(jsonText);
|
|
82
|
+
const classes = parsed.globalClasses || [];
|
|
83
|
+
const layoutClasses = classes.filter(c => typeof c.name === 'string' && isBlockClass(c.name, oldName));
|
|
84
|
+
// Derive companion block roots from actual class definitions. Match exact
|
|
85
|
+
// roots or BEM suffixes, not arbitrary prefix text (e.g. animation names).
|
|
86
|
+
const roots = new Set([oldName, 'card-' + oldName]);
|
|
87
|
+
for (const cls of layoutClasses) roots.add(cls.name.split(/__|--/)[0]);
|
|
88
|
+
const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
89
|
+
const names = new RegExp('(?<![a-zA-Z0-9_-])(?:' + [...roots].sort((a,b) => b.length-a.length).map(escape).join('|') + ')(?=__|--|$|[^a-zA-Z0-9_-])', 'g');
|
|
90
|
+
const renameRoot = root => root.startsWith('card-' + oldName)
|
|
91
|
+
? 'card-' + newName.trim() + root.slice(('card-' + oldName).length)
|
|
92
|
+
: newName.trim() + root.slice(oldName.length);
|
|
93
|
+
const cssTokens = layoutCssTokens(parsed, oldName, newName);
|
|
94
|
+
const newIds = generateUniqueIDs(layoutClasses.length, classes.map(c => c.id));
|
|
95
|
+
const idMap = new Map(layoutClasses.map((c, i) => [c.id, newIds[i]]));
|
|
96
|
+
function visit(value, field = '') {
|
|
97
|
+
if (typeof value === 'string') {
|
|
98
|
+
if (idMap.has(value)) return idMap.get(value);
|
|
99
|
+
return replaceLayoutTokens(value, cssTokens, field).replace(names, renameRoot);
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(value)) return value.map(v => visit(v, field));
|
|
102
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, visit(val, key)]));
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
const renamed = visit(parsed);
|
|
106
|
+
// Shared definitions are opaque: never rename their settings, IDs or metadata.
|
|
107
|
+
renamed.globalClasses = classes.map(c => layoutClasses.includes(c) ? visit(c) : bxClone(c));
|
|
108
|
+
return { ok: true, json: JSON.stringify(renamed), count: layoutClasses.length };
|
|
109
|
+
} catch (error) { return { ok: false, error: `Invalid Bricks JSON: ${error.message}` }; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
function semverGreater(a, b) {
|
|
115
|
+
const pa = (a||'0').split('.').map(Number), pb = (b||'0').split('.').map(Number);
|
|
116
|
+
for (let i=0;i<3;i++) { if((pa[i]||0)>(pb[i]||0)) return true; if((pa[i]||0)<(pb[i]||0)) return false; }
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
function deepEqual(a,b) { return JSON.stringify(a)===JSON.stringify(b); }
|
|
120
|
+
function bxClone(obj) { return JSON.parse(JSON.stringify(obj)); }
|
|
121
|
+
function uniqueId(used) {
|
|
122
|
+
const ch='abcdefghijklmnopqrstuvwxyz'; let id;
|
|
123
|
+
do { id=Array.from({length:6},()=>ch[Math.floor(Math.random()*ch.length)]).join(''); } while(used.has(id));
|
|
124
|
+
return id;
|
|
125
|
+
}
|
|
126
|
+
// Merge layouts, remapping colliding IDs and retaining identical shared classes.
|
|
127
|
+
function mergeSnippets(snippets) {
|
|
128
|
+
const out={content:[],source:'bricksCopiedElements',sourceUrl:'https://brixies.co',version:'0.0.0',globalClasses:[],globalElements:[]};
|
|
129
|
+
const usedN=new Set(), usedC=new Set(), cIdx=new Map();
|
|
130
|
+
function pushGC(gc,cMap) {
|
|
131
|
+
if(!gc||!gc.id) return;
|
|
132
|
+
let nid=gc.id;
|
|
133
|
+
if(usedC.has(nid)) {
|
|
134
|
+
const ex=out.globalClasses[cIdx.get(nid)];
|
|
135
|
+
if(deepEqual(ex,gc)){cMap[gc.id]=nid;return;}
|
|
136
|
+
nid=uniqueId(usedC);
|
|
137
|
+
const cl=bxClone(gc);cl.id=nid;out.globalClasses.push(cl);usedC.add(nid);cIdx.set(nid,out.globalClasses.length-1);
|
|
138
|
+
} else { const cl=bxClone(gc);out.globalClasses.push(cl);usedC.add(nid);cIdx.set(nid,out.globalClasses.length-1); }
|
|
139
|
+
cMap[gc.id]=nid;
|
|
140
|
+
}
|
|
141
|
+
for(const s of snippets) if(s&&semverGreater(s.version,out.version)) out.version=s.version;
|
|
142
|
+
if(out.version==='0.0.0') out.version='2.0.0';
|
|
143
|
+
for(const sn of snippets) {
|
|
144
|
+
if(!sn||typeof sn!=='object') continue;
|
|
145
|
+
const nodes=Array.isArray(sn.content)?sn.content:[];
|
|
146
|
+
const gC=Array.isArray(sn.globalClasses)?sn.globalClasses:[];
|
|
147
|
+
const gE=Array.isArray(sn.globalElements)?sn.globalElements:[];
|
|
148
|
+
const nMap=Object.create(null), cMap=Object.create(null);
|
|
149
|
+
for(const gc of gC) pushGC(gc,cMap);
|
|
150
|
+
for(const n of nodes) {
|
|
151
|
+
if(!n||!n.id) continue;
|
|
152
|
+
let nid=n.id; if(usedN.has(nid)) nid=uniqueId(usedN);
|
|
153
|
+
nMap[n.id]=nid; usedN.add(nid);
|
|
154
|
+
}
|
|
155
|
+
for(const n of nodes) {
|
|
156
|
+
if(!n||!n.id) continue;
|
|
157
|
+
const nd=bxClone(n); nd.id=nMap[n.id];
|
|
158
|
+
if(nd.parent&&nd.parent!==0&&nMap[nd.parent]) nd.parent=nMap[nd.parent];
|
|
159
|
+
if(Array.isArray(nd.children)&&nd.children.length) nd.children=nd.children.map(id=>nMap[id]||id);
|
|
160
|
+
if(nd.settings&&Array.isArray(nd.settings._cssGlobalClasses))
|
|
161
|
+
nd.settings._cssGlobalClasses=nd.settings._cssGlobalClasses.map(id=>cMap[id]||id);
|
|
162
|
+
const remapReferences = value => {
|
|
163
|
+
if (typeof value === 'string') {
|
|
164
|
+
if (nMap[value]) return nMap[value];
|
|
165
|
+
return value.replace(/brxe-([a-zA-Z0-9]{6})(?![a-zA-Z0-9_-])/g, (match, id) => nMap[id] ? 'brxe-' + nMap[id] : match);
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(value)) return value.map(remapReferences);
|
|
168
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, remapReferences(val)]));
|
|
169
|
+
return value;
|
|
170
|
+
};
|
|
171
|
+
if (nd.settings) nd.settings = remapReferences(nd.settings);
|
|
172
|
+
out.content.push(nd);
|
|
173
|
+
}
|
|
174
|
+
for(const ge of gE) out.globalElements.push(bxClone(ge));
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isBlockClass(name, blockName) {
|
|
180
|
+
return [blockName, `card-${blockName}`].some(base =>
|
|
181
|
+
name === base || name.startsWith(`${base}__`) || name.startsWith(`${base}-`));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
export { applyBemRename, mergeSnippets, validateBemName, isBlockClass, bxClone };
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brixies-mcp/server",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.12.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Brixies authenticated catalog MCP",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"test": "node test/migration.test.mjs && node test/rename.test.mjs && node test/requests.test.mjs && node test/preparations.test.mjs"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
16
|
+
"acorn": "8.18.0",
|
|
17
|
+
"acorn-walk": "8.3.5",
|
|
18
|
+
"postcss": "8.5.28",
|
|
19
|
+
"postcss-selector-parser": "7.1.6",
|
|
20
|
+
"zod": "3.25.76"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"brixies-mcp": "index.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"index.js",
|
|
27
|
+
"lib/"
|
|
28
|
+
],
|
|
29
|
+
"license": "UNLICENSED",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"tag": "next",
|
|
33
|
+
"registry": "https://registry.npmjs.org/"
|
|
34
|
+
}
|
|
35
|
+
}
|