Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/cli-component-doc-names-another-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/cli': patch
---

[feat] An integration component doc can name a differently named export of the file beside it: when `Slides.tsx` exports `AcmeCarousel` and nothing named `Slides`, `Slides.doc.mjs` can set `name: 'AcmeCarousel'`, and a kebab-case `acme-widget.doc.mjs` can name `AcmeWidget`. `component --list`, `component <Name>`, `search`, `swizzle`, the `doctor integration` checks and `integration verify` know the component by that name, and its file name still opens it. A name that Core or another doc's file already has stays theirs on every surface unless `--package` names the integration. Docs and sources are read without running them; a doc named after its file, or beside a file that exports its own name, reads exactly as before.

@josephfarina
52 changes: 45 additions & 7 deletions packages/cli/api/component/_adapter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,22 @@ import {
import {
CORE_PACKAGE,
discoverComponents,
docStem,
findComponentReadme,
findComponentSource,
findExternalComponentDoc,
findIntegrationComponentDoc,
findIntegrationComponentSource,
findIntegrationComponent,
resolveImportPath,
resolveIntegrationImportPath as resolveIntegrationImport,
unscopedComponentOwners,
} from '../../foundation/discovery/component-discovery.mjs';
import {Project} from '../../foundation/config/project.mjs';
import {loadComponentDoc as loadValidatedComponentDoc} from '../../foundation/discovery/component-loader.mjs';
import {resolveComponentReplacements} from '../../foundation/discovery/component-replacement.mjs';
import {searchComponents} from '../../foundation/text/string-utils.mjs';
import {AstryxError} from '../error.mjs';

export {CORE_PACKAGE};
export {CORE_PACKAGE, unscopedComponentOwners};

/**
* A loaded component doc. The shared validated loader accepts stamped and legacy
Expand Down Expand Up @@ -71,6 +72,8 @@ export {CORE_PACKAGE};
* @property {string|null} sourcePath
* @property {string|undefined} issuesUrl
* @property {import('../../foundation/integrations/integrations.mjs').LoadedIntegration|null} integration
* @property {import('../../foundation/discovery/component-discovery.mjs').NameHold} hold
* how the owner holds the name, for the owner rule
*/

/**
Expand Down Expand Up @@ -240,17 +243,19 @@ export function resolveOwners(coreDir, dirName, loadedIntegrations) {
sourcePath: findComponentSource(coreDir, dirName),
issuesUrl: undefined,
integration: null,
hold: 'core',
});
}
for (const integration of loadedIntegrations) {
const docPath = findIntegrationComponentDoc(integration, dirName);
if (!docPath) continue;
const found = findIntegrationComponent(integration, dirName);
if (!found) continue;
owners.push({
package: integration.name,
docPath,
sourcePath: findIntegrationComponentSource(integration, dirName),
docPath: found.docPath,
sourcePath: found.sourcePath,
issuesUrl: integration.issuesUrl,
integration,
hold: found.hold,
});
}
return owners;
Expand Down Expand Up @@ -361,6 +366,39 @@ export function resolveLegacyExternalDoc(ext, dirName) {
return extDocPath && extDocPath.endsWith('.doc.mjs') ? extDocPath : null;
}

/**
* Whether a legacy external package documents `dirName`.
* @param {string} cwd
* @param {string} dirName
* @returns {boolean}
*/
export function legacyExternalDocuments(cwd, dirName) {
return discoverExternalPackages(cwd).some(
ext => resolveLegacyExternalDoc(ext, dirName) != null,
);
}

/**
* Whether a legacy docs package keeps `dirName` over these owners, each of which
* holds it only as an export a doc records.
* @param {string} cwd
* @param {string} dirName
* @param {Array<{hold?: string, docPath?: string | null}>} owners
* @returns {boolean}
*/
export function legacyExternalKeeps(cwd, dirName, owners) {
return (
owners.length > 0 &&
owners.every(
owner =>
owner.hold === 'doc' &&
owner.docPath != null &&
docStem(owner.docPath).toLowerCase() !== dirName.toLowerCase(),
) &&
legacyExternalDocuments(cwd, dirName)
);
}

/**
* The core source file for `dirName` (the no-scope `--source` path reads core
* directly — it does NOT fall back to externals or fuzzy search).
Expand Down
15 changes: 12 additions & 3 deletions packages/cli/api/component/component.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import {
classifyScope,
assertUnambiguousOwners,
resolveLegacyExternalDoc,
legacyExternalKeeps,
resolveCoreSourcePath,
resolveUnscopedDoc,
loadComponentDoc,
scopeSubComponent,
ComponentAmbiguityError,
installedComponentPackageVersion,
resolveComponentReplacement,
unscopedComponentOwners,
} from './_adapter.mjs';
import {componentList} from './list/list.mjs';
import {componentDetail} from './detail/detail.mjs';
Expand Down Expand Up @@ -481,12 +483,14 @@ export async function component(name, options = {}) {
});
}

// The owner rule runs first, over valid and invalid owners alike.
const ruled = unscopedComponentOwners(owners);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule sees only Core, file-named and doc-held owners, so a lone doc-held owner is returned before resolveUnscopedDoc checks legacy astryx.docs packages for the exact name. With a legacy package documenting AcmeCarousel and Slides.doc.mjs naming AcmeCarousel, astryx component AcmeCarousel returns @legacy/kit on main and @acme/widgets here. Checking for an exact legacy docs match before returning an owner that holds the name only by doc keeps that answer.

// Invalid integration metadata does not create ambiguity against a valid
// owner. Keep raw owners only when no doc owner is valid, so an integration-
// owner. Keep ruled owners only when no doc owner is valid, so an integration-
// only component still exposes its source and returns ERR_INVALID_DOC for
// detail instead of degrading to an unrelated unknown-component error.
const validOwners = [];
for (const owner of owners) {
for (const owner of ruled) {
if (owner.package === CORE_PACKAGE) {
validOwners.push(owner);
continue;
Expand All @@ -498,7 +502,12 @@ export async function component(name, options = {}) {
// Project/Doctor report invalid metadata; it is not an effective owner.
}
}
const effectiveOwners = validOwners.length > 0 ? validOwners : owners;
const loadable = validOwners.length > 0 ? validOwners : ruled;
// A legacy docs package's exact doc keeps its name over an export name a doc
// records, as Core and file-named owners do.
const effectiveOwners = legacyExternalKeeps(cwd, dirName, loadable)
? []
: loadable;

// ── Ambiguity: owned by MORE THAN ONE valid package, no --package ──
assertUnambiguousOwners(effectiveOwners, dirName);
Expand Down
Loading
Loading