Skip to content

Refactor extension build process to use modular build steps#6867

Merged
alfonso-noriega merged 7 commits intomainfrom
02-wire-build-config-into-extension-specs
Mar 27, 2026
Merged

Refactor extension build process to use modular build steps#6867
alfonso-noriega merged 7 commits intomainfrom
02-wire-build-config-into-extension-specs

Conversation

@alfonso-noriega
Copy link
Copy Markdown
Contributor

@alfonso-noriega alfonso-noriega commented Feb 19, 2026

Wire extension specifications to use client steps

Summary

This PR wires all extension specifications to use the clientSteps contract introduced in #01-build-steps-infrastructure. The build() method in ExtensionInstance is refactored from a hard-coded mode switch to a generic step execution loop, and every spec now declares its own clientSteps array.

Depends on: 01-build-steps-infrastructure


What changed

ExtensionInstance.build() — mode switch replaced by step loop

Before:

async build(options: ExtensionBuildOptions): Promise<void> {
  const mode = this.specification.buildConfig.mode
  switch (mode) {
    case 'theme':
      await buildThemeExtension(this, options)
      return bundleThemeExtension(this, options)
    case 'function':
      return buildFunctionExtension(this, options)
    case 'ui':
      return buildUIExtension(this, options)
    case 'copy_files':
      return copyFilesForExtension(this, options)
    // ...
  }
}

After:

async build(options: ExtensionBuildOptions): Promise<void> {
  const {clientSteps} = this.specification

  const context: BuildContext = {
    extension: this,
    options,
    stepResults: new Map(),
  }

  const steps = clientSteps
    .filter((group) => group.lifecycle === 'deploy')
    .flatMap((group) => group.steps)

  for (const step of steps) {
    const result = await executeStep(step, context)
    context.stepResults.set(step.id, result)
  }
}

The mode switch and all its direct function call imports (buildThemeExtension, buildUIExtension, buildFunctionExtension, copyFilesForExtension) are removed. The step loop is uniform across all extension types.

extension.tsbuildThemeExtension removed

buildThemeExtension() and its runThemeCheck import are removed from extension.ts — the logic now lives exclusively in build-theme-step.ts.


Specs wired

All specs now declare clientSteps. The buildConfig.mode is kept as a transitional field.

UI extensions

// checkout_post_purchase, checkout_ui_extension, pos_ui_extension,
// product_subscription, web_pixel_extension, ui_extension
clientSteps: [{
  lifecycle: 'deploy',
  steps: [
    {id: 'bundle-ui',          name: 'Bundle UI Extension', type: 'bundle_ui',          config: {}},
    {id: 'copy-static-assets', name: 'Copy Static Assets',  type: 'copy_static_assets', config: {}},
  ],
}]

Theme

clientSteps: [{
  lifecycle: 'deploy',
  steps: [
    {id: 'build-theme',  name: 'Build Theme Extension',  type: 'build_theme',  config: {}},
    {id: 'bundle-theme', name: 'Bundle Theme Extension', type: 'bundle_theme', config: {}},
  ],
}]

Function

clientSteps: [{
  lifecycle: 'deploy',
  steps: [
    {id: 'build-function', name: 'Build Function', type: 'build_function', config: {}},
  ],
}]

Tax calculation

clientSteps: [{
  lifecycle: 'deploy',
  steps: [
    {id: 'create-tax-stub', name: 'Create Tax Stub', type: 'create_tax_stub', config: {}},
  ],
}]

Flow template

clientSteps: [{
  lifecycle: 'deploy',
  steps: [{
    id: 'copy-files', name: 'Copy Files', type: 'include_assets',
    config: {
      inclusions: [{
        type: 'pattern',
        include: ['**/*.flow', '**/*.json', '**/*.toml'],
      }],
    },
  }],
}]

Channel

clientSteps: [{
  lifecycle: 'deploy',
  steps: [{
    id: 'copy-files', name: 'Copy Files', type: 'include_assets',
    config: {
      inclusions: [{
        type: 'pattern',
        baseDir: 'specifications',
        destination: 'specifications',
        include: ['**/*.json', '**/*.toml', '**/*.yaml', '**/*.yml', '**/*.svg'],
      }],
    },
  }],
}]

Tests added

Each spec now has a dedicated build test file asserting:

  • buildConfig.mode is correct
  • clientSteps has the expected step structure
  • The config is JSON-serializable
  • extension.build() produces the correct output on disk (integration)
Test file Covers
ui_extension_build.test.ts bundle_ui + copy_static_assets steps
theme.test.ts build_theme + bundle_theme steps
function_build.test.ts build_function step delegates to buildFunctionExtension
tax_calculation_build.test.ts create_tax_stub writes (()=>{})();
flow_template.test.ts Copies .flow/.json/.toml, not .js/.ts; preserves subdirs
channel.test.ts Copies from specifications/ subdirectory into output/specifications/; filters by extension

