Skip to content
Merged
2 changes: 1 addition & 1 deletion portal-template/portal-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface BuildPaths {
specs: Record<string, string>;
/** Absolute: the `spec/` every document sits in, with the files they refer to. */
specDir: string;
/** Absolute path of the code samples, keyed by path then method; null when there are none. */
/** Absolute path of the code samples, a `CodeSamplesFile`; null when there are none. */
codeSamples: string | null;
/** Absolute. Always exists, and may be empty. */
contentDir: string;
Expand Down
29 changes: 25 additions & 4 deletions portal-template/src/lib/code-samples.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,34 @@ import type { Document } from 'fumadocs-openapi';
import { CODE_SAMPLES_EXTENSION } from './code-samples';
import { isJsonObject, type JsonObject } from './json';

/** Each endpoint's code samples, keyed by path and then by upper-case method, as the CLI writes them. */
export type CodeSamplesByEndpoint = Record<string, Record<string, unknown[]>>;
/** Each endpoint's code samples, keyed by path and then by upper-case method. */
export type CodeSamplesByEndpoint<Sample = unknown> = Record<string, Record<string, Sample[]>>;

/** What the CLI writes: the samples filed under each spec by its slug, and in `paths` those every section takes. */
export interface CodeSamplesFile<Sample = unknown> {
paths: CodeSamplesByEndpoint<Sample>;
specs: Record<string, CodeSamplesByEndpoint<Sample>>;
}

const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);

