@speedkit/cli 4.21.0 → 4.22.1

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -2
  3. package/dist/commands/generate-customer-config.d.ts +2 -0
  4. package/dist/commands/generate-customer-config.js +10 -2
  5. package/dist/services/bundler/bundle-service.d.ts +12 -0
  6. package/dist/services/bundler/bundle-service.js +28 -1
  7. package/dist/services/bundler/bundle-service.spec.d.ts +1 -0
  8. package/dist/services/bundler/bundle-service.spec.js +54 -0
  9. package/dist/services/customer-config/customer-config-service-context.d.ts +9 -1
  10. package/dist/services/customer-config/customer-config-service-context.js +11 -1
  11. package/dist/services/customer-config/customer-config-service-model.d.ts +6 -0
  12. package/dist/services/customer-config/customer-config-service-model.js +36 -0
  13. package/dist/services/customer-config/customer-config-service.d.ts +8 -3
  14. package/dist/services/customer-config/customer-config-service.js +71 -25
  15. package/dist/services/customer-config/template-source.d.ts +33 -0
  16. package/dist/services/customer-config/template-source.js +49 -0
  17. package/dist/services/integration-api/integration-api-model.js +3 -1
  18. package/oclif.manifest.json +20 -1
  19. package/package.json +8 -1
  20. package/dist/services/customer-config/templates/config_SpeedKit.js.hbs +0 -548
  21. package/dist/services/customer-config/templates/config_customer.json.hbs +0 -38
  22. package/dist/services/customer-config/templates/config_documentHandler.js.hbs +0 -362
  23. package/dist/services/customer-config/templates/config_dynamicBlocks.es6.hbs +0 -163
  24. package/dist/services/customer-config/templates/config_dynamicStyles.css.hbs +0 -137
  25. package/dist/services/customer-config/templates/config_loadHandler.js.hbs +0 -391
  26. package/dist/services/customer-config/templates/downtimeDetection.js.hbs +0 -209
package/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## [4.22.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.22.0...v4.22.1) (2026-08-21)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **bundler:** resolve alias targets from the customer directory ([ad30e2a](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/ad30e2ac48a84c7592dd59d41004c301df212205))
7
+
8
+ # [4.22.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.21.0...v4.22.0) (2026-08-20)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **customer-config:** contract-check against the CLI's variable inventory ([7ebd2d2](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/7ebd2d28331a59dc3754fa1f6cd859f978febf6c))
14
+
15
+
16
+ ### Features
17
+
18
+ * **customer-config:** --mode flag picks the template set ([b471e8b](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/b471e8b8c6de32babfd26f69688409cae04288ae))
19
+ * **customer-config:** graduate an existing pre-PoC config ([28af5e2](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/28af5e2d06c8c9571da3d72563ae229b12f27d56))
20
+ * **customer-config:** pre-poc mode asks only what the baseline needs ([35cbcbd](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/35cbcbd698144ac82dd275dcf63a0e62b03dba24))
21
+ * **customer-config:** render wizard templates from sk-onboarding ([266cbe3](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/266cbe3c68f5e4aafa431e2fa4a5f9311c3e28c9))
22
+
1
23
  # [4.21.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.20.5...v4.21.0) (2026-08-14)
2
24
 
3
25
 
package/README.md CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @speedkit/cli
21
21
  $ sk COMMAND
22
22
  running command...
23
23
  $ sk (--version)
24
- @speedkit/cli/4.21.0 linux-x64 node-v22.23.2
24
+ @speedkit/cli/4.22.1 linux-x64 node-v22.23.2
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -486,13 +486,18 @@ Generates a customer config guided by a wizard 🧙‍
486
486
 
