Skip to content

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

Merged
ejhammond merged 1 commit into
xds-unprefix-integrationfrom
astryx/unified-module-loading
Jun 30, 2026
Merged

refactor(cli): unified module loading with validation at the load boundary#3293
ejhammond merged 1 commit into
xds-unprefix-integrationfrom
astryx/unified-module-loading

Conversation

@ejhammond

Copy link
Copy Markdown
Contributor

What

Makes loading external user-authored JS uniform and moves validation to the load boundary.

Previously, four loaders each imported-and-picked-and-validated differently, and the create* factories also validated. This change:

  1. Adds one shared loadModuleWithSchema(file, schema, {label}) in lib/module-loader.mjs — "execute the module, take the default export, validate it against the expected schema."
  2. Makes all four create* factories pure typed identity (stamp-only, no runtime validation). Their value is the exported TypeScript surface for editor DX.
  3. Validates at the load/discovery boundary via the shared helper against an envelope schema.
  4. Removes the bespoke "must be a createCodemod result" structural check in favor of schema validation.

Behavior stays equivalent — genuinely invalid inputs are still rejected, just at load, not in the factory.

Envelope schemas (the load-boundary contract)

  • ConfigAstryxConfigSchema (unchanged; strict — unknown keys still rejected)
  • IntegrationAstryxIntegrationSchema (unchanged)
  • CodemodCodemodEnvelopeSchema — discriminated union over the stamped type ('code' may carry fileExtensions; 'config' may not)
  • TemplateTemplateEnvelopeSchemaBaseTemplateSchema + type: 'page' | 'block'

A plain object matching an envelope is accepted regardless of how it was produced (no more factory-identity coupling).

Migrated loaders

  • lib/project.mjs Project.loadloadModuleWithSchema(configPath, AstryxConfigSchema, {label: 'astryx.config'}) (strict config validation preserved)
  • lib/integrations.mjs loadManifestObject + loadIntegrationsloadModuleWithSchema(..., AstryxIntegrationSchema, ...) (default-export only; dropped the ?? mod.integration ?? mod fallback)
  • codemods/integration-discovery.mjsloadModuleWithSchema(file, CodemodEnvelopeSchema, ...) (replaces bespoke validateCodemodExport)
  • api/template.mjs loadIntegrationDocloadModuleWithSchema(file, TemplateEnvelopeSchema, ...) (default-export only; dropped ?? mod.doc fallback)

The built-in core .doc.mjs / template.doc.mjs loader (loadDocModuledocModule.doc) is untouched.

Out of scope

No changes to the upgrade command ordering or dry-run (separate follow-up). Config strictness at load is not loosened.

Test changes

  • Factory tests that asserted factory-time throws are re-pointed: stamp/identity behavior is verified on the factory, and rejection cases now assert the envelope schema (or the load path) rejects.
  • Codemod discovery tests asserting the old "must be a createCodemod result" message now assert the schema-validation message, plus a new test proving a plain-object envelope is accepted by discovery and a malformed one is rejected at load.
  • New focused loadModuleWithSchema tests (valid default export passes; missing default export throws; schema mismatch throws with a readable message; hermetic repo-local temp module).

Green gate

  • npx vitest run packages/cli1596 passed (97 files)
  • node scripts/sync-exports.js --check → 0
  • ./scripts/add-copyright.sh --check → 0
  • pnpm -F @astryxdesign/cli typecheck:json-api → 0
  • pnpm build + node scripts/verify-exports.mjs → 0
  • npx eslint on changed files → 0
  • Smoke: --help OK; component --list --json valid envelope; upgrade --list --json lists codemods
  • node scripts/check-changesets.mjs → 0

…ndary

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.
@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:11pm

Request Review

@ejhammond ejhammond merged commit 55c5c75 into xds-unprefix-integration Jun 30, 2026
3 checks passed
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