Files changed

File Change
models/extensions/extension-instance.ts build() rewritten to use executeStep loop
services/build/extension.ts Removed buildThemeExtension + runThemeCheck import
specifications/ui_extension.ts Added clientSteps
specifications/checkout_post_purchase.ts Added clientSteps
specifications/checkout_ui_extension.ts Added clientSteps
specifications/pos_ui_extension.ts Added clientSteps
specifications/product_subscription.ts Added clientSteps
specifications/web_pixel_extension.ts Added clientSteps
specifications/theme.ts Added clientSteps
specifications/function.ts Added clientSteps
specifications/tax_calculation.ts Added clientSteps
specifications/flow_template.ts Added clientSteps with include_assets config
specifications/channel.ts New spec — include_assets scoped to specifications/ subdirectory
Test files (6) Build integration tests for all spec types

Measuring impact

How do we know this change was effective? Please choose one:

  • n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix
  • Existing analytics will cater for this addition
  • PR includes analytics changes to measure impact

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes

Copy link
Copy Markdown
Contributor Author

alfonso-noriega commented Feb 19, 2026

@alfonso-noriega alfonso-noriega changed the title Abstract build steps to externalize the build configuration Refactor extension build process to use modular build steps Feb 19, 2026
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from 64d581f to c4c3353 Compare February 19, 2026 13:05
@alfonso-noriega alfonso-noriega force-pushed the 02-wire-build-config-into-extension-specs branch from 1c5c718 to 3ac919f Compare February 19, 2026 13:05
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from c4c3353 to d2a2b21 Compare February 19, 2026 13:05
@alfonso-noriega alfonso-noriega force-pushed the 02-wire-build-config-into-extension-specs branch 2 times, most recently from 2b6f7ba to 68c2a9f Compare February 19, 2026 13:20
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from d2a2b21 to 3837d71 Compare February 19, 2026 13:27
@alfonso-noriega alfonso-noriega force-pushed the 02-wire-build-config-into-extension-specs branch 2 times, most recently from 8096aa6 to f6ae0a0 Compare February 19, 2026 13:36
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch 2 times, most recently from 794d3e0 to 75deab1 Compare February 19, 2026 13:53
@alfonso-noriega alfonso-noriega force-pushed the 02-wire-build-config-into-extension-specs branch from f6ae0a0 to 1b503e0 Compare February 19, 2026 13:53
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from 75deab1 to feab6d2 Compare February 19, 2026 14:20
@alfonso-noriega alfonso-noriega force-pushed the 02-wire-build-config-into-extension-specs branch from 1b503e0 to 01cfc8b Compare February 19, 2026 14:20
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from feab6d2 to 27b8cfc Compare February 19, 2026 14:33
@alfonso-noriega alfonso-noriega force-pushed the 02-wire-build-config-into-extension-specs branch from 01cfc8b to 048e70a Compare February 19, 2026 14:33
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from 27b8cfc to 34005f5 Compare February 19, 2026 15:31
@alfonso-noriega alfonso-noriega force-pushed the 02-wire-build-config-into-extension-specs branch from 048e70a to c25acc0 Compare February 19, 2026 15:31
@alfonso-noriega alfonso-noriega force-pushed the 02-wire-build-config-into-extension-specs branch 5 times, most recently from 4dd6917 to 56f0d4a Compare February 20, 2026 12:51
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 20, 2026

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 82.4% 15148/18383
🟡 Branches 74.92% 7460/9957
🟢 Functions 81.43% 3805/4673
🟢 Lines 82.79% 14322/17299

Test suite run success

4000 tests passing in 1531 suites.

Report generated by 🧪jest coverage report action from 9c5a652

