Skip to content
Open
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
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
17 changes: 13 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,22 @@ 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: each spec's own samples by its slug, and the samples of the others in `paths`. */
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`. */
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) ? specs[slug] : paths;
}

/**
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);
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