@pixelated-tech/components 3.9.15 → 3.9.17
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/dist/components/config/config.js +35 -1
- package/dist/components/general/tiles.css +11 -1
- package/dist/components/general/tiles.js +5 -2
- package/dist/components/general/utilities.js +0 -8
- package/dist/config/pixelated.config.json.enc +1 -1
- package/dist/scripts/config-vault.js +134 -23
- package/dist/scripts/config-vault.ts +136 -27
- package/dist/scripts/create-pixelated-app.js +338 -38
- package/dist/scripts/pixelated-eslint-plugin.js +52 -3
- package/dist/scripts/release.sh +49 -5
- package/dist/scripts/setup-remotes.sh +35 -10
- package/dist/scripts/validate-exports.js +1 -1
- package/dist/types/components/config/config.d.ts.map +1 -1
- package/dist/types/components/general/tiles.d.ts +2 -0
- package/dist/types/components/general/tiles.d.ts.map +1 -1
- package/dist/types/components/general/utilities.d.ts +0 -6
- package/dist/types/components/general/utilities.d.ts.map +1 -1
- package/dist/types/scripts/create-pixelated-app.d.ts +8 -0
- package/dist/types/scripts/create-pixelated-app.d.ts.map +1 -1
- package/dist/types/scripts/pixelated-eslint-plugin.d.ts +24 -0
- package/dist/types/scripts/pixelated-eslint-plugin.d.ts.map +1 -1
- package/dist/types/tests/config-vault.test.d.ts.map +1 -1
- package/dist/types/tests/create-pixelated-app.github.test.d.ts +2 -0
- package/dist/types/tests/create-pixelated-app.github.test.d.ts.map +1 -0
- package/package.json +11 -8
|
@@ -8,14 +8,8 @@
|
|
|
8
8
|
* - optionally initializes a fresh git repo and adds a remote
|
|
9
9
|
*
|
|
10
10
|
* TODOs (placeholders for later work):
|
|
11
|
-
* - Create/patch pixelated.config.json and optionally run config:encrypt
|
|
12
11
|
* - Run `npm ci` / `npm run lint` / `npm test` and optionally build
|
|
13
|
-
* - Optionally create GitHub repo using API (with token from secure vault)
|
|
14
12
|
*
|
|
15
|
-
*
|
|
16
|
-
1) Recommended approach (short) ✅
|
|
17
|
-
Build a small Node-based CLI (easier cross-platform than a Bash script) called e.g. scripts/new-site.js or an npm create script.
|
|
18
|
-
Make it interactive with sensible CLI flags (--name, --domain, --repo, --git, --encrypt, --no-install) and a non-interactive mode for CI.
|
|
19
13
|
|
|
20
14
|
2) High-level workflow the script should perform 🔁
|
|
21
15
|
Validate inputs (target path, site slug, repo name).
|
|
@@ -39,31 +33,13 @@ certificates/ if using TLS — template may include placeholder paths.
|
|
|
39
33
|
FEATURE_CHECKLIST.md — optionally update default checklist for new site.
|
|
40
34
|
.gitignore — ensure pixelated.config.json is ignored if plaintext during setup.
|
|
41
35
|
|
|
42
|
-
4) Template hardening (recommended changes in pixelated-template) ⚠️
|
|
43
|
-
Add placeholder tokens for the things above (e.g., {{PACKAGE_NAME}}, {{SITE_DOMAIN}}) rather than literal values.
|
|
44
|
-
Add a template.json or template.meta that lists files & placeholders to auto-replace.
|
|
45
|
-
Ensure the template does not include real secrets. Provide pixelated.config.json.example with placeholders and an example .env.local.example.
|
|
46
|
-
Add a scripts/prepare-template.sh or test job that ensures no plaintext sensitive values remain.
|
|
47
|
-
Document the creation process in TEMPLATE_README.md for maintainers.
|
|
48
|
-
|
|
49
|
-
5) Security & operational notes 🔐
|
|
50
|
-
Do not commit plaintext pixelated.config.json. If the CLI accepts secret values, optionally run npm run config:encrypt immediately and only commit the .enc if needed (prefer not to commit it; store in site secrets store).
|
|
51
|
-
Add an option to write the encrypted file directly into dist/config when building a deployment bundle.
|
|
52
|
-
Provide an audit step: scan resulting repo for secret patterns before final commit/push.
|
|
53
|
-
|
|
54
36
|
6) Helpful CLI implementation details (tools & libs) 🛠️
|
|
55
|
-
Use Node + these packages: fs-extra (copy), replace-in-file or simple .replace() for template tokens, inquirer (prompt), execa (run commands), simple-git (git ops), node-fetch or @octokit/rest (optional GitHub repo creation).
|
|
56
|
-
Use safe file writes and atomic renames for config operations.
|
|
57
37
|
Provide a --dry-run and --preview mode so users can verify changes before creating repo or pushing.
|
|
58
38
|
|
|
59
39
|
7) Validation & post-creation steps ✅
|
|
60
40
|
npm run lint && npm test && npm run build — fail fast and show errors for the new site.
|
|
61
41
|
Optional: run npm run config:decrypt locally (with provided key) to confirm decryption works in your deploy workflow (BUT DO NOT store the key in the repo).
|
|
62
42
|
|
|
63
|
-
8) Example minimal CLI flow (pseudo)
|
|
64
|
-
Prompt: site name, package name, repo URL (optional), domain, author, contentful tokens, aws keys (optional), encrypt?
|
|
65
|
-
Copy template → replace tokens → create pixelated.config.json from pixelated.config.json.example → encrypt if requested → init git → run install/build → push to remote.
|
|
66
|
-
|
|
67
43
|
*/
|
|
68
44
|
|
|
69
45
|
import fs from 'fs/promises';
|
|
@@ -74,11 +50,16 @@ import readline from 'readline/promises';
|
|
|
74
50
|
import { stdin as input, stdout as output } from 'process';
|
|
75
51
|
import { fileURLToPath } from 'url';
|
|
76
52
|
import { loadManifest, findTemplateForSlug, pruneTemplateDirs, printAvailableTemplates } from './create-pixelated-app-template-mapper.js';
|
|
53
|
+
import { AmplifyClient, CreateAppCommand, CreateBranchCommand, UpdateAppCommand, UpdateBranchCommand } from '@aws-sdk/client-amplify';
|
|
77
54
|
|
|
78
55
|
const __filename = fileURLToPath(import.meta.url);
|
|
79
56
|
const __dirname = path.dirname(__filename);
|
|
80
57
|
|
|
58
|
+
let stepNumber = 1;
|
|
59
|
+
|
|
81
60
|
const exec = promisify(execCb);
|
|
61
|
+
// Exportable exec wrapper so tests can stub it.
|
|
62
|
+
export let _exec = exec;
|
|
82
63
|
|
|
83
64
|
async function exists(p) {
|
|
84
65
|
try {
|
|
@@ -232,21 +213,294 @@ export async function copyTemplateForPage(templatePathArg, templateSrc, template
|
|
|
232
213
|
}
|
|
233
214
|
|
|
234
215
|
|
|
216
|
+
export async function createAndPushRemote(destPath, siteName, defaultOwner) {
|
|
217
|
+
// Initialize a local git repo and make the initial commit
|
|
218
|
+
await _exec('git init -b main', { cwd: destPath });
|
|
219
|
+
await _exec('git add .', { cwd: destPath });
|
|
220
|
+
await _exec('git commit -m "chore: initial commit from pixelated-template"', { cwd: destPath });
|
|
221
|
+
console.log('✅ Git initialized and initial commit created.');
|
|
222
|
+
|
|
223
|
+
// If an encrypted config exists, attempt a non-fatal decrypt in the new site to ensure the token can be read
|
|
224
|
+
const encCandidates = [
|
|
225
|
+
path.join(destPath, 'src', 'app', 'config', 'pixelated.config.json.enc'),
|
|
226
|
+
path.join(destPath, 'src', 'config', 'pixelated.config.json.enc'),
|
|
227
|
+
path.join(destPath, 'src', 'pixelated.config.json.enc'),
|
|
228
|
+
path.join(destPath, 'pixelated.config.json.enc'),
|
|
229
|
+
path.join(destPath, 'dist', 'config', 'pixelated.config.json.enc')
|
|
230
|
+
];
|
|
231
|
+
for (const p of encCandidates) {
|
|
232
|
+
if (await exists(p)) {
|
|
233
|
+
console.log(`Found encrypted config at ${p}. Attempting to run 'npm run config:decrypt' in the new site (non-fatal)`);
|
|
234
|
+
try {
|
|
235
|
+
await _exec('npm run config:decrypt', { cwd: destPath, timeout: 60_000 });
|
|
236
|
+
console.log('Attempted config:decrypt (non-fatal)');
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.warn('config:decrypt failed or PIXELATED_CONFIG_KEY missing (non-fatal):', err?.message || err);
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Create a small temporary script inside the new site to reliably import the project's provider and print JSON to stdout
|
|
245
|
+
const tmpDir = path.join(destPath, '.px-scripts');
|
|
246
|
+
const tmpFile = path.join(tmpDir, 'get_github_token.ts');
|
|
247
|
+
await fs.mkdir(tmpDir, { recursive: true });
|
|
248
|
+
const tmpContent = `import('./src/components/config/config').then(m => {
|
|
249
|
+
const cfg = m.getFullPixelatedConfig();
|
|
250
|
+
// Only print the github object (or null) as JSON to stdout
|
|
251
|
+
console.log(JSON.stringify(cfg?.github || null));
|
|
252
|
+
}).catch(e => {
|
|
253
|
+
console.error('ERR_IMPORT', e?.message || e);
|
|
254
|
+
process.exit(2);
|
|
255
|
+
});`;
|
|
256
|
+
await fs.writeFile(tmpFile, tmpContent, 'utf8');
|
|
257
|
+
|
|
258
|
+
let execOut = null;
|
|
259
|
+
try {
|
|
260
|
+
execOut = await _exec(`npx tsx ${tmpFile}`, { cwd: destPath, timeout: 60_000 });
|
|
261
|
+
} catch (e) {
|
|
262
|
+
// Provide a helpful error message and ensure cleanup happens below
|
|
263
|
+
console.error('❌ Failed to run config provider to obtain GitHub token. Ensure PIXELATED_CONFIG_KEY is available (e.g., in .env.local) and the site includes an encrypted pixelated.config.json.enc');
|
|
264
|
+
throw e;
|
|
265
|
+
} finally {
|
|
266
|
+
// Always clean up the temporary script directory
|
|
267
|
+
try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore cleanup errors */ }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const outStr = (execOut && execOut.stdout) ? String(execOut.stdout).trim() : '';
|
|
271
|
+
if (!outStr) {
|
|
272
|
+
console.error('❌ No output from config provider; cannot locate github token');
|
|
273
|
+
throw new Error('Missing provider output');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let githubInfo = null;
|
|
277
|
+
try { githubInfo = JSON.parse(outStr); } catch (e) { console.error('❌ Invalid JSON from config provider:', outStr); throw e; }
|
|
278
|
+
const token = githubInfo?.token;
|
|
279
|
+
const cfgOwner = githubInfo?.defaultOwner;
|
|
280
|
+
if (!token) {
|
|
281
|
+
console.error('❌ github.token not found in decrypted config; cannot create remote repo.');
|
|
282
|
+
throw new Error('Missing github.token');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const repoName = siteName;
|
|
286
|
+
const ownerForMessage = cfgOwner || defaultOwner;
|
|
287
|
+
console.log(`Creating GitHub repo: ${ownerForMessage}/${repoName} ...`);
|
|
288
|
+
|
|
289
|
+
let resp;
|
|
290
|
+
try {
|
|
291
|
+
resp = await fetch('https://api.github.com/user/repos', {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
headers: {
|
|
294
|
+
'Authorization': `token ${token}`,
|
|
295
|
+
'Content-Type': 'application/json',
|
|
296
|
+
'User-Agent': 'create-pixelated-app'
|
|
297
|
+
},
|
|
298
|
+
body: JSON.stringify({ name: repoName, private: false })
|
|
299
|
+
});
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.error('❌ Failed to call GitHub API', e?.message || e);
|
|
302
|
+
throw e;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const body = await (async () => { try { return await resp.json(); } catch (e) { return null; } })();
|
|
306
|
+
if (!resp.ok) {
|
|
307
|
+
console.error(`❌ Failed to create GitHub repo: ${resp.status} ${resp.statusText} ${body?.message || ''}`);
|
|
308
|
+
throw new Error('GitHub repo creation failed');
|
|
309
|
+
}
|
|
310
|
+
const cloneUrl = body.clone_url;
|
|
311
|
+
if (!cloneUrl) {
|
|
312
|
+
console.error('❌ GitHub returned unexpected response (no clone_url)');
|
|
313
|
+
throw new Error('Invalid GitHub response');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Add remote and push using repo-name as remote
|
|
317
|
+
const remoteName = repoName;
|
|
318
|
+
await _exec(`git remote add ${remoteName} ${cloneUrl}`, { cwd: destPath });
|
|
319
|
+
await _exec('git branch --show-current || git branch -M main', { cwd: destPath });
|
|
320
|
+
try {
|
|
321
|
+
// If we have a github token available in the decrypted config, use it for an authenticated push (avoids relying on local credential helper)
|
|
322
|
+
if (token) {
|
|
323
|
+
await _exec(`git -c credential.helper= -c http.extraheader="Authorization: token ${token}" push -u ${remoteName} main`, { cwd: destPath });
|
|
324
|
+
await _exec('git branch -f dev main', { cwd: destPath });
|
|
325
|
+
await _exec(`git -c credential.helper= -c http.extraheader="Authorization: token ${token}" push -u ${remoteName} dev`, { cwd: destPath });
|
|
326
|
+
} else {
|
|
327
|
+
await _exec(`git push -u ${remoteName} main`, { cwd: destPath });
|
|
328
|
+
await _exec('git branch -f dev main', { cwd: destPath });
|
|
329
|
+
await _exec(`git push -u ${remoteName} dev`, { cwd: destPath });
|
|
330
|
+
}
|
|
331
|
+
console.log(`✅ Remote '${remoteName}' created and pushed (main, dev): ${cloneUrl}`);
|
|
332
|
+
// Return useful values for downstream steps (e.g., Amplify app creation)
|
|
333
|
+
return { cloneUrl, remoteName, token };
|
|
334
|
+
} catch (e) {
|
|
335
|
+
console.warn('⚠️ Failed to push branches automatically. The repo was created on GitHub, but you may need to push manually or configure your git credentials. Error:', e?.message || e);
|
|
336
|
+
// Still return partial info so caller can decide next steps
|
|
337
|
+
return { cloneUrl, remoteName, token };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Create an AWS Amplify app and connect repository branches (best-effort via AWS CLI).
|
|
342
|
+
// This uses the local AWS CLI configuration (credentials/profile) and optionally a GitHub
|
|
343
|
+
// personal access token to allow Amplify to connect to the repo automatically.
|
|
344
|
+
export async function createAmplifyApp(rl, siteName, cloneUrl, sitePath) {
|
|
345
|
+
// Use AWS region from components config if available; skip interactive region prompt
|
|
346
|
+
const componentsCfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
|
|
347
|
+
let regionToUse = 'us-east-2';
|
|
348
|
+
let creds = null;
|
|
349
|
+
try {
|
|
350
|
+
if (await exists(componentsCfgPath)) {
|
|
351
|
+
const cfgText = await fs.readFile(componentsCfgPath, 'utf8');
|
|
352
|
+
const cfg = JSON.parse(cfgText);
|
|
353
|
+
if (cfg?.aws?.region) {
|
|
354
|
+
regionToUse = cfg.aws.region;
|
|
355
|
+
console.log(`✅ Using AWS region from components config: ${regionToUse}`);
|
|
356
|
+
}
|
|
357
|
+
if (cfg?.aws?.access_key_id && cfg?.aws?.secret_access_key) {
|
|
358
|
+
creds = { accessKeyId: cfg.aws.access_key_id, secretAccessKey: cfg.aws.secret_access_key };
|
|
359
|
+
console.log('✅ Found AWS credentials in components config; they will be used for Amplify SDK operations.');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// ignore and continue without config values
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Prompt only for GitHub PAT (do not prompt for region)
|
|
367
|
+
const githubToken = (await rl.question('GitHub personal access token (PAT) to connect repo [leave blank to skip]: ')) || '';
|
|
368
|
+
|
|
369
|
+
console.log('Creating Amplify app (this may take a few seconds)...');
|
|
370
|
+
// Use AWS SDK Amplify client
|
|
371
|
+
const client = new AmplifyClient({ region: regionToUse, credentials: creds || undefined });
|
|
372
|
+
let createResp;
|
|
373
|
+
try {
|
|
374
|
+
createResp = await client.send(new CreateAppCommand({ name: siteName, platform: 'WEB_DYNAMIC', repository: cloneUrl || undefined, accessToken: githubToken || undefined }));
|
|
375
|
+
} catch (e) {
|
|
376
|
+
throw new Error('Failed to create Amplify app via SDK: ' + (e?.message || e));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const appId = createResp?.app?.appId || createResp?.appId || createResp?.id;
|
|
380
|
+
if (!appId) {
|
|
381
|
+
console.log('Amplify create app response:', createResp);
|
|
382
|
+
throw new Error('Unable to determine Amplify appId from SDK response');
|
|
383
|
+
}
|
|
384
|
+
console.log(`✅ Created Amplify app: ${appId}`);
|
|
385
|
+
console.log(`🔗 Open in console: https://${regionToUse}.console.aws.amazon.com/amplify/home?region=${regionToUse}#/d/${appId}`);
|
|
386
|
+
|
|
387
|
+
// Optionally: read site config (if sitePath provided) and set environment variables for the app & branches
|
|
388
|
+
let envVars = {};
|
|
389
|
+
if (sitePath) {
|
|
390
|
+
// Primary: look for a local .env.local and prefer variables defined there
|
|
391
|
+
const envLocalPath = path.join(sitePath, '.env.local');
|
|
392
|
+
if (await exists(envLocalPath)) {
|
|
393
|
+
try {
|
|
394
|
+
const envText = await fs.readFile(envLocalPath, 'utf8');
|
|
395
|
+
for (const line of envText.split(/\r?\n/)) {
|
|
396
|
+
const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*)$/);
|
|
397
|
+
if (!m) continue;
|
|
398
|
+
const k = m[1];
|
|
399
|
+
let v = m[2] || '';
|
|
400
|
+
// remove optional quotes
|
|
401
|
+
v = v.replace(/^"|"$/g, '');
|
|
402
|
+
if (k === 'PIXELATED_CONFIG_KEY' || k.startsWith('PIXELATED_CONFIG')) {
|
|
403
|
+
envVars[k] = v;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
console.log(`✅ Loaded environment values from ${envLocalPath} and will set matching Amplify environment variables.`);
|
|
407
|
+
} catch (e) {
|
|
408
|
+
console.warn('⚠️ Failed to read .env.local for env var population:', e?.message || e);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Secondary: if no PIXELATED_CONFIG_* vars were found in .env.local, look for pixelated.config.json
|
|
413
|
+
if (!envVars.PIXELATED_CONFIG_JSON && !envVars.PIXELATED_CONFIG_B64) {
|
|
414
|
+
const candidates = [
|
|
415
|
+
path.join(sitePath, 'src', 'app', 'config', 'pixelated.config.json'),
|
|
416
|
+
path.join(sitePath, 'src', 'config', 'pixelated.config.json'),
|
|
417
|
+
path.join(sitePath, 'src', 'pixelated.config.json'),
|
|
418
|
+
path.join(sitePath, 'pixelated.config.json')
|
|
419
|
+
];
|
|
420
|
+
for (const c of candidates) {
|
|
421
|
+
if (await exists(c)) {
|
|
422
|
+
try {
|
|
423
|
+
const raw = await fs.readFile(c, 'utf8');
|
|
424
|
+
envVars.PIXELATED_CONFIG_JSON = raw;
|
|
425
|
+
envVars.PIXELATED_CONFIG_B64 = Buffer.from(raw, 'utf8').toString('base64');
|
|
426
|
+
console.log(`✅ Loaded site pixelated.config.json from ${c} and will set PIXELATED_CONFIG_* environment variables in Amplify.`);
|
|
427
|
+
break;
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.warn('⚠️ Failed to read site config for env var population:', e?.message || e);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Attempt to resolve the ARN for the 'amplify-role' role (best-effort)
|
|
437
|
+
let iamRoleArn = null;
|
|
438
|
+
try {
|
|
439
|
+
const { IAMClient, GetRoleCommand } = await import('@aws-sdk/client-iam');
|
|
440
|
+
const iam = new IAMClient({ region: regionToUse, credentials: creds || undefined });
|
|
441
|
+
try {
|
|
442
|
+
const roleResp = await iam.send(new GetRoleCommand({ RoleName: 'amplify-role' }));
|
|
443
|
+
iamRoleArn = roleResp?.Role?.Arn;
|
|
444
|
+
if (iamRoleArn) console.log(`✅ Found amplify-role ARN: ${iamRoleArn}`);
|
|
445
|
+
} catch (e) {
|
|
446
|
+
// ignore; role may not exist or insufficient perms
|
|
447
|
+
console.warn('⚠️ Could not resolve amplify-role ARN; skipping automatic service-role assignment.');
|
|
448
|
+
}
|
|
449
|
+
} catch (e) {
|
|
450
|
+
// ignore if IAM client not available
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// If we have envVars or iamRoleArn, update the app and branches accordingly
|
|
454
|
+
if (Object.keys(envVars).length || iamRoleArn) {
|
|
455
|
+
try {
|
|
456
|
+
const updateParams = {};
|
|
457
|
+
if (Object.keys(envVars).length) updateParams.environmentVariables = envVars;
|
|
458
|
+
if (iamRoleArn) updateParams.iamServiceRoleArn = iamRoleArn;
|
|
459
|
+
await client.send(new UpdateAppCommand({ appId, ...updateParams }));
|
|
460
|
+
console.log('✅ Amplify app updated with environment variables and IAM service role (if available).');
|
|
461
|
+
|
|
462
|
+
for (const branch of ['dev','main']) {
|
|
463
|
+
try {
|
|
464
|
+
await client.send(new UpdateBranchCommand({ appId, branchName: branch, environmentVariables: envVars }));
|
|
465
|
+
console.log(`✅ Updated branch '${branch}' with environment variables.`);
|
|
466
|
+
} catch (e) {
|
|
467
|
+
console.warn(`⚠️ Failed to update branch ${branch} env vars:`, e?.message || e);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {
|
|
471
|
+
console.warn('⚠️ Failed to update Amplify app/branches with env vars or service role:', e?.message || e);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
console.log('ℹ️ Amplify app creation attempt finished. Verify the app in the AWS Console to ensure webhooks and branch connections are correct.');
|
|
476
|
+
}
|
|
477
|
+
|
|
235
478
|
async function main() {
|
|
236
479
|
const rl = readline.createInterface({ input, output });
|
|
237
480
|
try {
|
|
238
481
|
console.log('\n📦 Pixelated site creator — scaffold a new site from pixelated-template\n');
|
|
482
|
+
console.log('================================================================================\n');
|
|
483
|
+
|
|
239
484
|
|
|
485
|
+
// Prompt for basic site info
|
|
486
|
+
console.log(`\nStep ${stepNumber++}: Site Information`);
|
|
487
|
+
console.log('================================================================================\n');
|
|
240
488
|
const defaultName = 'my-site';
|
|
241
489
|
const siteName = (await rl.question(`Root directory name (kebab-case) [${defaultName}]: `)) || defaultName;
|
|
242
|
-
|
|
243
490
|
// Display name used in route titles: convert kebab to Title Case
|
|
244
491
|
const rootDisplayName = siteName.split(/[-_]+/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
|
|
245
492
|
|
|
493
|
+
|
|
246
494
|
// Additional site metadata for placeholder substitution
|
|
495
|
+
console.log(`\nStep ${stepNumber++}: Site Metadata`);
|
|
496
|
+
console.log('================================================================================\n');
|
|
247
497
|
const siteUrl = (await rl.question('Site URL (e.g. https://example.com) [leave blank to skip]: ')).trim();
|
|
248
498
|
const emailAddress = (await rl.question('Contact email address [leave blank to skip]: ')).trim();
|
|
249
499
|
|
|
500
|
+
|
|
501
|
+
// Create a copy of pixelated-template inside the current workspace
|
|
502
|
+
console.log(`\nStep ${stepNumber++}: Template Copy`);
|
|
503
|
+
console.log('================================================================================\n');
|
|
250
504
|
const workspaceRoot = path.resolve(__dirname, '..', '..', '..');
|
|
251
505
|
const templatePath = path.resolve(workspaceRoot, 'pixelated-template');
|
|
252
506
|
if (!(await exists(templatePath))) {
|
|
@@ -258,7 +512,6 @@ async function main() {
|
|
|
258
512
|
const manifest = await loadManifest(__dirname);
|
|
259
513
|
// Note: available templates will be printed later just before prompting for pages
|
|
260
514
|
|
|
261
|
-
|
|
262
515
|
// Destination is implicitly the top-level Git folder + site name to avoid prompting for it
|
|
263
516
|
const destPath = path.resolve(workspaceRoot, siteName);
|
|
264
517
|
console.log(`\nThe new site will be created at: ${destPath}`);
|
|
@@ -296,7 +549,10 @@ async function main() {
|
|
|
296
549
|
await fs.rm(gitDir, { recursive: true, force: true });
|
|
297
550
|
}
|
|
298
551
|
|
|
552
|
+
|
|
299
553
|
// Pages prompt: show available templates and ask which pages to create (comma-separated)
|
|
554
|
+
console.log(`\nStep ${stepNumber++}: Page Creation`);
|
|
555
|
+
console.log('================================================================================\n');
|
|
300
556
|
if (manifest && Array.isArray(manifest.templates) && manifest.templates.length) {
|
|
301
557
|
printAvailableTemplates(manifest);
|
|
302
558
|
}
|
|
@@ -411,7 +667,11 @@ async function main() {
|
|
|
411
667
|
console.log('Skipping page creation.');
|
|
412
668
|
}
|
|
413
669
|
}
|
|
670
|
+
|
|
671
|
+
|
|
414
672
|
// Automatically replace double-underscore template placeholders (e.g., __SITE_NAME__) with provided values
|
|
673
|
+
console.log(`\nStep ${stepNumber++}: Placeholder Tokens Replacement`);
|
|
674
|
+
console.log('================================================================================\n');
|
|
415
675
|
const replacements = {};
|
|
416
676
|
if (rootDisplayName) replacements.SITE_NAME = rootDisplayName;
|
|
417
677
|
if (siteUrl) replacements.SITE_URL = siteUrl;
|
|
@@ -435,24 +695,63 @@ async function main() {
|
|
|
435
695
|
console.warn('⚠️ Failed to replace placeholders in site copy:', e?.message || e);
|
|
436
696
|
}
|
|
437
697
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
// Prompt about creating a new GitHub repository. Default owner is read from components config `github.defaultOwner` (fallback: 'brianwhaley')
|
|
702
|
+
console.log(`\nStep ${stepNumber++}: GitHub Repository Creation`);
|
|
703
|
+
console.log('================================================================================\n');
|
|
704
|
+
const componentsCfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
|
|
705
|
+
let defaultOwner = 'brianwhaley';
|
|
706
|
+
try {
|
|
707
|
+
if (await exists(componentsCfgPath)) {
|
|
708
|
+
const compCfgText = await fs.readFile(componentsCfgPath, 'utf8');
|
|
709
|
+
const compCfg = JSON.parse(compCfgText);
|
|
710
|
+
if (compCfg?.github?.defaultOwner) defaultOwner = compCfg.github.defaultOwner;
|
|
711
|
+
}
|
|
712
|
+
} catch (e) {
|
|
713
|
+
// ignore and use fallback
|
|
714
|
+
}
|
|
715
|
+
const createRemoteAnswer = (await rl.question(`Create a new GitHub repository in '${defaultOwner}' and push the initial commit? (Y/n): `)) || 'y';
|
|
716
|
+
let remoteInfo = null;
|
|
717
|
+
if (createRemoteAnswer.toLowerCase() === 'y' || createRemoteAnswer.toLowerCase() === 'yes') {
|
|
441
718
|
try {
|
|
442
|
-
await
|
|
443
|
-
await exec('git add .', { cwd: destPath });
|
|
444
|
-
await exec('git commit -m "chore: initial commit from pixelated-template"', { cwd: destPath });
|
|
445
|
-
console.log('✅ Git initialized and initial commit created.');
|
|
719
|
+
remoteInfo = await createAndPushRemote(destPath, siteName, defaultOwner);
|
|
446
720
|
} catch (e) {
|
|
447
|
-
console.warn('⚠️
|
|
721
|
+
console.warn('⚠️ Repo creation or git push failed. Your local repository is still available at:', destPath);
|
|
722
|
+
console.warn(e?.stderr || e?.message || e);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// Optionally create an AWS Amplify app and connect branches (main, dev)
|
|
726
|
+
console.log(`\nStep ${stepNumber++}: AWS Amplify App Creation`);
|
|
727
|
+
console.log('================================================================================\n'); // Inform user what region will be used (config-backed)
|
|
728
|
+
try {
|
|
729
|
+
const cfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
|
|
730
|
+
if (await exists(cfgPath)) {
|
|
731
|
+
const cfgText = await fs.readFile(cfgPath, 'utf8');
|
|
732
|
+
const cfg = JSON.parse(cfgText);
|
|
733
|
+
if (cfg?.aws?.region) {
|
|
734
|
+
console.log(`ℹ️ Note: Amplify will use AWS region from config: ${cfg.aws.region}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} catch (e) {
|
|
738
|
+
// ignore errors reading config; nothing to do
|
|
739
|
+
} const createAmplifyAnswer = (await rl.question(`Create an AWS Amplify app for this repository and connect 'main' and 'dev' branches? (y/N): `)) || 'n';
|
|
740
|
+
if (createAmplifyAnswer.toLowerCase() === 'y' || createAmplifyAnswer.toLowerCase() === 'yes') {
|
|
741
|
+
try {
|
|
742
|
+
await createAmplifyApp(rl, siteName, remoteInfo?.cloneUrl);
|
|
743
|
+
} catch (e) {
|
|
744
|
+
console.warn('⚠️ Amplify app creation failed or was incomplete. You can create an app manually via the AWS Console or AWS CLI.');
|
|
745
|
+
console.warn(e?.stderr || e?.message || e);
|
|
448
746
|
}
|
|
449
747
|
}
|
|
450
748
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
console.log('
|
|
749
|
+
console.log('================================================================================\n');
|
|
750
|
+
console.log('🎉 Done.');
|
|
751
|
+
console.log('================================================================================\n');
|
|
752
|
+
console.log('Summary:');
|
|
454
753
|
console.log(` - Site copied to: ${destPath}`);
|
|
455
|
-
console.log('\nNote: A git remote was not set by this script. You can add one later with `git remote add
|
|
754
|
+
console.log('\nNote: A git remote was not set by this script. You can add one later with `git remote add <repo-name> <url>` (use the repository name as the remote) if desired.');
|
|
456
755
|
console.log('\nNext recommended steps (manual or to be automated in future):');
|
|
457
756
|
console.log(' - Update pixelated.config.json for this site and encrypt it with your config tool');
|
|
458
757
|
console.log(' - Run `npm run lint`, `npm test`, and `npm run build` inside the new site and fix any issues');
|
|
@@ -469,3 +768,4 @@ if (typeof process !== 'undefined' && new URL(import.meta.url).pathname === proc
|
|
|
469
768
|
// CLI entry point: run the interactive main flow
|
|
470
769
|
main();
|
|
471
770
|
}
|
|
771
|
+
|
|
@@ -6,8 +6,9 @@ import path from 'path';
|
|
|
6
6
|
* Enforces workspace standards for SEO, performance, and project structure.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
|
|
10
|
+
// DUPLICATE FROM components/general/utilities.ts --- KEEP IN SYNC ---
|
|
11
|
+
export const CLIENT_ONLY_PATTERNS = [
|
|
11
12
|
/\baddEventListener\b/,
|
|
12
13
|
/\bcreateContext\b/,
|
|
13
14
|
/\bdocument\./,
|
|
@@ -35,10 +36,11 @@ const CLIENT_ONLY_PATTERNS = [
|
|
|
35
36
|
/["']use client["']/ // Client directive
|
|
36
37
|
];
|
|
37
38
|
|
|
38
|
-
function isClientComponent(fileContent) {
|
|
39
|
+
export function isClientComponent(fileContent) {
|
|
39
40
|
return CLIENT_ONLY_PATTERNS.some(pattern => pattern.test(fileContent));
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
|
|
42
44
|
const propTypesInferPropsRule = {
|
|
43
45
|
meta: {
|
|
44
46
|
type: 'problem',
|
|
@@ -372,6 +374,51 @@ const noRawImgRule = {
|
|
|
372
374
|
},
|
|
373
375
|
};
|
|
374
376
|
|
|
377
|
+
/* ===== RULE: require-section-ids ===== */
|
|
378
|
+
const requireSectionIdsRule = {
|
|
379
|
+
meta: {
|
|
380
|
+
type: 'suggestion',
|
|
381
|
+
docs: {
|
|
382
|
+
description: 'Require `id` attributes on every <section> and <PageSection> for jump links and SEO',
|
|
383
|
+
category: 'Accessibility',
|
|
384
|
+
recommended: false,
|
|
385
|
+
},
|
|
386
|
+
messages: {
|
|
387
|
+
missingId: '`section` and `PageSection` elements must have an `id` attribute for jump-link support and SEO hierarchy.',
|
|
388
|
+
},
|
|
389
|
+
schema: [],
|
|
390
|
+
},
|
|
391
|
+
create(context) {
|
|
392
|
+
/*
|
|
393
|
+
* Helper: get a string name for a JSX element. Supports
|
|
394
|
+
* `JSXIdentifier` and `JSXMemberExpression` (e.g. `UI.PageSection`).
|
|
395
|
+
*/
|
|
396
|
+
function getJSXElementName(node) {
|
|
397
|
+
if (!node) return null;
|
|
398
|
+
if (node.type === 'JSXIdentifier') return node.name;
|
|
399
|
+
if (node.type === 'JSXMemberExpression') return node.property?.name || null;
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
JSXOpeningElement(node) {
|
|
405
|
+
try {
|
|
406
|
+
const name = getJSXElementName(node.name); if (!name || !['section','PageSection'].includes(name)) return;
|
|
407
|
+
|
|
408
|
+
const hasId = (node.attributes || []).some(attr => (
|
|
409
|
+
attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'id' && attr.value != null
|
|
410
|
+
));
|
|
411
|
+
if (!hasId) {
|
|
412
|
+
context.report({ node, messageId: 'missingId' });
|
|
413
|
+
}
|
|
414
|
+
} catch (e) {
|
|
415
|
+
// defensive: don't crash lint
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
375
422
|
const requiredFaqRule = {
|
|
376
423
|
meta: {
|
|
377
424
|
type: 'suggestion',
|
|
@@ -423,6 +470,7 @@ export default {
|
|
|
423
470
|
'required-schemas': requiredSchemasRule,
|
|
424
471
|
'required-files': requiredFilesRule,
|
|
425
472
|
'no-raw-img': noRawImgRule,
|
|
473
|
+
'require-section-ids': requireSectionIdsRule,
|
|
426
474
|
'required-faq': requiredFaqRule,
|
|
427
475
|
},
|
|
428
476
|
configs: {
|
|
@@ -432,6 +480,7 @@ export default {
|
|
|
432
480
|
'pixelated/required-schemas': 'warn',
|
|
433
481
|
'pixelated/required-files': 'warn',
|
|
434
482
|
'pixelated/no-raw-img': 'warn',
|
|
483
|
+
'pixelated/require-section-ids': 'warn',
|
|
435
484
|
'pixelated/required-faq': 'warn',
|
|
436
485
|
},
|
|
437
486
|
},
|
package/dist/scripts/release.sh
CHANGED
|
@@ -38,20 +38,58 @@ fi
|
|
|
38
38
|
echo ""
|
|
39
39
|
echo "🔑 Step $((STEP_COUNT++)): Choose the git Remote to release:"
|
|
40
40
|
echo "================================================="
|
|
41
|
-
#
|
|
41
|
+
# Helper: derive a sensible default remote (remote whose repo name matches local folder)
|
|
42
|
+
derive_default_remote() {
|
|
43
|
+
local remotes=($(git remote))
|
|
44
|
+
local local_repo
|
|
45
|
+
local_repo=$(basename "$(git rev-parse --show-toplevel)")
|
|
46
|
+
for remote in "${remotes[@]}"; do
|
|
47
|
+
url=$(git remote get-url "$remote" 2>/dev/null || true)
|
|
48
|
+
repo=$(basename -s .git "${url##*/}")
|
|
49
|
+
if [ -n "$repo" ] && [ "$repo" = "$local_repo" ]; then
|
|
50
|
+
echo "$remote"
|
|
51
|
+
return
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
# Fallback to first remote if no match
|
|
55
|
+
echo "${remotes[0]}"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Function to prompt for remote selection, showing a default derived from local repo name
|
|
42
59
|
prompt_remote_selection() {
|
|
43
60
|
echo "Available git remotes:" >&2
|
|
44
61
|
local remotes=($(git remote))
|
|
62
|
+
local count=${#remotes[@]}
|
|
45
63
|
local i=1
|
|
64
|
+
|
|
46
65
|
for remote in "${remotes[@]}"; do
|
|
47
66
|
echo "$i) $remote" >&2
|
|
48
67
|
((i++))
|
|
49
68
|
done
|
|
69
|
+
|
|
70
|
+
local default_remote
|
|
71
|
+
default_remote=$(derive_default_remote)
|
|
72
|
+
# find index of default_remote for user prompt
|
|
73
|
+
local default_index=1
|
|
74
|
+
for idx in "${!remotes[@]}"; do
|
|
75
|
+
if [ "${remotes[$idx]}" = "$default_remote" ]; then
|
|
76
|
+
default_index=$((idx+1))
|
|
77
|
+
break
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
local prompt="Select remote to use (1-$count) [default $default_index - $default_remote]: "
|
|
50
82
|
local choice
|
|
51
|
-
read -p "
|
|
83
|
+
read -p "$prompt" choice >&2
|
|
84
|
+
|
|
85
|
+
if [ -z "$choice" ]; then
|
|
86
|
+
echo "$default_remote"
|
|
87
|
+
return
|
|
88
|
+
fi
|
|
89
|
+
|
|
52
90
|
case $choice in
|
|
53
91
|
[1-9]|[1-9][0-9])
|
|
54
|
-
if [ "$choice" -le "$
|
|
92
|
+
if [ "$choice" -le "$count" ]; then
|
|
55
93
|
echo "${remotes[$((choice-1))]}"
|
|
56
94
|
else
|
|
57
95
|
echo "${remotes[0]}" # Default to first if invalid
|
|
@@ -213,7 +251,13 @@ prompt_version_type() {
|
|
|
213
251
|
echo "3) major (1.x.x)" >&2
|
|
214
252
|
echo "4) custom version" >&2
|
|
215
253
|
echo "5) no version bump" >&2
|
|
216
|
-
read -p "Enter choice (1-5): " choice >&2
|
|
254
|
+
read -p "Enter choice (1-5) [default 1]: " choice >&2
|
|
255
|
+
|
|
256
|
+
# Default to 1 (patch) when user presses Enter
|
|
257
|
+
if [ -z "$choice" ]; then
|
|
258
|
+
choice=1
|
|
259
|
+
fi
|
|
260
|
+
|
|
217
261
|
case $choice in
|
|
218
262
|
1) version_type="patch" ;;
|
|
219
263
|
2) version_type="minor" ;;
|
|
@@ -223,7 +267,7 @@ prompt_version_type() {
|
|
|
223
267
|
version_type="$custom_version"
|
|
224
268
|
;;
|
|
225
269
|
5) version_type="none" ;;
|
|
226
|
-
*) version_type="patch" ;; # default
|
|
270
|
+
*) version_type="patch" ;; # fallback default
|
|
227
271
|
esac
|
|
228
272
|
}
|
|
229
273
|
prompt_version_type
|