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/src/services/doctor.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import logger from "./logger";
|
|
4
|
+
import { GITIGNORE_TEMPLATE } from "../constants/config";
|
|
5
|
+
|
|
6
|
+
// lib/install-checks.js is hand-written runtime JS (not TS-compiled). The
|
|
7
|
+
// relative path resolves correctly from both src/services/doctor.ts (source)
|
|
8
|
+
// and bin/services/doctor.js (compiled output) because src/ and bin/ have
|
|
9
|
+
// parallel structure. Typed inline since the module isn't part of the TS graph.
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
+
const installChecks = require("../../lib/install-checks") as {
|
|
12
|
+
checkInstallEnvironment: (opts?: { localBinScript?: string; fixPermissions?: boolean }) => {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
problems: string[];
|
|
15
|
+
globalBin: string;
|
|
16
|
+
shimPath: string;
|
|
17
|
+
shimExecutable: boolean;
|
|
18
|
+
isWin: boolean;
|
|
19
|
+
isGlobalInstall: boolean;
|
|
20
|
+
nodeMajor: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
4
23
|
|
|
5
24
|
export interface DiagnosticIssue {
|
|
6
25
|
id: string;
|
|
@@ -23,15 +42,24 @@ export const runDiagnostics = async (projectPath: string, verbose: boolean = fal
|
|
|
23
42
|
await checkEnvFile(projectPath, issues);
|
|
24
43
|
await checkAdobeConfig(projectPath, issues);
|
|
25
44
|
await checkWatchConfig(projectPath, issues);
|
|
45
|
+
await checkGitignore(projectPath, issues);
|
|
26
46
|
await checkPackageJson(projectPath, issues);
|
|
27
47
|
await checkActivitiesFolder(projectPath, issues);
|
|
28
|
-
|
|
48
|
+
|
|
29
49
|
// Check environment variables
|
|
30
50
|
await checkEnvVariables(projectPath, issues);
|
|
31
|
-
|
|
51
|
+
|
|
32
52
|
// Check dependencies
|
|
33
53
|
await checkDependencies(projectPath, issues);
|
|
34
|
-
|
|
54
|
+
|
|
55
|
+
// Check build.config.json content for every activity
|
|
56
|
+
await checkBuildConfigs(projectPath, issues);
|
|
57
|
+
|
|
58
|
+
// Check install environment (PATH, atb shim, exec bit). Mirrors the
|
|
59
|
+
// postinstall validator so users who installed with --ignore-scripts
|
|
60
|
+
// (or who dismissed the postinstall warning) can still self-diagnose.
|
|
61
|
+
await checkInstallEnv(issues);
|
|
62
|
+
|
|
35
63
|
return issues;
|
|
36
64
|
};
|
|
37
65
|
|
|
@@ -71,8 +99,10 @@ const fixIssue = async (issue: DiagnosticIssue, projectPath: string, verbose: bo
|
|
|
71
99
|
return await createEnvFile(projectPath);
|
|
72
100
|
case 'missing-adobe-config':
|
|
73
101
|
return await createAdobeConfig(projectPath);
|
|
74
|
-
case '
|
|
75
|
-
return await
|
|
102
|
+
case 'legacy-watch-config':
|
|
103
|
+
return await migrateWatchConfig(projectPath);
|
|
104
|
+
case 'missing-gitignore':
|
|
105
|
+
return await createGitignore(projectPath);
|
|
76
106
|
case 'missing-activities-folder':
|
|
77
107
|
return await createActivitiesFolder(projectPath);
|
|
78
108
|
case 'missing-package-json':
|
|
@@ -142,19 +172,42 @@ const checkAdobeConfig = async (projectPath: string, issues: DiagnosticIssue[]):
|
|
|
142
172
|
};
|
|
143
173
|
|
|
144
174
|
/**
|
|
145
|
-
*
|
|
175
|
+
* Flag a legacy watch-config.json. As of this release VARIATION/PAGE live in
|
|
176
|
+
* .env; watch-config.json is supported as a fallback (puppeteer.js prefers it
|
|
177
|
+
* if present with a deprecation warning) but new projects don't get one.
|
|
178
|
+
*
|
|
179
|
+
* The fix migrates VARIATION/PAGE into .env and deletes watch-config.json.
|
|
146
180
|
*/
|
|
147
181
|
const checkWatchConfig = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
148
182
|
const configPath = path.join(projectPath, 'watch-config.json');
|
|
149
|
-
|
|
150
|
-
|
|
183
|
+
if (!fs.existsSync(configPath)) return;
|
|
184
|
+
|
|
185
|
+
issues.push({
|
|
186
|
+
id: 'legacy-watch-config',
|
|
187
|
+
severity: 'warning',
|
|
188
|
+
message: 'watch-config.json is deprecated — VARIATION/PAGE now live in .env',
|
|
189
|
+
suggestion: 'Run "atb doctor --fix" to migrate values into .env and delete watch-config.json',
|
|
190
|
+
fixable: true,
|
|
191
|
+
file: 'watch-config.json'
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if .gitignore exists. Flags as a warning (not blocking) but fixable —
|
|
197
|
+
* `atb doctor --fix` writes the default template covering .env, .deploy-lock,
|
|
198
|
+
* dist/, etc. Does not modify an existing .gitignore.
|
|
199
|
+
*/
|
|
200
|
+
const checkGitignore = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
201
|
+
const gitignorePath = path.join(projectPath, '.gitignore');
|
|
202
|
+
|
|
203
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
151
204
|
issues.push({
|
|
152
|
-
id: 'missing-
|
|
205
|
+
id: 'missing-gitignore',
|
|
153
206
|
severity: 'warning',
|
|
154
|
-
message: '
|
|
155
|
-
suggestion: 'Create
|
|
207
|
+
message: '.gitignore file is missing',
|
|
208
|
+
suggestion: 'Create .gitignore with at-builder defaults (.env, .deploy-lock, dist/, etc.)',
|
|
156
209
|
fixable: true,
|
|
157
|
-
file: '
|
|
210
|
+
file: '.gitignore'
|
|
158
211
|
});
|
|
159
212
|
}
|
|
160
213
|
};
|
|
@@ -177,24 +230,26 @@ const checkPackageJson = async (projectPath: string, issues: DiagnosticIssue[]):
|
|
|
177
230
|
}
|
|
178
231
|
};
|
|
179
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Resolve the configured activities folder name. Reads ACTIVITIES_BASE_FOLDER
|
|
235
|
+
* from the consumer .env (matching webpack/at-deploy/at-sync resolution), falls
|
|
236
|
+
* back to the default "Activities".
|
|
237
|
+
*/
|
|
238
|
+
const resolveActivitiesFolderName = (projectPath: string): string => {
|
|
239
|
+
const envPath = path.join(projectPath, '.env');
|
|
240
|
+
if (!fs.existsSync(envPath)) return 'Activities';
|
|
241
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
242
|
+
const match = envContent.match(/ACTIVITIES_BASE_FOLDER=["']?([^"'\n\r]+)["']?/);
|
|
243
|
+
return match ? match[1] : 'Activities';
|
|
244
|
+
};
|
|
245
|
+
|
|
180
246
|
/**
|
|
181
247
|
* Check if Activities folder exists
|
|
182
248
|
*/
|
|
183
249
|
const checkActivitiesFolder = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
184
|
-
|
|
185
|
-
let activitiesFolder = 'Activities';
|
|
186
|
-
const envPath = path.join(projectPath, '.env');
|
|
187
|
-
|
|
188
|
-
if (fs.existsSync(envPath)) {
|
|
189
|
-
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
190
|
-
const match = envContent.match(/ACTIVITIES_BASE_FOLDER=["']?([^"'\n\r]+)["']?/);
|
|
191
|
-
if (match) {
|
|
192
|
-
activitiesFolder = match[1];
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
250
|
+
const activitiesFolder = resolveActivitiesFolderName(projectPath);
|
|
196
251
|
const activitiesPath = path.join(projectPath, activitiesFolder);
|
|
197
|
-
|
|
252
|
+
|
|
198
253
|
if (!fs.existsSync(activitiesPath)) {
|
|
199
254
|
issues.push({
|
|
200
255
|
id: 'missing-activities-folder',
|
|
@@ -207,6 +262,172 @@ const checkActivitiesFolder = async (projectPath: string, issues: DiagnosticIssu
|
|
|
207
262
|
}
|
|
208
263
|
};
|
|
209
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Validate every shared/build.config.json under the activities folder.
|
|
267
|
+
*
|
|
268
|
+
* Catches the deploy-time errors before deploy:
|
|
269
|
+
* - missing or invalid activityType (must be "ab" or "xt")
|
|
270
|
+
* - null id (deploy will fail when it tries to GET the activity)
|
|
271
|
+
* - multi-page variation values without an activityInfo.pages map
|
|
272
|
+
* - variation/page folders referenced in config but absent on disk
|
|
273
|
+
*
|
|
274
|
+
* Issues are not auto-fixable (they're content problems requiring human
|
|
275
|
+
* judgment), so each one is reported with a clear suggestion.
|
|
276
|
+
*/
|
|
277
|
+
const checkBuildConfigs = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
278
|
+
const activitiesFolder = resolveActivitiesFolderName(projectPath);
|
|
279
|
+
const activitiesPath = path.join(projectPath, activitiesFolder);
|
|
280
|
+
if (!fs.existsSync(activitiesPath)) return; // already flagged elsewhere
|
|
281
|
+
|
|
282
|
+
const activityDirs = fs.readdirSync(activitiesPath, { withFileTypes: true })
|
|
283
|
+
.filter(d => d.isDirectory() && !d.name.startsWith('.'));
|
|
284
|
+
|
|
285
|
+
for (const activityDir of activityDirs) {
|
|
286
|
+
const activityRoot = path.join(activitiesPath, activityDir.name);
|
|
287
|
+
const configCandidates = [
|
|
288
|
+
path.join(activityRoot, 'shared', 'build.config.json'),
|
|
289
|
+
path.join(activityRoot, 'Shared', 'build.config.json')
|
|
290
|
+
];
|
|
291
|
+
const configPath = configCandidates.find(p => fs.existsSync(p));
|
|
292
|
+
if (!configPath) continue; // missing build.config is its own concern
|
|
293
|
+
|
|
294
|
+
const relConfig = path.relative(projectPath, configPath);
|
|
295
|
+
|
|
296
|
+
let parsed: { activityInfo?: Record<string, unknown> };
|
|
297
|
+
try {
|
|
298
|
+
parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
299
|
+
} catch (err) {
|
|
300
|
+
issues.push({
|
|
301
|
+
id: `invalid-build-config-json:${activityDir.name}`,
|
|
302
|
+
severity: 'error',
|
|
303
|
+
message: `${relConfig} is not valid JSON`,
|
|
304
|
+
suggestion: `Open ${relConfig} and fix the JSON syntax. Error: ${(err as Error).message}`,
|
|
305
|
+
fixable: false,
|
|
306
|
+
file: relConfig
|
|
307
|
+
});
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const activityInfo = (parsed && parsed.activityInfo) as Record<string, unknown> | undefined;
|
|
312
|
+
if (!activityInfo || typeof activityInfo !== 'object') {
|
|
313
|
+
issues.push({
|
|
314
|
+
id: `missing-activity-info:${activityDir.name}`,
|
|
315
|
+
severity: 'error',
|
|
316
|
+
message: `${relConfig} is missing activityInfo`,
|
|
317
|
+
suggestion: 'Re-run "atb new" or restore activityInfo manually',
|
|
318
|
+
fixable: false,
|
|
319
|
+
file: relConfig
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// activityType validation
|
|
325
|
+
const activityType = activityInfo.activityType as string | undefined;
|
|
326
|
+
if (!activityType) {
|
|
327
|
+
issues.push({
|
|
328
|
+
id: `missing-activity-type:${activityDir.name}`,
|
|
329
|
+
severity: 'warning',
|
|
330
|
+
message: `${relConfig} is missing activityType`,
|
|
331
|
+
suggestion: 'Add `"activityType": "ab"` (or "xt") to activityInfo',
|
|
332
|
+
fixable: false,
|
|
333
|
+
file: relConfig
|
|
334
|
+
});
|
|
335
|
+
} else if (!['ab', 'xt'].includes(String(activityType).toLowerCase())) {
|
|
336
|
+
issues.push({
|
|
337
|
+
id: `invalid-activity-type:${activityDir.name}`,
|
|
338
|
+
severity: 'error',
|
|
339
|
+
message: `${relConfig} has invalid activityType "${activityType}"`,
|
|
340
|
+
suggestion: 'activityType must be "ab" or "xt"',
|
|
341
|
+
fixable: false,
|
|
342
|
+
file: relConfig
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// id check (warning only — null is fine until first deploy)
|
|
347
|
+
if (activityInfo.id === null || activityInfo.id === undefined) {
|
|
348
|
+
issues.push({
|
|
349
|
+
id: `unset-activity-id:${activityDir.name}`,
|
|
350
|
+
severity: 'warning',
|
|
351
|
+
message: `${relConfig} has no activity id (deploy will fail until set)`,
|
|
352
|
+
suggestion: 'Set activityInfo.id once the activity exists in Adobe Target, or run "atb sync"',
|
|
353
|
+
fixable: false,
|
|
354
|
+
file: relConfig
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Variations validation
|
|
359
|
+
const variations = activityInfo.variations as Record<string, unknown> | unknown[] | undefined;
|
|
360
|
+
if (!variations || (Array.isArray(variations) && variations.length === 0) || (typeof variations === 'object' && !Array.isArray(variations) && Object.keys(variations).length === 0)) {
|
|
361
|
+
issues.push({
|
|
362
|
+
id: `missing-variations:${activityDir.name}`,
|
|
363
|
+
severity: 'error',
|
|
364
|
+
message: `${relConfig} has no variations`,
|
|
365
|
+
suggestion: 'Add at least one variation to activityInfo.variations',
|
|
366
|
+
fixable: false,
|
|
367
|
+
file: relConfig
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Multi-page detection: any variation value is an object with a `pages` key
|
|
373
|
+
const variationEntries = Array.isArray(variations)
|
|
374
|
+
? []
|
|
375
|
+
: Object.entries(variations);
|
|
376
|
+
const isMultiPage = variationEntries.some(([, v]) =>
|
|
377
|
+
typeof v === 'object' && v !== null && 'pages' in (v as object)
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
if (isMultiPage) {
|
|
381
|
+
const pages = activityInfo.pages as Record<string, unknown> | undefined;
|
|
382
|
+
if (!pages || typeof pages !== 'object' || Object.keys(pages).length === 0) {
|
|
383
|
+
issues.push({
|
|
384
|
+
id: `multi-page-missing-pages:${activityDir.name}`,
|
|
385
|
+
severity: 'error',
|
|
386
|
+
message: `${relConfig} uses multi-page variations but has no activityInfo.pages map`,
|
|
387
|
+
suggestion: 'Add `"pages": { "<pageName>": <locationLocalId>, ... }` or run "atb sync" to populate it from Adobe Target',
|
|
388
|
+
fixable: false,
|
|
389
|
+
file: relConfig
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Each multi-page variation: pages.<page> should map to a folder that exists
|
|
394
|
+
for (const [variationKey, variationValue] of variationEntries) {
|
|
395
|
+
if (typeof variationValue !== 'object' || variationValue === null || !('pages' in (variationValue as object))) continue;
|
|
396
|
+
const pagesMap = (variationValue as { pages?: Record<string, unknown> }).pages || {};
|
|
397
|
+
for (const [pageName, subPath] of Object.entries(pagesMap)) {
|
|
398
|
+
if (typeof subPath !== 'string') continue;
|
|
399
|
+
const folderAbs = path.join(activityRoot, subPath);
|
|
400
|
+
if (!fs.existsSync(folderAbs)) {
|
|
401
|
+
issues.push({
|
|
402
|
+
id: `missing-page-folder:${activityDir.name}:${variationKey}:${pageName}`,
|
|
403
|
+
severity: 'warning',
|
|
404
|
+
message: `${relConfig}: variation "${variationKey}" → page "${pageName}" expects folder "${subPath}" but it doesn't exist`,
|
|
405
|
+
suggestion: 'Run "atb sync --scaffold" to auto-create missing variation folders, or create them manually',
|
|
406
|
+
fixable: false,
|
|
407
|
+
file: relConfig
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
// Single-page: each variation key should map to a folder under the activity root
|
|
414
|
+
for (const [variationKey] of variationEntries) {
|
|
415
|
+
const folderAbs = path.join(activityRoot, variationKey);
|
|
416
|
+
if (!fs.existsSync(folderAbs)) {
|
|
417
|
+
issues.push({
|
|
418
|
+
id: `missing-variation-folder:${activityDir.name}:${variationKey}`,
|
|
419
|
+
severity: 'warning',
|
|
420
|
+
message: `${relConfig}: variation "${variationKey}" has no folder at ${path.relative(projectPath, folderAbs)}`,
|
|
421
|
+
suggestion: 'Create the folder, run "atb new" to scaffold variations, or remove the entry from build.config.json',
|
|
422
|
+
fixable: false,
|
|
423
|
+
file: relConfig
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
210
431
|
/**
|
|
211
432
|
* Check environment variables content
|
|
212
433
|
*/
|
|
@@ -247,6 +468,20 @@ const checkEnvVariables = async (projectPath: string, issues: DiagnosticIssue[])
|
|
|
247
468
|
fixable: false
|
|
248
469
|
});
|
|
249
470
|
}
|
|
471
|
+
|
|
472
|
+
// ADOBE_TENANT — if missing or empty, BASE_URL in adobe.config.js will fall
|
|
473
|
+
// back to the literal "YOUR_TENANT" placeholder and every API call will 404.
|
|
474
|
+
const tenantMatch = envContent.match(/^ADOBE_TENANT=(.*)$/m);
|
|
475
|
+
const tenantValue = tenantMatch ? tenantMatch[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
476
|
+
if (!tenantValue) {
|
|
477
|
+
issues.push({
|
|
478
|
+
id: 'empty-adobe-tenant',
|
|
479
|
+
severity: 'warning',
|
|
480
|
+
message: 'ADOBE_TENANT is missing or empty',
|
|
481
|
+
suggestion: 'Set ADOBE_TENANT to your AT tenant slug (the segment after mc.adobe.io/ in the AT URL)',
|
|
482
|
+
fixable: false
|
|
483
|
+
});
|
|
484
|
+
}
|
|
250
485
|
};
|
|
251
486
|
|
|
252
487
|
/**
|
|
@@ -255,9 +490,9 @@ const checkEnvVariables = async (projectPath: string, issues: DiagnosticIssue[])
|
|
|
255
490
|
const checkDependencies = async (projectPath: string, issues: DiagnosticIssue[]): Promise<void> => {
|
|
256
491
|
const packagePath = path.join(projectPath, 'package.json');
|
|
257
492
|
if (!fs.existsSync(packagePath)) return;
|
|
258
|
-
|
|
493
|
+
|
|
259
494
|
const nodeModulesPath = path.join(projectPath, 'node_modules');
|
|
260
|
-
|
|
495
|
+
|
|
261
496
|
if (!fs.existsSync(nodeModulesPath)) {
|
|
262
497
|
issues.push({
|
|
263
498
|
id: 'missing-node-modules',
|
|
@@ -269,6 +504,41 @@ const checkDependencies = async (projectPath: string, issues: DiagnosticIssue[])
|
|
|
269
504
|
}
|
|
270
505
|
};
|
|
271
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Validate that the at-builder global install is healthy:
|
|
509
|
+
* - global npm bin is on PATH
|
|
510
|
+
* - the `atb` shim exists in the global bin
|
|
511
|
+
* - on POSIX, the shim and the package's bin/index.js are executable
|
|
512
|
+
* (auto-chmods 755 if missing the bit)
|
|
513
|
+
*
|
|
514
|
+
* Same checks as lib/postinstall.js, just surfaced through `atb doctor` for
|
|
515
|
+
* users who installed with --ignore-scripts or dismissed the postinstall
|
|
516
|
+
* warning. Issues are flagged but not auto-fixable here — the suggested
|
|
517
|
+
* commands need to run in the user's shell, not in this Node process.
|
|
518
|
+
*/
|
|
519
|
+
const checkInstallEnv = async (issues: DiagnosticIssue[]): Promise<void> => {
|
|
520
|
+
let result;
|
|
521
|
+
try {
|
|
522
|
+
result = installChecks.checkInstallEnvironment();
|
|
523
|
+
} catch (err) {
|
|
524
|
+
// Don't let a bug in install-checks block the rest of doctor.
|
|
525
|
+
logger.error("checkInstallEnv", `Skipped: ${(err as Error).message}`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
for (const problem of result.problems) {
|
|
530
|
+
issues.push({
|
|
531
|
+
id: 'install-env',
|
|
532
|
+
severity: 'warning',
|
|
533
|
+
message: problem,
|
|
534
|
+
suggestion: result.isWin
|
|
535
|
+
? `On Windows: ensure ${result.globalBin || 'your npm global bin (npm config get prefix)'} is on PATH, then reinstall: npm i -g at-builder`
|
|
536
|
+
: `On macOS/Linux: chmod +x "${result.shimPath || 'atb shim'}" and ensure ${result.globalBin || '$(npm config get prefix)/bin'} is on PATH; then run: hash -r`,
|
|
537
|
+
fixable: false
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
272
542
|
// Fix functions
|
|
273
543
|
|
|
274
544
|
const createEnvFile = async (projectPath: string): Promise<boolean> => {
|
|
@@ -278,14 +548,22 @@ ACTIVITY_FOLDER_NAME=""
|
|
|
278
548
|
PUPPETEER_LANDING_PAGE=""
|
|
279
549
|
TARGET_URL=""
|
|
280
550
|
LOGIN_URL=""
|
|
551
|
+
|
|
552
|
+
# Dev-server selection (used by \`atb dev --browser\`).
|
|
553
|
+
# Edit and save while puppeteer is running to hot-swap the previewed bundle.
|
|
554
|
+
# PAGE is only meaningful for multi-page activities — leave empty otherwise.
|
|
281
555
|
VARIATION="Variation-1"
|
|
556
|
+
PAGE=""
|
|
557
|
+
|
|
282
558
|
NODE_ENV="development"
|
|
283
559
|
VERBOSE=false
|
|
284
560
|
|
|
285
561
|
# Adobe Target Deployment Configuration
|
|
562
|
+
# ADOBE_TENANT is your AT tenant slug — find it in the AT URL after "mc.adobe.io/".
|
|
563
|
+
ADOBE_TENANT=""
|
|
286
564
|
ADOBE_CLIENT_ID=""
|
|
287
565
|
ADOBE_CLIENT_SECRET=""`;
|
|
288
|
-
|
|
566
|
+
|
|
289
567
|
try {
|
|
290
568
|
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
291
569
|
return true;
|
|
@@ -298,14 +576,19 @@ const createAdobeConfig = async (projectPath: string): Promise<boolean> => {
|
|
|
298
576
|
const configPath = path.join(projectPath, 'adobe.config.js');
|
|
299
577
|
const configContent = `/**
|
|
300
578
|
* Adobe Target API Configuration
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
579
|
+
*
|
|
580
|
+
* Used by at-sync.js and at-deploy.js. BASE_URL is the activities root —
|
|
581
|
+
* callers append \`\${activityType}/\${activityId}\` (e.g. ab/12345, xt/67890).
|
|
582
|
+
*
|
|
583
|
+
* ADOBE_TENANT comes from the consumer .env. Both at-sync and at-deploy load
|
|
584
|
+
* dotenv before requiring this file, so process.env is populated by the time
|
|
585
|
+
* BASE_URL is built.
|
|
304
586
|
*/
|
|
305
587
|
|
|
588
|
+
const TENANT = process.env.ADOBE_TENANT || 'YOUR_TENANT';
|
|
589
|
+
|
|
306
590
|
module.exports = {
|
|
307
|
-
|
|
308
|
-
BASE_URL: 'https://mc.adobe.io/ups/target/activities/ab/',
|
|
591
|
+
BASE_URL: \`https://mc.adobe.io/\${TENANT}/target/activities/\`,
|
|
309
592
|
IMS_TOKEN_URL: 'https://ims-na1.adobelogin.com/ims/token/v3',
|
|
310
593
|
IMS_SCOPE: 'openid,AdobeID,target_sdk,additional_info.projectedProductContext'
|
|
311
594
|
};`;
|
|
@@ -318,14 +601,69 @@ module.exports = {
|
|
|
318
601
|
}
|
|
319
602
|
};
|
|
320
603
|
|
|
321
|
-
|
|
604
|
+
/**
|
|
605
|
+
* Migrate VARIATION/PAGE from a legacy watch-config.json into .env, then
|
|
606
|
+
* delete the watch-config.json. Idempotent: existing .env entries are
|
|
607
|
+
* overwritten so the migrated values win (matching puppeteer's runtime
|
|
608
|
+
* precedence — watch-config.json was the source of truth).
|
|
609
|
+
*/
|
|
610
|
+
const migrateWatchConfig = async (projectPath: string): Promise<boolean> => {
|
|
322
611
|
const configPath = path.join(projectPath, 'watch-config.json');
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
612
|
+
const envPath = path.join(projectPath, '.env');
|
|
613
|
+
if (!fs.existsSync(configPath)) return true; // nothing to migrate
|
|
614
|
+
|
|
615
|
+
let watchCfg: { VARIATION?: string; PAGE?: string };
|
|
616
|
+
try {
|
|
617
|
+
watchCfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
618
|
+
} catch (err) {
|
|
619
|
+
console.error(`Failed to parse watch-config.json: ${(err as Error).message}`);
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const variation = (watchCfg.VARIATION || '').trim();
|
|
624
|
+
const page = (watchCfg.PAGE || '').trim();
|
|
625
|
+
|
|
626
|
+
if (!fs.existsSync(envPath)) {
|
|
627
|
+
// No .env yet — synthesize a minimal one with just the migrated keys.
|
|
628
|
+
// The user can add the rest via `atb init` or by editing manually.
|
|
629
|
+
fs.writeFileSync(envPath, `VARIATION="${variation}"\nPAGE="${page}"\n`, 'utf8');
|
|
630
|
+
} else {
|
|
631
|
+
let envContent = fs.readFileSync(envPath, 'utf8');
|
|
632
|
+
envContent = upsertEnvLine(envContent, 'VARIATION', variation);
|
|
633
|
+
envContent = upsertEnvLine(envContent, 'PAGE', page);
|
|
634
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
fs.unlinkSync(configPath);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error(`Migrated values into .env, but failed to delete watch-config.json: ${(err as Error).message}`);
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
console.log(`✓ Migrated VARIATION="${variation}" PAGE="${page}" from watch-config.json into .env`);
|
|
645
|
+
return true;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Insert or replace a `KEY="value"` line in a .env file body. Preserves
|
|
650
|
+
* surrounding lines; appends if the key wasn't present.
|
|
651
|
+
*/
|
|
652
|
+
const upsertEnvLine = (envContent: string, key: string, value: string): string => {
|
|
653
|
+
const line = `${key}="${value}"`;
|
|
654
|
+
const re = new RegExp(`^${key}=.*$`, 'm');
|
|
655
|
+
if (re.test(envContent)) {
|
|
656
|
+
return envContent.replace(re, line);
|
|
657
|
+
}
|
|
658
|
+
if (!envContent.endsWith('\n')) envContent += '\n';
|
|
659
|
+
return envContent + line + '\n';
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const createGitignore = async (projectPath: string): Promise<boolean> => {
|
|
663
|
+
const gitignorePath = path.join(projectPath, '.gitignore');
|
|
664
|
+
|
|
327
665
|
try {
|
|
328
|
-
fs.writeFileSync(
|
|
666
|
+
fs.writeFileSync(gitignorePath, GITIGNORE_TEMPLATE, 'utf8');
|
|
329
667
|
return true;
|
|
330
668
|
} catch (error) {
|
|
331
669
|
return false;
|