at-builder 1.2.9 → 1.4.0
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 +23 -8
- 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 +107 -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/lib/at-sync.js
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* at-sync.js — Sync build.config.json with Adobe Target activity details
|
|
6
|
+
* ----------------------------------------------------------------------
|
|
7
|
+
* Reads activity id/type from build.config.json, fetches the full activity
|
|
8
|
+
* from the AT API, and rewrites build.config.json with:
|
|
9
|
+
* - Activity name
|
|
10
|
+
* - Experiences/variations with correct names
|
|
11
|
+
* - Pages and locationLocalId mapping (for multi-page activities)
|
|
12
|
+
* - locationsMeta block (informational; preserved across syncs)
|
|
13
|
+
*
|
|
14
|
+
* Usage (via CLI):
|
|
15
|
+
* atb sync [--scaffold]
|
|
16
|
+
*
|
|
17
|
+
* Usage (direct, when developing at-builder itself):
|
|
18
|
+
* node lib/at-sync.js [--scaffold]
|
|
19
|
+
*
|
|
20
|
+
* Path resolution:
|
|
21
|
+
* at-builder is a globally-installed CLI. Every consumer file resolves against
|
|
22
|
+
* PWD = process.env.executionPath || process.cwd() so that the user's project
|
|
23
|
+
* directory is the source of truth, not at-builder's install location.
|
|
24
|
+
*
|
|
25
|
+
* Environment variables (consumer's .env):
|
|
26
|
+
* ACTIVITY_FOLDER_NAME (required)
|
|
27
|
+
* ACTIVITIES_BASE_FOLDER (optional, defaults to "Activities")
|
|
28
|
+
* ADOBE_CLIENT_ID (required)
|
|
29
|
+
* ADOBE_CLIENT_SECRET (required)
|
|
30
|
+
* ADOBE_TENANT (optional, your AT tenant slug — enables clickable AT UI link)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/* eslint-env node */
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const path = require('path');
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const axios = require('axios');
|
|
39
|
+
const qs = require('querystring');
|
|
40
|
+
const dotenv = require('dotenv');
|
|
41
|
+
|
|
42
|
+
const PWD = process.env.executionPath || process.cwd();
|
|
43
|
+
dotenv.config({ path: path.join(PWD, '.env'), quiet: true });
|
|
44
|
+
|
|
45
|
+
const SCAFFOLD = process.argv.includes('--scaffold');
|
|
46
|
+
|
|
47
|
+
// Fallback to ANSI codes since chalk 5.x is ESM only
|
|
48
|
+
const chalk = {
|
|
49
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
50
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
51
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
52
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
53
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
54
|
+
gray: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
55
|
+
white: (s) => `\x1b[37m${s}\x1b[0m`
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ─── Validate env ───
|
|
59
|
+
if (!process.env.ACTIVITY_FOLDER_NAME || process.env.ACTIVITY_FOLDER_NAME.trim() === '') {
|
|
60
|
+
console.error(chalk.red('❌ Error: ACTIVITY_FOLDER_NAME environment variable is required but not set.'));
|
|
61
|
+
console.error(chalk.yellow('💡 Set ACTIVITY_FOLDER_NAME in your .env file or run "atb doctor --fix".'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (!process.env.ADOBE_CLIENT_ID || !process.env.ADOBE_CLIENT_SECRET) {
|
|
65
|
+
console.error(chalk.red('❌ Error: ADOBE_CLIENT_ID and ADOBE_CLIENT_SECRET are required.'));
|
|
66
|
+
console.error(chalk.yellow('💡 Set your Adobe Target API credentials in your .env file.'));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { BASE_URL, IMS_TOKEN_URL, IMS_SCOPE } = require(path.join(PWD, 'adobe.config'));
|
|
71
|
+
|
|
72
|
+
const API_KEY = process.env.ADOBE_CLIENT_ID;
|
|
73
|
+
const ACTIVITIES_BASE_FOLDER = process.env.ACTIVITIES_BASE_FOLDER || 'Activities';
|
|
74
|
+
const ACTIVITY_FOLDER = process.env.ACTIVITY_FOLDER_NAME.trim();
|
|
75
|
+
const BUILD_ROOT = path.join(PWD, ACTIVITIES_BASE_FOLDER, ACTIVITY_FOLDER);
|
|
76
|
+
|
|
77
|
+
const CONFIG_PATHS = [
|
|
78
|
+
path.join(BUILD_ROOT, 'shared', 'build.config.json'),
|
|
79
|
+
path.join(BUILD_ROOT, 'Shared', 'build.config.json')
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// ─── Auth ───
|
|
83
|
+
async function fetchToken() {
|
|
84
|
+
const data = qs.stringify({
|
|
85
|
+
grant_type: 'client_credentials',
|
|
86
|
+
client_id: process.env.ADOBE_CLIENT_ID,
|
|
87
|
+
client_secret: process.env.ADOBE_CLIENT_SECRET,
|
|
88
|
+
scope: IMS_SCOPE
|
|
89
|
+
});
|
|
90
|
+
const res = await axios.post(IMS_TOKEN_URL, data, {
|
|
91
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
92
|
+
});
|
|
93
|
+
return res.data.access_token;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Build a clickable AT UI link if ADOBE_TENANT is set; otherwise return null.
|
|
97
|
+
function buildAtUiLink(activityType, activityId) {
|
|
98
|
+
const tenant = (process.env.ADOBE_TENANT || '').trim();
|
|
99
|
+
if (!tenant) return null;
|
|
100
|
+
return `https://experience.adobe.com/#/@${tenant}/target/activities/${activityType}/${activityId}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Main ───
|
|
104
|
+
(async () => {
|
|
105
|
+
try {
|
|
106
|
+
console.log(chalk.cyan(`📦 Syncing activity: ${ACTIVITY_FOLDER}`));
|
|
107
|
+
console.log(chalk.cyan(`📁 Activities folder: ${ACTIVITIES_BASE_FOLDER}`));
|
|
108
|
+
|
|
109
|
+
// 1. Read existing config
|
|
110
|
+
const configPath = CONFIG_PATHS.find(p => fs.existsSync(p));
|
|
111
|
+
if (!configPath) {
|
|
112
|
+
console.error(chalk.red('❌ No build.config.json found. Checked:'));
|
|
113
|
+
CONFIG_PATHS.forEach(p => console.error(chalk.gray(` ${p}`)));
|
|
114
|
+
console.error(chalk.yellow('💡 Run "atb new" to create an activity, then add the activity id to build.config.json.'));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
119
|
+
const { id, activityType } = config.activityInfo || {};
|
|
120
|
+
|
|
121
|
+
if (!id) {
|
|
122
|
+
console.error(chalk.red('❌ No activity id found in build.config.json'));
|
|
123
|
+
console.error(chalk.yellow('💡 Edit shared/build.config.json and set activityInfo.id to your AT activity id.'));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const type = (activityType || 'ab').toLowerCase();
|
|
128
|
+
console.log(chalk.cyan(`\n🔄 Syncing activity ${type.toUpperCase()} #${id}...`));
|
|
129
|
+
|
|
130
|
+
// 2. Fetch from AT API
|
|
131
|
+
const token = await fetchToken();
|
|
132
|
+
const activityUrl = `${BASE_URL}${type}/${id}`;
|
|
133
|
+
|
|
134
|
+
const res = await axios.get(activityUrl, {
|
|
135
|
+
headers: {
|
|
136
|
+
'X-Api-Key': API_KEY,
|
|
137
|
+
Accept: 'application/vnd.adobe.target.v3+json',
|
|
138
|
+
Authorization: `Bearer ${token}`
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const activity = res.data;
|
|
143
|
+
console.log(chalk.green(`✓ Fetched: "${activity.name}" (state: ${activity.state})`));
|
|
144
|
+
|
|
145
|
+
// 3. Extract locations (pages)
|
|
146
|
+
const selectors = activity.locations?.selectors || activity.locations?.mboxes || [];
|
|
147
|
+
const hasMultipleLocations = selectors.length > 1;
|
|
148
|
+
|
|
149
|
+
if (hasMultipleLocations) {
|
|
150
|
+
console.log(chalk.cyan(`\n📄 Pages (${selectors.length} locations):`));
|
|
151
|
+
selectors.forEach(s => {
|
|
152
|
+
console.log(chalk.gray(` [${s.locationLocalId}] ${s.name}`));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4. Extract experiences
|
|
157
|
+
console.log(chalk.cyan(`\n👥 Experiences (${activity.experiences.length}):`));
|
|
158
|
+
activity.experiences.forEach(exp => {
|
|
159
|
+
const optionIds = [...new Set(exp.optionLocations.map(ol => ol.optionLocalId))];
|
|
160
|
+
console.log(chalk.gray(` ${exp.name} → options: [${optionIds.join(', ')}]`));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// 5. Build updated config
|
|
164
|
+
const updatedConfig = { activityInfo: {} };
|
|
165
|
+
updatedConfig.activityInfo.id = id;
|
|
166
|
+
updatedConfig.activityInfo.activityType = type;
|
|
167
|
+
updatedConfig.activityInfo.name = activity.name;
|
|
168
|
+
|
|
169
|
+
if (hasMultipleLocations) {
|
|
170
|
+
// Preserve existing page names (AT API only returns generic "Location N";
|
|
171
|
+
// descriptive names live in the UI and the config carries them across syncs).
|
|
172
|
+
const existingPages = config.activityInfo?.pages || {};
|
|
173
|
+
const locIdToExistingName = {};
|
|
174
|
+
for (const [name, locId] of Object.entries(existingPages)) {
|
|
175
|
+
locIdToExistingName[locId] = name;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
updatedConfig.activityInfo.pages = {};
|
|
179
|
+
const hasGenericNames = [];
|
|
180
|
+
selectors.forEach(s => {
|
|
181
|
+
const customName = locIdToExistingName[s.locationLocalId];
|
|
182
|
+
const pageName = customName || s.name;
|
|
183
|
+
updatedConfig.activityInfo.pages[pageName] = s.locationLocalId;
|
|
184
|
+
if (!customName) hasGenericNames.push(pageName);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (hasGenericNames.length > 0) {
|
|
188
|
+
console.log(chalk.yellow(`\n⚠️ ${hasGenericNames.length} page(s) have generic names: ${hasGenericNames.join(', ')}`));
|
|
189
|
+
console.log(chalk.yellow(' Rename them in build.config.json → pages to match your AT UI page names.'));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const locIdToName = {};
|
|
193
|
+
for (const [name, locId] of Object.entries(updatedConfig.activityInfo.pages)) {
|
|
194
|
+
locIdToName[locId] = name;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
updatedConfig.activityInfo.variations = {};
|
|
198
|
+
|
|
199
|
+
activity.experiences.forEach(exp => {
|
|
200
|
+
const expName = exp.name.trim();
|
|
201
|
+
const folderName = expName.replace(/\s+/g, '-');
|
|
202
|
+
|
|
203
|
+
const expPages = {};
|
|
204
|
+
exp.optionLocations.forEach(ol => {
|
|
205
|
+
// Skip locations using Default Content (optionLocalId 0) —
|
|
206
|
+
// they have no code to deploy, so no asset path needed.
|
|
207
|
+
if (ol.optionLocalId === 0) return;
|
|
208
|
+
|
|
209
|
+
const pageName = locIdToName[ol.locationLocalId];
|
|
210
|
+
if (pageName) {
|
|
211
|
+
const pageFolder = pageName
|
|
212
|
+
.replace(/[^a-zA-Z0-9\s]/g, '')
|
|
213
|
+
.replace(/\s+/g, '')
|
|
214
|
+
.trim();
|
|
215
|
+
expPages[pageName] = `${folderName}/${pageFolder}`;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
updatedConfig.activityInfo.variations[folderName] = {
|
|
220
|
+
experienceName: expName,
|
|
221
|
+
pages: expPages
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ── locationsMeta: human-readable location reference ──
|
|
226
|
+
// Purely informational — never read by at-deploy.js.
|
|
227
|
+
// Shows selector, audiences, and per-experience active/inactive status.
|
|
228
|
+
// The "note" field is freeform and preserved across syncs.
|
|
229
|
+
{
|
|
230
|
+
const existingMeta = config.activityInfo?.locationsMeta || {};
|
|
231
|
+
|
|
232
|
+
const expContentMap = {};
|
|
233
|
+
activity.experiences.forEach(exp => {
|
|
234
|
+
const folderName = exp.name.trim().replace(/\s+/g, '-');
|
|
235
|
+
expContentMap[folderName] = {};
|
|
236
|
+
exp.optionLocations.forEach(ol => {
|
|
237
|
+
expContentMap[folderName][ol.locationLocalId] = ol.optionLocalId !== 0;
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const locationsMeta = {};
|
|
242
|
+
selectors.forEach(s => {
|
|
243
|
+
const pageName = locIdToName[s.locationLocalId] || s.name;
|
|
244
|
+
const existing = existingMeta[pageName] || {};
|
|
245
|
+
|
|
246
|
+
const experiences = {};
|
|
247
|
+
Object.entries(expContentMap).forEach(([expFolder, locMap]) => {
|
|
248
|
+
const hasContent = locMap[s.locationLocalId];
|
|
249
|
+
experiences[expFolder] = hasContent === true ? 'active'
|
|
250
|
+
: hasContent === false ? 'inactive'
|
|
251
|
+
: 'not-assigned';
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
locationsMeta[pageName] = {
|
|
255
|
+
id: s.locationLocalId,
|
|
256
|
+
selector: s.selector,
|
|
257
|
+
note: existing.note || '',
|
|
258
|
+
experiences
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
updatedConfig.activityInfo.locationsMeta = locationsMeta;
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
// Single-page: simple string variations
|
|
266
|
+
updatedConfig.activityInfo.variations = {};
|
|
267
|
+
activity.experiences.forEach(exp => {
|
|
268
|
+
const expName = exp.name.trim();
|
|
269
|
+
const folderName = expName.replace(/\s+/g, '-');
|
|
270
|
+
updatedConfig.activityInfo.variations[folderName] = expName;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Preserve buildFolders if present (Vanalytics, etc.)
|
|
275
|
+
if (config.activityInfo?.buildFolders) {
|
|
276
|
+
updatedConfig.activityInfo.buildFolders = config.activityInfo.buildFolders;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 6. Write updated config
|
|
280
|
+
fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 4) + '\n');
|
|
281
|
+
console.log(chalk.green(`\n✅ Updated ${configPath}`));
|
|
282
|
+
|
|
283
|
+
// 7. Summary
|
|
284
|
+
console.log(chalk.bold('\n--- Sync Summary ---'));
|
|
285
|
+
console.log(chalk.white(`Activity: ${activity.name}`));
|
|
286
|
+
console.log(chalk.white(`Type: ${type.toUpperCase()}`));
|
|
287
|
+
console.log(chalk.white(`State: ${activity.state}`));
|
|
288
|
+
console.log(chalk.white(`Pages: ${hasMultipleLocations ? selectors.length : 1}`));
|
|
289
|
+
console.log(chalk.white(`Experiences: ${activity.experiences.length}`));
|
|
290
|
+
|
|
291
|
+
const variations = updatedConfig.activityInfo.variations;
|
|
292
|
+
console.log(chalk.cyan('\nGenerated variations:'));
|
|
293
|
+
Object.entries(variations).forEach(([folder, value]) => {
|
|
294
|
+
if (typeof value === 'string') {
|
|
295
|
+
console.log(chalk.gray(` ${folder}/ → "${value}"`));
|
|
296
|
+
} else {
|
|
297
|
+
console.log(chalk.gray(` ${folder}/ → "${value.experienceName}"`));
|
|
298
|
+
Object.entries(value.pages).forEach(([page, subpath]) => {
|
|
299
|
+
console.log(chalk.gray(` └── ${page} → ${subpath}/`));
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// 8. Folder status
|
|
305
|
+
console.log(chalk.cyan('\nFolder status:'));
|
|
306
|
+
const foldersToCreate = [];
|
|
307
|
+
Object.entries(variations).forEach(([folder, value]) => {
|
|
308
|
+
if (typeof value === 'string') {
|
|
309
|
+
const fullPath = path.join(BUILD_ROOT, folder);
|
|
310
|
+
const exists = fs.existsSync(fullPath);
|
|
311
|
+
console.log(` ${exists ? '✅' : '❌'} ${folder}/`);
|
|
312
|
+
if (!exists) foldersToCreate.push(folder);
|
|
313
|
+
} else {
|
|
314
|
+
Object.values(value.pages).forEach(subpath => {
|
|
315
|
+
const fullPath = path.join(BUILD_ROOT, subpath);
|
|
316
|
+
const exists = fs.existsSync(fullPath);
|
|
317
|
+
console.log(` ${exists ? '✅' : '❌'} ${subpath}/`);
|
|
318
|
+
if (!exists) foldersToCreate.push(subpath);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (foldersToCreate.length > 0) {
|
|
324
|
+
if (SCAFFOLD) {
|
|
325
|
+
console.log(chalk.green(`\n📁 Scaffolding ${foldersToCreate.length} missing folder(s)...`));
|
|
326
|
+
foldersToCreate.forEach(f => {
|
|
327
|
+
const fullPath = path.join(BUILD_ROOT, f);
|
|
328
|
+
scaffoldFolder(fullPath);
|
|
329
|
+
});
|
|
330
|
+
console.log(chalk.green('✅ All folders scaffolded!'));
|
|
331
|
+
} else {
|
|
332
|
+
console.log(chalk.yellow(`\n⚠️ ${foldersToCreate.length} folder(s) don't exist yet.`));
|
|
333
|
+
console.log(chalk.yellow(' Run "atb sync --scaffold" to auto-create, or create manually:'));
|
|
334
|
+
foldersToCreate.forEach(f => {
|
|
335
|
+
console.log(chalk.gray(` mkdir -p "${path.join(BUILD_ROOT, f)}"`));
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const atUiLink = buildAtUiLink(type, id);
|
|
341
|
+
if (atUiLink) {
|
|
342
|
+
console.log(chalk.cyan(`\n🔗 Activity: ${atUiLink}`));
|
|
343
|
+
} else {
|
|
344
|
+
console.log(chalk.cyan(`\n🔗 Activity ID: ${id} (set ADOBE_TENANT in .env for a clickable link)`));
|
|
345
|
+
}
|
|
346
|
+
console.log(chalk.bold('\n--------------------\n'));
|
|
347
|
+
} catch (err) {
|
|
348
|
+
if (err.response) {
|
|
349
|
+
console.error(chalk.red('❌ API Error:'), err.response.data);
|
|
350
|
+
} else {
|
|
351
|
+
console.error(chalk.red('❌ Error:'), err.message);
|
|
352
|
+
}
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
})();
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Scaffold a missing variation folder with at-builder's variation skeleton:
|
|
359
|
+
* index.js, scripts/app.js, css/style.scss, constants/index.js
|
|
360
|
+
*
|
|
361
|
+
* Mirrors the boilerplate emitted by .plop/templates so scaffolded variations
|
|
362
|
+
* are indistinguishable from ones created via `atb new`.
|
|
363
|
+
*/
|
|
364
|
+
function scaffoldFolder(fullPath) {
|
|
365
|
+
const indexContent = `/**
|
|
366
|
+
* This is the test entry point and main variations gets executed from here
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
// Load styles
|
|
370
|
+
import "./css/style.scss";
|
|
371
|
+
|
|
372
|
+
import { App } from './scripts/app';
|
|
373
|
+
const app = new App();
|
|
374
|
+
app.init();
|
|
375
|
+
`;
|
|
376
|
+
|
|
377
|
+
const appContent = `// import * as CONSTANTS from '../constants';
|
|
378
|
+
|
|
379
|
+
export class App {
|
|
380
|
+
/**
|
|
381
|
+
* @param define constructor properties
|
|
382
|
+
*/
|
|
383
|
+
constructor() {
|
|
384
|
+
this.state = {}; // For managing app state
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Initializes the application, sets up observer and logs the event
|
|
389
|
+
*/
|
|
390
|
+
init = () => {
|
|
391
|
+
this.isRunning = true;
|
|
392
|
+
this.onStart();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Starts the app or app-specific functionality
|
|
397
|
+
*/
|
|
398
|
+
onStart = () => {
|
|
399
|
+
// Additional initialization logic (e.g., setting up listeners, data fetching)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Reset the app to its initial state
|
|
404
|
+
*/
|
|
405
|
+
reset = () => {
|
|
406
|
+
this.state = {};
|
|
407
|
+
this.onStart();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Disconnect mutation observer, clear any window variable or perform cleanup activity
|
|
412
|
+
*/
|
|
413
|
+
destroy = () => {
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Mutation Observer Callback
|
|
418
|
+
*/
|
|
419
|
+
callback = () => {
|
|
420
|
+
// Callback logic for mutation observer
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
`;
|
|
424
|
+
|
|
425
|
+
const styleContent = `/* Add variation specific scss properties here */
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
`;
|
|
429
|
+
|
|
430
|
+
const constantsContent = `/**
|
|
431
|
+
* Declare all Variation level constants here
|
|
432
|
+
*/
|
|
433
|
+
|
|
434
|
+
export const VariationConstants = {
|
|
435
|
+
sampleConstants: { country: "us" }
|
|
436
|
+
};
|
|
437
|
+
`;
|
|
438
|
+
|
|
439
|
+
const files = [
|
|
440
|
+
{ path: path.join(fullPath, 'index.js'), content: indexContent },
|
|
441
|
+
{ path: path.join(fullPath, 'scripts', 'app.js'), content: appContent },
|
|
442
|
+
{ path: path.join(fullPath, 'css', 'style.scss'), content: styleContent },
|
|
443
|
+
{ path: path.join(fullPath, 'constants', 'index.js'), content: constantsContent }
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
files.forEach(({ path: filePath, content }) => {
|
|
447
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
448
|
+
if (!fs.existsSync(filePath)) {
|
|
449
|
+
fs.writeFileSync(filePath, content);
|
|
450
|
+
console.log(chalk.green(` ✅ Created ${path.relative(BUILD_ROOT, filePath)}`));
|
|
451
|
+
} else {
|
|
452
|
+
console.log(chalk.gray(` ⏭️ Skipped ${path.relative(BUILD_ROOT, filePath)} (exists)`));
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
@@ -68,8 +68,16 @@ class ESLintFlatConfigPlugin {
|
|
|
68
68
|
...overrideConfigs
|
|
69
69
|
];
|
|
70
70
|
|
|
71
|
+
// overrideConfigFile: true tells ESLint v9 to skip the
|
|
72
|
+
// cwd-based flat-config lookup entirely and treat
|
|
73
|
+
// overrideConfig as the complete configuration. Without
|
|
74
|
+
// this, ESLint walks the consumer's PWD looking for an
|
|
75
|
+
// `eslint.config.js` (which they don't have, since at-builder
|
|
76
|
+
// ships its own) and fails the build with "Could not find
|
|
77
|
+
// config file." even though we provided everything inline.
|
|
71
78
|
const eslint = new ESLint({
|
|
72
79
|
cwd: this.options.context,
|
|
80
|
+
overrideConfigFile: true,
|
|
73
81
|
overrideConfig: mergedConfig,
|
|
74
82
|
ignore: false, // Don't use default ignore patterns
|
|
75
83
|
fix: this.options.fix // Enable auto-fix if option is set
|
|
@@ -82,44 +90,37 @@ class ESLintFlatConfigPlugin {
|
|
|
82
90
|
await ESLint.outputFixes(results);
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
// Push every ESLint message as a discrete error/warning on
|
|
94
|
+
// the compilation. Webpack renders each one with its file
|
|
95
|
+
// path, line/column, message, and rule. The build fails
|
|
96
|
+
// automatically when compilation.errors is non-empty, so
|
|
97
|
+
// there's no need to throw — throwing here used to abort
|
|
98
|
+
// the hook before webpack could render the queued errors,
|
|
99
|
+
// leaving the user with a generic "Build failed due to
|
|
100
|
+
// linting errors" and no detail.
|
|
88
101
|
for (const result of results) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
warning.file = result.filePath;
|
|
107
|
-
compilation.warnings.push(warning);
|
|
102
|
+
for (const message of result.messages) {
|
|
103
|
+
const formatted = new Error(
|
|
104
|
+
`[ESLint] ${result.filePath}:${message.line}:${message.column}\n ${message.message} (${message.ruleId})`
|
|
105
|
+
);
|
|
106
|
+
formatted.file = result.filePath;
|
|
107
|
+
|
|
108
|
+
const isError = message.severity === 2;
|
|
109
|
+
const isWarning = message.severity === 1;
|
|
110
|
+
const treatAsError =
|
|
111
|
+
(isError && this.options.failOnError) ||
|
|
112
|
+
(isWarning && this.options.failOnWarning);
|
|
113
|
+
|
|
114
|
+
if (treatAsError) {
|
|
115
|
+
compilation.errors.push(formatted);
|
|
116
|
+
} else if (isError || isWarning) {
|
|
117
|
+
compilation.warnings.push(formatted);
|
|
108
118
|
}
|
|
109
119
|
}
|
|
110
120
|
}
|
|
111
|
-
|
|
112
|
-
if (this.options.failOnError && hasErrors) {
|
|
113
|
-
throw new Error('[ESLint] Build failed due to linting errors');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (this.options.failOnWarning && hasWarnings) {
|
|
117
|
-
throw new Error('[ESLint] Build failed due to linting warnings');
|
|
118
|
-
}
|
|
119
121
|
} catch (error) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
122
|
+
// ESLint setup itself failed (config load, file resolution,
|
|
123
|
+
// etc.). Surface as a single compilation error.
|
|
123
124
|
compilation.errors.push(error);
|
|
124
125
|
}
|
|
125
126
|
});
|