@pixelated-tech/components 3.9.16 ā 3.9.18
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/general/accordion.css +4 -4
- package/dist/components/general/faq-accordion.js +4 -2
- 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/create-pixelated-app.js +217 -35
- package/dist/scripts/pixelated-eslint-plugin.js +116 -18
- 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/general/faq-accordion.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 +6 -1
- 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/required-faq.rule.test.d.ts +2 -0
- package/dist/types/tests/required-faq.rule.test.d.ts.map +1 -0
- package/package.json +9 -6
- package/dist/scripts/create-pixelated-app.js.bak +0 -590
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
.accordion-title {
|
|
18
18
|
cursor: pointer;
|
|
19
19
|
padding: 1rem;
|
|
20
|
-
font-size: 1rem;
|
|
21
|
-
font-weight: 600;
|
|
20
|
+
/* font-size: 1rem;
|
|
21
|
+
font-weight: 600; */
|
|
22
22
|
color: #1a202c;
|
|
23
23
|
background: none;
|
|
24
24
|
border: none;
|
|
@@ -62,8 +62,8 @@ details[open] .accordion-title::before {
|
|
|
62
62
|
|
|
63
63
|
.accordion-title h3 {
|
|
64
64
|
margin: 0;
|
|
65
|
-
font-size: inherit;
|
|
66
|
-
font-weight: inherit;
|
|
65
|
+
/* font-size: inherit;
|
|
66
|
+
font-weight: inherit; */
|
|
67
67
|
color: inherit;
|
|
68
68
|
flex: 1;
|
|
69
69
|
}
|
|
@@ -10,7 +10,8 @@ const categoryIcons = {
|
|
|
10
10
|
'Content & Management': 'š',
|
|
11
11
|
'Support & Maintenance': 'š ļø',
|
|
12
12
|
'Ownership & Legal': 'š',
|
|
13
|
-
'Services': 'š¼'
|
|
13
|
+
'Services': 'š¼',
|
|
14
|
+
'': ''
|
|
14
15
|
};
|
|
15
16
|
FAQAccordion.propTypes = {
|
|
16
17
|
faqsData: PropTypes.shape({
|
|
@@ -55,7 +56,8 @@ export function FAQAccordion({ faqsData }) {
|
|
|
55
56
|
const accordionItems = filteredFaqs.map((faq, index) => {
|
|
56
57
|
const content = Array.isArray(faq.acceptedAnswer.text) ? (_jsx("div", { children: faq.acceptedAnswer.text.map((paragraph, pIndex) => (_jsx("p", { dangerouslySetInnerHTML: { __html: paragraph } }, pIndex))) })) : (_jsx("div", { dangerouslySetInnerHTML: { __html: faq.acceptedAnswer.text } }));
|
|
57
58
|
return {
|
|
58
|
-
title: `${categoryIcons[faq.category] || 'ā'} ${faq.name}`,
|
|
59
|
+
/* title: `${categoryIcons[faq.category as CategoryKey] || 'ā'} ${faq.name}`, */
|
|
60
|
+
title: `${categoryIcons[faq.category] || ''} ${faq.name}`,
|
|
59
61
|
content,
|
|
60
62
|
open: expandedStates[index] || undefined
|
|
61
63
|
};
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
margin: 10px;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
.tile-image.clickable {
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
.tile-image img {
|
|
23
27
|
width: 100%;
|
|
24
28
|
height: 100% !important;
|
|
@@ -44,6 +48,12 @@
|
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
.tile-image-overlay,
|
|
52
|
+
.tile-image-overlay * {
|
|
53
|
+
pointer-events: none;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
.tile-image:hover .tile-image-overlay {
|
|
48
58
|
visibility: visible;
|
|
49
59
|
width: 100% !important;
|
|
@@ -67,7 +77,7 @@
|
|
|
67
77
|
transition-delay: 0.25s;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
.tile-image-overlay-title{
|
|
80
|
+
.tile-image-overlay-title {
|
|
71
81
|
margin-bottom: 10px;
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -9,11 +9,12 @@ import "./tiles.css";
|
|
|
9
9
|
Tiles.propTypes = {
|
|
10
10
|
cards: PropTypes.array.isRequired,
|
|
11
11
|
rowCount: PropTypes.number,
|
|
12
|
+
imgClick: PropTypes.func,
|
|
12
13
|
};
|
|
13
14
|
export function Tiles(props) {
|
|
14
15
|
const rowCount = props.rowCount ?? 2;
|
|
15
16
|
if (props.cards && props.cards.length > 0) {
|
|
16
|
-
return (_jsx("div", { className: "tiles-container", children: _jsx("div", { className: `tile-container row-${rowCount}col`, children: props.cards.map((card, i) => (_jsx("div", { className: "gridItem", children: _jsx(Tile, { index: i, cardLength: props.cards.length, link: card.link, image: card.image, imageAlt: card.imageAlt, bodyText: card.bodyText }) }, i))) }) }));
|
|
17
|
+
return (_jsx("div", { className: "tiles-container", children: _jsx("div", { className: `tile-container row-${rowCount}col`, children: props.cards.map((card, i) => (_jsx("div", { className: "gridItem", children: _jsx(Tile, { index: i, cardLength: props.cards.length, link: card.link, image: card.image, imageAlt: card.imageAlt, bodyText: card.bodyText, imgClick: props.imgClick }) }, i))) }) }));
|
|
17
18
|
}
|
|
18
19
|
else {
|
|
19
20
|
return (_jsx(Loading, {}));
|
|
@@ -27,10 +28,12 @@ Tile.propTypes = {
|
|
|
27
28
|
image: PropTypes.string.isRequired,
|
|
28
29
|
imageAlt: PropTypes.string,
|
|
29
30
|
bodyText: PropTypes.string,
|
|
31
|
+
imgClick: PropTypes.func,
|
|
30
32
|
};
|
|
31
33
|
function Tile(props) {
|
|
32
34
|
const config = usePixelatedConfig();
|
|
33
|
-
const
|
|
35
|
+
const imgClick = props.imgClick;
|
|
36
|
+
const tileBody = _jsxs("div", { className: "tile-image" + (imgClick ? " clickable" : ""), children: [_jsx(SmartImage, { src: props.image, title: props?.imageAlt ?? undefined, alt: props?.imageAlt ?? "", onClick: imgClick ? (event) => imgClick(event, props.image) : undefined, cloudinaryEnv: config?.cloudinary?.product_env ?? undefined }), _jsx("div", { className: "tile-image-overlay", children: _jsxs("div", { className: "tile-image-overlay-text", children: [_jsx("div", { className: "tile-image-overlay-title", children: props.imageAlt }), _jsx("div", { className: "tile-image-overlay-body", children: props.bodyText })] }) })] });
|
|
34
37
|
return (_jsx("div", { className: "tile", id: 'tile-' + props.index, children: props.link ?
|
|
35
38
|
_jsx("a", { href: props.link, className: "tileLink", children: tileBody })
|
|
36
39
|
:
|
|
@@ -151,14 +151,6 @@ export const CLIENT_ONLY_PATTERNS = [
|
|
|
151
151
|
/\bwindow\./,
|
|
152
152
|
/["']use client["']/ // Client directive
|
|
153
153
|
];
|
|
154
|
-
/**
|
|
155
|
-
* Determines if a component file contains client-only code that requires browser execution
|
|
156
|
-
* @param fileContent - The source code content of the file
|
|
157
|
-
* @returns true if the file contains client-only patterns
|
|
158
|
-
*/
|
|
159
|
-
export function isClientComponent(fileContent) {
|
|
160
|
-
return CLIENT_ONLY_PATTERNS.some(pattern => pattern.test(fileContent));
|
|
161
|
-
}
|
|
162
154
|
/* ===== COMPONENT FILE DETECTION ===== */
|
|
163
155
|
/**
|
|
164
156
|
* Glob patterns for finding component files
|
|
@@ -1 +1 @@
|
|
|
1
|
-
pxl:v1:
|
|
1
|
+
pxl:v1:e782f45daa51bb928539fac3:115a6b53f654fd4fe26a72c37a21cdea:16c14158f160ce9e53070dd7776d016d98f1355027a0b150c66e85da0f88422dd7770d25f1cd03f90d52d0743d12df1df936ca7266b12da0a5e9e872825985cb3da3985ed65e9d95509b653c5f87632d0847bf417e1b7fb36ec973964d912178f9acb8fa8f63bd9df93196f73521971dfe2c3cbc69f9fcaab1ac29c1c40ba802ff7bfed9a880efb099920eefac4f16bb01de23bf1fe57aff4597eb3bc4fee3e7e0715bf81f653ac06e2d7c7835b775176e61dcca434ae3da1f1983226513ae9e2b4f70704e3387981904ebcab67bb4034b573ac28c331b8adb86025672351c0fde64380b80f16669ff5acf5293623cf87ae06bcefbf65046776c582fc87803b777f0cc4edef5f3eeac96ea41e9daee0ba16e62e581487e899ed8101dd5f0f043b32c128578cd4e2613d690ce6a290842d42221cbb0ea91ded435314b188589329c09ff3602e3bfaa473975b5cf8833de0729cac2129a3b54206ca02de526e28e931d0fefb9fcf006cef8b2ce7ee8b9a558289f95029a92ea918b1f9cde77f163460d65a4c68b96ccd46778acf22075dd0b60840c3a9abc86b5e84f9daef1583388102acf2262ae9ff441ac95c0d49b069e67c5b1d3d700d89c305e6f6e24af2e06e623101515cde459564d2217c4d5b5bfe4b9a0c426306b0c354f51b7556be2d3bf7b7fcc36dc97697aee564bb8c75f9f8a7a279230902512d503309e1f072550cbbdfb40eeaaab2fbde2951ca4b6960fb9b2afae78ef00d03bb5a1e90d11684cb30b0abe79615e263872b88fdfe1f02aaa6bcc4d12e8d3bb0ef68be0f4682c745f838cf2fd5859aaae268d75a21b0bc89b8e63db17aed45bf7fe86c85a1f86ed7b95007821aeac7fcce94e1c4bbeabde111b43edd4a70f6332cd88ac6651ca033bedcab40eb635aa54fb748811ef304c574b1c15db0ebedb8d21fc1a7200a83b5960567c8a65c325a49c8a5ebeab063c95d94ce6e0625a77fe42d98d1afe1bb6c5f98b92d41487f20fcb771c2b9850514f6849c2849f23c4e845795efec2f7502101b1e6870eae0a7b0ee536d5e8589b87e9a1da0c5805c4be9fe3c91697267946bd3540a110da410f1b350b0b29f51ff7f06c2776d519e9a7aa94024324d6fe3ff4b84c8b5a2b7132e9fcd01a0d775d0553ad9b053eac28f98e255d98dfb75c6dbe995f8da1738245d94b5476d2f2933cc214fc1cca3c4246cff28c56843cca16c63bd8b95c161df6b1e2fbad299f9e010e6781d2e003cc472a0c030e07168b23abe475ef081269c56d8e8bd748a82e496ec5e21e3d62301b2956af2f30927f4cf52a3a83a00576e5a81a91b808f547b4c2c7472cf6933873b1941f64782e2447d4807b751777b38b9c84114ce1a29874ec3683dba66c2a9b4498acfd96d361c14b39135e98342834ece9dee616aa774594d8cfc755886fb7c737cab68c5577515866f10f886da7f92f0ac6b6c4c7cf63d74fd6291134e84950dbf1c00b250d5a13e56763b79abfe2b66c01e5fdce64d15fee6cde069bd2cc1570f4201fde7b9bfc53f9b9ca8a64e85a261e23356548150043180fcb8466dea7b9711d89f147fb718d93ed8177b5f59b6510ec747728f580597f36a39a891f25bd35c55aba9d7cb0e9644d1b6c428d8a7110ee330cce1c28ce8cc8cebd638f9f5101a87e060a1fb903a934a4c9d87eb7d47f5d3172c19f05fadb52291a2b54d2861341066217b2976878597caa0f411cc269669127f4c4316b594085ee9422f4124bde8856455b58784ad74d7b2ec8e4ca886ce01fc5c35585245da981e5a3f51a6a640d77fc5fd547b74d1047ae05d91c62f533f7ca8c46f71a4dd9634b9f7a2248a85a0133afeb4eb5170952dcd9c35d5756872e523d84ad6401a0f294239b1327cee4bb0a8b31f40a6ad0a7b7abd3217364ee197b37a4ac8411da33dd0dd9040247517de7ac2c25af805f14bef2856ee0418b2d99a6359b77493dce9c2bfdc1c10c9af103a801a58395b5b4b9f82934bfeda0dfefd5c12f7d946ffaf55d0f556ee3f76dfd0ca3ac7ca3faa5b263cc6c79771353e81a36ce25cc083764f7e2e949f65f7e00d656037c277065d94aa1614f8d3f65b59f004995903d03c8f674d26303704eb98b7f87681a5485a52f0a9ea3d1192bcc57b873b67303793f4a83bae61b166574bb4e85969a50157c031a25d0890fe2df943071db1993d848df88ef98a0102104c61b8d2d6354b8b1c0e87e417aec9b7a3ae01f9130df7c1654517b3d169613c1dbe94b8a75fa1eccb085f0292d312625039947c04cede0591575b1c81ff4d7fbb6183fed8b954af014eb77500ecfef90dd30c366f297a8a5ffa8ec84e24af2d22db51f0b7bc2444e72766401c0565fcf7e6d20274959895c25419faa3d533062f410d6fda5b682a231c3edcd905b05021dcf74c5d667a07a4873f82963c0696b4db4e4867969681278e03044ec5ecd73ee95801fd1965287c0fd78df966a82c9bc62601ab50753b3992d935ee1d76e639b84ffa811fc48535e001130432b758c64842b1ffb9d7b6fe28cfb25a14d6ba086621800cae5613583449e77d036f2b2a2748adb48a3ca1634ae4d9888c984eba30165f7b2653a53173d3166494b20ec74ce98bcb03343d9d2bd70d805a00667987a1f11e64d76f8c41f38f0a1c70514f2455ee11202cf55c31f5abc88690437daf3a761850c5cf91fb91d3c9424e18c3932703326029c03f8c18969b5a59cec8fd755b66ee972dee5c90637b8cadbb1319fdba26e1f783c12eb79d290aebb84b24b274f99f9e7b1a106dd947044d7dd9f5047f45cdeacfea3d419fd3530563bea5969543aed03e2eca3a7186f9ad461601644b475f063f0aa35797709da3dfbd13bf51b4bd47b5dd6701efa7637bda9002ec10a1235c08f8e5b4ca2c63acaff2cdfc4528ca9813117786ac776269f2d590bc3ed0537c547c6083ef306a80b7197bbe514c4f6b672cee07516af002146f6580162463a2591b61cb514c2c299fecc13cd78961a8768283aad580f61107e07caf0742c4c0d7472cd851fd14ee12fbde0e665aa0ab62723b67de7bc3e83a5177b9d47d67504cc2604617a85eeb58c414958483443cf861a326fd7cc6364de17ff220fd9fcf75e9615ae7b4127bbf0af7562c6a2b3603cb81f9e6185b68e830592b1b332828648aa6dfcfb689343ac29941ddc71b36dcfda990bd2ea168f653c3789881de424503a658701acd9eb15fffe9eb9673e7a9fee71553526157bf19b9dc463e0ea599333a4cac639d6479580576fdb2875701461ad663fdc65d4e81fe26c3929d1fd1bbfffe1d8dcc37d801f83fab3f83a047945df7b22d05d393148ffb4a78cf642d0c64d1d2c075905ee8846f7307257c826fd19da00dd1ff2fd1015b831af7bef16baf067532087a83fbf24125085cc066d465cf1a16517575335889e5c676e808491b5c7bcf0428aafa0c1f9ac0f383ca192fe294591a82d839d4ea6106f07510a479ac3927383f9e0322d2c8cfe145a6504f89f541d4868ab4ceeb2c4ed464e481f047399f7d2d2901762abb86c05d4d40abf912606e173df0cb8ec9a95c5af9db8e78d70e40d5f916372fbf1e0237564ea222e126ad54c756641c8e9bd962b5cf7634ea60b80462562b10fdc7ef7f7164084db690ec60c13f02deea52a8d67990a44abc866cd706e8caf11102c1c15a694e7d31ffd7259f174f748c8d58214d52fcddc7cefa5d0d4ed6bd9c0fe0dd9564430b2064b98af41aebec95055bed3806a1c1e5e43b02d9b5d9488520efdee2439795ef3ad515d6cb7c77712c17191d1034f194666495c1fcc041d673db059c61f15ce49b2dab93653f54cb26ba66b60705c464a0ae30d089d1aa6977f7f95e960234a50ca0f879d87ccac777749fcfd282bebc511469485ad5b062fdf8c0d41e8e74ac8c6fcc76ecf0c370bfa5b01eed1d2d84d0d799609b671a0faa2af74c0833de5cff09826a82e3906b2ef0b135d3a19ebfe86c1869d97b36a0e17d9c8a36308486c2f18e2f37d209b93c06b740e8708318a63535e81bbf041a85efc60fa3806d844da736e869cd085ed22e3e39b31948cb360918393bf910b3691c2758b8220dca7b18852bf4b62e957f617a8ef176e5b4d3749ab80bfe9d7ebf6e5079d96ec972a3be7c1e9b81c0aa40fdcc48e59fe281f0d5afef13f981ac48f7e2ec5dd17b598225130998f0ac0740796ca7bdac8aca59bab8d8574c904dfb40b3a7ebca11ed658024992c894b548db90ab0ee64f76e9efaad5965325d38fd69b14d6dcc66a1e14b301e5da5e1bd6ad85e3c3170b22c0d1e723a5c087047e8257fd2315da98eb13116f7ca043c3f81aa67a86bb80cd679191ca557ed700c5fa11ba75e038fb9f44c42b82c73fe3faefb4664419663f9166c3cc729274c89b6bc86888a7ccb9cf7509e8cdb62b5b82c037cd37299bff061a5523da8357c0f0415c6930adadca35f866577e0a00f5bc20ae53eabf8c37d8c95b8e54a905f8e8bfd6b73d601fcde08e72e99abedf123f00570d3790660ae8110abff5d0f14b24da298fc7a42698d89679a62c3321d7b9d437b3a17d2880953a226123c37645d2505fa77433fd475d909c2252f83e32249066c5a902440aaf7397d4995051c72c64c395145259b68cfdefed47a8f8721e4816f6cd03e76b3c55b45028b4e740e5c5f3861dd981344aad041777e46e5d78619b66a64b67be410e57989b2f95dd8cdd9f56533e03bc7257dd243998579263ea00f69a8770d6984b173941f617327835cbfb150c0adf64b4143e23e33d451d0f3aaa5bdaa674fa140a2354672c62f0a27fdaee5f67887c13a1b04afb3d07ff5946fbebd62b62052c27a7c133e406b22f738efaa23af3a8aa7f6b3f6e78c61cb7fe42585395822f3f9a42f57e9ed93700c4679c2538d23e2df32a45918d39c3580c406cba0f3c1e96d08e207c3be5193d3a7a7e865ada35d0b19fab5625ca48f737e205d52afed1c6140c703bb3ad80e73d69da475222efee5f92aa98a6f590da6c666f81e65e6bc94c6122a7c98ca8891f5fc36d240de206a01933291a30e6c6bf34aa769ab9594a54f021b9f84ec46f403c324b50f87cc9540d11bb0a6e2a02d8c1fc3c0f36377d73530a61f5e165af302ea1ea24cd4c69e203cd91d8d63dc08a9804072701
|
|
@@ -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,10 +50,13 @@ 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);
|
|
82
61
|
// Exportable exec wrapper so tests can stub it.
|
|
83
62
|
export let _exec = exec;
|
|
@@ -334,27 +313,194 @@ export async function createAndPushRemote(destPath, siteName, defaultOwner) {
|
|
|
334
313
|
throw new Error('Invalid GitHub response');
|
|
335
314
|
}
|
|
336
315
|
|
|
337
|
-
// Add remote and push
|
|
338
|
-
|
|
316
|
+
// Add remote and push using repo-name as remote
|
|
317
|
+
const remoteName = repoName;
|
|
318
|
+
await _exec(`git remote add ${remoteName} ${cloneUrl}`, { cwd: destPath });
|
|
339
319
|
await _exec('git branch --show-current || git branch -M main', { cwd: destPath });
|
|
340
|
-
|
|
341
|
-
|
|
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.');
|
|
342
476
|
}
|
|
477
|
+
|
|
343
478
|
async function main() {
|
|
344
479
|
const rl = readline.createInterface({ input, output });
|
|
345
480
|
try {
|
|
346
481
|
console.log('\nš¦ Pixelated site creator ā scaffold a new site from pixelated-template\n');
|
|
482
|
+
console.log('================================================================================\n');
|
|
347
483
|
|
|
484
|
+
|
|
485
|
+
// Prompt for basic site info
|
|
486
|
+
console.log(`\nStep ${stepNumber++}: Site Information`);
|
|
487
|
+
console.log('================================================================================\n');
|
|
348
488
|
const defaultName = 'my-site';
|
|
349
489
|
const siteName = (await rl.question(`Root directory name (kebab-case) [${defaultName}]: `)) || defaultName;
|
|
350
|
-
|
|
351
490
|
// Display name used in route titles: convert kebab to Title Case
|
|
352
491
|
const rootDisplayName = siteName.split(/[-_]+/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
|
|
353
492
|
|
|
493
|
+
|
|
354
494
|
// Additional site metadata for placeholder substitution
|
|
495
|
+
console.log(`\nStep ${stepNumber++}: Site Metadata`);
|
|
496
|
+
console.log('================================================================================\n');
|
|
355
497
|
const siteUrl = (await rl.question('Site URL (e.g. https://example.com) [leave blank to skip]: ')).trim();
|
|
356
498
|
const emailAddress = (await rl.question('Contact email address [leave blank to skip]: ')).trim();
|
|
357
499
|
|
|
500
|
+
|
|
501
|
+
// Create a copy of pixelated-template inside the current workspace
|
|
502
|
+
console.log(`\nStep ${stepNumber++}: Template Copy`);
|
|
503
|
+
console.log('================================================================================\n');
|
|
358
504
|
const workspaceRoot = path.resolve(__dirname, '..', '..', '..');
|
|
359
505
|
const templatePath = path.resolve(workspaceRoot, 'pixelated-template');
|
|
360
506
|
if (!(await exists(templatePath))) {
|
|
@@ -366,7 +512,6 @@ async function main() {
|
|
|
366
512
|
const manifest = await loadManifest(__dirname);
|
|
367
513
|
// Note: available templates will be printed later just before prompting for pages
|
|
368
514
|
|
|
369
|
-
|
|
370
515
|
// Destination is implicitly the top-level Git folder + site name to avoid prompting for it
|
|
371
516
|
const destPath = path.resolve(workspaceRoot, siteName);
|
|
372
517
|
console.log(`\nThe new site will be created at: ${destPath}`);
|
|
@@ -404,7 +549,10 @@ async function main() {
|
|
|
404
549
|
await fs.rm(gitDir, { recursive: true, force: true });
|
|
405
550
|
}
|
|
406
551
|
|
|
552
|
+
|
|
407
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');
|
|
408
556
|
if (manifest && Array.isArray(manifest.templates) && manifest.templates.length) {
|
|
409
557
|
printAvailableTemplates(manifest);
|
|
410
558
|
}
|
|
@@ -519,7 +667,11 @@ async function main() {
|
|
|
519
667
|
console.log('Skipping page creation.');
|
|
520
668
|
}
|
|
521
669
|
}
|
|
670
|
+
|
|
671
|
+
|
|
522
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');
|
|
523
675
|
const replacements = {};
|
|
524
676
|
if (rootDisplayName) replacements.SITE_NAME = rootDisplayName;
|
|
525
677
|
if (siteUrl) replacements.SITE_URL = siteUrl;
|
|
@@ -543,7 +695,12 @@ async function main() {
|
|
|
543
695
|
console.warn('ā ļø Failed to replace placeholders in site copy:', e?.message || e);
|
|
544
696
|
}
|
|
545
697
|
}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
|
|
546
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');
|
|
547
704
|
const componentsCfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
|
|
548
705
|
let defaultOwner = 'brianwhaley';
|
|
549
706
|
try {
|
|
@@ -556,20 +713,45 @@ async function main() {
|
|
|
556
713
|
// ignore and use fallback
|
|
557
714
|
}
|
|
558
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;
|
|
559
717
|
if (createRemoteAnswer.toLowerCase() === 'y' || createRemoteAnswer.toLowerCase() === 'yes') {
|
|
560
718
|
try {
|
|
561
|
-
await createAndPushRemote(destPath, siteName, defaultOwner);
|
|
719
|
+
remoteInfo = await createAndPushRemote(destPath, siteName, defaultOwner);
|
|
562
720
|
} catch (e) {
|
|
563
721
|
console.warn('ā ļø Repo creation or git push failed. Your local repository is still available at:', destPath);
|
|
564
722
|
console.warn(e?.stderr || e?.message || e);
|
|
565
723
|
}
|
|
566
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);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
567
748
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
console.log('
|
|
749
|
+
console.log('================================================================================\n');
|
|
750
|
+
console.log('š Done.');
|
|
751
|
+
console.log('================================================================================\n');
|
|
752
|
+
console.log('Summary:');
|
|
571
753
|
console.log(` - Site copied to: ${destPath}`);
|
|
572
|
-
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.');
|
|
573
755
|
console.log('\nNext recommended steps (manual or to be automated in future):');
|
|
574
756
|
console.log(' - Update pixelated.config.json for this site and encrypt it with your config tool');
|
|
575
757
|
console.log(' - Run `npm run lint`, `npm test`, and `npm run build` inside the new site and fix any issues');
|