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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,45 @@ Open the server to a specific directory:
```
auro dev --open src/
```

`auro context`
Prints an AI-ready context document describing the Auro Design System, its components, and usage patterns. Designed to be piped into or pasted into AI coding assistants (Claude, Cursor, Copilot, etc.) to prime them on Auro.

#### Options

- `-o, --output <file>` Write the context document to a file instead of stdout (e.g. `AURO_CONTEXT.md`).

#### Examples

Print the context to the terminal:

```
auro context
```

Write the context to a file for your AI tool:

```
auro context --output AURO_CONTEXT.md
```

`auro cem`
Aggregates the Custom Elements Manifests (`custom-elements.json`) of every published Auro component into a single file. Each component's manifest is fetched from the latest published version on unpkg; components that do not publish a manifest yet are skipped. Useful for feeding a complete, machine-readable component API index to IDEs, docs tooling, and AI assistants.

#### Options

- `-o, --output <file>` Path to write the aggregated manifest (default: `custom-elements.aggregate.json`).

#### Examples

Generate the aggregated manifest:

```
auro cem
```

Write it to a specific path:

```
auro cem --output dist/custom-elements.aggregate.json
```
1 change: 1 addition & 0 deletions build-scripts/build-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const aliases = {
"#configs": resolve(projectRoot, "src/configs"),
"#commands": resolve(projectRoot, "src/commands"),
"#scripts": resolve(projectRoot, "src/scripts"),
"#static": resolve(projectRoot, "src/static"),
"#utils": resolve(projectRoot, "src/utils"),
};

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"#configs/*": "./src/configs/*",
"#commands/*": "./src/commands/*",
"#scripts/*": "./src/scripts/*",
"#static/*": "./src/static/*",
"#utils/*": "./src/utils/*"
},
"publishConfig": {
Expand Down
194 changes: 194 additions & 0 deletions src/commands/cem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { Logger } from "@aurodesignsystem/auro-library/scripts/utils/logger.mjs";
import { program } from "commander";
import ora from "ora";
import { AURO_COMPONENT_PACKAGES } from "#static/auroComponents.js";

const UNPKG_BASE = "https://unpkg.com";

interface CemModule {
path: string;
[key: string]: unknown;
}

interface Manifest {
schemaVersion?: string;
modules?: CemModule[];
[key: string]: unknown;
}

interface FetchOutcome {
pkg: string;
manifest: Manifest | null;
/** Human-readable reason the manifest was skipped. */
reason?: string;
/** True when the skip was caused by a transient error (network/5xx), not a genuine 404. */
transient?: boolean;
}

interface ManifestSource {
pkg: string;
manifest: Manifest;
}

/**
* Fetch a single package's Custom Elements Manifest from unpkg.
* Never throws — failures are returned as an outcome so the caller can
* distinguish a genuine absence (404) from a transient error.
*/
async function fetchManifest(pkg: string): Promise<FetchOutcome> {
const url = `${UNPKG_BASE}/${pkg}/custom-elements.json`;

let response: Response;
try {
response = await fetch(url);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { pkg, manifest: null, reason: `request failed (${message})`, transient: true };
}

if (response.status === 404) {
return { pkg, manifest: null, reason: "no custom-elements.json published" };
}

if (!response.ok) {
return { pkg, manifest: null, reason: `HTTP ${response.status}`, transient: true };
}

try {
return { pkg, manifest: (await response.json()) as Manifest };
} catch {
return { pkg, manifest: null, reason: "custom-elements.json is not valid JSON", transient: true };
}
}

/**
* Recursively namespace every local module reference in a CEM node with the
* owning package. A reference is local only when it has a `module` string and
* no `package` sibling (a `package` means it points at an external package, so
* its `module` must be left untouched). This keeps the merged manifest's
* internal references (exports' `declaration.module`, `references[].module`,
* etc.) pointing at the same paths as their now-namespaced modules.
*/
function namespaceReferences(node: unknown, pkg: string): unknown {
if (Array.isArray(node)) {
return node.map((item) => namespaceReferences(item, pkg));
}

if (node && typeof node === "object") {
const source = node as Record<string, unknown>;
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(source)) {
if (key === "module" && typeof value === "string" && source.package == null) {
result[key] = `${pkg}/${value}`;
} else {
result[key] = namespaceReferences(value, pkg);
}
}
return result;
}

return node;
}

/**
* Merge per-package manifests into a single Custom Elements Manifest.
* Every module (and its internal references) is namespaced with its package so
* paths stay unique and every declaration remains traceable to its component.
*/
function mergeManifests(sources: ManifestSource[]): Manifest {
const modules: CemModule[] = [];

for (const { pkg, manifest } of sources) {
for (const module of manifest.modules ?? []) {
const namespaced = namespaceReferences(module, pkg) as CemModule;
namespaced.path = `${pkg}/${module.path}`;
modules.push(namespaced);
}
}

const schemaVersions = new Set(
sources.map((source) => source.manifest.schemaVersion).filter(Boolean),
);
if (schemaVersions.size > 1) {
Logger.warn(
`Merging mixed CEM schema versions: ${[...schemaVersions].join(", ")}`,
);
}

return {
schemaVersion: sources[0]?.manifest.schemaVersion ?? "1.0.0",
modules,
};
}

