gatsby-attainlabs-cms 1.0.49 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,17 +55,27 @@ module.exports = {
55
55
  {
56
56
  resolve: "gatsby-attainlabs-cms",
57
57
  options: {
58
- brand: "Cash Money", // LendDirect | Cash Money | Heights Finance adding-puck-for-visual-code-editing
58
+ brand: "Cash Money", // LendDirect | Cash Money | Heights Finance | Attain Finance
59
59
  // personalAccessToken: "optional-fallback", // not recommended, but supported
60
- environment: "production", // production | dev
61
- fetch: ["blogs", "disclaimers", "faqs"] // Array of data to fetch from database
62
- debug: false // Console logs blocks created
60
+ environment: "production", // production | staging | dev
61
+ // azureBranch: "my-feature-branch", // Optional: Override branch for non-production environments
62
+ fetch: ["blogs", "disclaimers", "faqs"], // Array of data to fetch from database
63
+ debug: false, // Console logs blocks created
63
64
  },
64
65
  },
65
66
  ],
66
67
  };
67
68
  ```
68
69
 
70
+ ### Environment Options
71
+
72
+ - **`production`**: Forces content sync from the `master` branch.
73
+ - **`staging`**: Automatically fetches content from the **latest active Pull Request** branch in Azure DevOps. Falls back to `master` if no PR is active.
74
+ - **`dev`**: Skips component sync and data fetching. Useful for local development speed.
75
+ - **Default behavior**: If `environment` is unspecified or other values, it defaults to the `azureBranch` option or `master`.
76
+
77
+ ```
78
+
69
79
  ---
70
80
 
71
81
  ## Behavior