@alfonso-noriega alfonso-noriega marked this pull request as ready for review February 20, 2026 13:10
@alfonso-noriega alfonso-noriega requested a review from a team as a code owner February 20, 2026 13:10
@github-actions
Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/public/node/path.d.ts
 import type { URL } from 'url';
 /**
  * Joins a list of paths together.
  *
  * @param paths - Paths to join.
  * @returns Joined path.
  */
 export declare function joinPath(...paths: string[]): string;
 /**
  * Normalizes a path.
  *
  * @param path - Path to normalize.
  * @returns Normalized path.
  */
 export declare function normalizePath(path: string): string;
 /**
  * Resolves a list of paths together.
  *
  * @param paths - Paths to resolve.
  * @returns Resolved path.
  */
 export declare function resolvePath(...paths: string[]): string;
 /**
  * Returns the relative path from one path to another.
  *
  * @param from - Path to resolve from.
  * @param to - Path to resolve to.
  * @returns Relative path.
  */
 export declare function relativePath(from: string, to: string): string;
 /**
  * Returns whether the path is absolute.
  *
  * @param path - Path to check.
  * @returns Whether the path is absolute.
  */
 export declare function isAbsolutePath(path: string): boolean;
 /**
  * Returns the directory name of a path.
  *
  * @param path - Path to get the directory name of.
  * @returns Directory name.
  */
 export declare function dirname(path: string): string;
 /**
  * Returns the base name of a path.
  *
  * @param path - Path to get the base name of.
  * @param ext - Optional extension to remove from the result.
  * @returns Base name.
  */
 export declare function basename(path: string, ext?: string): string;
 /**
  * Returns the extension of the path.
  *
  * @param path - Path to get the extension of.
  * @returns Extension.
  */
 export declare function extname(path: string): string;
 /**
  * Parses a path into its components (root, dir, base, ext, name).
  *
  * @param path - Path to parse.
  * @returns Parsed path object.
  */
 export declare function parsePath(path: string): {
     root: string;
     dir: string;
     base: string;
     ext: string;
     name: string;
 };
 /**
  * Given an absolute filesystem path, it makes it relative to
  * the current working directory. This is useful when logging paths
  * to allow the users to click on the file and let the OS open it
  * in the editor of choice.
  *
  * @param path - Path to relativize.
  * @param dir - Current working directory.
  * @returns Relativized path.
  */
 export declare function relativizePath(path: string, dir?: string): string;
 /**
  * Given 2 paths, it returns whether the second path is a subpath of the first path.
  *
  * @param mainPath - The main path.
  * @param subpath - The subpath.
  * @returns Whether the subpath is a subpath of the main path.
  */
 export declare function isSubpath(mainPath: string, subpath: string): boolean;
 /**
  * Given a module's import.meta.url it returns the directory containing the module.
  *
  * @param moduleURL - The value of import.meta.url in the context of the caller module.
  * @returns The path to the directory containing the caller module.
  */
 export declare function moduleDirectory(moduleURL: string | URL): string;
 /**
  * When running a script using `npm run`, something interesting happens. If the current
  * folder does not have a `package.json` or a `node_modules` folder, npm will traverse
  * the directory tree upwards until it finds one. Then it will run the script and set
  * `process.cwd()` to that folder, while the actual path is stored in the INIT_CWD
  * environment variable (see here: https://docs.npmjs.com/cli/v9/commands/npm-run-script#description).
  *
  * @returns The path to the current working directory.
  */
 export declare function cwd(): string;
 /**
  * Tries to get the value of the `--path` argument, if provided.
  *
  * @param argv - The arguments to search for the `--path` argument.
  * @returns The value of the `--path` argument, if provided.
  */
 export declare function sniffForPath(argv?: string[]): string | undefined;
 /**
  * Returns whether the `--json` or `-j` flags are present in the arguments.
  *
  * @param argv - The arguments to search for the `--json` and `-j` flags.
  * @returns Whether the `--json` or `-j` flag is present in the arguments.
  */
 export declare function sniffForJson(argv?: string[]): boolean;
+/**
+ * Removes any `..` traversal segments from a relative path and calls `warn`
+ * if any were stripped. Normal `..` that cancel out within the path (e.g.
+ * `foo/../bar` → `bar`) are collapsed but never allowed to escape the root.
+ * Both `/` and `\` are treated as separators for cross-platform safety.
+ *
+ * @param input - The relative path to sanitize.
+ * @param warn - Called with a human-readable warning when traversal segments are removed.
+ * @returns The sanitized path (may be an empty string if all segments were traversal).
+ */
+export declare function sanitizeRelativePath(input: string, warn: (msg: string) => void): string;

@alfonso-noriega
Copy link
Copy Markdown
Contributor Author

The concern raised here is a false alarm after investigation.

The two copy_files specs in the codebase — flow_template and channel — neither defines getOutputRelativePath. For those specs:

  • Old code: if (mode === 'copy_files') return ''''
  • New code: basename(this.specification.getOutputRelativePath?.(this) ?? '')basename(undefined ?? '')basename('')''

The result is identical ('') for all existing copy_files specs, so outputPath remains directory, and no file copy destinations are affected.

alfonso-noriega and others added 7 commits March 27, 2026 15:38
…ests

The build integration tests in channel.test.ts exercised copy behavior
already tested by include-assets-step.test.ts. The step-structure and
serialization tests in ui_extension_build.test.ts tested implementation
details without adding meaningful value. Keeping only the mode/config
shape assertions that verify the spec wires up the correct build config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep only 'uses copy_files mode' — the step config shape is covered
by include-assets-step tests and doesn't need duplication here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After stripping implementation-detail tests only one trivial assertion
remained. Removing the file entirely per reviewer feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These tests were testing implementation details already covered by
dedicated unit tests in the build steps layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants