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/puppeteer.js CHANGED
@@ -4,15 +4,93 @@ import path from 'path';
4
4
  import puppeteer from 'puppeteer';
5
5
  import fs from 'fs';
6
6
  import chokidar from 'chokidar';
7
+
7
8
  const executionPath = process.env.executionPath || process.cwd();
8
- dotenv.config({ path: path.join(executionPath, ".env") });
9
+ const envPath = path.join(executionPath, ".env");
10
+ dotenv.config({ path: envPath });
11
+
12
+ // Honor consumer's ACTIVITIES_BASE_FOLDER (falls back to "Activities" to
13
+ // match webpack/at-deploy/at-sync/plop). Hardcoding it here previously caused
14
+ // `atb dev --browser` to silently read from the wrong path when users
15
+ // customized this in their .env.
16
+ const ACTIVITIES_BASE_FOLDER = process.env.ACTIVITIES_BASE_FOLDER || "Activities";
17
+
18
+ // Legacy watch-config.json path. Kept as a fallback source for VARIATION/PAGE
19
+ // so existing projects keep working. New projects (atb init) only get .env;
20
+ // `atb doctor --fix` migrates any existing watch-config.json into .env.
21
+ const watchConfig = path.join(executionPath, "watch-config.json");
22
+
23
+ let legacyWarningShown = false;
24
+
25
+ /**
26
+ * Resolve the active dev-server selection (VARIATION + PAGE).
27
+ *
28
+ * Precedence:
29
+ * 1. watch-config.json (if it exists) — preserves existing user behavior
30
+ * where this file was the canonical override. A one-time deprecation
31
+ * warning nudges the user to migrate via `atb doctor --fix`.
32
+ * 2. process.env (loaded from .env) — the new canonical source.
33
+ *
34
+ * Returns { VARIATION, PAGE } as plain strings (PAGE may be empty for
35
+ * single-page activities).
36
+ */
37
+ function readDevSelection() {
38
+ if (fs.existsSync(watchConfig)) {
39
+ try {
40
+ const cfg = JSON.parse(fs.readFileSync(watchConfig, 'utf-8'));
41
+ if (!legacyWarningShown) {
42
+ console.warn(
43
+ '\x1b[33m⚠️ watch-config.json is deprecated — VARIATION/PAGE now live in .env.\x1b[0m\n' +
44
+ '\x1b[33m Run "atb doctor --fix" to migrate values into .env and remove this file.\x1b[0m'
45
+ );
46
+ legacyWarningShown = true;
47
+ }
48
+ return {
49
+ VARIATION: cfg.VARIATION || process.env.VARIATION || '',
50
+ PAGE: (cfg.PAGE || process.env.PAGE || '').trim()
51
+ };
52
+ } catch (err) {
53
+ console.error(`Failed to read watch-config.json (${err.message}); falling back to .env.`);
54
+ }
55
+ }
56
+ return {
57
+ VARIATION: process.env.VARIATION || '',
58
+ PAGE: (process.env.PAGE || '').trim()
59
+ };
60
+ }
9
61
 