export async function readCodeSamples(file: string | null): Promise<CodeSamplesByEndpoint> {
return file === null ? {} : (JSON.parse(await readFile(file, 'utf8')) as CodeSamplesByEndpoint);
/** The samples of the section mounted at `slug`: those in `paths`, and those filed under its slug. */
export async function readCodeSamples(file: string | null, slug: string): Promise<CodeSamplesByEndpoint> {
if (file === null) return {};
const { paths, specs } = JSON.parse(await readFile(file, 'utf8')) as CodeSamplesFile;
return Object.hasOwn(specs, slug) ? combined(paths, specs[slug]) : paths;
}

/** Each endpoint's samples from both, `first`'s ahead of `second`'s. */
function combined(first: CodeSamplesByEndpoint, second: CodeSamplesByEndpoint): CodeSamplesByEndpoint {
const merged: CodeSamplesByEndpoint = {};
for (const [path, methods] of [...Object.entries(first), ...Object.entries(second)]) {
const byMethod = (merged[path] ??= {});
for (const [method, samples] of Object.entries(methods)) {
byMethod[method] = [...(byMethod[method] ?? []), ...samples];
}
}
return merged;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion portal-template/src/lib/openapi-section.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { apiBaseDir } from './shared';
*/
export async function openApiSection(slug: string, file: string, codeSamplesFile: string | null) {
const load = async () =>
placeCodeSamples(withoutInternalOperations(await bundleSpec(file)), await readCodeSamples(codeSamplesFile));
placeCodeSamples(withoutInternalOperations(await bundleSpec(file)), await readCodeSamples(codeSamplesFile, slug));
const specName = path.basename(file);
const baseDir = `${apiBaseDir}/${slug}`;
const unsafe = new Set<string>();
Expand Down
7 changes: 3 additions & 4 deletions src/actions/portal/prepare-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export class PreparePortalProjectAction {
this.prompts.contentNotices(source.value.contentNotices, sourceDirectory);
this.prompts.unmergedSpecs(source.value.unmergedSpecs, sourceDirectory);
this.prompts.leftOutSpecs(source.value.leftOutSpecs, sourceDirectory);
this.prompts.sharedEndpoints(source.value.sharedEndpoints, sourceDirectory);

if (!(await confirm())) {
return ActionResult.cancelled();
Expand All @@ -94,9 +93,9 @@ export class PreparePortalProjectAction {
return ActionResult.failed();
}

this.prompts.unplacedSamples(
artifacts.value.codeSampleCatalogs.unplacedIn(source.value.specs.flatMap((spec) => spec.endpoints))
);
const codeSamples = artifacts.value.codeSampleCatalogs.placedOn(source.value.specs, source.value.specDirectory);
this.prompts.sharedEndpoints(codeSamples.sharedEndpoints(), sourceDirectory);
Comment thread
MuHamza30 marked this conversation as resolved.
Resolved
this.prompts.unplacedSamples(codeSamples.unplaced(), sourceDirectory);

const missing = source.value.generatedPages.missingFrom(artifacts.value);
if (missing !== null) {
Expand Down
9 changes: 4 additions & 5 deletions src/infrastructure/portal-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,7 @@ export class PortalProjectService {
projectDirectory,
source,
await this.contentDirectory(projectDirectory, source),
await this.writeCodeSamples(
projectDirectory,
artifacts.codeSampleCatalogs.without(source.sharedEndpoints.map(({ key }) => key))
),
await this.writeCodeSamples(projectDirectory, source, artifacts.codeSampleCatalogs),
await this.writeDownloads(projectDirectory, artifacts)
);

Expand Down Expand Up @@ -192,13 +189,15 @@ export class PortalProjectService {
// The template places the samples on the specs as it bundles them, so the specs are read where they are.
private async writeCodeSamples(
projectDirectory: DirectoryPath,
source: PortalSource,
codeSampleCatalogs: CodeSampleCatalogs
): Promise<FilePath | null> {
if (codeSampleCatalogs.isEmpty()) {
return null;
}
const file = new FilePath(projectDirectory, new FileName('code-samples.json'));
await this.fileService.writeContents(file, JSON.stringify(codeSampleCatalogs.toJson()));
const placed = codeSampleCatalogs.placedOn(source.specs, source.specDirectory);
await this.fileService.writeContents(file, JSON.stringify(placed.toJson()));
return file;
}

Expand Down
22 changes: 19 additions & 3 deletions src/prompts/portal/code-samples.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import { log } from '@clack/prompts';
import { DirectoryPath } from '../../types/file/directoryPath.js';
import { SharedEndpoint, UnplacedSamples } from '../../types/portal/code-samples.js';
import { format as f } from '../format.js';
import { sharedLines } from './source.js';

export function reportUnplacedSamples(endpoints: string[]): void {
if (endpoints.length === 0) {
export function reportSharedEndpoints(shared: SharedEndpoint[], sourceDirectory: DirectoryPath): void {
if (shared.length === 0) {
return;
}
log.warn(`No operation in ${f.var('spec')} takes the code samples for ${endpoints.join(', ')}.`);
const one = shared.length === 1;
log.warn(
`More than one API definition declares ${one ? 'this operation' : 'these operations'}, so the portal shows no ` +
`SDK sample on ${one ? 'it' : 'them'}:`
);
log.message(sharedLines(shared, sourceDirectory));
}

export function reportUnplacedSamples(unplaced: UnplacedSamples[], sourceDirectory: DirectoryPath): void {
for (const { where, endpoints } of unplaced) {
log.warn(
`No operation in ${f.var(where.relativeTo(sourceDirectory))} takes the code samples for ${endpoints.join(', ')}.`
);
}
}
10 changes: 5 additions & 5 deletions src/prompts/portal/prepare-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import { PortalAuthorizationFailure } from '../../infrastructure/services/portal
import { DirectoryPath } from '../../types/file/directoryPath.js';
import { FileName } from '../../types/file/fileName.js';
import { FilePath } from '../../types/file/filePath.js';
import { SharedEndpoint, UnplacedSamples } from '../../types/portal/code-samples.js';
import { ContentNotices } from '../../types/portal/content-notices.js';
import { MissingArtifacts } from '../../types/portal/generated-pages.js';
import { PortalArtifacts } from '../../types/portal/portal-artifacts.js';
import { PortalSourceProblem, SharedEndpoint } from '../../types/portal/portal-source.js';
import { PortalSourceProblem } from '../../types/portal/portal-source.js';
import { format as f } from '../format.js';
import { describeMissingArtifacts, generateArtifacts } from './artifacts.js';
import { reportAuthorizationFailure } from './authorization.js';
import { reportUnplacedSamples } from './code-samples.js';
import { reportSharedEndpoints, reportUnplacedSamples } from './code-samples.js';
import {
reportContentNotices,
reportLeftOutSpecs,
reportShadowedFiles,
reportSharedEndpoints,
reportSourceProblem,
reportUnmergedSpecs
} from './source.js';
Expand Down Expand Up @@ -75,8 +75,8 @@ export class PreparePortalProjectPrompts {
reportSharedEndpoints(shared, sourceDirectory);
}

public unplacedSamples(endpoints: string[]) {
reportUnplacedSamples(endpoints);
public unplacedSamples(unplaced: UnplacedSamples[], sourceDirectory: DirectoryPath) {
reportUnplacedSamples(unplaced, sourceDirectory);
}

// A run delivers everything or nothing, so a gap is the server's, and trying again is the fix.
Expand Down
15 changes: 1 addition & 14 deletions src/prompts/portal/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
PortalSourceProblem,
ReservedAddressPage,
SharedAddress,
SharedEndpoint,
SpecConversion,
UnreadableSpecReason
} from '../../types/portal/portal-source.js';
Expand Down Expand Up @@ -57,7 +56,7 @@ const relative = (file: FilePath, sourceDirectory: DirectoryPath): string => f.v
const listed = (files: FilePath[], sourceDirectory: DirectoryPath): string =>
listedInProse(files.map((file) => relative(file, sourceDirectory)));

const sharedLines = <Key>(claims: Shared<Key>[], sourceDirectory: DirectoryPath): string =>
export const sharedLines = <Key>(claims: Shared<Key>[], sourceDirectory: DirectoryPath): string =>
claims.map(({ key, files }) => ` • ${f.var(String(key))}: ${listed(files, sourceDirectory)}`).join('\n');

const spelt = (found: FilePath, sourceDirectory: DirectoryPath): string =>
Expand Down Expand Up @@ -417,18 +416,6 @@ export function reportLeftOutSpecs(files: FilePath[], sourceDirectory: Directory
);
}

export function reportSharedEndpoints(shared: SharedEndpoint[], sourceDirectory: DirectoryPath): void {
if (shared.length === 0) {
return;
}
const one = shared.length === 1;
log.warn(
`More than one API definition declares ${one ? 'this operation' : 'these operations'}, so the portal shows no ` +
`SDK sample on ${one ? 'it' : 'them'}:`
);
log.message(sharedLines(shared, sourceDirectory));
}

export function reportIgnoredNavigationFiles(files: FilePath[], sourceDirectory: DirectoryPath): void {
if (files.length === 0) {
return;
Expand Down
2 changes: 0 additions & 2 deletions src/types/portal-source-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import {
SpecConversion,
UnreadableSpecReason
} from './portal/portal-source.js';
import { shared } from './portal/shared.js';
import { ReferencedFile, SpecBundle } from './portal/spec-bundle.js';
import { SpecMetadata } from './portal/spec-metadata.js';
import { SpecContext } from './spec-context.js';
Expand Down Expand Up @@ -172,7 +171,6 @@ export class PortalSourceContext {
specDirectory: this.specDirectory,
unmergedSpecs,
leftOutSpecs,
sharedEndpoints: shared(specs.flatMap(({ file, endpoints }) => endpoints.map((key) => ({ key, file })))),
contentDirectory,
staticDirectory,
shadowedFiles: staticDirectory === null ? [] : await this.shadowedFiles(staticDirectory),
Expand Down
Loading
Loading