mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-27 04:49:43 +08:00
fix: tighten Drupal wiring evidence and pending release metadata
This commit is contained in:
+68
-12
@@ -35,6 +35,47 @@ function codeMask(source) {
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
// Only metadata immediately attached to a declaration counts. Walking backward
|
||||
// over balanced attributes avoids carrying examples from an earlier class body.
|
||||
function declarationMetadata(source, code, offset) {
|
||||
const parts = [];
|
||||
let end = offset;
|
||||
for (;;) {
|
||||
while (end > 0 && /\s/.test(source[end - 1])) end--;
|
||||
const tailStart = Math.max(0, end - 16);
|
||||
const modifier = /\b(?:public|protected|private|static|final|abstract|readonly)\s*$/.exec(code.slice(tailStart, end));
|
||||
if (modifier && tailStart + modifier.index + modifier[0].length === end) { end = tailStart + modifier.index; continue; }
|
||||
if (source.slice(end - 2, end) === '*/') {
|
||||
const start = source.lastIndexOf('/*', end - 2);
|
||||
if (start < 0) break;
|
||||
if (source.startsWith('/**', start)) parts.unshift(source.slice(start, end));
|
||||
end = start; continue;
|
||||
}
|
||||
if (code[end - 1] === ']') {
|
||||
let depth = 1, start = end - 2;
|
||||
for (; start >= 0; start--) {
|
||||
if (code[start] === ']') depth++;
|
||||
if (code[start] === '[' && --depth === 0) break;
|
||||
}
|
||||
if (start < 1 || code[start - 1] !== '#') break;
|
||||
parts.unshift(source.slice(start - 1, end)); end = start - 1; continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
function callArguments(source, code, open) {
|
||||
const args = [];
|
||||
let depth = 1, start = open + 1;
|
||||
for (let i = start; i < code.length; i++) {
|
||||
if ('([{'.includes(code[i])) depth++;
|
||||
if (')]}'.includes(code[i])) {
|
||||
if (--depth === 0) { args.push(source.slice(start, i).trim()); return args; }
|
||||
}
|
||||
if (code[i] === ',' && depth === 1) { args.push(source.slice(start, i).trim()); start = i + 1; }
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function yaml(source) {
|
||||
const doc = YAML.parseDocument(source, { uniqueKeys: true, maxAliasCount: 50 });
|
||||
if (doc.errors.length) return {};
|
||||
@@ -68,11 +109,9 @@ function serviceNodes(file, source) {
|
||||
function pluginDeclarations(file, source) {
|
||||
const nodes = [];
|
||||
const code = codeMask(source);
|
||||
let previous = 0;
|
||||
for (const cls of source.matchAll(/\b(?:final\s+|abstract\s+)?class\s+(\w+)/g)) {
|
||||
if (code[cls.index] === ' ') continue;
|
||||
const prelude = source.slice(previous, cls.index);
|
||||
previous = cls.index + cls[0].length;
|
||||
const prelude = declarationMetadata(source, code, cls.index);
|
||||
const annotation = /@(Block|FieldType|FieldFormatter|FieldWidget|Action|QueueWorker|Condition|Filter|MenuLink|MigrateSource|MigrateProcess|MigrateDestination|Views\w*|SearchPlugin|RenderElement|FormElement)\s*\([\s\S]*?\bid\s*=\s*["']([^"']+)["']/g;
|
||||
const attribute = /#\[\s*(?:[\w\\]+\\)?(Block|FieldType|FieldFormatter|FieldWidget|Action|QueueWorker|Condition|Filter|MenuLink|MigrateSource|MigrateProcess|MigrateDestination|Views\w*|SearchPlugin|RenderElement|FormElement)\s*\([\s\S]*?(?:\bid\s*:\s*)?["']([^"']+)["']/g;
|
||||
const matches = [...prelude.matchAll(annotation), ...prelude.matchAll(attribute)];
|
||||
@@ -110,7 +149,9 @@ module.exports = () => ({
|
||||
const files = ctx.getAllFiles().slice().sort();
|
||||
const nodesInFile = new Map();
|
||||
const sourceCache = new Map();
|
||||
const codeCache = new Map();
|
||||
const read = f => { if (!sourceCache.has(f)) sourceCache.set(f, ctx.readFile(f) || ''); return sourceCache.get(f); };
|
||||
const codeFor = f => { if (!codeCache.has(f)) codeCache.set(f, codeMask(read(f))); return codeCache.get(f); };
|
||||
const nodes = f => { if (!nodesInFile.has(f)) nodesInFile.set(f, ctx.getNodesInFile(f)); return nodesInFile.get(f); };
|
||||
const classes = new Map();
|
||||
const services = new Map();
|
||||
@@ -131,7 +172,7 @@ module.exports = () => ({
|
||||
function fullClass(name, file) {
|
||||
if (!name) return '';
|
||||
if (name.startsWith('\\')) return clean(name);
|
||||
const source = read(file);
|
||||
const source = codeFor(file);
|
||||
const first = name.split('\\')[0];
|
||||
for (const u of source.matchAll(/^\s*use\s+([\w\\]+)(?:\s+as\s+(\w+))?\s*;/gm)) {
|
||||
if ((u[2] || simple(u[1])) === first) return clean(u[1]) + name.slice(first.length);
|
||||
@@ -165,15 +206,18 @@ module.exports = () => ({
|
||||
if (constant[2] === 'class') return name;
|
||||
if (cls) {
|
||||
const source = read(cls.filePath);
|
||||
const value = new RegExp('\\bconst\\s+' + constant[2] + '\\s*=\\s*["\\\']([^"\\\']+)["\\\']').exec(source);
|
||||
if (value) return value[1];
|
||||
const pattern = new RegExp('\\bconst\\s+' + constant[2] + '\\s*=\\s*["\\\']([^"\\\']+)["\\\']', 'g');
|
||||
for (const value of source.matchAll(pattern)) {
|
||||
const line = lineAt(source, value.index);
|
||||
if (codeFor(cls.filePath)[value.index] !== ' ' && line >= cls.startLine && line <= cls.endLine) return value[1];
|
||||
}
|
||||
}
|
||||
return name + '::' + constant[2];
|
||||
}
|
||||
|
||||
for (const file of files.filter(phpFile)) {
|
||||
const source = read(file);
|
||||
const ns = /\bnamespace\s+([\w\\]+)\s*[;{]/.exec(source)?.[1];
|
||||
const ns = /\bnamespace\s+([\w\\]+)\s*[;{]/.exec(codeFor(file))?.[1];
|
||||
for (const cls of nodes(file).filter(n => n.kind === 'class')) add(classes, ns ? ns+'\\'+cls.name : cls.name, cls);
|
||||
await yieldToLoop();
|
||||
}
|
||||
@@ -215,6 +259,7 @@ module.exports = () => ({
|
||||
}
|
||||
for (const file of files.filter(phpFile)) {
|
||||
const source = read(file), lines = source.split('\n');
|
||||
const offsets = [0]; for (let i = 0; i < lines.length; i++) offsets.push(offsets[i] + lines[i].length + 1);
|
||||
for (const n of nodes(file)) {
|
||||
if (n.id.startsWith('plugin:drupal:') && n.kind === 'component') {
|
||||
const cls = nodes(file).find(c => c.kind === 'class' && c.startLine <= n.startLine && c.endLine >= n.startLine);
|
||||
@@ -222,8 +267,9 @@ module.exports = () => ({
|
||||
const id = n.name.slice(n.name.indexOf(':') + 1); if (cls) add(pluginClasses, id, cls);
|
||||
}
|
||||
if (!['method', 'function'].includes(n.kind)) continue;
|
||||
const priorNodeEnd = Math.max(0, ...nodes(file).filter(p => ['method', 'function'].includes(p.kind) && p.endLine < n.startLine).map(p => p.endLine));
|
||||
const prelude = lines.slice(priorNodeEnd, n.startLine).join('\n');
|
||||
const start = offsets[n.startLine - 1];
|
||||
const declaration = /\bfunction\s+&?\s*[A-Za-z_]\w*\s*\(/.exec(codeFor(file).slice(start, offsets[n.endLine]));
|
||||
const prelude = declaration ? declarationMetadata(source, codeFor(file), start + declaration.index) : '';
|
||||
const hookAttribute = /#\[\s*(?:[\w\\]+\\)?Hook\s*\(\s*['"]([^'"]+)['"]/g;
|
||||
for (const h of prelude.matchAll(hookAttribute)) add(hooks, h[1], n);
|
||||
if (n.kind === 'function' && /\.(module|install|theme|inc)$/.test(file)) {
|
||||
@@ -239,8 +285,11 @@ module.exports = () => ({
|
||||
const cls = nodes(file).find(c => c.kind === 'class' && n.startLine >= c.startLine && n.endLine <= c.endLine);
|
||||
if (!cls || !/EventSubscriberInterface/.test(source.slice(0, source.indexOf('{', source.indexOf('class '+cls.name))))) continue;
|
||||
const body = lines.slice(n.startLine - 1, n.endLine).join('\n');
|
||||
const bodyCode = codeMask(body);
|
||||
const registration = /(?:\$events\s*\[([^\]]+)\]\s*(?:\[\])?\s*=|(['"][^'"]+['"]|[\w\\]+::\w+)\s*=>)\s*\[?\s*['"](\w+)['"]/g;
|
||||
for (const r of body.matchAll(registration)) {
|
||||
const executable = bodyCode.slice(r.index, r.index + r[0].length);
|
||||
if (r[1] ? executable[0] !== '$' : !executable.includes('=>')) continue;
|
||||
const event = eventName(r[1] || r[2], file), handler = method(cls, r[3]);
|
||||
if (event && handler) add(subscribers, event, { handler, file, line: n.startLine + lineAt(body, r.index) - 1 });
|
||||
}
|
||||
@@ -250,15 +299,16 @@ module.exports = () => ({
|
||||
}
|
||||
for (const file of files.filter(phpFile)) {
|
||||
const source = read(file);
|
||||
const code = codeMask(source);
|
||||
const code = codeFor(file);
|
||||
const patterns = [
|
||||
{ re: /(?:->invokeAll|->alter)\s*\(\s*['"]([^'"]+)['"]/g, kind: 'hook' },
|
||||
{ re: /(?:\\?Drupal::service|\$container->get)\s*\(\s*['"]([^'"]+)['"]/g, kind: 'service' },
|
||||
{ re: /->createInstance\s*\(\s*['"]([^'"]+)['"]/g, kind: 'plugin' },
|
||||
{ re: /->dispatch\s*\(\s*[^,\n]+,\s*([^\)\n]+)\)/g, kind: 'event' },
|
||||
{ re: /->dispatch\s*\(/g, kind: 'event' },
|
||||
];
|
||||
for (const { re, kind } of patterns) for (const m of source.matchAll(re)) {
|
||||
if (code[m.index] === ' ') continue;
|
||||
if (kind === 'service' && /[A-Za-z0-9_\\]/.test(source[m.index - 1] || '')) continue;
|
||||
const line = lineAt(source, m.index), from = enclosing(file, line); if (!from) continue;
|
||||
if (kind === 'hook') {
|
||||
const hook = m[0].startsWith('->alter') ? m[1] + '_alter' : m[1];
|
||||
@@ -269,7 +319,13 @@ module.exports = () => ({
|
||||
// Literal id must uniquely identify a declaration across managers.
|
||||
edge(from, unique(pluginClasses.get(m[1]) || []), 'Drupal plugin construction', file, line, 'instantiates');
|
||||
} else {
|
||||
const event = eventName(m[1], file);
|
||||
// Symfony changed from dispatch(name, event) to dispatch(event, name).
|
||||
// Accept either explicit literal/constant slot, never infer a dynamic name.
|
||||
const args = callArguments(source, code, m.index + m[0].length - 1);
|
||||
if (args.length !== 2) continue;
|
||||
const names = args.map(arg => eventName(arg, file)).filter(Boolean);
|
||||
if (names.length !== 1) continue;
|
||||
const event = names[0];
|
||||
for (const target of subscribers.get(event) || []) edge(from, target.handler, `Drupal event ${event}`, target.file, target.line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
{
|
||||
"name": "@colbymchenry/codegraph-drupal",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Drupal routes, service wiring, hooks, plugins and event subscribers.",
|
||||
"main": "dist/index.cjs",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/colbymchenry/codegraph",
|
||||
"codegraph": { "id": "drupal", "apiVersion": 1, "engines": ">=1.6.0 <2", "capabilities": ["frameworks", "synthPasses"] }
|
||||
"codegraph": {
|
||||
"id": "drupal",
|
||||
"apiVersion": 1,
|
||||
"engines": ">=1.6.0 <2",
|
||||
"capabilities": [
|
||||
"frameworks",
|
||||
"synthPasses"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -34,6 +34,8 @@ async function api(path, options) {
|
||||
function project() { return state.snapshot?.projects.find(p => p.id === state.project); }
|
||||
function installed(id) { return project()?.extensions.find(e => e.name === `managed:${id}`); }
|
||||
function selected(extension) { return state.selections[extension.id]?.release || extension; }
|
||||
function releaseKnown(id) { return !state.snapshot || !!state.selections[id]?.release; }
|
||||
function releaseVersion(extension) { return releaseKnown(extension.id) ? extension.version : state.selections[extension.id]?.error ? 'No compatible release' : 'Checking compatibility…'; }
|
||||
function compatibility(extension) {
|
||||
if (!state.snapshot) return 'Connect a project to select a compatible stable release.';
|
||||
const choice = state.selections[extension.id];
|
||||
@@ -77,7 +79,7 @@ function progress() {
|
||||
function card(extension) {
|
||||
extension = selected(extension);
|
||||
const current = installed(extension.id);
|
||||
return `<article class="extension-card"><div class="card-top"><div class="extension-icon">${extension.id === 'drupal' ? icon : escape(extension.name.slice(0, 1))}</div><div><h2><a data-link href="/extensions/${escape(extension.id)}">${escape(extension.name)}</a></h2><div class="byline">${escape(extension.publisher)} ${extension.official ? '<span class="official">✓ OFFICIAL</span>' : '<span>Community</span>'}</div></div><span class="version">v${escape(extension.version)}</span></div><p class="card-copy">${escape(extension.description)}</p><div class="tags">${extension.id === 'drupal' ? '<span class="tag">PHP</span><span class="tag">Drupal 8–11</span>' : ''}${extension.capabilities.map(c => `<span class="tag">${c === 'frameworks' ? 'Framework' : 'Semantic analysis'}</span>`).join('')}</div>${extension.id === 'drupal' ? '<div class="graph" aria-label="Example framework flow: route to controller to service"><span class="graph-node">/your-route</span><span class="graph-line"></span><span class="graph-node blue">Controller</span><span class="graph-line"></span><span class="graph-node">Service</span></div>' : ''}<p class="small muted compatibility">${escape(compatibility(extension))}</p><div class="card-bottom"><a data-link class="text-link" href="/extensions/${escape(extension.id)}">${current ? (current.enabled === false ? 'Disabled · Manage →' : 'Installed · Manage →') : 'Explore extension ↗'}</a>${button(extension)}</div></article>`;
|
||||
return `<article class="extension-card"><div class="card-top"><div class="extension-icon">${extension.id === 'drupal' ? icon : escape(extension.name.slice(0, 1))}</div><div><h2><a data-link href="/extensions/${escape(extension.id)}">${escape(extension.name)}</a></h2><div class="byline">${escape(extension.publisher)} ${extension.official ? '<span class="official">✓ OFFICIAL</span>' : '<span>Community</span>'}</div></div><span class="version">${releaseKnown(extension.id) ? `v${escape(extension.version)}` : 'Checking…'}</span></div><p class="card-copy">${escape(extension.description)}</p><div class="tags">${extension.id === 'drupal' ? '<span class="tag">PHP</span><span class="tag">Drupal 8–11</span>' : ''}${extension.capabilities.map(c => `<span class="tag">${c === 'frameworks' ? 'Framework' : 'Semantic analysis'}</span>`).join('')}</div>${extension.id === 'drupal' ? '<div class="graph" aria-label="Example framework flow: route to controller to service"><span class="graph-node">/your-route</span><span class="graph-line"></span><span class="graph-node blue">Controller</span><span class="graph-line"></span><span class="graph-node">Service</span></div>' : ''}<p class="small muted compatibility">${escape(compatibility(extension))}</p><div class="card-bottom"><a data-link class="text-link" href="/extensions/${escape(extension.id)}">${current ? (current.enabled === false ? 'Disabled · Manage →' : 'Installed · Manage →') : 'Explore extension ↗'}</a>${button(extension)}</div></article>`;
|
||||
}
|
||||
function renderCatalog() {
|
||||
app.innerHTML = `<section class="hero"><div><span class="eyebrow">THE CODEGRAPH MARKETPLACE</span><h1>Make your graph<br>speak your framework.</h1><p>Add the connections that matter to your codebase.<br>Discover extensions, install in a click, and keep<br>your intelligence local.</p></div>${connectionPanel()}</section><section class="section"><div class="toolbar"><div class="tabs"><button class="tab ${state.tab === 'all' ? 'active' : ''}" data-tab="all">All extensions<span class="count">${state.catalog.length}</span></button><button class="tab ${state.tab === 'official' ? 'active' : ''}" data-tab="official">Official</button><button class="tab ${state.tab === 'installed' ? 'active' : ''}" data-tab="installed">Installed<span class="count">${project()?.extensions.length || 0}</span></button></div><label class="search">⌕<input id="search" aria-label="Search extensions" placeholder="Search frameworks, capabilities…" value="${escape(state.search)}"></label></div>${progress()}<div class="catalog-label"><span>${state.tab === 'installed' ? 'YOUR EXTENSIONS' : 'EXTEND WHAT YOUR GRAPH UNDERSTANDS'}</span><span>FRAMEWORK + SEMANTIC EXTENSIONS</span></div><div id="catalog-items"></div><div class="note-row"><span>◉ Source code stays on your machine</span><span>↻ Updates you control</span><span>◇ Open extension API</span></div></section>`;
|
||||
@@ -93,7 +95,8 @@ function renderDetail(id) {
|
||||
const e = item && selected(item);
|
||||
if (!e) { app.innerHTML = '<section class="section detail"><h1>Extension not found</h1><a data-link href="/">Back to marketplace</a></section>'; return; }
|
||||
const current = installed(id);
|
||||
app.innerHTML = `<section class="section detail"><div class="breadcrumb"><a data-link href="/">Marketplace</a> / ${escape(e.name)}</div><div class="detail-layout"><article><div class="card-top"><div class="extension-icon">${id === 'drupal' ? icon : escape(e.name.slice(0,1))}</div><div><h2>${escape(e.name)}</h2><div class="byline">${escape(e.publisher)} ${e.official ? '<span class="official">✓ OFFICIAL</span>' : 'Community publisher'}</div></div></div><p class="card-copy">${escape(e.description)}</p><div class="tags">${e.capabilities.map(c => `<span class="tag">${c === 'frameworks' ? 'Framework support' : 'Semantic analysis'}</span>`).join('')}</div><div class="readme">${escape(e.readme)}</div></article><aside class="metadata"><span class="eyebrow">${current ? (current.enabled === false ? 'INSTALLED · DISABLED' : 'INSTALLED') : 'READY FOR YOUR PROJECT'}</span>${button(e,true)}<p class="small muted compatibility">${escape(compatibility(e))}</p>${progress()}<p class="small muted">${project() ? `Destination: ${escape(project().name)}` : 'Connect CodeGraph once to install.'}</p><p class="small muted">Extensions run with your local permissions. Review the publisher and source before installing.</p><dl><dt>Version</dt><dd>${escape(e.version)}</dd><dt>CodeGraph compatibility</dt><dd>${escape(e.engines)}</dd><dt>Source</dt><dd><a href="${escape(e.source)}" target="_blank" rel="noopener">View source repository ↗</a></dd><dt>Published</dt><dd>${escape(new Date(e.publishedAt).toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric'}))}</dd><dt>Publisher identity</dt><dd class="integrity">${escape(e.publisherId)}</dd><dt>Release fingerprint</dt><dd class="integrity">${escape(e.integrity)}</dd></dl><a class="text-link" href="/api/download/${escape(e.id)}/${escape(e.version)}">Download for CLI install ↓</a></aside></div></section>`;
|
||||
const known = releaseKnown(id);
|
||||
app.innerHTML = `<section class="section detail"><div class="breadcrumb"><a data-link href="/">Marketplace</a> / ${escape(e.name)}</div><div class="detail-layout"><article><div class="card-top"><div class="extension-icon">${id === 'drupal' ? icon : escape(e.name.slice(0,1))}</div><div><h2>${escape(e.name)}</h2><div class="byline">${escape(e.publisher)} ${e.official ? '<span class="official">✓ OFFICIAL</span>' : 'Community publisher'}</div></div></div><p class="card-copy">${escape(e.description)}</p><div class="tags">${e.capabilities.map(c => `<span class="tag">${c === 'frameworks' ? 'Framework support' : 'Semantic analysis'}</span>`).join('')}</div><div class="readme">${escape(e.readme)}</div></article><aside class="metadata"><span class="eyebrow">${current ? (current.enabled === false ? 'INSTALLED · DISABLED' : 'INSTALLED') : 'READY FOR YOUR PROJECT'}</span>${button(e,true)}<p class="small muted compatibility">${escape(compatibility(e))}</p>${progress()}<p class="small muted">${project() ? `Destination: ${escape(project().name)}` : 'Connect CodeGraph once to install.'}</p><p class="small muted">Extensions run with your local permissions. Review the publisher and source before installing.</p><dl>${current ? `<dt>Installed version</dt><dd data-installed-version>${escape(current.version)}</dd>` : ''}<dt>${state.snapshot ? 'Selected version' : 'Latest catalog version'}</dt><dd data-selected-version>${escape(releaseVersion(e))}</dd>${known ? `<dt>CodeGraph compatibility</dt><dd>${escape(e.engines)}</dd><dt>Source</dt><dd><a href="${escape(e.source)}" target="_blank" rel="noopener">View source repository ↗</a></dd><dt>Published</dt><dd>${escape(new Date(e.publishedAt).toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric'}))}</dd><dt>Publisher identity</dt><dd class="integrity">${escape(e.publisherId)}</dd><dt>Release fingerprint</dt><dd class="integrity">${escape(e.integrity)}</dd>` : ''}</dl>${known ? `<a class="text-link" href="/api/download/${escape(e.id)}/${escape(e.version)}">Download for CLI install ↓</a>` : ''}</aside></div></section>`;
|
||||
}
|
||||
function renderPublish() {
|
||||
app.innerHTML = `<section class="section publish"><a data-link class="breadcrumb" href="/">← Marketplace</a><h1>Share what you know.</h1><p class="publish-intro">Give CodeGraph the framework knowledge your team needs. Upload a versioned extension and publish it to the community.</p><div class="publish-layout"><form id="publish-form"><div class="upload"><label for="artifact">Your extension package</label><p class="small muted">Upload the .cgext file from CodeGraph’s extension pack command.</p><input type="file" id="artifact" accept=".cgext,application/json" required><p id="package-summary" class="small"></p></div><div class="form-row"><div><label for="name">Extension name</label><input id="name" maxlength="80" placeholder="Your framework" required></div><div><label for="publisher">Publisher name</label><input id="publisher" maxlength="80" placeholder="Your name or team" required></div></div><label for="description">Short description</label><input id="description" maxlength="240" placeholder="What does this extension help people understand?" required><label for="source">Source repository</label><input id="source" type="url" placeholder="https://github.com/your-team/your-extension" required><label for="readme">Documentation</label><textarea id="readme" rows="8" maxlength="30000" placeholder="Supported patterns, configuration, examples, and known limitations." required></textarea><div class="form-actions"><button class="primary" type="submit">Publish release <span>↗</span></button><span class="small muted">Published versions are immutable.<br>Community publication does not confer official status.</span></div><p id="publish-result" role="status"></p></form><aside class="aside-notes"><span class="eyebrow">BEFORE YOU PUBLISH</span><h3>Build. Test. Share.</h3><p>Package framework or semantic contributions, and test deterministic indexing before publishing.</p><h3>Your publisher identity</h3><p>Your browser creates a signing key for your releases. Back it up to publish updates from another browser. Your key never leaves this browser during publication.</p><button id="backup-key">Back up publisher key ↓</button><label for="restore-key">Restore an existing publisher key</label><input type="file" id="restore-key" accept=".json"><p class="small">Keep your backup private. Anyone with it can publish your extensions.</p></aside></div></section>`;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { build } from 'esbuild';
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
|
||||
await build({ entryPoints: ['extensions/drupal/index.cjs'], bundle: true, platform: 'node', target: 'node22', format: 'cjs', outfile: 'extensions/drupal/dist/index.cjs' });
|
||||
const pkg = JSON.parse(readFileSync('extensions/drupal/package.json', 'utf8'));
|
||||
const artifact = { format: 'codegraph-extension-1', package: pkg, files: { 'dist/index.cjs': readFileSync('extensions/drupal/dist/index.cjs', 'utf8') } };
|
||||
mkdirSync('dist/extensions', { recursive: true });
|
||||
writeFileSync('dist/extensions/drupal.cgext', JSON.stringify(artifact));
|
||||
writeFileSync(`dist/extensions/drupal-${pkg.version}.cgext`, JSON.stringify(artifact));
|
||||
// Historical bytes remain installable and reviewable after the source evolves.
|
||||
copyFileSync('extensions/drupal/releases/drupal-0.1.0.cgext', 'dist/extensions/drupal-0.1.0.cgext');
|
||||
console.log('Built official Drupal extension');
|
||||
|
||||
@@ -15,9 +15,15 @@ test('Drupal declarations and event registrations distinguish executable wiring
|
||||
namespace Drupal\\sample;
|
||||
class Calls {
|
||||
public function good() { \\Drupal::service('sample.dep'); $dispatcher->dispatch($event, 'done'); }
|
||||
public function constantEvent() { $dispatcher->dispatch($event, Events::DONE); }
|
||||
public function legacy() { $dispatcher->dispatch('done', new Event($one, $two)); }
|
||||
public function bad() { NotDrupal::service('sample.dep'); $dispatcher->dispatch($event, 'fake'); }
|
||||
}
|
||||
class Dependency {}
|
||||
class Events {
|
||||
// const DONE = 'fake';
|
||||
const DONE = 'done';
|
||||
}
|
||||
`,
|
||||
'Subscriber.php':`<?php
|
||||
namespace Drupal\\sample;
|
||||
@@ -56,7 +62,7 @@ class Hooks {
|
||||
const db=new DatabaseSync(path.join(root,'.codegraph/codegraph.db'),{readOnly:true});
|
||||
try {
|
||||
const edges=db.prepare("SELECT s.name source,t.name target,json_extract(e.metadata,'$.label') label FROM edges e JOIN nodes s ON s.id=e.source JOIN nodes t ON t.id=e.target WHERE json_extract(e.metadata,'$.synthesizedBy')='drupal'").all();
|
||||
for(const [source,target,label] of [['good','Dependency','Drupal service lookup'],['good','onDone','Drupal event done'],['dispatch','actualHook','Drupal hook done']])assert.ok(edges.some(e=>e.source===source&&e.target===target&&e.label===label),JSON.stringify({source,target,label,edges}));
|
||||
for(const [source,target,label] of [['good','Dependency','Drupal service lookup'],['good','onDone','Drupal event done'],['legacy','onDone','Drupal event done'],['constantEvent','onDone','Drupal event done'],['dispatch','actualHook','Drupal hook done']])assert.ok(edges.some(e=>e.source===source&&e.target===target&&e.label===label),JSON.stringify({source,target,label,edges}));
|
||||
assert.deepEqual(edges.filter(e=>e.source==='bad'||e.target==='onFake'||e.target==='notHook'),[],'Unrelated static receiver and commented/string declarations are not wiring');
|
||||
assert.deepEqual(db.prepare("SELECT name FROM nodes WHERE kind='component' AND id LIKE 'plugin:drupal:%' ORDER BY name").all().map(n=>n.name),['Block:real']);
|
||||
}finally{db.close();}
|
||||
|
||||
@@ -94,6 +94,21 @@ async function cli(args, expected = 0) {
|
||||
assert.equal(await page.evaluate(() => state.snapshot.error), undefined);
|
||||
await expectGraph(roots[1], '1.10.0'); assert.deepEqual(await graph(roots[0]), []);
|
||||
checks.push('one browser Install selects 1.10.0 over incompatible 9.0.0 and preview; actual Python graph refreshed only in chosen destination');
|
||||
// Hold actual bridge resolution to inspect the transient destination state.
|
||||
await page.evaluate(() => {
|
||||
const originalRpc = rpc;
|
||||
const gate = new Promise(resolve => window.releaseSelection = resolve);
|
||||
rpc = (type, value) => type === 'command' && value?.action === 'resolve' ? gate.then(() => originalRpc(type, value)) : originalRpc(type, value);
|
||||
window.pendingSelection = refreshSelections().finally(() => { rpc = originalRpc; });
|
||||
});
|
||||
await page.screenshot({ path: path.join(out, 'metadata-pending.png'), fullPage: true });
|
||||
assert.equal(await page.locator('[data-installed-version]').innerText(), '1.10.0');
|
||||
assert.equal(await page.locator('[data-selected-version]').innerText(), 'Checking compatibility…');
|
||||
assert.equal(await page.locator('a[href^="/api/download/"]').count(), 0);
|
||||
await page.evaluate(async () => { releaseSelection(); await pendingSelection; });
|
||||
assert.equal(await page.locator('[data-selected-version]').innerText(), '1.10.0');
|
||||
assert.equal(await page.locator('[data-installed-version]').innerText(), '1.10.0');
|
||||
checks.push('pending compatibility refresh never shows incompatible catalog version as selected; installed and selected versions remain distinct');
|
||||
await page.screenshot({ path: path.join(out, 'desktop-installed.png'), fullPage: true });
|
||||
const config = fs.readFileSync(path.join(roots[1], 'codegraph.json'), 'utf8');
|
||||
const noChoice = await page.evaluate(async () => {
|
||||
|
||||
Reference in New Issue
Block a user