@@ -79,7 +89,7 @@ module.exports = {
79
89
 
80
90
  - If the PAT is missing, the plugin will **warn and skip execution** instead of failing the build.
81
91
 
82
- - If the `brand` option is missing or invalid, the plugin will **throw an error** and stop the build.
92
+ - If the `brand` option is missing or invalid, the plugin will **throw an error** and stop the build.
83
93
  Valid values are:
84
94
  - `LendDirect`
85
95
  - `Cash Money`
@@ -95,7 +105,9 @@ module.exports = {
95
105
  If you see:
96
106
 
97
107
  ```
108
+
98
109
  ⚠️ [gatsby-attainlabs-cms] No PERSONAL_ACCESS_TOKEN found...
110
+
99
111
  ```
100
112
 
101
113
  ➡️ Double-check that `.env` exists and is loaded in `gatsby-config.js`.
@@ -107,8 +119,10 @@ If you see:
107
119
  If you see:
108
120
 
109
121
  ```
122
+
110
123
  [gatsby-attainlabs-cms] Invalid or missing "brand" option.
111
124
  You must specify one of: LendDirect, Cash Money, Heights Finance
125
+
112
126
  ```
113
127
 
114
128
  ➡️ Make sure you pass a valid brand in `gatsby-config.js`.
@@ -117,5 +131,6 @@ You must specify one of: LendDirect, Cash Money, Heights Finance
117
131
 
118
132
  ### Token in client code
119
133
 
120
- Don’t use `GATSBY_PERSONAL_ACCESS_TOKEN`. That will leak your secret into the browser bundle.
134
+ Don’t use `GATSBY_PERSONAL_ACCESS_TOKEN`. That will leak your secret into the browser bundle.
121
135
  Always use `PERSONAL_ACCESS_TOKEN`.
136
+ ```
package/gatsby-browser.js CHANGED
@@ -1,7 +1,7 @@
1
1
  exports.onClientEntry = (_, pluginOptions) => {
2
2
  const { brand } = pluginOptions;
3
3
 
4
- if (brand === "LendDirect") {
4
+ if (brand === "LendDirect" || brand === "Cash Money") {
5
5
  import("@fontsource/open-sans/300.css");
6
6
  import("@fontsource/open-sans/400.css");
7
7
  import("@fontsource/open-sans/500.css");
package/gatsby-node.js CHANGED
@@ -4,7 +4,7 @@ const path = require("path");
4
4
  const prettier = require("prettier");
5
5
  const fse = require("fs-extra");
6
6
  const crypto = require("crypto");
7
- const { graphql } = require("gatsby");
7
+ const { graphql } = require("gatsby"); // kept (even if unused here) to match your existing file
8
8
 
9
9
  const brands = {
10
10
  LendDirect: "lenddirect",
@@ -15,7 +15,227 @@ const brands = {
15
15
 
16
16
  const generateSliceWrapper = () => {};
17
17
 
18
- const generatePage = async (blocks, layout, isNested) => {
18
+ /**
19
+ * ----------------------------
20
+ * Promise-based download helpers
21
+ * ----------------------------
22
+ */
23
+
24
+ function httpsGet(url, options) {
25
+ return new Promise((resolve, reject) => {
26
+ https
27
+ .get(url, options, (res) => {
28
+ let data = "";
29
+ res.on("data", (chunk) => (data += chunk));
30
+ res.on("end", () => resolve({ res, data }));
31
+ })
32
+ .on("error", reject);
33
+ });
34
+ }
35
+
36
+ async function listItems({ org, project, repo, repoPath, branch, options }) {
37
+ const listUrl = `https://dev.azure.com/${org}/${project}/_apis/git/repositories/${encodeURIComponent(
38
+ repo
39
+ )}/items?scopePath=${encodeURIComponent(
40
+ repoPath
41
+ )}&recursionLevel=full&includeContentMetadata=true&versionDescriptor.version=${encodeURIComponent(
42
+ branch
43
+ )}&versionDescriptor.versionType=branch&api-version=7.0`;
44
+
45
+ const { res, data } = await httpsGet(listUrl, options);
46
+
47
+ if (res.statusCode !== 200) {
48
+ throw new Error(
49
+ `Failed to list items for ${repoPath}: ${res.statusCode} ${res.statusMessage}\n${data}`
50
+ );
51
+ }
52
+
53
+ const result = JSON.parse(data);
54
+ const posix = path.posix;
55
+
56
+ // Exclude files named config.tsx and preview.tsx (same as your original logic)
57
+ return (result.value || []).filter(
58
+ (i) =>
59
+ !i.isFolder &&
60
+ i.gitObjectType === "blob" &&
61
+ posix.basename(i.path) !== "config.tsx" &&
62
+ posix.basename(i.path) !== "preview.tsx"
63
+ );
64
+ }
65
+
66
+ function downloadFileAsync({
67
+ org,
68
+ project,
69
+ repo,
70
+ repoPath,
71
+ filePath,
72
+ localBasePath,
73
+ branch,
74
+ options,
75
+ }) {
76
+ return new Promise((resolve, reject) => {
77
+ const fileUrl = `https://dev.azure.com/${org}/${project}/_apis/git/repositories/${encodeURIComponent(
78
+ repo
79
+ )}/items?path=${encodeURIComponent(
80
+ filePath
81
+ )}&versionDescriptor.version=${encodeURIComponent(
82
+ branch
83
+ )}&versionDescriptor.versionType=branch&api-version=7.0&download=true`;
84
+
85
+ const posix = path.posix;
86
+ const relativePath = posix.relative(repoPath, filePath);
87
+ const destFile = path.join(localBasePath, ...relativePath.split("/"));
88
+ const destDir = path.dirname(destFile);
89
+
90
+ if (!fs.existsSync(destDir)) {
91
+ fs.mkdirSync(destDir, { recursive: true });
92
+ }
93
+
94
+ const doRequest = (url) => {
95
+ https
96
+ .get(url, options, (res) => {
97
+ // Follow redirects
98
+ if (
99
+ res.statusCode &&
100
+ res.statusCode >= 300 &&
101
+ res.statusCode < 400 &&
102
+ res.headers.location
103
+ ) {
104
+ res.resume();
105
+ return doRequest(res.headers.location);
106
+ }
107
+
108
+ if (res.statusCode !== 200) {
109
+ res.resume();
110
+ return reject(
111
+ new Error(
112
+ `Failed to download ${filePath}: ${res.statusCode} ${res.statusMessage}`
113
+ )
114
+ );
115
+ }
116
+
117
+ const uniqueId = Math.random().toString(36).substring(7);
118
+ const tmpFile = destFile + "." + uniqueId + ".part";
119
+ const fileStream = fs.createWriteStream(tmpFile);
120
+
121
+ res.pipe(fileStream);
122
+
123
+ fileStream.on("error", reject);
124
+ res.on("error", reject);
125
+
126
+ fileStream.on("finish", () => {
127
+ fileStream.close(() => {
128
+ try {
129
+ const ext = path.extname(destFile).toLowerCase();
130
+ let generatedComment = "";
131
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
132
+ generatedComment =
133
+ "// GENERATED FILE // This file is automatically generated by the AttainLabs CMS plugin. Do not edit this file directly.\n";
134
+ }
135
+
136
+ const fileContent = fs.readFileSync(tmpFile, "utf8");
137
+ fs.writeFileSync(destFile, generatedComment + fileContent);
138
+ fs.unlinkSync(tmpFile);
139
+
140
+ resolve(destFile);
141
+ } catch (e) {
142
+ reject(e);
143
+ }
144
+ });
145
+ });
146
+ })
147
+ .on("error", reject);
148
+ };
149
+
150
+ doRequest(fileUrl);
151
+ });
152
+ }
153
+
154
+ async function downloadTarget({
155
+ org,
156
+ project,
157
+ repo,
158
+ repoPath,
159
+ localBasePath,
160
+ branch,
161
+ options,
162
+ }) {
163
+ const items = await listItems({
164
+ org,
165
+ project,
166
+ repo,
167
+ repoPath,
168
+ branch,
169
+ options,
170
+ });
171
+
172
+ console.log(`Found ${items.length} files in ${repoPath}`);
173
+
174
+ const results = await Promise.all(
175
+ items.map((item) =>
176
+ downloadFileAsync({
177
+ org,
178
+ project,
179
+ repo,
180
+ repoPath,
181
+ filePath: item.path,
182
+ localBasePath,
183
+ branch,
184
+ options,
185
+ })
186
+ )
187
+ );
188
+
189
+ console.log(`✅ Completed downloading all files for target: ${repoPath}`);
190
+ return results;
191
+ }
192
+
193
+ function rewriteSliceWrapper(fetchConfig) {
194
+ const sliceWrapperPath = path.resolve(
195
+ "./src/cms/components/sliceWrapper.tsx"
196
+ );
197
+
198
+ if (!fs.existsSync(sliceWrapperPath)) {
199
+ console.warn(
200
+ `⚠️ [gatsby-attainlabs-cms] sliceWrapper.tsx not found at ${sliceWrapperPath}. Skipping rewrite.`
201
+ );
202
+ return;
203
+ }
204
+
205
+ let content = fs.readFileSync(sliceWrapperPath, "utf8");
206
+
207
+ if (!fetchConfig || !fetchConfig.includes("blogs")) {
208
+ content = content.replace(
209
+ /# CHECK_BLOGS_START[\s\S]*?# CHECK_BLOGS_END/g,
210
+ ""
211
+ );
212
+ content = content.replace(
213
+ /\/\* CHECK_BLOGS_START \*\/[\s\S]*?\/\* CHECK_BLOGS_END \*\//g,
214
+ ""
215
+ );
216
+ }
217
+
218
+ if (!fetchConfig || !fetchConfig.includes("disclaimers")) {
219
+ content = content.replace(
220
+ /# CHECK_DISCLAIMERS_START[\s\S]*?# CHECK_DISCLAIMERS_END/g,
221
+ ""
222
+ );
223
+ content = content.replace(
224
+ /\/\* CHECK_DISCLAIMERS_START \*\/[\s\S]*?\/\* CHECK_DISCLAIMERS_END \*\//g,
225
+ ""
226
+ );
227
+ }
228
+
229
+ fs.writeFileSync(sliceWrapperPath, content);
230
+ }
231
+
232
+ /**
233
+ * ----------------------------
234
+ * Existing code (mostly unchanged)
235
+ * ----------------------------
236
+ */
237
+
238
+ const generatePage = async (blocks, layout, isNested, environment, page) => {
19
239
  // Validate input parameters
20
240
  if (!Array.isArray(blocks)) {
21
241
  throw new Error("Invalid parameters passed to createPage.");
@@ -33,8 +253,8 @@ const generatePage = async (blocks, layout, isNested) => {
33
253
  import SEO from "../${isNested && "../"}../cms/components/SEO";
34
254
 
35
255
  export const Head = ({ pageContext }: any) => {
36
- const blocks = pageContext.blocks;
37
- const meta = blocks.root.props.meta;
256
+ const blocks = ${environment === "staging" ? (page.draft ? "pageContext.draft.blocks" : "pageContext.blocks") : "pageContext.blocks"}
257
+ const meta = blocks.root.props.meta
38
258
  return (
39
259
  <SEO
40
260
  title={meta ? meta.title : ""}
@@ -90,6 +310,7 @@ const getAllBlogs = (data) => {
90
310
  };
91
311
 
92
312
  require("dotenv").config();
313
+
93
314
  // Load PAT from env instead of hardcoding (fallback to old const for now but warn)
94
315
  exports.onPreInit = async (_, pluginOptions) => {
95
316
  const {
@@ -133,23 +354,100 @@ exports.onPreInit = async (_, pluginOptions) => {
133
354
  );
134
355
  }
135
356
 
357
+ // Helper to fetch latest PR branch
358
+ const getLatestPullRequestBranch = (org, project, repo, pat) => {
359
+ return new Promise((resolve) => {
360
+ const options = {
361
+ headers: {
362
+ Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}`,
363
+ "User-Agent": "gatsby-attainlabs-cms",
364
+ },
365
+ };
366
+
367
+ // Fetch top 10 active PRs
368
+ const url = `https://dev.azure.com/${org}/${project}/_apis/git/repositories/${encodeURIComponent(
369
+ repo
370
+ )}/pullrequests?searchCriteria.status=active&top=10&api-version=7.0`;
371
+
372
+ https
373
+ .get(url, options, (res) => {
374
+ let data = "";
375
+ res.on("data", (chunk) => (data += chunk));
376
+ res.on("end", () => {
377
+ if (res.statusCode !== 200) {
378
+ console.error(
379
+ `Failed to fetch PRs: ${res.statusCode} ${res.statusMessage}`
380
+ );
381
+ resolve(null);
382
+ return;
383
+ }
384
+ try {
385
+ const result = JSON.parse(data);
386
+ if (result.value && result.value.length > 0) {
387
+ const latestPr = result.value.sort(
388
+ (a, b) =>
389
+ new Date(b.creationDate).getTime() -
390
+ new Date(a.creationDate).getTime()
391
+ )[0];
392
+
393
+ const sourceRef = latestPr.sourceRefName;
394
+ const branchName = sourceRef.replace("refs/heads/", "");
395
+ resolve(branchName);
396
+ } else {
397
+ resolve(null);
398
+ }
399
+ } catch (e) {
400
+ console.error("Error parsing PR response:", e);
401
+ resolve(null);
402
+ }
403
+ });
404
+ })
405
+ .on("error", (e) => {
406
+ console.error("Request error fetching PRs:", e);
407
+ resolve(null);
408
+ });
409
+ });
410
+ };
411
+
136
412
  const org = "CuroFinTech";
