Skip to content

fix(cli): run core codemods before loading config in upgrade (with dry-run guidance)#3295

Merged
ejhammond merged 1 commit into
xds-unprefix-integrationfrom
astryx/upgrade-config-codemod-ordering
Jun 30, 2026
Merged

fix(cli): run core codemods before loading config in upgrade (with dry-run guidance)#3295
ejhammond merged 1 commit into
xds-unprefix-integrationfrom
astryx/upgrade-config-codemod-ordering

Conversation

@ejhammond

Copy link
Copy Markdown
Contributor

Problem

astryx upgrade called Project.load(process.cwd()) up front, before running any codemods. Project.load strict-validates astryx.config.* (unknown keys rejected) and throws on failure. This created a chicken-and-egg:

A consumer whose config still has the legacy layout.components key — exactly what the v0.1.3 codemod migrate-layout-components-to-experimental exists to fix — gets their config rejected at load, so the upgrade aborts and the codemod that would repair it never runs.

Fix

Reorder the upgrade pipeline so core codemods run before the config is loaded:

  1. version detect (unchanged)
  2. up-to-date check (unchanged)
  3. resolve + run CORE codemods (registry getTransformsBetweenrunCodemods). Config codemods read astryx.config.* directly via runConfigCodemod; code codemods scan --path. Neither needs the loaded config. In --apply the config codemod writes the repaired config; in dry-run it only previews.
  4. load config via Project.load (strict validation unchanged), now wrapped in a graceful dry-run catch:
    • dry-run + a pending core config codemod actually previewed a change → don't abort. Report that the config currently fails validation but a pending config codemod would fix it, print the exact astryx upgrade --from <from> --codemod <name> --apply command to repair it, and finish the dry-run cleanly (exit 0). Integrations are skipped for the preview and noted as deferred to the --apply run.
    • otherwise (apply mode, or dry-run with no config codemod that would fix it — including a no-op or throwing codemod) → genuine config error, abort as before.
  5. discover + run integration codemods (only on a successful config load); the integration-issue nudge moves here too.
  6. postCodemod hooks + agent-docs refresh (unchanged, still last).

Config strictness is not loosened. The codemod-execution-error gate still aborts on a throwing transform.

Tests

New upgrade.config-ordering.test.mjs (repo-local temp project + chdir):

  • dry-run with legacy layout.components → reports the fixable status + suggested --codemod ... --apply command; exit 0; config not written; integrations skipped.
  • --apply with the same config → core config codemod rewrites it to experimental.xle.components, strict load then succeeds, upgrade completes.
  • dry-run with a genuinely invalid config (integrations: 5) and no fixing codemod → still aborts (exit nonzero).
  • happy path (valid config, no config codemod) → unchanged.

All existing upgrade/runner/integration tests stay green.

Before / after (manual smoke)

Config export default { layout: { components: { KpiCard: '@/components/KpiCard' } } }:

  • astryx upgrade --from 0.1.2 (dry-run): previews the fix, prints the fixable-config guidance + suggested command, exit 0 (no crash on the legacy key).
  • astryx upgrade --from 0.1.2 --apply: rewrites the config to experimental.xle.components, completes.

…y-run guidance)

upgrade loaded astryx.config strictly before running any codemods, so a
consumer whose config still had the legacy layout.components key — exactly
what the v0.1.3 config codemod migrate-layout-components-to-experimental
repairs — got rejected at load and the codemod that would fix it never ran.

Reorder the pipeline so core codemods (config + code) run before
Project.load. In --apply the config codemod writes the repaired config to
disk, then the strict load succeeds. In dry-run, a config that fails
validation but is fixable by a pending core config codemod no longer aborts:
it reports the fixable error and the exact --codemod ... --apply command to
repair it, then finishes the dry-run cleanly (integrations are processed on
the --apply run). Strictness is unchanged; a genuine config error or a
throwing transform still aborts.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jun 30, 2026
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jun 30, 2026 10:59pm

Request Review

@ejhammond ejhammond merged commit 0214e82 into xds-unprefix-integration Jun 30, 2026
3 checks passed
ejhammond added a commit that referenced this pull request Jul 1, 2026
* ci: run PR checks on xds-unprefix-integration base branch