487
487
  ```
488
488
  USAGE
489
- $ sk generate-customer-config [-a <value>] [-d <value>]
489
+ $ sk generate-customer-config [-a <value>] [-d <value>] [--templates <value>] [--mode customer-config|pre-poc]
490
490
 
491
491
  FLAGS
492
492
  -a, --app=<value> The SK app name which is also being used the config output directory name
493
493
  (kebab-case)
494
494
  -d, --customersDirectory=<value> Name of the sk-onboarding directory where all customer configs are being stored
495
495
  (default: "customers")
496
+ --mode=<option> [default: customer-config] Which config to scaffold: the full acceleration config,
497
+ or the tracking-only pre-PoC config
498
+ <options: customer-config|pre-poc>
499
+ --templates=<value> Path to the templates/ root (default: the sk-onboarding checkout the CLI runs in, or
500
+ the sk-onboarding repo on GitLab when run standalone)
496
501
 
497
502
  DESCRIPTION
498
503
  Generates a customer config guided by a wizard 🧙‍
@@ -5,6 +5,8 @@ export default class GenerateCustomerConfig extends Command {
5
5
  static flags: {
6
6
  app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
7
  customersDirectory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ templates: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ mode: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
10
  };
9
11
  run(): Promise<void>;
10
12
  }
@@ -16,10 +16,18 @@ export default class GenerateCustomerConfig extends Command {
16
16
  char: CLIParametersChar.CustomersDirectory,
17
17
  description: `Name of the sk-onboarding directory where all customer configs are being stored (default: "${CLIParametersExample.CustomersDirectory}")`,
18
18
  }),
19
+ templates: Flags.string({
20
+ description: "Path to the templates/ root (default: the sk-onboarding checkout the CLI runs in, or the sk-onboarding repo on GitLab when run standalone)",
21
+ }),
22
+ mode: Flags.string({
23
+ options: ["customer-config", "pre-poc"],
24
+ default: "customer-config",
25
+ description: "Which config to scaffold: the full acceleration config, or the tracking-only pre-PoC config",
26
+ }),
19
27
  };
20
28
  async run() {
21
- const { flags: { app, customersDirectory }, } = await this.parse(GenerateCustomerConfig);
22
- const context = new CustomerConfigServiceContext(app, customersDirectory);
29
+ const { flags: { app, customersDirectory, templates, mode }, } = await this.parse(GenerateCustomerConfig);
30
+ const context = new CustomerConfigServiceContext(app, customersDirectory, templates, mode);
23
31
  const customerConfigService = await new CustomerConfigServiceFactory(context).buildService();
24
32
  await customerConfigService.generateConfig();
25
33
  }
@@ -14,6 +14,18 @@ export declare class BundleService {
14
14
  logLevel?: LogLevel;
15
15
  platform?: "browser" | "node" | "neutral";
16
16
  }): Promise<string>;
17
+ /**
18
+ * Turns each alias target into an absolute path, resolved from the same directory the bundled
19
+ * code resolves its own imports from.
20
+ *
21
+ * esbuild resolves an alias target from its working directory, not from the entry point's
22
+ * resolveDir. `{ "node-fetch": "node-fetch-native" }` therefore only resolved while `sk` ran
23
+ * inside the customer checkout, and failed from anywhere else — even though the bundled code
24
+ * could import the very same module by name.
25
+ *
26
+ * A target that is not installed stays a bare name, so esbuild reports it as before.
27
+ */
28
+ private resolveAliases;
17
29
  private entryPointsPlugin;
18
30
  baqendPlugin(basePaths: BundleBasePaths): {
19
31
  name: string;
@@ -4,6 +4,7 @@ import { URL } from "node:url";
4
4
  import { BundleResolveError } from "./error/bundle-resolve-error.js";
5
5
  import { BundleFetchError } from "./error/bundle-fetch-error.js";
6
6
  import { resolve } from "node:path";
7
+ import { createRequire } from "node:module";
7
8
  const importCache = {};
8
9
  export class BundleService {
9
10
  moduleDiscoveryDir;
@@ -29,13 +30,39 @@ export class BundleService {
29
30
  entryPoints: Object.keys(entryPoints),
30
31
  platform: platform || "neutral",
31
32
  logLevel,
32
- alias,
33
+ alias: this.resolveAliases(alias),
33
34
  });
34
35
  return result.outputFiles
35
36
  .map((outputFile) => outputFile.text)
36
37
  .join("")
37
38
  .trim();
38
39
  }
40
+ /**
41
+ * Turns each alias target into an absolute path, resolved from the same directory the bundled
42
+ * code resolves its own imports from.
43
+ *
44
+ * esbuild resolves an alias target from its working directory, not from the entry point's
45
+ * resolveDir. `{ "node-fetch": "node-fetch-native" }` therefore only resolved while `sk` ran
46
+ * inside the customer checkout, and failed from anywhere else — even though the bundled code
47
+ * could import the very same module by name.
48
+ *
49
+ * A target that is not installed stays a bare name, so esbuild reports it as before.
50
+ */
51
+ resolveAliases(alias) {
52
+ if (Object.keys(alias).length === 0) {
53
+ return alias;
54
+ }
55
+ // The filename never has to exist; only its directory decides where the lookup starts.
56
+ const requireFrom = createRequire(resolve(this.moduleDiscoveryDir, "noop.js"));
57
+ return Object.fromEntries(Object.entries(alias).map(([from, to]) => {
58
+ try {
59
+ return [from, requireFrom.resolve(to)];
60
+ }
61
+ catch {
62
+ return [from, to];
63
+ }
64
+ }));
65
+ }
39
66
  entryPointsPlugin(entryPoints) {
40
67
  // eslint-disable-next-line @typescript-eslint/no-this-alias
41
68
  const me = this;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { expect } from "chai";
2
+ import { after, before, describe, it } from "mocha";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { BundleService } from "./bundle-service.js";
7
+ /**
8
+ * The customer checkout installs the modules a document handler imports, so resolution must follow
9
+ * the customer directory rather than the directory `sk` happens to run in. The fixture below is
10
+ * only reachable from the customer directory, which is what makes the assertion meaningful.
11
+ */
12
+ describe("BundleService alias resolution", () => {
13
+ const MARKER = "fixture-fetch-implementation";
14
+ let customerDirectory;
15
+ before(() => {
16
+ customerDirectory = mkdtempSync(join(tmpdir(), "sk-bundle-"));
17
+ const packageDirectory = join(customerDirectory, "node_modules", "fetch-native-fixture");
18
+ mkdirSync(packageDirectory, { recursive: true });
19
+ writeFileSync(join(packageDirectory, "package.json"), JSON.stringify({
20
+ name: "fetch-native-fixture",
21
+ version: "1.0.0",
22
+ main: "index.js",
23
+ }));
24
+ writeFileSync(join(packageDirectory, "index.js"), `module.exports = "${MARKER}";`);
25
+ });
26
+ after(() => {
27
+ rmSync(customerDirectory, { recursive: true, force: true });
28
+ });
29
+ it("resolves an alias target from the customer directory, not from the working directory", async () => {
30
+ const bundle = await new BundleService(customerDirectory).bundle({
31
+ entryPoints: {
32
+ "handler.js": "module.exports = require('node-fetch');",
33
+ },
34
+ minify: false,
35
+ platform: "node",
36
+ logLevel: "silent",
37
+ alias: { "node-fetch": "fetch-native-fixture" },
38
+ });
39
+ expect(bundle).to.contain(MARKER);
40
+ });
41
+ it("leaves an uninstalled alias target to esbuild", async () => {
42
+ const bundling = new BundleService(customerDirectory).bundle({
43
+ entryPoints: {
44
+ "handler.js": "module.exports = require('node-fetch');",
45
+ },
46
+ minify: false,
47
+ platform: "node",
48
+ logLevel: "silent",
49
+ alias: { "node-fetch": "not-installed-anywhere" },
50
+ });
51
+ const error = await bundling.then(() => undefined, (reason) => reason);
52
+ expect(String(error)).to.contain("not-installed-anywhere");
53
+ });
54
+ });
@@ -1,5 +1,13 @@
1
1
  export declare class CustomerConfigServiceContext {
2
2
  appName: string;
3
3
  customersDirectory: string;
4
- constructor(appName?: string, customersDirectory?: string);
4
+ /** Explicit templates/ root; overrides checkout detection and the remote fetch. */
5
+ templatesPath?: string;
6
+ /** Template set = wizard mode: "customer-config" (default) or "pre-poc". */
7
+ mode: string;
8
+ constructor(appName?: string, customersDirectory?: string,
9
+ /** Explicit templates/ root; overrides checkout detection and the remote fetch. */
10
+ templatesPath?: string,
11
+ /** Template set = wizard mode: "customer-config" (default) or "pre-poc". */
12
+ mode?: string);
5
13
  }
@@ -2,10 +2,20 @@ import { CLIParametersExample } from "../../models/cli-parameters.js";
2
2
  export class CustomerConfigServiceContext {
3
3
  appName;
4
4
  customersDirectory;
5
- constructor(appName = "", customersDirectory = CLIParametersExample.CustomersDirectory) {
5
+ templatesPath;
6
+ mode;
7
+ constructor(appName = "", customersDirectory = CLIParametersExample.CustomersDirectory,
8
+ /** Explicit templates/ root; overrides checkout detection and the remote fetch. */
9
+ templatesPath,
10
+ /** Template set = wizard mode: "customer-config" (default) or "pre-poc". */
11
+ mode = "customer-config") {
6
12
  this.appName = appName;
7
13
  this.customersDirectory = customersDirectory;
14
+ this.templatesPath = templatesPath;
15
+ this.mode = mode;
8
16
  this.appName = appName;
9
17
  this.customersDirectory = customersDirectory;
18
+ this.templatesPath = templatesPath;
19
+ this.mode = mode;
10
20
  }
11
21
  }
@@ -59,3 +59,9 @@ export interface CustomerConfigSettings {
59
59
  isPlentymarkets?: boolean;
60
60
  addDowntimeDetection?: boolean;
61
61
  }
62
+ /**
63
+ * Every variable this CLI can hand to the wizard templates — the template
64
+ * manifest in sk-onboarding must be a subset. Extend this together with
65
+ * `CustomerConfigSettings` when the wizard gains a question.
66
+ */
67
+ export declare const TEMPLATE_VARIABLES: readonly string[];
@@ -32,3 +32,39 @@ export var EnvironmentType;
32
32
  EnvironmentType["STAGING"] = "staging";
33
33
  EnvironmentType["TEST"] = "test";
34
34
  })(EnvironmentType || (EnvironmentType = {}));
35
+ /**
36
+ * Every variable this CLI can hand to the wizard templates — the template
37
+ * manifest in sk-onboarding must be a subset. Extend this together with
38
+ * `CustomerConfigSettings` when the wizard gains a question.
39
+ */
40
+ export const TEMPLATE_VARIABLES = [
41
+ "appName",
42
+ "production.host",
43
+ "production.forceInstall",
44
+ "staging.host",
45
+ "staging.forceInstall",
46
+ "activateScopedDeployments",
47
+ "subRouteScope",
48
+ "includeServiceWorker",
49
+ "activateImageOptimisation",
50
+ "removeLazyLoading",
51
+ "addSSR",
52
+ "addSSRInnerShadowDomSupport",
53
+ "addDeviceDetection",
54
+ "useGATracking",
55
+ "useScrapingBee",
56
+ "withGoogleOptimize",
57
+ "activateCfRocketLoaderPlugin",
58
+ "hasSoftNavigations",
59
+ "convertSoftToHardNavigations",
60
+ "handleUsercentrics",
61
+ "isPOV",
62
+ "useDisabledSitesApproach",
63
+ "shopSystemName",
64
+ "isShopify",
65
+ "isShopware",
66
+ "isSalesforce",
67
+ "isOxid",
68
+ "isPlentymarkets",
69
+ "addDowntimeDetection",
70
+ ];
@@ -10,15 +10,20 @@ export declare class CustomerConfigService {
10
10
  private set appName(value);
11
11
  private set outputDirPath(value);
12
12
  private get outputDirPath();
13
- private templates;
13
+ /** Set from the "Downtime Detection" feature answer in the wizard. */
14
+ private includeDowntimeDetection;
14
15
  constructor(context: CustomerConfigServiceContext, cli: CliService);
15
16
  private getOutputDirPath;
16
- private getHandlebarsOutputFileName;
17
+ private getOutputFileName;
17
18
  private getPathWithChangedDir;
18
19
  private isDirNotEmpty;
19
20
  private chooseNewOutputDirectoryName;
21
+ /** A pre-PoC config marks itself via splitTestId "pre_poc" in config_SpeedKit.js. */
22
+ private isPrePocConfig;
23
+ /** Parks every config_* file as *_pre-poc (suffix before the extension). */
24
+ private renamePrePocFiles;
20
25
  private selectOutputDirectory;
21
- private compileHandlebarsFiles;
26
+ private compileTemplateFiles;
22
27
  private getEnvironmentConfig;
23
28
  private shouldFileBeOverwritten;
24
29
  private getConfigSettings;
@@ -1,9 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import Handlebars from "handlebars";
4
3
  import { analyzeSite, } from "../../helpers/site-analyzer.js";
5
- import { DirectoryOverwriteOption, EnvironmentType, ShopSystem, } from "./customer-config-service-model.js";
6
- import { fileURLToPath } from "node:url";
4
+ import { DirectoryOverwriteOption, EnvironmentType, ShopSystem, TEMPLATE_VARIABLES, } from "./customer-config-service-model.js";
5
+ import { findUnknownVariables, resolveTemplateSource, } from "./template-source.js";
7
6
  export class CustomerConfigService {
8
7
  context;
9
8
  cli;
@@ -22,14 +21,8 @@ export class CustomerConfigService {
22
21
  get outputDirPath() {
23
22
  return this._outputDirPath;
24
23
  }
25
- templates = [
26
- "config_customer.json.hbs",
27
- "config_documentHandler.js.hbs",
28
- "config_dynamicBlocks.es6.hbs",
29
- "config_dynamicStyles.css.hbs",
30
- "config_loadHandler.js.hbs",
31
- "config_SpeedKit.js.hbs",
32
- ];
24
+ /** Set from the "Downtime Detection" feature answer in the wizard. */
25
+ includeDowntimeDetection = false;
33
26
  constructor(context, cli) {
34
27
  this.context = context;
35
28
  const { appName } = this.context;
@@ -39,8 +32,8 @@ export class CustomerConfigService {
39
32
  getOutputDirPath(directoryName = "") {
40
33
  return path.join(process.cwd(), this.context.customersDirectory, directoryName);
41
34
  }
42
- getHandlebarsOutputFileName(fileName) {
43
- return fileName.replace(".hbs", "");
35
+ getOutputFileName(fileName) {
36
+ return fileName.replace(/\.njk$/, "");
44
37
  }
45
38
  getPathWithChangedDir(directoryPath, newDirectory) {
46
39
  return path.join(path.dirname(directoryPath), newDirectory);
@@ -65,7 +58,41 @@ export class CustomerConfigService {
65
58
  });
66
59
  return this.selectOutputDirectory(this.getPathWithChangedDir(directoryPath, newDirectoryName));
67
60
  }
61
+ /** A pre-PoC config marks itself via splitTestId "pre_poc" in config_SpeedKit.js. */
62
+ isPrePocConfig(directoryPath) {
63
+ const speedKitConfigPath = path.join(directoryPath, "config_SpeedKit.js");
64
+ if (!fs.existsSync(speedKitConfigPath)) {
65
+ return false;
66
+ }
67
+ return /["']pre_poc["']/.test(fs.readFileSync(speedKitConfigPath, "utf-8"));
68
+ }
69
+ /** Parks every config_* file as *_pre-poc (suffix before the extension). */
70
+ renamePrePocFiles(directoryPath) {
71
+ const configFilePattern = /^(config_[^.]+)\.(.+)$/;
72
+ for (const fileName of fs.readdirSync(directoryPath)) {
73
+ const match = fileName.match(configFilePattern);
74
+ if (!match) {
75
+ continue;
76
+ }
77
+ fs.renameSync(path.join(directoryPath, fileName), path.join(directoryPath, `${match[1]}_pre-poc.${match[2]}`));
78
+ }
79
+ }
68
80
  async selectOutputDirectory(directoryPath) {
81
+ // Graduation: a full-config run over an existing pre-PoC config parks the
82
+ // pre-PoC files as *_pre-poc so both versions can be merged afterwards.
83
+ if (this.context.mode !== "pre-poc" && this.isPrePocConfig(directoryPath)) {
84
+ this.cli.spacer();
85
+ this.cli.write(`${directoryPath} holds a pre-PoC config (splitTestId "pre_poc").\n` +
86
+ "On continue, its config_* files are renamed to *_pre-poc and the full " +
87
+ "config is scaffolded next to them. Merge the verified trackers into the " +
88
+ "new config_loadHandler.js afterwards, then delete the *_pre-poc files.\n");
89
+ const graduate = await this.cli.confirm("Rename the pre-PoC files and continue?");
90
+ if (graduate) {
91
+ this.renamePrePocFiles(directoryPath);
92
+ return directoryPath;
93
+ }
94
+ // declined → fall through to the regular not-empty protection
95
+ }
69
96
  // protect output directory if already existing and not empty:
70
97
  if (this.isDirNotEmpty(directoryPath)) {
71
98
  this.cli.spacer();
@@ -86,13 +113,22 @@ export class CustomerConfigService {
86
113
  }
87
114
  return directoryPath;
88
115
  }
89
- async compileHandlebarsFiles(configSettings) {
116
+ async compileTemplateFiles(configSettings) {
117
+ const source = await resolveTemplateSource(this.context.templatesPath, this.context.mode);
118
+ this.cli.write(`Templates: ${source.origin}\n`);
119
+ // Contract check: the templates may only use variables this CLI knows.
120
+ const unknown = findUnknownVariables(source.manifest, TEMPLATE_VARIABLES);
121
+ if (unknown.length > 0) {
122
+ throw new Error(`The templates use variables this CLI does not know: ${unknown.join(", ")}. ` +
123
+ "Your sk CLI is older than the templates — update it (npm i) and retry.");
124
+ }
90
125
  // overwriting protection for output directory:
91
126
  this.outputDirPath = await this.selectOutputDirectory(this.outputDirPath);
92
127
  this.cli.spacer();
128
+ const templateFiles = source.manifest.files.filter((file) => file !== "downtimeDetection.js.njk" || this.includeDowntimeDetection);
93
129
  // write files:
94
- for (const templateFile of this.templates) {
95
- const outputFileName = this.getHandlebarsOutputFileName(templateFile);
130
+ for (const templateFile of templateFiles) {
131
+ const outputFileName = this.getOutputFileName(templateFile);
96
132
  this.cli.startAction(`CREATE:CUSTOMER:CONFIG:${outputFileName}`, `Adding ${outputFileName}`);
97
133
  try {
98
134
  const outputFilePath = path.join(this.outputDirPath, outputFileName);
@@ -100,11 +136,7 @@ export class CustomerConfigService {
100
136
  this.cli.endAction(`CREATE:CUSTOMER:CONFIG:${outputFileName}`, "skipped!");
101
137
  continue;
102
138
  }
103
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
104
- const filePath = path.resolve(__dirname, "templates", templateFile);
105
- const content = fs.readFileSync(filePath, "utf8");
106
- const template = Handlebars.compile(content);
107
- const result = template(configSettings);
139
+ const result = await source.render(templateFile, configSettings);
108
140
  fs.mkdirSync(path.dirname(outputFilePath), { recursive: true });
109
141
  fs.writeFileSync(outputFilePath, result);
110
142
  this.cli.successAction(`CREATE:CUSTOMER:CONFIG:${outputFileName}`);
@@ -238,6 +270,22 @@ export class CustomerConfigService {
238
270
  }
239
271
  this.cli.spacer();
240
272
  }
273
+ // Pre-PoC: the tracking-only set uses no acceleration answers — stop asking here.
274
+ if (this.context.mode === "pre-poc") {
275
+ return {
276
+ appName: this.appName,
277
+ production,
278
+ staging,
279
+ shopSystemName: selectedShopSystem === ShopSystem.OTHER
280
+ ? undefined
281
+ : selectedShopSystem,
282
+ isShopify,
283
+ isShopware,
284
+ isSalesforce,
285
+ isOxid,
286
+ isPlentymarkets,
287
+ };
288
+ }
241
289
  const resolve = (feature, fallback) => {
242
290
  if (!siteAnalysis)
243
291
  return { label: "", checked: fallback };
@@ -420,10 +468,8 @@ export class CustomerConfigService {
420
468
  async generateConfig() {
421
469
  this.cli.write("\nLet's generate a config...\n");
422
470
  const configSettings = await this.getConfigSettings();
423
- if (configSettings.addDowntimeDetection) {
424
- this.templates.push("downtimeDetection.js.hbs");
425
- }
426
- await this.compileHandlebarsFiles(configSettings);
471
+ this.includeDowntimeDetection = Boolean(configSettings.addDowntimeDetection);
472
+ await this.compileTemplateFiles(configSettings);
427
473
  await this.logWarnings(configSettings);
428
474
  this.cli.writeSuccess("\nConfig generation completed 👏\n");
429
475
  }
@@ -0,0 +1,33 @@
1
+ /** The templates/<set>/manifest.json contract in sk-onboarding. */
2
+ export interface TemplateManifest {
3
+ engine: string;
4
+ engineOptions: Record<string, boolean>;
5
+ files: string[];
6
+ partials?: string[];
7
+ variables: Record<string, {
8
+ type: "boolean" | "string" | "string?";
9
+ sample?: unknown;
10
+ }>;
11
+ }
12
+ export interface TemplateSource {
13
+ /** Where the templates come from — shown to the user. */
14
+ origin: string;
15
+ manifest: TemplateManifest;
16
+ /** Renders one of manifest.files with the wizard settings ({% include %} resolves). */
17
+ render(file: string, settings: Record<string, unknown>): Promise<string>;
18
+ }
19
+ /**
20
+ * Resolves where the wizard templates come from, in order:
21
+ * 1. an explicit --templates path (the templates/ root),
22
+ * 2. the sk-onboarding checkout the CLI runs in (walking up from cwd).
23
+ *
24
+ * `set` picks the template set (wizard mode), e.g. "customer-config" or "pre-poc".
25
+ */
26
+ export declare function resolveTemplateSource(explicitPath?: string, set?: string): Promise<TemplateSource>;
27
+ /**
28
+ * The templates may only use variables this CLI knows — conditional wizard
29
+ * answers are legitimately absent at runtime, so the contract is checked
30
+ * against the CLI's variable inventory, not one run's settings object.
31
+ * A non-empty result means this CLI is older than the templates.
32
+ */
33
+ export declare function findUnknownVariables(manifest: TemplateManifest, knownVariables: readonly string[]): string[];
@@ -0,0 +1,49 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import nunjucks from "nunjucks";
4
+ const TEMPLATES_DIR = "templates";
5
+ /**
6
+ * Resolves where the wizard templates come from, in order:
7
+ * 1. an explicit --templates path (the templates/ root),
8
+ * 2. the sk-onboarding checkout the CLI runs in (walking up from cwd).
9
+ *
10
+ * `set` picks the template set (wizard mode), e.g. "customer-config" or "pre-poc".
11
+ */
12
+ export async function resolveTemplateSource(explicitPath, set = "customer-config") {
13
+ if (explicitPath)
14
+ return localSource(path.resolve(explicitPath), set);
15
+ for (let dir = process.cwd();; dir = path.dirname(dir)) {
16
+ const candidate = path.join(dir, TEMPLATES_DIR);
17
+ if (fs.existsSync(path.join(candidate, set, "manifest.json"))) {
18
+ return localSource(candidate, set);
19
+ }
20
+ if (dir === path.dirname(dir))
21
+ break;
22
+ }
23
+ throw new Error("No wizard templates found. Run inside an sk-onboarding checkout or " +
24
+ "pass --templates <path>.");
25
+ }
26
+ function buildEnvironment(loader, manifest) {
27
+ return new nunjucks.Environment(loader, manifest.engineOptions);
28
+ }
29
+ function localSource(templatesRoot, set) {
30
+ const setDir = path.join(templatesRoot, set);
31
+ const manifest = JSON.parse(fs.readFileSync(path.join(setDir, "manifest.json"), "utf8"));
32
+ const env = buildEnvironment(new nunjucks.FileSystemLoader(templatesRoot), manifest);
33
+ const origin = setDir;
34
+ return {
35
+ origin,
36
+ manifest,
37
+ render: async (file, settings) => env.renderString(fs.readFileSync(path.join(setDir, file), "utf8"), settings),
38
+ };
39
+ }
40
+ /**
41
+ * The templates may only use variables this CLI knows — conditional wizard
42
+ * answers are legitimately absent at runtime, so the contract is checked
43
+ * against the CLI's variable inventory, not one run's settings object.
44
+ * A non-empty result means this CLI is older than the templates.
45
+ */
46
+ export function findUnknownVariables(manifest, knownVariables) {
47
+ const known = new Set(knownVariables);
48
+ return Object.keys(manifest.variables).filter((name) => !known.has(name));
49
+ }
@@ -26,7 +26,9 @@ export const DEFAULT_BLOCKED_FOLDERS = [
26
26
  ".idea",
27
27
  "page-examples",
28
28
  ];
29
- export const DEFAULT_BLOCKED_FILES = ["page-examples"];
29
+ // "_pre-poc": parked pre-PoC files created by the graduation flow of
30
+ // generate-customer-config — kept for the manual merge, never deployed.
31
+ export const DEFAULT_BLOCKED_FILES = ["page-examples", "_pre-poc"];
30
32
  export class IntegrationApiContext {
31
33
  basePath;
32
34
  configName;
@@ -402,6 +402,25 @@
402
402
  "hasDynamicHelp": false,
403
403
  "multiple": false,
404
404
  "type": "option"
405
+ },
406
+ "templates": {
407
+ "description": "Path to the templates/ root (default: the sk-onboarding checkout the CLI runs in, or the sk-onboarding repo on GitLab when run standalone)",
408
+ "name": "templates",
409
+ "hasDynamicHelp": false,
410
+ "multiple": false,
411
+ "type": "option"
412
+ },
413
+ "mode": {
414
+ "description": "Which config to scaffold: the full acceleration config, or the tracking-only pre-PoC config",
415
+ "name": "mode",
416
+ "default": "customer-config",
417
+ "hasDynamicHelp": false,
418
+ "multiple": false,
419
+ "options": [
420
+ "customer-config",
421
+ "pre-poc"
422
+ ],
423
+ "type": "option"
405
424
  }
406
425
  },
407
426
  "hasDynamicHelp": false,
@@ -1112,5 +1131,5 @@
1112
1131
  ]
1113
1132
  }
1114
1133
  },
1115
- "version": "4.21.0"
1134
+ "version": "4.22.1"
1116
1135
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "4.21.0",
4
+ "version": "4.22.1",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"
@@ -110,6 +110,7 @@
110
110
  "iconv-lite": "^0.7.2",
111
111
  "json2csv": "^6.0.0-alpha.2",
112
112
  "node-fetch": "^3.3.2",
113
+ "nunjucks": "^3.2.4",
113
114
  "nypm": "^0.6.6",
114
115
  "puppeteer": "^24.41.0",
115
116
  "puppeteer-extra": "^3.3.6",
@@ -128,6 +129,7 @@
128
129
  "@oclif/test": "^4.1.18",
129
130
  "@types/chai": "^4.3.10",
130
131
  "@types/node": "^25.6.0",
132
+ "@types/nunjucks": "^3.2.6",
131
133
  "@types/uuid": "^9.0.8",
132
134
  "chai": "^4.4.1",
133
135
  "copyfiles": "^2.4.1",
@@ -154,5 +156,10 @@
154
156
  "*.ts": [
155
157
  "eslint --fix --quiet"
156
158
  ]
159
+ },
160
+ "overrides": {
161
+ "nunjucks": {
162
+ "chokidar": "$chokidar"
163
+ }
157
164
  }
158
165
  }