137
413
  const project = "Marketing";
138
414
  const repo = "Attain Labs";
139
- const branch = azureBranch || "master";
140
415
 
141
- const localTargets = [
142
- {
143
- localPath: path.resolve(__dirname, "src/sliceWrapper.tsx"),
144
- targetPath: path.resolve("./src/cms/components/"),
145
- },
146
- {
147
- localPath: path.resolve(__dirname, "src/map.ts"),
148
- targetPath: path.resolve("./src/cms/components/"),
149
- },
150
- ];
416
+ let branch = azureBranch || "master";
417
+
418
+ if (environment === "production") {
419
+ branch = "master";
420
+ } else if (environment === "staging") {
421
+ try {
422
+ console.log(
423
+ "ℹ️ [gatsby-attainlabs-cms] Staging mode: Checking for active PRs..."
424
+ );
425
+ const latestPrBranch = await getLatestPullRequestBranch(
426
+ org,
427
+ project,
428
+ repo,
429
+ pat
430
+ );
431
+ if (latestPrBranch) {
432
+ branch = latestPrBranch;
433
+ console.log(
434
+ `ℹ️ [gatsby-attainlabs-cms] Staging mode: Using latest PR branch '${branch}'`
435
+ );
436
+ } else {
437
+ console.warn(
438
+ "⚠️ [gatsby-attainlabs-cms] Staging mode: No active PRs found. Falling back to 'master'."
439
+ );
440
+ branch = "master";
441
+ }
442
+ } catch (e) {
443
+ console.error(
444
+ "⚠️ [gatsby-attainlabs-cms] Failed to fetch latest PR branch, falling back to 'master':",
445
+ e
446
+ );
447
+ branch = "master";
448
+ }
449
+ }
151
450
 