* feat(cli): remove gap-report; swizzle prints feedback note (#3255)

Delete the standalone gap-report command and its supporting helpers.
After a successful swizzle the CLI now prints a short maintainer
feedback note pointing at the issue tracker (and, when the gh CLI is
available, a ready-to-run 'gh issue create' command) instead of
filing issues directly.

The swizzle.copy JSON result drops the gapReport fields and adds an
optional feedback object { issuesUrl, ghCommand? }.

* feat(cli): strict config + integration v1 schema and /integration export (#3261)

Replace the loose passthrough config and integration API with a locked,
strict v1 surface.

AstryxConfig is now { integrations?: string[]; issuesUrl?: string;
hooks?: { postCodemod?: PostCodemodHook[] } } and validates with a strict
Zod schema — unknown keys are hard errors. AstryxIntegration is now
{ components?; templates?; codemods?; issuesUrl? }, also strict.

- New @astryxdesign/cli/integration subpath export with createIntegration;
  createConfig stays on @astryxdesign/cli/config.
- Integrations resolve by package name only. Each package declares a single
  conventional root manifest (astryx.integration.{ts,mjs,js}) sibling to its
  package.json; identity (name/version) comes from package.json. .ts manifests
  load via jiti.
- App config loads from astryx.config.{ts,mjs,js} sibling to the nearest
  package.json (no upward closest-config walk); multiple configs is an error,
  missing yields an empty config.
- Post-codemod hooks now come from app config (hooks.postCodemod) and run via
  execFile; dry-run previews, apply executes in order and fails on nonzero exit.

Clean breaking change — the prior config/integration API was not in real use.

* feat(cli): static template API and /template export (#3263)

Add createPageTemplate/createBlockTemplate authoring helpers exported from
@astryxdesign/cli/template, validated with zod and tagging each doc with its
page/block type.

Add type-driven, package-scoped discovery for integration-provided templates:
a template id is the path under the integration's templates root minus the
.doc.* suffix, with a required same-stem <id>.tsx source. The doc's type
decides scaffolding (no /pages vs /blocks requirement). The template command
gains --package, ambiguity errors that list candidates, and list entries that
always carry id/name/description/type/package. Built-in template discovery,
command behavior, and tests are unchanged.

* feat(cli): file-based codemod API and /codemod export (#3264)

Add createCodemod/createConfigCodemod authoring helpers behind a new
@astryxdesign/cli/codemod export, and consume an integration's resolved
codemods directory during `astryx upgrade`. Integration codemods are
discovered version-first (<codemodsRoot>/<version>/<id>.<ext>), validated
strictly, and run alongside the core registry codemods ordered by version
(config codemods first, then code codemods). Broken discovery or a throwing
transform fails the upgrade.

* feat(cli): component package ownership and source discovery (#3265)

* feat(cli): validate-integration command (#3266)

Add 'astryx validate-integration' to validate one integration package at a
time — its conventional manifest, declared contribution roots, and the
codemod/template/component contributions — reporting findings with the
AstryxIntegrationIssue model (code, severity, message).

With no argument it validates the local package (nearest package.json +
sibling manifest); with a package name it validates an installed package
from node_modules. Supports --json via the integration.validate envelope
and exits 1 when any error-severity issue is present.

* feat(cli): swizzle integration-owned components + config-routed feedback (#3267)

Generalize `astryx swizzle` so it can copy integration-owned components,
not just core. Component resolution now goes through package ownership
(core + configured integrations); --package scopes the lookup and an
ambiguous name across packages errors with the candidate list instead of
silently preferring core.

Escaping relative imports are rewritten to the OWNING package's subpaths
(rewriteImports gains an optional ownerPackage param defaulting to core).
The copy now also excludes *.doc.* files. Maintainer feedback is routed
through config: core uses config.issuesUrl (falling back to the default),
integration components use their manifest issuesUrl, and the note is
omitted entirely when no issues URL is available. The swizzle.copy JSON
envelope gains an owner `package` field.

* fix(cli): align error-code type declarations with runtime codes (#3272)

* feat(cli): warn when a configured integration has validation issues (#3274)

Add a compact, non-blocking nudge: when a configured integration has
validation issues, component, template, and upgrade print exactly one
line per integration to stderr pointing at validate-integration, instead
of silently skipping broken contributions.

The nudge reuses the validate-integration validators via a new
validateLoadedIntegration export (no validation logic duplicated) and a
shared warnOnIntegrationIssues helper. It writes to stderr only (never
corrupts --json envelopes), is suppressed in --json mode, never throws,
and never changes the exit code.

* feat(cli): move XLE app components to experimental.xle.components config (#3275)

Register app-local XLE components through the validated config under
experimental.xle.components (object form: { from, description?, default? })
instead of the previous unvalidated layout.components read. The raw-read
shim and its special cwd-first config lookup are removed; XLE now resolves
the config via loadConfig, consistent with the rest of the CLI.

* refactor(cli): extract shared module-loader util for config + integration loading (#3276)

Config and integration loading each duplicated a jiti singleton + user-module
import helper and a conventional-file-by-basename discovery filter. Extract both
into packages/cli/src/lib/module-loader.mjs (importUserModule, findPresentFiles)
and route both callers through it. No behavior change; all error messages and
return shapes are preserved.

* feat(cli): Project configuration API (replaces loadConfig) (#3277)

* feat(cli): add Project configuration API with pluggable cache

Introduce the Project class as a single, lazy, memoized entry point for
reading resolved project configuration and discovery (components, templates,
codemods) plus integration issue routing. Add a small pluggable cache
(InMemoryConfigCache) keyed by a config content hash + cwd + discovery kind.

* refactor(cli): migrate all callers to Project and remove loadConfig

Route every config/discovery read through Project.load: discover, doctor,
layout, template, component, swizzle, and the component/template command
wrappers. findConfigPath now lives in lib/project.mjs as the single home.
Removes lib/config.mjs (loadConfig) with no shim.

* feat(cli): skip broken integrations on upgrade and add --skip-codemod

Upgrade now treats an integration definition error (bad manifest/export,
duplicate ids, missing root) as skip-and-warn rather than a hard failure,
while an execution-time codemod throw still aborts the run. Adds a variadic
--skip-codemod flag to exclude named codemods (core transform name or
integration codemod id) so users can re-run past a failed codemod.

* refactor(cli): use shared module-loader for codemod and template-doc loading (#3278)

* ci: restore main-only PR triggers ahead of integration merge

* refactor(cli): unify config codemod runner and remove obsolete xds config-surface codemod (#3286)

Remove the legacy in-CLI config codemod (migrate-xds-config-surfaces) and
its bespoke {config:{packageJson,astryxConfig,xdsConfig}} execution machinery.
The @xds/* config rename is handled by a separate out-of-band migration, so
this codemod is redundant for the remaining holdouts — they reach 0.1.x with
configs already renamed.

Extract the modern (file, api)/jscodeshift config and code runners into a
shared run-codemod.mjs used by both the core registry runner and the
integration runner, so there is a single implementation. Core registry config
codemods now route through the same path: a core entry signals config via
meta.codemodType === 'config'.

* feat(cli): v0.1.3 codemod migrating layout.components to experimental.xle.components (#3290)

The published 0.1.2 CLI read XLE app-component registration from
astryx.config.* under layout.components. The next release relocates this
to experimental.xle.components, validated by a strict schema that rejects
unknown keys, so consumers with layout.components would hard-error on
upgrade. This adds a v0.1.3 config codemod that performs the straight
relocation.

Semantics are identical between the old and new shapes; this is purely a
move plus a string to object normalization:
- string 'X' becomes { from: 'X' } (named import, key = export name)
- object { from, description?, default? } is carried over unchanged

The transform recognizes the default export as either a bare object
literal or a createConfig({ ... }) wrapper, and bails with a clear
migrate-manually error when the config cannot be statically analyzed,
when experimental.xle already exists, when layout has keys other than
components, or when an entry is neither a string nor an object literal.

* refactor(cli): unified module loading with validation at the load boundary (#3293)

Add a single shared loadModuleWithSchema(file, schema, {label}) that imports a
user-authored module, takes its default export, and validates it against a zod
schema. Route all four user-module loaders (config, integration, codemod,
template) through it so loading and validation are uniform and happen at the
load/discovery boundary.

The create* factories (createConfig, createIntegration, createCodemod,
createConfigCodemod, createPageTemplate, createBlockTemplate) become pure typed
identity helpers: they stamp the type discriminator where applicable and return
the definition unchanged, performing no runtime validation. Their value is the
exported TypeScript surface for editor DX.

Define the metadata envelope schemas the loader validates: AstryxConfigSchema
and AstryxIntegrationSchema (existing), CodemodEnvelopeSchema (a discriminated
union over the stamped type), and TemplateEnvelopeSchema (BaseTemplateSchema +
type). The bespoke 'must default-export a createCodemod result' structural
check is removed in favor of schema validation, so a plain object matching the
envelope is accepted regardless of how it was produced.

Config strictness at the load boundary is preserved (unknown keys still
rejected). The built-in core .doc.mjs / template.doc.mjs loader and convention
are untouched; only the integration template path moves to loadModuleWithSchema.

* fix(cli): run core codemods before loading config in upgrade (with dry-run guidance) (#3295)

upgrade loaded astryx.config strictly before running any codemods, so a
consumer whose config still had the legacy layout.components key — exactly
what the v0.1.3 config codemod migrate-layout-components-to-experimental
repairs — got rejected at load and the codemod that would fix it never ran.

Reorder the pipeline so core codemods (config + code) run before
Project.load. In --apply the config codemod writes the repaired config to
disk, then the strict load succeeds. In dry-run, a config that fails
validation but is fixable by a pending core config codemod no longer aborts:
it reports the fixable error and the exact --codemod ... --apply command to
repair it, then finishes the dry-run cleanly (integrations are processed on
the --apply run). Strictness is unchanged; a genuine config error or a
throwing transform still aborts.

* fix(cli): sync API/CLI parity harness with package-qualified component list (#3308)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant