Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions plugins/power-pages/scripts/launch-playwright-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,31 @@
// Self-contained — no external dependencies required.

const { spawn } = require('child_process');
const path = require('path');
const { detectBrowser } = require('./lib/detect-browser');

const browser = detectBrowser();
const child = spawn('npx', ['@playwright/mcp@latest', '--browser', browser, '--viewport-size', '1024,768'], {
stdio: 'inherit',
shell: true,
});
function buildMcpArgs(browser) {
return [
'@playwright/mcp@latest',
'--browser',
browser,
'--config',
path.join(__dirname, 'playwright-mcp-fullscreen.config.json'),
];
}

child.on('exit', (code) => process.exit(code || 0));
function launch({ browser = detectBrowser(), spawnFn = spawn, onExit = (code) => process.exit(code || 0) } = {}) {
const child = spawnFn('npx', buildMcpArgs(browser), {
stdio: 'inherit',
shell: true,
});

child.on('exit', onExit);
return child;
}

if (require.main === module) {
launch();
}

module.exports = { buildMcpArgs, launch };
10 changes: 8 additions & 2 deletions plugins/power-pages/scripts/lib/render-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,17 @@ function renderTemplate({ templatePath, outputPath, dataPath, dataObject, requir
process.exit(1);
}

// Replace all __KEY__ placeholders with corresponding values from the data object
// Replace all __KEY__ placeholders with corresponding values from the data object.
// For non-string values (arrays/objects serialized to JSON), escape `<` as `\u003c`
// so a literal `</script>` inside string data cannot close a containing <script> tag.
// String values are left as-is — they must be placed in safe HTML text contexts only
// (see plugins/power-pages/AGENTS.md for the convention).
let result = template;
for (const [key, value] of Object.entries(data)) {
const placeholder = `__${key}__`;
const replacement = typeof value === 'string' ? value : JSON.stringify(value);
const replacement = typeof value === 'string'
? value
: JSON.stringify(value).replace(/</g, '\\u003c');
result = result.split(placeholder).join(replacement);
Comment thread
priyanshu92 marked this conversation as resolved.
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"browser": {
"launchOptions": {
"args": [
"--start-maximized",
"--start-fullscreen"
]
},
"contextOptions": {
"viewport": null
}
}
}
77 changes: 77 additions & 0 deletions plugins/power-pages/scripts/render-createsite-plan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* render-createsite-plan.js — Renders the create-site implementation plan HTML.
*
* Usage (inline JSON):
* node render-createsite-plan.js --output <path> --data-inline '<json>'
*
* Usage (file-based):
* node render-createsite-plan.js --output <path> --data <json-file>
*
* Required keys in the data:
* SITE_NAME, PLAN_TITLE, FRAMEWORK, AESTHETIC, MOOD, SUMMARY,
* TYPOGRAPHY_DATA, PALETTE_DATA, MOTION_DATA, BACKGROUNDS_DATA,
* PAGES_DATA, COMPONENTS_DATA, ROUTES_DATA, REVIEW_DATA, DEPLOYMENT_DATA
*/

const path = require('path');
const fs = require('fs');
const { renderTemplate, parseArgs } = require('./lib/render-template');

const args = parseArgs(process.argv);

if (!args.output || (!args['data-inline'] && !args.data)) {
console.error(
'Usage: node render-createsite-plan.js --output <path> --data-inline \'<json>\'\n' +
' node render-createsite-plan.js --output <path> --data <json-file>'
);
process.exit(1);
}

const templatePath = path.join(
__dirname,
'..',
'skills',
'create-site',
'assets',
'create-site-plan.html'
);

const requiredKeys = [
'SITE_NAME',
'PLAN_TITLE',
'FRAMEWORK',
'AESTHETIC',
'MOOD',
'SUMMARY',
'TYPOGRAPHY_DATA',
'PALETTE_DATA',
'MOTION_DATA',
'BACKGROUNDS_DATA',
'PAGES_DATA',
'COMPONENTS_DATA',
'ROUTES_DATA',
'REVIEW_DATA',
'DEPLOYMENT_DATA',
];

function withDerivedTemplateData(dataObject) {
return {
...dataObject,
SUMMARY_DATA: { text: String(dataObject.SUMMARY ?? '') },
};
}

if (args['data-inline']) {
let dataObject;
try {
dataObject = JSON.parse(args['data-inline']);
} catch {
console.error('Error: --data-inline value is not valid JSON');
process.exit(1);
}
renderTemplate({ templatePath, outputPath: path.resolve(args.output), dataObject: withDerivedTemplateData(dataObject), requiredKeys });
} else {
const dataObject = JSON.parse(fs.readFileSync(path.resolve(args.data), 'utf8'));
Comment thread
priyanshu92 marked this conversation as resolved.
Outdated
renderTemplate({ templatePath, outputPath: path.resolve(args.output), dataObject: withDerivedTemplateData(dataObject), requiredKeys });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
Comment thread
priyanshu92 marked this conversation as resolved.
Outdated
const test = require('node:test');

const createSiteRoot = path.join(__dirname, '..', '..', 'skills', 'create-site', 'assets');
const loaderTemplates = [
'react/src/pages/Home.tsx',
'vue/src/pages/Home.vue',
'angular/src/app/pages/home.component.ts',
'astro/src/pages/index.astro',
];

test('create-site loader keeps awaiting-input banner persistent across templates', () => {
for (const template of loaderTemplates) {
const content = fs.readFileSync(path.join(createSiteRoot, template), 'utf8');

assert.match(content, /if \(banner\) banner\.hidden = !awaiting/, template);
assert.match(content, /if \(!lastAwaiting\)/, template);
assert.doesNotMatch(content, /inputBannerClose|input-banner-close|userDismissed|dismissedPrompt/, template);
}
Comment thread
priyanshu92 marked this conversation as resolved.
Outdated
});
51 changes: 51 additions & 0 deletions plugins/power-pages/scripts/tests/launch-playwright-mcp.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const test = require('node:test');
const { EventEmitter } = require('events');
Comment thread
priyanshu92 marked this conversation as resolved.
Outdated

const {
buildMcpArgs,
launch,
} = require('../launch-playwright-mcp');

test('buildMcpArgs launches Playwright MCP with fullscreen config', () => {
const args = buildMcpArgs('chrome');
const configIndex = args.indexOf('--config');

assert.deepEqual(args.slice(0, 3), ['@playwright/mcp@latest', '--browser', 'chrome']);
assert.equal(args.includes('--viewport-size'), false);
assert.notEqual(configIndex, -1);
assert.equal(path.basename(args[configIndex + 1]), 'playwright-mcp-fullscreen.config.json');
});

test('fullscreen config maximizes the browser and uses the real viewport size', () => {
const configPath = path.join(__dirname, '..', 'playwright-mcp-fullscreen.config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));

assert.deepEqual(config.browser.launchOptions.args, ['--start-maximized', '--start-fullscreen']);
assert.equal(config.browser.contextOptions.viewport, null);
});

test('launch wires spawn and process exit handling', () => {
let spawnCall;
const child = new EventEmitter();

launch({
browser: 'msedge',
spawnFn(command, args, options) {
spawnCall = { command, args, options };
return child;
},
onExit(code) {
spawnCall.exitCode = code;
},
});

assert.equal(spawnCall.command, 'npx');
assert.deepEqual(spawnCall.args.slice(0, 3), ['@playwright/mcp@latest', '--browser', 'msedge']);
assert.deepEqual(spawnCall.options, { stdio: 'inherit', shell: true });

child.emit('exit', 7);
assert.equal(spawnCall.exitCode, 7);
});
Loading
Loading