152
- // List of folders to download from
153
451
  const targets = [
154
452
  {
155
453
  repoPath: `/apps/cms/src/cms/editors/visual-block-editor/brands/${brands[brand]}/`,
@@ -172,179 +470,30 @@ exports.onPreInit = async (_, pluginOptions) => {
172
470
  },
173
471
  };
174
472
 
175
- // Copy local files first
176
- localTargets.forEach(({ localPath, targetPath }) => {
177
- if (!fs.existsSync(targetPath)) {
178
- fs.mkdirSync(targetPath, { recursive: true });
179
- // console.log(`📂 Created directory: ${targetPath}`);
180
- }
181
- const fileName = path.basename(localPath);
182
- const destFile = path.join(targetPath, fileName);
183
-
184
- if (fileName === "sliceWrapper.tsx") {
185
- let content = fs.readFileSync(localPath, "utf8");
186
-
187
- if (!fetchConfig || !fetchConfig.includes("blogs")) {
188
- content = content.replace(
189
- /# CHECK_BLOGS_START[\s\S]*?# CHECK_BLOGS_END/g,
190
- ""
191
- );
192
- content = content.replace(
193
- /\/\* CHECK_BLOGS_START \*\/[\s\S]*?\/\* CHECK_BLOGS_END \*\//g,
194
- ""
195
- );
196
- }
197
-
198
- if (!fetchConfig || !fetchConfig.includes("disclaimers")) {
199
- content = content.replace(
200
- /# CHECK_DISCLAIMERS_START[\s\S]*?# CHECK_DISCLAIMERS_END/g,
201
- ""
202
- );
203
- content = content.replace(
204
- /\/\* CHECK_DISCLAIMERS_START \*\/[\s\S]*?\/\* CHECK_DISCLAIMERS_END \*\//g,
205
- ""
206
- );
207
- }
208
-
209
- fs.writeFileSync(localPath, content);
210
- fs.writeFileSync(destFile, content);
211
- console.log(`✅ Copied and transformed ${destFile}`);
212
- } else {
213
- fs.copyFileSync(localPath, destFile);
214
- console.log(`✅ Copied ${destFile}`);
215
- }
216
- });
217
-
218
- // Loop through targets
219
- targets.forEach(({ repoPath, localBasePath }) => {
220
- const listUrl = `https://dev.azure.com/${org}/${project}/_apis/git/repositories/${encodeURIComponent(
221
- repo
222
- )}/items?scopePath=${encodeURIComponent(
223
- repoPath
224
- )}&recursionLevel=full&includeContentMetadata=true&versionDescriptor.version=${encodeURIComponent(
225
- branch
226
- )}&versionDescriptor.versionType=branch&api-version=7.0`;
227
-
228
- https
229
- .get(listUrl, options, (res) => {
230
- let data = "";
231
- res.on("data", (chunk) => (data += chunk));
232
- res.on("end", () => {
233
- if (res.statusCode !== 200) {
234
- console.error(
235
- `Failed to list items for ${repoPath}: ${res.statusCode} ${res.statusMessage}`
236
- );
237
- console.error(data);
238
- return;
239
- }
240
-
241
- const result = JSON.parse(data);
242
- const posix = path.posix;
243
-
244
- // Exclude files named config.tsx
245
- const items = (result.value || []).filter(
246
- (i) =>
247
- !i.isFolder &&
248
- i.gitObjectType === "blob" &&
249
- posix.basename(i.path) !== "config.tsx" &&
250
- posix.basename(i.path) !== "preview.tsx"
251
- );
252
-
253
- console.log(`Found ${items.length} files in ${repoPath}`);
254
- let completedDownloads = 0;
255
-
256
- items.forEach((item) => {
257
- downloadFile(item.path, repoPath, localBasePath, () => {
258
- completedDownloads++;
259
- if (completedDownloads === items.length) {
260
- console.log(
261
- `✅ Completed downloading all files for target: ${repoPath}`
262
- );
263
- }
264
- });
265
- });
266
- });
473
+ /**
474
+ * Awaitable download pipeline:
475
+ * - list items
476
+ * - download all files
477
+ * - only after all targets complete, rewrite sliceWrapper.tsx
478
+ */
479
+ await Promise.all(
480
+ targets.map((t) =>
481
+ downloadTarget({
482
+ org,
483
+ project,
484
+ repo,
485
+ repoPath: t.repoPath,
486
+ localBasePath: t.localBasePath,
487
+ branch,
488
+ options,
267
489
  })
268
- .on("error", (err) => {
269
- console.error("Request error:", err.message);
270
- });
271
- });
272
-
273
- // Download a single file preserving folder structure under localBasePath
274
- function downloadFile(filePath, repoPath, localBasePath, callback) {
275
- const fileUrl = `https://dev.azure.com/${org}/${project}/_apis/git/repositories/${encodeURIComponent(
276
- repo
277
- )}/items?path=${encodeURIComponent(
278
- filePath
279
- )}&versionDescriptor.version=${encodeURIComponent(
280
- branch
281
- )}&versionDescriptor.versionType=branch&api-version=7.0&download=true`;
282
-
283
- const posix = path.posix;
284
- const relativePath = posix.relative(repoPath, filePath); // structure under repoPath
285
- const destFile = path.join(localBasePath, ...relativePath.split("/"));
286
- const destDir = path.dirname(destFile);
287
-
288
- if (!fs.existsSync(destDir)) {
289
- fs.mkdirSync(destDir, { recursive: true });
290
- // console.log(`📂 Created directory: ${destDir}`);
291
- }
292
-
293
- const doRequest = (url) => {
294
- https
295
- .get(url, options, (res) => {
296
- if (
297
- res.statusCode &&
298
- res.statusCode >= 300 &&
299
- res.statusCode < 400 &&
300
- res.headers.location
301
- ) {
302
- return doRequest(res.headers.location);
303
- }
304
-
305
- if (res.statusCode !== 200) {
306
- console.error(
307
- `Failed to download ${filePath}: ${res.statusCode} ${res.statusMessage}`
308
- );
309
- res.resume();
310
- return;
311
- }
312
-
313
- const uniqueId = Math.random().toString(36).substring(7);
314
- const tmpFile = destFile + "." + uniqueId + ".part";
315
- const fileStream = fs.createWriteStream(tmpFile);
316
-
317
- res.pipe(fileStream);
318
- fileStream.on("finish", () => {
319
- fileStream.close(() => {
320
- const ext = path.extname(destFile).toLowerCase();
321
- let generatedComment = "";
322
- if (
323
- ext === ".ts" ||
324
- ext === ".tsx" ||
325
- ext === ".js" ||
326
- ext === ".jsx"
327
- ) {
328
- generatedComment =
329
- "// GENERATED FILE // This file is automatically generated by the AttainLabs CMS plugin. Do not edit this file directly.\n";
330
- }
331
- const fileContent = fs.readFileSync(tmpFile, "utf8");
332
- fs.writeFileSync(destFile, generatedComment + fileContent);
333
-
334
- // Remove the temporary file
335
- fs.unlinkSync(tmpFile);
336
- if (callback) callback();
337
- });
338
- });
339
- })
340
- .on("error", (err) => {
341
- console.error("Request error:", err.message);
342
- });
343
- };
490
+ )
491
+ );
344
492
 
345
- doRequest(fileUrl);
346
- }
493
+ // ✅ Now safe: no later download will overwrite your changes.
494
+ rewriteSliceWrapper(fetchConfig);
347
495
  };
496
+
348
497
  exports.sourceNodes = async (
349
498
  { actions, createNodeId, createContentDigest },
350
499
  pluginOptions
@@ -368,13 +517,15 @@ exports.sourceNodes = async (
368
517
  console.log(
369
518
  `ℹ️ [gatsby-attainlabs-cms] Fetching Trustpilot data for brand: ${brand}`
370
519
  );
371
- // // Map brand names to Trustpilot business unit IDs
520
+
372
521
  const businessIds = {
373
522
  LendDirect: "599affea0000ff0005a95acd",
374
523
  "Cash Money": "599afd420000ff0005a95a9d",
375
524
  "Heights Finance": "5e72238d600d1a0001be01eb",
376
525
  };
526
+
377
527
  const businessUnitId = businessIds[brand];
528
+
378
529
  if (!apiKey) {
379
530
  console.warn(
380
531
  "⚠️ [gatsby-attainlabs-cms] No TRUSTPILOT_API_KEY found. " +
@@ -382,7 +533,7 @@ exports.sourceNodes = async (
382
533
  "Example .env:\n" +
383
534
  "TRUSTPILOT_API_KEY=xxxxxxx\n"
384
535
  );
385
- return; // stop execution early
536
+ return;
386
537
  }
387
538
 
388
539
  if (!brand || !businessUnitId) {
@@ -391,13 +542,9 @@ exports.sourceNodes = async (
391
542
  `Current value: '${brand}'. Supported brands: ${Object.keys(businessIds).join(", ")}. ` +
392
543
  "Trustpilot data will not be fetched."
393
544
  );
394
- return; // stop execution early
545
+ return;
395
546
  }
396
547
 
397
- console.log(
398
- `ℹ️ [gatsby-attainlabs-cms] Fetching Firebase Blogs Data brand: ${brand}`
399
- );
400
-
401
548
  const fetchTrustpilotData = async (endpoint) => {
402
549
  try {
403
550
  const response = await fetch(
@@ -462,7 +609,12 @@ exports.sourceNodes = async (
462
609
 
463
610
  if (fetchConfig) {
464
611
  const firebaseData = await fetchData();
612
+
465
613
  if (fetchConfig.includes("blogs")) {
614
+ console.log(
615
+ `ℹ️ [gatsby-attainlabs-cms] Fetching Firebase Blogs Data brand: ${brand}`
616
+ );
617
+
466
618
  const allBlogs = getAllBlogs(firebaseData.pages);
467
619
 
468
620
  for (const blog of allBlogs) {
@@ -470,6 +622,7 @@ exports.sourceNodes = async (
470
622
  blog.blocks.root.props.datePublished = new Date(
471
623
  blog.blocks.root.props.datePublished
472
624
  );
625
+
473
626
  const node = {
474
627
  ...blog,
475
628
  id,
@@ -481,12 +634,17 @@ exports.sourceNodes = async (
481
634
  contentDigest: createContentDigest(blog),
482
635
  },
483
636
  };
637
+
484
638
  createNode(node);
485
639
  }
486
640
  }
487
641
 
488
642
  // Disclaimers Source Node
489
643
  if (fetchConfig.includes("disclaimers")) {
644
+ console.log(
645
+ `ℹ️ [gatsby-attainlabs-cms] Fetching Firebase Disclaimers Data brand: ${brand}`
646
+ );
647
+
490
648
  const allDisclaimers = Object.values(firebaseData.disclaimers);
491
649
  for (const disclaimer of allDisclaimers) {
492
650
  const id = createNodeId(`attain-cms-disclaimer-${disclaimer.id}`);
@@ -506,16 +664,19 @@ exports.sourceNodes = async (
506
664
  }
507
665
  }
508
666
  };
667
+
509
668
  exports.createPages = async ({ actions, store }, pluginOptions) => {
510
669
  const { brand, environment, debug } = pluginOptions;
511
670
  const { createSlice, createPage } = actions;
512
671
  const siteRoot = store.getState().program.directory;
672
+
513
673
  if (environment === "dev") {
514
674
  console.log(
515
675
  "ℹ️ [gatsby-attainlabs-cms] Running in 'dev' environment mode. Skipping page creation."
516
676
  );
517
677
  return;
518
678
  }
679
+
519
680
  // 🚨 Validate brand option
520
681
  if (!brand || !brands[brand]) {
521
682
  throw new Error(
@@ -528,6 +689,7 @@ exports.createPages = async ({ actions, store }, pluginOptions) => {
528
689
  `}`
529
690
  );
530
691
  }
692
+
531
693
  const firebaseData = await fetch(
532
694
  `https://attain-finance-cms-default-rtdb.firebaseio.com/cms/brands/${brands[brand]}/pages.json`
533
695
  );
@@ -542,44 +704,50 @@ exports.createPages = async ({ actions, store }, pluginOptions) => {
542
704
  });
543
705
 
544
706
  async function processPage(page, parentPath = "") {
545
- // Check for folderUrl first — handle nested folders immediately
707
+ // Handle folders
546
708
  if (
547
709
  page.folderUrl &&
548
710
  page.children &&
549
711
  Object.keys(page.children).length > 0
550
712
  ) {
551
- // use folderUrl instead of pageUrl for folder structure
552
713
  const folderPath = parentPath
553
714
  ? path.join(parentPath, page.folderUrl)
554
715
  : page.folderUrl;
555
- const folderFullPath = path.join(siteRoot, "src/cms/pages", folderPath);
556
716
 
717
+ const folderFullPath = path.join(siteRoot, "src/cms/pages", folderPath);
557
718
  await fse.ensureDir(folderFullPath);
558
719
 
559
720
  for (const child of Object.values(page.children)) {
560
721
  await processPage(child, folderPath);
561
722
  }
562
723
 
563
- return; // Stop here for folders — no page created for folder node
724
+ return;
564
725
  }
565
726
 
566
- // 🧩 Step 2: Process actual pages (no folderUrl)
567
- if (page.template === "visual-editor" && page.published) {
727
+ // Pages
728
+ if (
729
+ (page.published && environment === "production") ||
730
+ (environment === "staging" && page.hasOwnProperty("type"))
731
+ ) {
732
+ let blocks =
733
+ environment === "staging"
734
+ ? page.draft
735
+ ? page.draft.blocks
736
+ : page.blocks
737
+ : page.blocks;
738
+
568
739
  const {
569
- blocks: {
570
- content,
571
- root: {
572
- props: { pageUrl, layout },
573
- },
740
+ content,
741
+ root: {
742
+ props: { pageUrl, layout },
574
743
  },
575
- id,
576
- } = page;
744
+ } = blocks;
577
745
 
578
746
  // Create slice for each block
579
747
  await Promise.all(
580
748
  content.map(async (b) => {
581
749
  const name = b.props.component.name;
582
- const sliceId = `block--${pageUrl}--${name}--${id}--${crypto.randomUUID()}`;
750
+ const sliceId = `block--${pageUrl}--${name}--${page.id}--${crypto.randomUUID()}`;
583
751
 
584
752
  createSlice({
585
753
  id: sliceId,
@@ -600,7 +768,13 @@ exports.createPages = async ({ actions, store }, pluginOptions) => {
600
768
  );
601
769
 
602
770
  // Generate page file
603
- const pageSource = await generatePage(content, layout, parentPath);
771
+ const pageSource = await generatePage(
772
+ content,
773
+ layout,
774
+ parentPath,
775
+ environment,
776
+ page
777
+ );
604
778
  const folderPath = parentPath ? path.join(parentPath, pageUrl) : pageUrl;
605
779
  const outPath = path.join(siteRoot, "src/cms/pages", `${folderPath}.tsx`);
606
780
  const gatsbyPagePath = path.join(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gatsby-attainlabs-cms",
3
- "version": "1.0.49",
3
+ "version": "1.1.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/tsconfig.json CHANGED
@@ -9,6 +9,5 @@
9
9
  "esModuleInterop": true,
10
10
  "moduleResolution": "node",
11
11
  "skipLibCheck": true
12
- },
13
- "include": ["src"]
12
+ }
14
13
  }
package/dist/index.d.ts DELETED
@@ -1,9 +0,0 @@
1
- import React from "react";
2
- import type { SliceComponentProps } from "gatsby";
3
- interface SliceWrapperProps extends SliceComponentProps {
4
- sliceContext: {
5
- componentPath: string;
6
- };
7
- }
8
- declare function SliceWrapper({ sliceContext }: SliceWrapperProps): React.DetailedReactHTMLElement<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
9
- export { SliceWrapper };
package/dist/index.js DELETED
@@ -1,8 +0,0 @@
1
- import React from "react";
2
- function SliceWrapper({ sliceContext }) {
3
- const { componentPath, ...props } = sliceContext;
4
- // Dynamically import your component
5
- const Component = require(componentPath).default;
6
- return React.createElement(Component, props);
7
- }
8
- export { SliceWrapper };
package/dist/map.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import React from "react";
2
- export declare const CMS_COMPONENTS: Record<string, React.ComponentType<any>>;
3
- export type CMSComponentName = keyof typeof CMS_COMPONENTS;
package/dist/map.js DELETED
@@ -1,9 +0,0 @@
1
- // This requires Webpack to include all .tsx files in the folder
2
- // @ts-ignore
3
- const context = require.context("./", true, /\.tsx$/);
4
- export const CMS_COMPONENTS = {};
5
- context.keys().forEach((key) => {
6
- // Remove "./" and ".tsx" from the path to get the component name
7
- const name = key.replace(/^.\//, "").replace(/\.tsx$/, "");
8
- CMS_COMPONENTS[name] = context(key).default;
9
- });
@@ -1,10 +0,0 @@
1
- interface SliceWrapperProps {
2
- sliceContext: {
3
- componentPath: string;
4
- [key: string]: any;
5
- };
6
- data: any;
7
- }
8
- export default function SliceWrapper({ sliceContext, data, }: SliceWrapperProps): import("react/jsx-runtime").JSX.Element | null;
9
- export declare const TrustPilotQuery: import("gatsby").StaticQueryDocument;
10
- export {};
@@ -1,139 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { graphql } from "gatsby";
3
- import { CMS_COMPONENTS } from "./map";
4
- export default function SliceWrapper({ sliceContext, data, }) {
5
- const { componentPath, ...props } = sliceContext;
6
- const pathFormatted = componentPath
7
- .replace("src/cms/components/", "")
8
- .replace(/^.\//, "")
9
- .replace(/\.tsx$/, "");
10
- const Component = CMS_COMPONENTS[pathFormatted];
11
- if (!Component) {
12
- console.warn(`Component "${pathFormatted}" not found in CMS_COMPONENTS`);
13
- return null;
14
- }
15
- return (_jsx(Component, { ...props, trustPilotData: data.allTrustPilotReviews.edges[0].node,
16
- /* CHECK_DISCLAIMERS_START */
17
- disclaimers: data.allDisclaimers,
18
- /* CHECK_DISCLAIMERS_END */
19
- /* CHECK_BLOGS_START */
20
- blogs: {
21
- sliderBlogs: {
22
- ...data.sliderBlogs,
23
- },
24
- allBlogs: {
25
- ...data.allBlogs,
26
- },
27
- } }));
28
- }
29
- export const TrustPilotQuery = graphql `
30
- query {
31
- allTrustPilotReviews {
32
- edges {
33
- node {
34
- recentReviews {
35
- reviews {
36
- numberOfLikes
37
- stars
38
- text
39
- title
40
- createdAt
41
- isVerified
42
- reviewVerificationLevel
43
- consumer {
44
- displayName
45
- }
46
- }
47
- }
48
- businessData {
49
- score {
50
- stars
51
- trustScore
52
- }
53
- displayName
54
- numberOfReviews {
55
- total
56
- usedForTrustScoreCalculation
57
- }
58
- }
59
- }
60
- }
61
- }
62
- # CHECK_DISCLAIMERS_START
63
- allDisclaimers: allAttainLabsCmsDisclaimers {
64
- edges {
65
- node {
66
- content
67
- published
68
- order
69
- }
70
- }
71
- }
72
- # CHECK_DISCLAIMERS_END
73
- # CHECK_BLOGS_START
74
- allBlogs: allAttainLabsCmsBlogs {
75
- edges {
76
- node {
77
- blocks {
78
- content {
79
- props {
80
- content {
81
- props {
82
- text
83
- image {
84
- desktop {
85
- url
86
- }
87
- }
88
- }
89
- # type
90
- }
91
- }
92
- }
93
- root {
94
- props {
95
- author
96
- datePublished
97
- pageUrl
98
- }
99
- }
100
- }
101
- }
102
- }
103
- }
104
- sliderBlogs: allAttainLabsCmsBlogs(
105
- limit: 9
106
- sort: { blocks: { root: { props: { datePublished: DESC } } } }
107
- ) {
108
- edges {
109
- node {
110
- blocks {
111
- content {
112
- props {
113
- content {
114
- props {
115
- text
116
- image {
117
- desktop {
118
- url
119
- }
120
- }
121
- }
122
- # type
123
- }
124
- }
125
- }
126
- root {
127
- props {
128
- author
129
- datePublished
130
- pageUrl
131
- }
132
- }
133
- }
134
- }
135
- }
136
- }
137
- # CHECK_BLOGS_END
138
- }
139
- `;
package/src/index.ts DELETED
@@ -1,18 +0,0 @@
1
- import React from "react";
2
- import type { SliceComponentProps } from "gatsby";
3
-
4
- interface SliceWrapperProps extends SliceComponentProps {
5
- sliceContext: {
6
- componentPath: string;
7
- };
8
- }
9
-
10
- function SliceWrapper({ sliceContext }: SliceWrapperProps) {
11
- const { componentPath, ...props } = sliceContext;
12
-
13
- // Dynamically import your component
14
- const Component = require(componentPath).default;
15
- return React.createElement(Component, props);
16
- }
17
-
18
- export { SliceWrapper };
package/src/map.ts DELETED
@@ -1,16 +0,0 @@
1
- // GENERATED FILE // This file is automatically generated by the AttainLabs CMS plugin. Do not edit this file directly.
2
- import React from "react";
3
-
4
- // This requires Webpack to include all .tsx files in the folder
5
- // @ts-ignore
6
- const context = require.context("./", true, /\.tsx$/);
7
-
8
- export const CMS_COMPONENTS: Record<string, React.ComponentType<any>> = {};
9
-
10
- context.keys().forEach((key: string) => {
11
- // Remove "./" and ".tsx" from the path to get the component name
12
- const name = key.replace(/^.\//, "").replace(/\.tsx$/, "");
13
- CMS_COMPONENTS[name] = context(key).default;
14
- });
15
-
16
- export type CMSComponentName = keyof typeof CMS_COMPONENTS;
@@ -1,161 +0,0 @@
1
- // GENERATED FILE // This file is automatically generated by the AttainLabs CMS plugin. Do not edit this file directly.
2
-
3
- import React from "react";
4
- import { graphql, type SliceComponentProps } from "gatsby";
5
- import { CMS_COMPONENTS } from "./map";
6
-
7
- interface SliceWrapperProps {
8
- sliceContext: {
9
- componentPath: string;
10
- [key: string]: any;
11
- };
12
- data: any;
13
- }
14
-
15
- export default function SliceWrapper({
16
- sliceContext,
17
- data,
18
- }: SliceWrapperProps) {
19
- const { componentPath, ...props } = sliceContext;
20
- const pathFormatted = componentPath
21
- .replace("src/cms/components/", "")
22
- .replace(/^.\//, "")
23
- .replace(/\.tsx$/, "");
24
- const Component = CMS_COMPONENTS[pathFormatted];
25
- if (!Component) {
26
- console.warn(`Component "${pathFormatted}" not found in CMS_COMPONENTS`);
27
- return null;
28
- }
29
-
30
- return (
31
- <Component
32
- {...props}
33
- trustPilotData={data.allTrustPilotReviews.edges[0].node}
34
- /* CHECK_DISCLAIMERS_START */
35
- disclaimers={data.allDisclaimers}
36
- /* CHECK_DISCLAIMERS_END */
37
- /* CHECK_BLOGS_START */
38
- blogs={{
39
- sliderBlogs: {
40
- ...data.sliderBlogs,
41
- },
42
- allBlogs: {
43
- ...data.allBlogs,
44
- },
45
- }}
46
- /* CHECK_BLOGS_END */
47
- />
48
- );
49
- }
50
-
51
- export const TrustPilotQuery = graphql`
52
- query {
53
- allTrustPilotReviews {
54
- edges {
55
- node {
56
- recentReviews {
57
- reviews {
58
- numberOfLikes
59
- stars
60
- text
61
- title
62
- createdAt
63
- isVerified
64
- reviewVerificationLevel
65
- consumer {
66
- displayName
67
- }
68
- }
69
- }
70
- businessData {
71
- score {
72
- stars
73
- trustScore
74
- }
75
- displayName
76
- numberOfReviews {
77
- total
78
- usedForTrustScoreCalculation
79
- }
80
- }
81
- }
82
- }
83
- }
84
- # CHECK_DISCLAIMERS_START
85
- allDisclaimers: allAttainLabsCmsDisclaimers {
86
- edges {
87
- node {
88
- content
89
- published
90
- order
91
- }
92
- }
93
- }
94
- # CHECK_DISCLAIMERS_END
95
- # CHECK_BLOGS_START
96
- allBlogs: allAttainLabsCmsBlogs {
97
- edges {
98
- node {
99
- blocks {
100
- content {
101
- props {
102
- content {
103
- props {
104
- text
105
- image {
106
- desktop {
107
- url
108
- }
109
- }
110
- }
111
- # type
112
- }
113
- }
114
- }
115
- root {
116
- props {
117
- author
118
- datePublished
119
- pageUrl
120
- }
121
- }
122
- }
123
- }
124
- }
125
- }
126
- sliderBlogs: allAttainLabsCmsBlogs(
127
- limit: 9
128
- sort: { blocks: { root: { props: { datePublished: DESC } } } }
129
- ) {
130
- edges {
131
- node {
132
- blocks {
133
- content {
134
- props {
135
- content {
136
- props {
137
- text
138
- image {
139
- desktop {
140
- url
141
- }
142
- }
143
- }
144
- # type
145
- }
146
- }
147
- }
148
- root {
149
- props {
150
- author
151
- datePublished
152
- pageUrl
153
- }
154
- }
155
- }
156
- }
157
- }
158
- }
159
- # CHECK_BLOGS_END
160
- }
161
- `;