at-builder 1.2.9 → 1.3.3

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/webpack.config.js CHANGED
@@ -40,12 +40,76 @@ let ignoreFolders = [
40
40
 
41
41
 
42
42
  /**
43
- * Method to traverse folder recursively
44
- * @param {*} dir - Directory name
43
+ * Resolve which top-level folders to build, and optionally nested page folders.
44
+ * Priority: build.config.json (variations keys + buildFolders) → V prefix fallback
45
+ * Returns: { topLevel: string[] | null, nested: Set<string> | null }
45
46
  */
46
- var traversePath = function (dir) {
47
+ function resolveEntryConfig(buildRoot) {
48
+ const configPaths = [
49
+ path.join(buildRoot, 'shared', 'build.config.json'),
50
+ path.join(buildRoot, 'Shared', 'build.config.json')
51
+ ];
52
+ const configPath = configPaths.find(p => fs.existsSync(p));
53
+
54
+ if (configPath) {
55
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
56
+ const variations = config.activityInfo?.variations || {};
57
+ const variationFolders = Array.isArray(variations)
58
+ ? variations
59
+ : Object.keys(variations);
60
+ const extraFolders = config.activityInfo?.buildFolders || [];
61
+ const allFolders = [...new Set([...variationFolders, ...extraFolders])];
62
+
63
+ if (allFolders.length > 0) {
64
+ // Filter to only folders that exist on disk
65
+ const existingFolders = allFolders.filter(f => {
66
+ const fullPath = path.join(buildRoot, f);
67
+ return fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory();
68
+ });
69
+ const skipped = allFolders.filter(f => !existingFolders.includes(f));
70
+ if (skipped.length > 0) {
71
+ console.log('\x1b[33m%s\x1b[0m', `[Config] Skipped (not on disk): ${skipped.join(', ')}`);
72
+ }
73
+ console.log('\x1b[36m%s\x1b[0m', `[Config] Entry folders: ${existingFolders.join(', ')}`);
74
+
75
+ // Extract nested page folder leaf names from multi-page variations.
76
+ // e.g. "Post-Migration/Global" → "Global"
77
+ const nestedFolders = new Set();
78
+ for (const value of Object.values(variations)) {
79
+ if (typeof value === 'object' && value !== null && value.pages) {
80
+ for (const subPath of Object.values(value.pages)) {
81
+ const parts = subPath.split('/');
82
+ if (parts.length > 1) {
83
+ nestedFolders.add(parts[parts.length - 1]);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ if (nestedFolders.size > 0) {
90
+ console.log('\x1b[36m%s\x1b[0m', `[Config] Page folders: ${[...nestedFolders].join(', ')}`);
91
+ }
92
+
93
+ return {
94
+ topLevel: existingFolders.length > 0 ? existingFolders : null,
95
+ nested: nestedFolders.size > 0 ? nestedFolders : null
96
+ };
97
+ }
98
+ }
99
+
100
+ // Fallback: scan for V-prefixed folders (legacy single-page convention)
101
+ console.log('\x1b[33m%s\x1b[0m', '[Fallback] No build.config.json — using V prefix convention');
102
+ return { topLevel: null, nested: null };
103
+ }
104
+
105
+ /**
106
+ * Traverse activity folder to discover JS/TS entry points.
107
+ * @param {string} dir - Directory to traverse
108
+ * @param {boolean} isTopLevel - true for the activity root; false for nested page subfolders
109
+ */
110
+ var traversePath = function (dir, isTopLevel) {
47
111
  var foldername = dir.split("/").pop();
48
- // Skip path if it start with . or present in ignoredFolders
112
+ // Skip path if it starts with . or is in ignoreFolders
49
113
  if (foldername.startsWith(".") || ~ignoreFolders.indexOf(foldername)) {
50
114
  return;
51
115
  }
@@ -55,18 +119,30 @@ var traversePath = function (dir) {
55
119
  var file = dir + '/' + fileName;
56
120
  var stat = fs.statSync(file);
57
121
  if (stat && stat.isDirectory()) {
58
- if (!fileName.startsWith("V")) {
59
- return;
122
+ if (isTopLevel) {
123
+ // Top-level: use config-driven list, or V prefix fallback
124
+ if (entryConfig.topLevel) {
125
+ if (!entryConfig.topLevel.includes(fileName)) return;
126
+ } else {
127
+ if (!fileName.startsWith("V")) return;
128
+ }
129
+ } else {
130
+ // Nested (page subfolder under an experience): use config nested set,
131
+ // or V prefix fallback (legacy)
132
+ if (entryConfig.nested) {
133
+ if (!entryConfig.nested.has(fileName)) return;
134
+ } else {
135
+ if (!fileName.startsWith("V")) return;
136
+ }
137
+ }
138
+ var res = traversePath(file, false);
139
+ if (res && res.length) {
140
+ results = results.concat(res);
60
141
  }
61
- // console.log(++count + ")", "\x1b[36m", fileName, "\x1b[0m", `\n Build path: ${file}/dist/build.js`);
62
- var res = traversePath(file);
63
- // Process only if have child nodes
64
- if (res && res.length)
65
- results = results.concat(traversePath(file));
66
142
  } else {
67
- // Process only if it is a js file
68
- if (path.extname(file) == ".js" || path.extname(file) == ".ts")
143
+ if (path.extname(file) == ".js" || path.extname(file) == ".ts") {
69
144
  results.push(file);
145
+ }
70
146
  }
71
147
  });
72
148
  return results;
@@ -97,7 +173,11 @@ if (!fs.existsSync(activityPath)) {
97
173
  process.exit(1);
98
174
  }
99
175
 
100
- var files = traversePath(activityPath.toString());
176
+ // Resolve config-driven entry layout (must happen before traversePath is invoked,
177
+ // since traversePath closes over entryConfig).
178
+ const entryConfig = resolveEntryConfig(activityPath.toString());
179
+
180
+ var files = traversePath(activityPath.toString(), true);
101
181
 
102
182
  // Check if we found any files to build
103
183
  if (!files || files.length === 0) {
@@ -186,35 +266,130 @@ plugins.push({
186
266
  const project = process.env.ACTIVITY_FOLDER_NAME || 'Project';
187
267
  const info = stats.toJson({ all: false, assets: true, modules: true, version: true, timings: true });
188
268
 
189
- const buildModules = Object.entries(entries).map(([out, src], idx) => {
190
- const parts = out.split(path.sep);
191
- let name = parts[parts.length - 3] || `Module-${idx + 1}`;
192
- name = name.replace(/dist|build/gi, '').replace(/[-_]/g, '').trim() || `Module-${idx + 1}`;
193
- return { idx: idx + 1, name, path: out };
194
- });
269
+ // Read build config for grouping context
270
+ let configVariations = {};
271
+ let configType = 'ab';
272
+ const summaryConfigPaths = [
273
+ path.join(activityPath, 'shared', 'build.config.json'),
274
+ path.join(activityPath, 'Shared', 'build.config.json')
275
+ ];
276
+ const cfgPath = summaryConfigPaths.find(p => fs.existsSync(p));
277
+ if (cfgPath) {
278
+ try {
279
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
280
+ configVariations = cfg.activityInfo?.variations || {};
281
+ configType = (cfg.activityInfo?.activityType || 'ab').toUpperCase();
282
+ } catch (_e) { /* ignore — fall back to single-page summary */ }
283
+ }
284
+
285
+ const isMultiPage = Object.values(configVariations).some(v => typeof v === 'object' && v !== null && v.pages);
195
286
 
196
287
  const duration = (stats.endTime - stats.startTime) / 1000;
197
288
  const mode = environment === PRODUCTION ? 'Production' : 'Development';
198
289
 
199
290
  console.log(kleur.cyan(`\n📦 ${mode} Build Summary`));
200
291
  console.log(kleur.gray('├── ') + kleur.bold('Activity: ') + kleur.yellow(project));
201
- console.log(kleur.gray('├── ') + kleur.bold('Modules: '));
202
-
203
- buildModules.forEach((m, i) => {
204
- const assetPath = m.path.replace(/^\/+/, '');
205
- const assetName = assetPath + '.js';
206
- const asset = info.assets.find(a => a.name === assetName);
207
- const sizeKB = asset ? (asset.size / 1024).toFixed(2) : '0.00';
208
- const absPath = path.resolve(PWD, assetPath.replace(/\/build$/, ""), environment === PRODUCTION ? 'at-build.html' : 'build.js');
209
- const atBuildLink = makeClickableLink(absPath, 1, 1);
210
-
211
- const prefix = (i === buildModules.length - 1) ? '└──' : '├──';
212
- console.log(kleur.gray(`│ ${prefix} `) + kleur.green(`${m.name} (${sizeKB} KB)`));
213
- if (environment === PRODUCTION) {
214
- console.log(`🔗 ${kleur.blue(`${m.name} Production Build`)} 🔗`);
215
- console.log(`${kleur.gray().underline(atBuildLink)}`);
216
- }
217
- });
292
+ console.log(kleur.gray('├── ') + kleur.bold('Type: ') + kleur.magenta(configType));
293
+
294
+ if (isMultiPage) {
295
+ // Group entries by experience, with each page nested under it.
296
+ const groups = {};
297
+ Object.entries(entries).forEach(([out]) => {
298
+ let matchedExp = null;
299
+ let matchedPage = null;
300
+
301
+ for (const [folder, config] of Object.entries(configVariations)) {
302
+ if (typeof config === 'object' && config && config.pages) {
303
+ for (const [pageName, subpath] of Object.entries(config.pages)) {
304
+ if (out.includes(`/${subpath}/`) || out.includes(`\\${subpath}\\`)) {
305
+ matchedExp = folder;
306
+ matchedPage = pageName;
307
+ break;
308
+ }
309
+ }
310
+ }
311
+ if (matchedExp) break;
312
+ }
313
+
314
+ if (!matchedExp) {
315
+ // Likely a buildFolders entry (e.g. Vanalytics)
316
+ const parts = out.split(path.sep);
317
+ const folderName = parts[parts.length - 3] || 'Unknown';
318
+ matchedExp = '📊 Build Folders';
319
+ matchedPage = folderName;
320
+ }
321
+
322
+ if (!groups[matchedExp]) groups[matchedExp] = [];
323
+ groups[matchedExp].push({ page: matchedPage, out });
324
+ });
325
+
326
+ console.log(kleur.gray('├── ') + kleur.bold('Modules:'));
327
+
328
+ const groupKeys = Object.keys(groups);
329
+ groupKeys.forEach((expName, gi) => {
330
+ const isLastGroup = gi === groupKeys.length - 1;
331
+ const groupPrefix = isLastGroup ? '└──' : '├──';
332
+ const childBranch = isLastGroup ? ' ' : '│ ';
333
+
334
+ const configEntry = configVariations[expName];
335
+ const displayName = (typeof configEntry === 'object' && configEntry ? configEntry.experienceName : configEntry) || expName;
336
+ const isControl = displayName.toString().toLowerCase().includes('control');
337
+ const labelColor = isControl ? kleur.blue : kleur.yellow;
338
+
339
+ console.log(kleur.gray(`│ ${groupPrefix} `) + labelColor().bold(`${displayName}`));
340
+
341
+ groups[expName].forEach((item, pi) => {
342
+ const isLastPage = pi === groups[expName].length - 1;
343
+ const pagePrefix = isLastPage ? '└──' : '├──';
344
+
345
+ const assetPath = item.out.replace(/^\/+/, '');
346
+ const assetName = assetPath + '.js';
347
+ const asset = info.assets.find(a => a.name === assetName);
348
+ const sizeKB = asset ? (asset.size / 1024).toFixed(2) : '0.00';
349
+
350
+ console.log(kleur.gray(`│ ${childBranch}${pagePrefix} `) + kleur.green(`${item.page} (${sizeKB} KB)`));
351
+
352
+ if (environment === PRODUCTION) {
353
+ const absPath = path.resolve(PWD, assetPath.replace(/\/build$/, ""), 'at-build.html');
354
+ const atBuildLink = makeClickableLink(absPath, 1, 1);
355
+ console.log(` ${kleur.gray(childBranch)} 🔗 ${kleur.blue(`${displayName}/${item.page}`)} 🔗`);
356
+ console.log(` ${kleur.gray(childBranch)} ${kleur.gray().underline(atBuildLink)}`);
357
+ }
358
+ });
359
+ });
360
+ } else {
361
+ // Single-page: flat list (matches at-builder's pre-multi-page output)
362
+ console.log(kleur.gray('├── ') + kleur.bold('Modules:'));
363
+
364
+ const buildModules = Object.entries(entries).map(([out, src], idx) => {
365
+ const parts = out.split(path.sep);
366
+ let name = parts[parts.length - 3] || `Module-${idx + 1}`;
367
+ name = name.replace(/dist|build/gi, '').replace(/[-_]/g, '').trim() || `Module-${idx + 1}`;
368
+ const configKey = Object.keys(configVariations).find(k => k.toLowerCase() === name.toLowerCase());
369
+ const configEntry = configKey ? configVariations[configKey] : null;
370
+ const displayName = (typeof configEntry === 'string' ? configEntry : null) || name;
371
+ const isControl = displayName.toString().toLowerCase().includes('control');
372
+ return { name: displayName.toString(), path: out, isControl };
373
+ });
374
+
375
+ buildModules.forEach((m, i) => {
376
+ const assetPath = m.path.replace(/^\/+/, '');
377
+ const assetName = assetPath + '.js';
378
+ const asset = info.assets.find(a => a.name === assetName);
379
+ const sizeKB = asset ? (asset.size / 1024).toFixed(2) : '0.00';
380
+ const absPath = path.resolve(PWD, assetPath.replace(/\/build$/, ""), environment === PRODUCTION ? 'at-build.html' : 'build.js');
381
+ const atBuildLink = makeClickableLink(absPath, 1, 1);
382
+
383
+ const prefix = (i === buildModules.length - 1) ? '└──' : '├──';
384
+ const labelColor = m.isControl ? kleur.blue : kleur.yellow;
385
+ console.log(kleur.gray(`│ ${prefix} `) + labelColor(`${m.name} (${sizeKB} KB)`));
386
+ if (environment === PRODUCTION) {
387
+ console.log(`🔗 ${kleur.blue(`${m.name} Production Build`)} 🔗`);
388
+ console.log(`${kleur.gray().underline(atBuildLink)}`);
389
+ }
390
+ });
391
+ }
392
+
218
393
  console.log(kleur.gray('│ '));
219
394
  console.log(kleur.gray('├── ') + kleur.bold('Assets: ') + kleur.magenta(info.assets.length));
220
395
  console.log(kleur.gray('├── ') + kleur.bold('Modules: ') + kleur.magenta(info.modules.length));
@@ -230,7 +405,21 @@ if (isProdMode) {
230
405
  fileName: "at-build",
231
406
  fileType: "html",
232
407
  header: function (source, outputPath) {
233
- return `<script targetExp="${process.env.ACTIVITY_FOLDER_NAME}" variation="${(outputPath.split(path.sep)).slice(-3, -1)[1]}" type="application/javascript">`;
408
+ // outputPath is the dist folder. The generator passes it through
409
+ // path.join(targetPath, "../"), which leaves a trailing separator,
410
+ // so split() would produce an empty trailing element and skew the
411
+ // slice indexes. Strip trailing separators first.
412
+ // single-page: …/Activities/<activity>/Variation-1/dist
413
+ // multi-page: …/Activities/<activity>/Variation-1/Global/dist
414
+ const cleanPath = outputPath.replace(/[\\/]+$/, '');
415
+ const parts = cleanPath.split(path.sep);
416
+ // parts ends with the dist folder; the variation/page folder is its parent.
417
+ const folderName = parts[parts.length - 2];
418
+ const isNestedPage = entryConfig.nested && entryConfig.nested.has(folderName);
419
+ const variation = isNestedPage
420
+ ? `${parts[parts.length - 3]}/${folderName}` // e.g. "Variation-1/Global"
421
+ : folderName; // e.g. "Variation-1"
422
+ return `<script targetExp="${process.env.ACTIVITY_FOLDER_NAME}" variation="${variation}" type="application/javascript">`;
234
423
  },
235
424
  footer: function () {
236
425
  return `</script>`;
@@ -308,7 +497,7 @@ module.exports = {
308
497
  options: {
309
498
  injectType: 'singletonStyleTag', // styleTag | singletonStyleTag | lazyStyleTag | lazySingletonStyleTag | linkTag
310
499
  attributes: {
311
- id: process.env.STYLE_ID || "ddo-styles"
500
+ id: process.env.STYLE_ID || "atb-styles"
312
501
  }
313
502
  // sourceMap: environment !== PRODUCTION
314
503
  // insert: "body" | (element) => {}
@@ -1,7 +0,0 @@
1
- (function(){
2
- if(!window.{{hashId}}){
3
- //# sourceURL={{hashId}}.js
4
- {{build}}
5
- window.{{hashId}} = true;
6
- }
7
- })();
@@ -1,7 +0,0 @@
1
- {
2
- "activityInfo": {
3
- "id": null,
4
- "name": "",
5
- "variations": {}
6
- }
7
- }
@@ -1,18 +0,0 @@
1
- export class Observer {
2
- constructor() {
3
- this.config = {
4
- childlist: true,
5
- subtree: true,
6
- attributes: true
7
- }
8
- }
9
-
10
- setObserver(cb) {
11
- const mt = new MutationObserver(cb);
12
- if (window.NodeList && !NodeList.prototype.forEach) {
13
- NodeList.prototype.forEach = Array.prototype.forEach;
14
- }
15
- mt.observe(document.body, this.config);
16
- }
17
-
18
- }