10
- let watchConfig = '';
11
- try {
12
- watchConfig = path.join(executionPath, "watch-config.json");
62
+ /**
63
+ * Compose the dist folder for the currently-selected variation, page-aware.
64
+ *
65
+ * Single-page (PAGE empty/missing): <Activities>/<activity>/<VARIATION>/dist
66
+ * Multi-page (PAGE set): <Activities>/<activity>/<VARIATION>/<PAGE>/dist
67
+ *
68
+ * Editing .env (or the legacy watch-config.json, if still present) mid-session
69
+ * hot-swaps which variation/page is previewed without restarting the dev server.
70
+ */
71
+ function variationDistFolder(sel) {
72
+ const segments = [
73
+ executionPath,
74
+ ACTIVITIES_BASE_FOLDER,
75
+ process.env.ACTIVITY_FOLDER_NAME,
76
+ sel.VARIATION
77
+ ];
78
+ if (sel.PAGE && sel.PAGE.trim()) {
79
+ segments.push(sel.PAGE.trim());
80
+ }
81
+ segments.push("dist");
82
+ return path.join(...segments);
13
83
  }
14
- catch (e) {
15
- throw new Error(`Error : Couldn't find watch-config.json file at location ${executionPath}`);
84
+
85
+ // Validate VARIATION up-front so we fail fast with a clear message instead of
86
+ // reading from a nonsense path inside the puppeteer event loop.
87
+ {
88
+ const initial = readDevSelection();
89
+ if (!initial.VARIATION) {
90
+ console.error('\x1b[31m❌ VARIATION is not set.\x1b[0m');
91
+ console.error('\x1b[33m💡 Add VARIATION="Variation-1" (and optional PAGE="Global" for multi-page) to .env, or run "atb doctor --fix".\x1b[0m');
92
+ process.exit(1);
93
+ }
16
94
  }
17
95
 
18
96
 
@@ -54,39 +132,64 @@ const watcher = chokidar.watch("file", {
54
132
  // inject script
55
133
  this.injectCodeSnippet();
56
134
 
57
- watcher.add(watchConfig).on('change', async (event, path) => {
58
- try {
59
- const distFolder = this.buildFolderPath;
60
- watcher.unwatch(distFolder);
135
+ // Tracks the dist folder we're currently watching so we can unwatch
136
+ // the *old* one when the user edits .env (or legacy watch-config.json).
137
+ this.currentWatchedFolder = null;
61
138
 
62
- // restart watch with another variation
63
- this.watchBuildFolder();
139
+ // Initial watches: build folder for the active variation/page,
140
+ // plus the config sources (.env always; watch-config.json if legacy).
141
+ this.watchBuildFolder();
142
+ watcher.add(envPath);
143
+ if (fs.existsSync(watchConfig)) {
144
+ watcher.add(watchConfig);
145
+ }
64
146
 
65
- await this.page.reload();
66
- this.injectCodeSnippet();
147
+ // Single change listener for the whole session. Branches on path:
148
+ // .env or watch-config.json → re-resolve VARIATION/PAGE, re-target
149
+ // anything under currentWatchedFolder → reload (webpack rebuilt)
150
+ // Registered once (instead of inside .add(...).on(...)) so we don't
151
+ // accumulate listeners on every variation/page switch.
152
+ const envPathAbs = path.resolve(envPath);
153
+ const watchConfigAbs = path.resolve(watchConfig);
154
+ watcher.on('change', async (changedPath) => {
155
+ try {
156
+ const changedAbs = path.resolve(changedPath);
157
+ const isConfigChange = changedAbs === envPathAbs || changedAbs === watchConfigAbs;
158
+
159
+ if (isConfigChange) {
160
+ // .env change requires re-loading dotenv with override so
161
+ // process.env actually reflects the edits. watch-config.json
162
+ // is re-read each time via readDevSelection().
163
+ if (changedAbs === envPathAbs) {
164
+ dotenv.config({ path: envPath, override: true });
165
+ }
166
+ if (this.currentWatchedFolder) {
167
+ watcher.unwatch(this.currentWatchedFolder);
168
+ }
169
+ this.watchBuildFolder();
170
+ await this.page.reload();
171
+ this.injectCodeSnippet();
172
+ } else if (this.currentWatchedFolder && changedPath.startsWith(this.currentWatchedFolder)) {
173
+ await this.page.reload();
174
+ this.injectCodeSnippet();
175
+ }
67
176
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
68
177
  } catch (error) {
69
-
178
+ // Swallow transient FS races; next change event will retry.
70
179
  }
71
180
  });
72
181
 
73
- this.watchBuildFolder();
74
182
  return this.trackNavigation();
75
183
  }
76
184
 
77
185
  get buildFolderPath() {
78
- const env = this.getEnvConfig;
79
- const distFolder = path.join(process.env.executionPath, "Activities", process.env.ACTIVITY_FOLDER_NAME, env.VARIATION, "dist");
80
- return distFolder;
186
+ return variationDistFolder(readDevSelection());
81
187
  }
82
188
 
83
189
  watchBuildFolder() {
84
190
  const distFolder = this.buildFolderPath;
85
- // Watch folder changes
86
- watcher.add(distFolder).on('change', async (event, path) => {
87
- await this.page.reload();
88
- this.injectCodeSnippet();
89
- });
191
+ watcher.add(distFolder);
192
+ this.currentWatchedFolder = distFolder;
90
193
  }
91
194
 
92
195
  async trackNavigation() {
@@ -102,15 +205,8 @@ const watcher = chokidar.watch("file", {
102
205
  this.trackNavigation();
103
206
  }
104
207
 
105
- get getEnvConfig() {
106
- const env = JSON.parse(fs.readFileSync(watchConfig));
107
- return env;
108
- }
109
-
110
208
  get getScript() {
111
- const env = this.getEnvConfig;
112
- // console.log("Env", env, "Process Env", process.env);
113
- var filePath = path.join(process.env.executionPath, "Activities", process.env.ACTIVITY_FOLDER_NAME, env.VARIATION, "dist", "build.js");
209
+ const filePath = path.join(variationDistFolder(readDevSelection()), "build.js");
114
210
  const scriptContent = fs.readFileSync(filePath, 'utf-8');
115
211
  return scriptContent;
116
212
  }
@@ -71,8 +71,12 @@ ${formatText("COMMANDS", 'yellow', true)}
71
71
  ${formatText("dev --browser", 'cyan', true)} Start development server and open in browser
72
72
  ${formatText("deploy", 'cyan', true)} Deploy activity to Adobe Target
73
73
  ${formatText("deploy --dry-run", 'cyan', true)} Deploy in dry-run mode without actual deployment
74
+ ${formatText("deploy --force", 'cyan', true)} Override the 60s post-deploy cooldown lock
75
+ ${formatText("sync", 'cyan', true)} Sync build.config.json with the AT activity (pages, experiences, names)
76
+ ${formatText("sync --scaffold", 'cyan', true)} Sync and auto-create any missing variation folders with boilerplate
74
77
  ${formatText("doctor", 'cyan', true)} Diagnose and fix project configuration issues
75
78
  ${formatText("doctor --fix", 'cyan', true)} Automatically fix detected configuration issues
79
+ ${formatText("install-extension", 'cyan', true)} Install the at-builder VSCode extension from the Marketplace
76
80
 
77
81
  ${formatText("GLOBAL OPTIONS", 'yellow', true)}
78
82
  ${formatText("-v, --verbose", 'green')} Enable detailed logging
@@ -100,7 +104,13 @@ ${formatText("EXAMPLES", 'yellow', true)}
100
104
 
101
105
  ${formatText("# Deploy in dry-run mode", 'gray')}
102
106
  ${formatText("atb deploy --dry-run", 'white')}
103
-
107
+
108
+ ${formatText("# Sync build.config.json from Adobe Target", 'gray')}
109
+ ${formatText("atb sync", 'white')}
110
+
111
+ ${formatText("# Sync and scaffold missing variation folders", 'gray')}
112
+ ${formatText("atb sync --scaffold", 'white')}
113
+
104
114
  ${formatText("# Check project configuration", 'gray')}
105
115
  ${formatText("atb doctor", 'white')}
106
116
 
@@ -136,11 +146,19 @@ ACTIVITY_FOLDER_NAME=""
136
146
  PUPPETEER_LANDING_PAGE=""
137
147
  TARGET_URL=""
138
148
  LOGIN_URL=""
149
+
150
+ # Dev-server selection (used by \`atb dev --browser\`).
151
+ # Edit and save while puppeteer is running to hot-swap the previewed bundle.
152
+ # PAGE is only meaningful for multi-page activities — leave empty otherwise.
139
153
  VARIATION="Variation-1"
154
+ PAGE=""
155
+
140
156
  NODE_ENV="development"
141
157
  VERBOSE=false
142
158
 
143
159
  # Adobe Target Deployment Configuration
160
+ # ADOBE_TENANT is your AT tenant slug — find it in the AT URL after "mc.adobe.io/".
161
+ ADOBE_TENANT=""
144
162
  ADOBE_CLIENT_ID=""
145
163
  ADOBE_CLIENT_SECRET=""
146
164
  `;
@@ -150,8 +168,55 @@ ADOBE_CLIENT_SECRET=""
150
168
  } else {
151
169
  console.log('.env file already exists!');
152
170
  }
153
- createWatchConfig(basePath);
171
+ // watch-config.json is no longer scaffolded by `atb init` — VARIATION/PAGE
172
+ // now live in .env. Existing projects with a watch-config.json keep working
173
+ // (puppeteer.js prefers it with a deprecation warning); `atb doctor --fix`
174
+ // migrates the values into .env and deletes the legacy file.
154
175
  createAdobeConfig(basePath);
176
+ createGitignore(basePath);
177
+ }
178
+
179
+ /**
180
+ * Default .gitignore content for at-builder consumer projects.
181
+ *
182
+ * Shared between `atb init` (via setupEnv) and `atb doctor --fix` so the two
183
+ * paths stay in lockstep. Covers secrets, deploy-runtime artifacts, build
184
+ * output, and OS noise. Does NOT ignore the Activities folder — those are the
185
+ * user's source files and should be committed.
186
+ */
187
+ export const GITIGNORE_TEMPLATE = `# Dependencies
188
+ node_modules/
189
+
190
+ # Environment / secrets — never commit
191
+ .env
192
+ .env.local
193
+
194
+ # Adobe Target deploy runtime (cooldown lock written by \`atb deploy\`)
195
+ .deploy-lock
196
+
197
+ # Build output (regenerated by \`atb build\`)
198
+ dist/
199
+
200
+ # OS noise
201
+ .DS_Store
202
+
203
+ # Editor / local-history
204
+ .history/
205
+
206
+ # Logs
207
+ *.log
208
+ npm-debug.log*
209
+ `;
210
+
211
+ const createGitignore = (basePath) => {
212
+ const gitignorePath = path.join(basePath, '.gitignore');
213
+
214
+ if (!fs.existsSync(gitignorePath)) {
215
+ fs.writeFileSync(gitignorePath, GITIGNORE_TEMPLATE, 'utf8');
216
+ console.log('.gitignore file created successfully!');
217
+ } else {
218
+ console.log('.gitignore file already exists!');
219
+ }
155
220
  }
156
221
 
157
222
 
@@ -160,9 +225,14 @@ const createWatchConfig = (basePath) => {
160
225
  // Path to the watch-config.json file
161
226
  const watchConfigPath = path.join(basePath, 'watch-config.json');
162
227
 
163
- // Default content for watch-config.json
228
+ // Default content for watch-config.json.
229
+ // VARIATION : which variation/experience to preview in `atb dev --browser`
230
+ // PAGE : leave empty for single-page activities; set to the page
231
+ // subfolder name (e.g. "Global", "Cart") for multi-page.
232
+ // Editing this file mid-session hot-swaps the preview.
164
233
  const watchConfigContent = {
165
- "VARIATION": "Variation-1"
234
+ "VARIATION": "Variation-1",
235
+ "PAGE": ""
166
236
  };
167
237
 
168
238
  // Check if the watch-config.json file already exists
@@ -181,14 +251,19 @@ const createAdobeConfig = (basePath) => {
181
251
  // Default content for adobe.config.js
182
252
  const adobeConfigContent = `/**
183
253
  * Adobe Target API Configuration
184
- *
185
- * Configuration constants for Adobe Target API integration.
186
- * These values are used by the deployment script to connect to Adobe Target.
254
+ *
255
+ * Used by at-sync.js and at-deploy.js. BASE_URL is the activities root —
256
+ * callers append \`\${activityType}/\${activityId}\` (e.g. ab/12345, xt/67890).
257
+ *
258
+ * ADOBE_TENANT comes from the consumer .env. Both at-sync and at-deploy load
259
+ * dotenv before requiring this file, so process.env is populated by the time
260
+ * BASE_URL is built.
187
261
  */
188
262
 
263
+ const TENANT = process.env.ADOBE_TENANT || 'YOUR_TENANT';
264
+
189
265
  module.exports = {
190
- BASE_URL_NEW: 'https://mc.adobe.io/ups/target/',
191
- BASE_URL: 'https://mc.adobe.io/ups/target/activities/ab/',
266
+ BASE_URL: \`https://mc.adobe.io/\${TENANT}/target/activities/\`,
192
267
  IMS_TOKEN_URL: 'https://ims-na1.adobelogin.com/ims/token/v3',
193
268
  IMS_SCOPE: 'openid,AdobeID,target_sdk,additional_info.projectedProductContext'
194
269
  };`;
package/src/index.ts CHANGED
@@ -85,10 +85,21 @@ const setupCommander = async () => {
85
85
  .command('deploy')
86
86
  .description('Deploy activity to Adobe Target using at-deploy.js')
87
87
  .option('--dry-run', 'Run deployment in dry-run mode without actual deployment')
88
+ .option('--force', 'Override the 60s post-deploy cooldown lock')
88
89
  .action(async (options, command) => {
89
90
  const globalOpts = command.parent.opts();
90
91
  checkEnvFile();
91
- await handleDeploy(options.dryRun, globalOpts.verbose);
92
+ await handleDeploy(options.dryRun, options.force, globalOpts.verbose);
93
+ });
94
+
95
+ program
96
+ .command('sync')
97
+ .description('Sync build.config.json with the Adobe Target activity definition')
98
+ .option('--scaffold', 'Auto-create missing variation folders with boilerplate')
99
+ .action(async (options, command) => {
100
+ const globalOpts = command.parent.opts();
101
+ checkEnvFile();
102
+ await handleSync(options.scaffold, globalOpts.verbose);
92
103
  });
93
104
 
94
105
  program
@@ -100,6 +111,15 @@ const setupCommander = async () => {
100
111
  await handleDoctor(options.fix, globalOpts.verbose);
101
112
  });
102
113
 
114
+ program
115
+ .command('install-extension')
116
+ .description('Install the at-builder VSCode extension from the Marketplace')
117
+ .option('--editor <bin>', 'Editor CLI to use (e.g. code, cursor, codium)', 'code')
118
+ .action(async (options, command) => {
119
+ const globalOpts = command.parent.opts();
120
+ await handleInstallExtension(options.editor, globalOpts.verbose);
121
+ });
122
+
103
123
  await program.parseAsync(process.argv);
104
124
 
105
125
  logger.info("setupCommander", "Commander setup complete");
@@ -110,16 +130,22 @@ const setupCommander = async () => {
110
130
  /**
111
131
  * Spawn a command using `npm` with the given command array and environment object.
112
132
  *
133
+ * Any entries in `scriptArgs` are forwarded to the underlying script via npm's
134
+ * `--` passthrough (e.g. `npm run foo -- --dry-run`).
135
+ *
113
136
  * @param commandArr - The array of command strings to pass to `npm`.
114
137
  * @param env - The environment object to pass to the spawned process.
138
+ * @param scriptArgs - Optional flags/args to forward to the npm script itself.
115
139
  */
116
- const runCommand = (commandArr: string[], env: NodeJS.ProcessEnv): void => {
117
- logger.info("runCommand", `Running npm command [${commandArr.join(", ")}]`);
140
+ const runCommand = (commandArr: string[], env: NodeJS.ProcessEnv, scriptArgs: string[] = []): void => {
141
+ const fullArgs = scriptArgs.length > 0
142
+ ? [...commandArr, "--", ...scriptArgs]
143
+ : commandArr;
144
+
145
+ logger.info("runCommand", `Running npm command [${fullArgs.join(", ")}]`);
118
146
  logger.info("runCommand", `Current working directory: ${process.cwd()}`);
119
- // logger.log(`Environment variables: ${JSON.stringify(env, null, 2)}`);
120
147
 
121
- // Spawn the command using `npm` with the given environment and current working directory.
122
- spawn("npm", commandArr, {
148
+ spawn("npm", fullArgs, {
123
149
  cwd: path.join(__dirname, "../"),
124
150
  env,
125
151
  shell: true,
@@ -191,12 +217,82 @@ const handleDev = async (browser: boolean, verbose: boolean): Promise<void> => {
191
217
  /**
192
218
  * Handles the deploy command
193
219
  */
194
- const handleDeploy = async (dryRun: boolean, verbose: boolean): Promise<void> => {
195
- if (verbose) logger.info("verbose", `Deploying to Adobe Target with dry-run=${dryRun}`);
196
-
220
+ const handleDeploy = async (dryRun: boolean, force: boolean, verbose: boolean): Promise<void> => {
221
+ if (verbose) logger.info("verbose", `Deploying to Adobe Target with dry-run=${dryRun} force=${force}`);
222
+
197
223
  logger.info("handleDeploy", "Running Adobe Target deployment");
198
-
199
- runCommand(['run', 'atb:build:deploy'], productionEnv);
224
+
225
+ const scriptArgs: string[] = [];
226
+ if (dryRun) scriptArgs.push('--dry-run');
227
+ if (force) scriptArgs.push('--force');
228
+
229
+ runCommand(['run', 'atb:build:deploy'], productionEnv, scriptArgs);
230
+ };
231
+
232
+ /**
233
+ * Handles the sync command
234
+ */
235
+ const handleSync = async (scaffold: boolean, verbose: boolean): Promise<void> => {
236
+ if (verbose) logger.info("verbose", `Syncing build.config.json with scaffold=${scaffold}`);
237
+
238
+ logger.info("handleSync", "Running Adobe Target sync");
239
+
240
+ const scriptArgs: string[] = [];
241
+ if (scaffold) scriptArgs.push('--scaffold');
242
+
243
+ runCommand(['run', 'atb:build:sync'], productionEnv, scriptArgs);
244
+ };
245
+
246
+ /**
247
+ * Handles the install-extension command.
248
+ *
249
+ * Installs the at-builder VSCode extension from the VS Code Marketplace
250
+ * using the editor's CLI. Defaults to `code`; users on Cursor/VSCodium
251
+ * can pass --editor.
252
+ */
253
+ const handleInstallExtension = async (editor: string, verbose: boolean): Promise<void> => {
254
+ const extensionId = "UpendraSengar.at-builder";
255
+
256
+ if (verbose) logger.info("verbose", `Installing ${extensionId} via ${editor}`);
257
+
258
+ console.log(`📦 Installing ${extensionId} from the Marketplace via "${editor}"...`);
259
+
260
+ const result = spawn(editor, ["--install-extension", extensionId], {
261
+ stdio: "inherit",
262
+ shell: true
263
+ });
264
+
265
+ result.on("error", (err) => {
266
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
267
+ console.error(`❌ "${editor}" CLI not found in PATH.`);
268
+ if (editor === "code") {
269
+ console.error("💡 Open VSCode and run Command Palette → \"Shell Command: Install 'code' command in PATH\".");
270
+ } else {
271
+ console.error(`💡 Make sure the "${editor}" CLI is installed and on your PATH, or pass a different --editor.`);
272
+ }
273
+ } else {
274
+ console.error(`❌ Failed to launch "${editor}": ${err.message}`);
275
+ }
276
+ process.exit(1);
277
+ });
278
+
279
+ result.on("exit", (code) => {
280
+ if (code === 0) {
281
+ console.log("✅ Extension installed. Reload your editor window to activate.");
282
+ return;
283
+ }
284
+ if (code === 127) {
285
+ console.error(`❌ "${editor}" CLI not found in PATH.`);
286
+ if (editor === "code") {
287
+ console.error("💡 Open VSCode and run Command Palette → \"Shell Command: Install 'code' command in PATH\".");
288
+ } else {
289
+ console.error(`💡 Make sure the "${editor}" CLI is installed and on your PATH, or pass a different --editor.`);
290
+ }
291
+ process.exit(1);
292
+ }
293
+ console.error(`❌ "${editor} --install-extension" exited with code ${code}.`);
294
+ process.exit(code ?? 1);
295
+ });
200
296
  };
201
297
 
202
298
  /**