export default program
.command("cem")
.description(
"Fetch every published Auro component's custom-elements.json and merge them into a single aggregated manifest",
)
.option(
"-o, --output <file>",
"Path to write the aggregated manifest",
"custom-elements.aggregate.json",
)
.action(async (options) => {
const spinner = ora(
`Fetching manifests for ${AURO_COMPONENT_PACKAGES.length} components...`,
).start();

const outcomes = await Promise.all(
AURO_COMPONENT_PACKAGES.map((pkg) => fetchManifest(pkg)),
);

const sources: ManifestSource[] = [];
for (const outcome of outcomes) {
if (outcome.manifest) {
sources.push({ pkg: outcome.pkg, manifest: outcome.manifest });
}
}

if (sources.length === 0) {
spinner.fail("No component manifests could be fetched.");
process.exit(1);
}

spinner.text = "Merging manifests...";
const aggregate = mergeManifests(sources);

const outputPath = path.resolve(process.cwd(), options.output);
try {
await writeFile(
outputPath,
`${JSON.stringify(aggregate, null, 2)}\n`,
"utf-8",
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
spinner.fail(`Failed to write ${options.output}: ${message}`);
process.exit(1);
}

spinner.succeed(
`Aggregated ${sources.length}/${AURO_COMPONENT_PACKAGES.length} component manifests (${aggregate.modules?.length ?? 0} modules) to ${options.output}`,
);

// Report skips after the spinner so output isn't garbled. Genuine 404s are
// expected (not every component publishes a CEM yet); transient failures
// mean the aggregate is incomplete and are treated as an error.
const skipped = outcomes.filter((outcome) => !outcome.manifest);
const transientFailures = skipped.filter((outcome) => outcome.transient);

for (const outcome of skipped) {
Logger.info(`Skipped ${outcome.pkg}: ${outcome.reason}`);
}

if (transientFailures.length > 0) {
Logger.error(
`${transientFailures.length} component(s) failed to fetch transiently; the aggregate may be incomplete. Re-run to retry.`,
);
process.exit(1);
}
});
35 changes: 35 additions & 0 deletions src/commands/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import fs from "node:fs/promises";
import path from "node:path";
import { program } from "commander";
import ora from "ora";
import { AURO_CONTEXT } from "#static/auroContext.js";

export default program
.command("context")
.description(
"Generate an AI assistant context document for the Auro Design System",
)
.option(
"-o, --output <path>",
"Write context to a file instead of stdout (e.g. AURO_CONTEXT.md)",
)
.action(async (options) => {
if (options.output) {
const spinner = ora(`Writing context to ${options.output}...`).start();
try {
const outputPath = path.resolve(process.cwd(), options.output);
await fs.writeFile(outputPath, AURO_CONTEXT, "utf-8");
spinner.succeed(`Auro context written to ${options.output}`);
console.log(
"\nPaste this file into your AI coding tool (Claude, Cursor, Copilot, etc.) to prime it on Auro components.",
);
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
spinner.fail(`Failed to write context: ${message}`);
process.exit(1);
}
} else {
process.stdout.write(AURO_CONTEXT);
}
});
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import "#commands/check-commits.ts";
import "#commands/pr-release.ts";
import "#commands/test.js";
import "#commands/agent.ts";
import "#commands/cem.ts";
import "#commands/context.ts";
import "#commands/docs.ts";
import "#commands/ado.ts";
import "#commands/rc-workflow.ts";
Expand Down
43 changes: 43 additions & 0 deletions src/static/auroComponents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Candidate Auro component packages to check for a Custom Elements Manifest
* (`custom-elements.json`) at their package root. Not every component publishes
* one yet — the `auro cem` command fetches each and skips those that 404, so
* this list can safely include components ahead of their CEM adoption.
*
* Keep this list in sync with the components published under the
* `@aurodesignsystem/` npm scope.
*/
export const AURO_COMPONENT_PACKAGES = [
"@aurodesignsystem/auro-accordion",
"@aurodesignsystem/auro-alert",
"@aurodesignsystem/auro-avatar",
"@aurodesignsystem/auro-background",
"@aurodesignsystem/auro-backtotop",
"@aurodesignsystem/auro-badge",
"@aurodesignsystem/auro-banner",
"@aurodesignsystem/auro-button",
"@aurodesignsystem/auro-card",
"@aurodesignsystem/auro-carousel",
"@aurodesignsystem/auro-datetime",
"@aurodesignsystem/auro-dialog",
"@aurodesignsystem/auro-drawer",
"@aurodesignsystem/auro-flight",
"@aurodesignsystem/auro-flightline",
"@aurodesignsystem/auro-formkit",
"@aurodesignsystem/auro-header",
"@aurodesignsystem/auro-hyperlink",
"@aurodesignsystem/auro-icon",
"@aurodesignsystem/auro-loader",
"@aurodesignsystem/auro-lockup",
"@aurodesignsystem/auro-nav",
"@aurodesignsystem/auro-pane",
"@aurodesignsystem/auro-popover",
"@aurodesignsystem/auro-sidenav",
"@aurodesignsystem/auro-skeleton",
"@aurodesignsystem/auro-slideshow",
"@aurodesignsystem/auro-table",
"@aurodesignsystem/auro-tabs",
"@aurodesignsystem/auro-tail",
"@aurodesignsystem/auro-toast",
"@aurodesignsystem/auro-tokenlist",
];
Loading
Loading