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/.claude/settings.local.json +53 -11
- package/.plop/constants/index.js +0 -7
- package/.plop/generators/actions.js +217 -126
- package/.plop/generators/prompts.js +50 -18
- package/.plop/utils/index.js +19 -5
- package/.vscode/settings.json +6 -0
- package/DEVELOPMENT.md +164 -0
- package/README.md +16 -1
- package/at-builder-0.0.2.vsix +0 -0
- package/bin/constants/config.js +169 -167
- package/bin/index.js +494 -182
- package/bin/services/doctor.js +752 -290
- package/bin/services/logger.js +40 -20
- package/lib/at-deploy.js +379 -145
- package/lib/at-sync.js +455 -0
- package/lib/eslint-flat-config-plugin.js +34 -33
- package/lib/install-checks.js +236 -0
- package/lib/postinstall.js +90 -0
- package/package/package.json +86 -0
- package/package.json +18 -11
- package/puppeteer.js +128 -32
- package/src/constants/config.ts +84 -9
- package/src/index.ts +131 -11
- package/src/services/doctor.ts +377 -39
- package/webpack.config.js +228 -39
- package/.plop/templates/build-template.hbs +0 -7
- package/.plop/templates/build.config.hbs +0 -7
- package/.plop/templates/observer.hbs +0 -18
package/webpack.config.js
CHANGED
|
@@ -40,12 +40,76 @@ let ignoreFolders = [
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
-
|
|
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
|
|
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 (
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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('
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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 || "
|
|
500
|
+
id: process.env.STYLE_ID || "atb-styles"
|
|
312
501
|
}
|
|
313
502
|
// sourceMap: environment !== PRODUCTION
|
|
314
503
|
// insert: "body" | (element) => {}
|
|
@@ -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
|
-
}
|