diff --git a/docs/package.json b/docs/package.json index a26bc859d..d0c2970fd 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,7 +3,6 @@ "version": "1.0.0-beta.10", "private": true, "scripts": { - "build:dependencies": "wireit", "build-api": "wireit", "build-api:watch": "wireit", "build-guides": "wireit", @@ -123,32 +122,32 @@ "node": ">=16.14" }, "wireit": { - "build:dependencies": { - "command": "pnpm --filter upscaler build", - "dependencies": [ - "../models/esrgan-medium:build" - ] - }, "build-api": { - "command": "pnpm --filter @upscalerjs/scripts docs:build-api", + "command": "pnpm --filter @internals/scripts write:docs:api", "dependencies": [ - "../internals/common:build" + "../internals/scripts:build" ] }, "build-api:watch": { - "command": "nodemon -e ts --ignore '../packages/upscalerjs/**/*.generated.ts' --watch '../packages/core/**/*' --watch '../packages/upscalerjs/**/*' --watch '../scripts/package-scripts/docs/build-api.ts' -x \"pnpm run build-api\"" + "command": "nodemon -e ts --ignore '../packages/upscalerjs/**/*.generated.ts' --watch '../packages/upscalerjs/**/*' --watch '../internals/scripts/src/write/docs/shared/**' --watch '../internals/scripts/src/write/docs/api/**' -x \"pnpm run build-api\"" }, "build-guides": { - "command": "pnpm --filter @upscalerjs/scripts docs:build-guides" + "command": "pnpm --filter @internals/scripts write:docs:guides", + "dependencies": [ + "../internals/scripts:build" + ] }, "build-guides:watch": { - "command": "nodemon -e md --watch '../examples/**/*' --watch '../scripts/package-scripts/docs/build-guides.ts' -x \"pnpm run build-guides\"" + "command": "nodemon -e md --watch '../examples/**/*' --watch '../internals/scripts/src/write/docs/shared/**' --watch '../internals/scripts/src/write/docs/guides/**' -x \"pnpm run build-guides\"" }, "link-model-readmes": { - "command": "pnpm --filter @upscalerjs/scripts docs:link-model-readmes" + "command": "pnpm --filter @internals/scripts write:docs:models", + "dependencies": [ + "../internals/scripts:build" + ] }, "link-model-readmes:watch": { - "command": "nodemon -e md -e mdx --watch '../models/**/*' --watch '../scripts/package-scripts/docs/link-model-readmes.ts' -x \"pnpm run link-model-readmes\"" + "command": "nodemon -e md -e mdx --watch '../models/**/*' --watch '../internals/scripts/src/write/docs/shared/**' --watch '../internals/scripts/src/write/docs/models/**' -x \"pnpm run link-model-readmes\"" }, "docusaurus": { "command": "docusaurus" @@ -166,7 +165,11 @@ "command": "docusaurus build" }, "build": { - "command": "concurrently \"pnpm build-guides -- --shouldClearMarkdown\" \"pnpm build-api -- --shouldClearMarkdown\" \"pnpm link-model-readmes -- --shouldClearMarkdown\" && pnpm tense-checks && pnpm build:dependencies && pnpm build:only" + "command": "concurrently \"pnpm build-guides -- --shouldClearMarkdown\" \"pnpm build-api -- --shouldClearMarkdown\" \"pnpm link-model-readmes -- --shouldClearMarkdown\" && pnpm tense-checks && pnpm build:only", + "dependencies": [ + "../models/esrgan-medium:build", + "../packages/upscalerjs:build" + ] }, "swizzle": { "command": "docusaurus swizzle" diff --git a/internals/scripts/package.json b/internals/scripts/package.json index b93a87b29..33004634c 100644 --- a/internals/scripts/package.json +++ b/internals/scripts/package.json @@ -10,9 +10,14 @@ "author": "Kevin Scott", "license": "MIT", "dependencies": { - "@internals/common": "workspace:*" + "@internals/common": "workspace:*", + "typedoc": "^0.24.8" }, "scripts": { + "write:docs:check-tense": "wireit", + "write:docs:guides": "wireit", + "write:docs:models": "wireit", + "write:docs:api": "wireit", "start:guide": "wireit", "update:npm-dependencies": "wireit", "build": "wireit", @@ -20,6 +25,32 @@ "test": "wireit" }, "wireit": { + "write:docs:check-tense": { + "command": "node ./dist/bin/write/docs/check-tense.js", + "dependencies": [ + "build" + ] + }, + "write:docs:guides": { + "command": "node ./dist/bin/write/docs/guides/index.js", + "dependencies": [ + "build" + ] + }, + "write:docs:models": { + "command": "node ./dist/bin/write/docs/models/index.js", + "dependencies": [ + "build" + ] + }, + "write:docs:api": { + "command": "node ./dist/bin/write/docs/api/index.js", + "dependencies": [ + "../../packages/upscalerjs:build:browser:esm", + "../../packages/upscalerjs:build:node", + "build" + ] + }, "start:guide": { "command": "node ./dist/bin/start/guide.js", "dependencies": [ @@ -67,7 +98,7 @@ "node": ">=20.0.0" }, "devDependencies": { - "wireit": "latest", - "vitest": "^0.34.2" + "vitest": "^0.34.2", + "wireit": "latest" } } diff --git a/internals/scripts/src/bin/write/docs/api/index.ts b/internals/scripts/src/bin/write/docs/api/index.ts new file mode 100644 index 000000000..79c7c0664 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/index.ts @@ -0,0 +1,27 @@ +import path from 'path'; +import { DOCS_DIR, } from '@internals/common/constants'; +import { mkdirp } from '@internals/common/fs'; +import { writeAPIDocs } from './lib/write-api-docs.js'; +import { info, verbose } from '@internals/common/logger'; +import { getSharedArgs } from '../shared/get-shared-args.js'; +import { clearOutMarkdownFiles } from '../shared/clear-out-markdown-files.js'; + +const EXAMPLES_DOCS_DEST = path.resolve(DOCS_DIR, 'docs/documentation/api'); + +const writeAPIDocumentation = async ({ shouldClearMarkdown }: { shouldClearMarkdown: boolean }) => { + info('Writing API documentation'); + await mkdirp(EXAMPLES_DOCS_DEST); + if (shouldClearMarkdown) { + verbose(`Clearing out markdown files in ${EXAMPLES_DOCS_DEST}`) + await clearOutMarkdownFiles(EXAMPLES_DOCS_DEST); + } + + return writeAPIDocs(EXAMPLES_DOCS_DEST); +}; + + +const main = async () => { + return writeAPIDocumentation(getSharedArgs()); +}; + +main(); diff --git a/internals/scripts/src/bin/write/docs/api/lib/_templates/index.md.t b/internals/scripts/src/bin/write/docs/api/lib/_templates/index.md.t new file mode 100644 index 000000000..8034b8bf0 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/_templates/index.md.t @@ -0,0 +1,9 @@ +# API + +API Documentation for UpscalerJS. + +Available methods: + +<% for (const method of methods) { %> +- [`<%- method.name %>`](./<%- method.name %>) +<% } %> diff --git a/internals/scripts/src/bin/write/docs/api/lib/_templates/source.md.t b/internals/scripts/src/bin/write/docs/api/lib/_templates/source.md.t new file mode 100644 index 000000000..aace97a79 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/_templates/source.md.t @@ -0,0 +1 @@ +Defined in <%- prettyFileName %>:<%- line %> diff --git a/internals/scripts/src/bin/write/docs/api/lib/constants.ts b/internals/scripts/src/bin/write/docs/api/lib/constants.ts new file mode 100644 index 000000000..5bc3ebc5d --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/constants.ts @@ -0,0 +1,68 @@ +import { UPSCALER_DIR } from "@internals/common/constants"; +import path from 'path'; +import { DeclarationReflection, ReflectionKind, TypeParameterReflection } from "typedoc"; +import * as url from 'url'; +import { Definitions } from "./types.js"; +import { writePlatformSpecificDefinitions } from "./write-api-documentation-files/write-parameter.js"; + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +export const REPO_ROOT = 'https://github.com/thekevinscott/UpscalerJS'; +export const UPSCALER_TSCONFIG_PATH = path.resolve(UPSCALER_DIR, 'tsconfig.esm.json'); +export const UPSCALER_SRC_PATH = path.resolve(UPSCALER_DIR, 'src'); +export const VALID_EXPORTS_FOR_WRITING_DOCS = ['default']; +export const VALID_METHODS_FOR_WRITING_DOCS = [ + 'constructor', + 'upscale', + 'execute', + 'warmup', + 'abort', + 'dispose', + 'getModel', +]; +export const INTRINSIC_TYPES = [ + 'string', + 'number', + 'boolean', +]; +export const TYPES_TO_EXPAND: Record = { + 'upscale': ['Input', 'Progress'], + 'warmup': ['WarmupSizes'], +}; +export const TEMPLATES_DIR = path.resolve(__dirname, '_templates'); + +export const makeNewExternalType = (name: string, _url: string): DeclarationReflection => { + const type = new DeclarationReflection(name, ReflectionKind['SomeType']); + type.sources = []; + return type; +}; + +export const EXTERNALLY_DEFINED_TYPES: Record = { + 'AbortSignal': makeNewExternalType( + 'AbortSignal', + 'https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal' + ), + 'SerializableConstructor': makeNewExternalType( + 'SerializableConstructor', + 'https://github.com/tensorflow/tfjs/blob/38f8462fe642011ff1b7bcbb52e018f3451be58b/tfjs-core/src/serialization.ts#L54', + ), +} + +export const EXPANDED_TYPE_CONTENT: Record) => string> = { + 'Input': (definitions) => writePlatformSpecificDefinitions(definitions), + 'WarmupSizes': () => ([ + '- `number` - a number representing both the size (width and height) of the patch.', + '- `{patchSize: number; padding?: number}` - an object with the `patchSize` and optional `padding` properties.', + '- `number[]` - an array of numbers representing the size (width and height) of the patch.', + '- `{patchSize: number; padding?: number}[]` - an array of objects with the `patchSize` and optional `padding` properties.', + ].join('\n')), + 'Progress': () => ([ + 'The progress callback function has the following four parameters:', + '- `progress` - a number between 0 and 1 representing the progress of the upscale.', + '- `slice` - a string or 3D tensor representing the current slice of the image being processed. The type returned is specified by the `progressOutput` option, or if not present, the `output` option, or if not present, string for the browser and tensor for node.', + '- `row` - the row of the image being processed.', + '- `col` - the column of the image being processed.', + '', + '[See the guide on progress for more information.](/documentation/guides/browser/usage/progress)', + ].join('\n')), +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-all-declaration-reflections.test.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-all-declaration-reflections.test.ts new file mode 100644 index 000000000..2e8afae52 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-all-declaration-reflections.test.ts @@ -0,0 +1,36 @@ +import { PlatformSpecificFileDeclarationReflection } from "../types.js"; +import { getAllDeclarationReflections } from "./get-all-declaration-reflections.js"; +import { getDeclarationReflectionsFromPackages } from "./get-declaration-reflections-from-packages.js"; +import { getTypesFromPlatformSpecificUpscalerFiles } from "./get-types-from-platform-specific-upscaler-files.js"; +import { DeclarationReflection } from "typedoc"; + +vi.mock('./get-declaration-reflections-from-packages.js', () => ({ + getDeclarationReflectionsFromPackages: vi.fn(), +})); + +vi.mock('./get-types-from-platform-specific-upscaler-files.js', () => ({ + getTypesFromPlatformSpecificUpscalerFiles: vi.fn(), +})); + +describe('getAllDeclarationReflections()', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('gets merged declaration reflections', async () => { + vi.mocked(getDeclarationReflectionsFromPackages).mockImplementation(() => { + return [ + 'foo', + ] as unknown as DeclarationReflection[]; + }); + + vi.mocked(getTypesFromPlatformSpecificUpscalerFiles).mockImplementation(() => { + return Promise.resolve([ + 'bar', + ] as unknown as PlatformSpecificFileDeclarationReflection[]); + }); + + const results = await getAllDeclarationReflections(); + expect(results).toEqual(['foo', 'bar']); + + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-all-declaration-reflections.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-all-declaration-reflections.ts new file mode 100644 index 000000000..0c5b9347d --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-all-declaration-reflections.ts @@ -0,0 +1,16 @@ +import { UPSCALER_DIR } from "@internals/common/constants"; +import { UPSCALER_TSCONFIG_PATH } from "../constants.js"; +import { DeclarationReflection } from "typedoc"; +import { getDeclarationReflectionsFromPackages } from "./get-declaration-reflections-from-packages.js"; +import { getTypesFromPlatformSpecificUpscalerFiles } from "./get-types-from-platform-specific-upscaler-files.js"; +import { PlatformSpecificFileDeclarationReflection } from "../types.js"; + +const DECLARATION_REFLECTION_FILE_DEFINITIONS = [{ + tsconfigPath: UPSCALER_TSCONFIG_PATH, + projectRoot: UPSCALER_DIR, +}]; + +export const getAllDeclarationReflections = async (): Promise<(DeclarationReflection | PlatformSpecificFileDeclarationReflection)[]> => ([ + ...(await getTypesFromPlatformSpecificUpscalerFiles([{ fileName: 'image', typeName: 'Input' }])), + ...getDeclarationReflectionsFromPackages(DECLARATION_REFLECTION_FILE_DEFINITIONS), +]); diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-declaration-reflections-from-packages.test.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-declaration-reflections-from-packages.test.ts new file mode 100644 index 000000000..3c6e0fe9b --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-declaration-reflections-from-packages.test.ts @@ -0,0 +1,45 @@ +import { ProjectReflection } from "typedoc"; +import { getDeclarationReflectionsFromPackages } from "./get-declaration-reflections-from-packages.js"; +import { getPackageAsTree } from "./get-package-as-tree.js"; + +vi.mock('./get-package-as-tree.js', () => { + return { + getPackageAsTree: vi.fn(), + } +}); + +describe('getDeclarationReflectionsFromPackages', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('returns an array of DeclarationReflections', () => { + vi.mocked(getPackageAsTree).mockImplementation(() => { + return { + children: [ + 'foo', + 'bar', + ], + } as unknown as ProjectReflection; + }); + expect(getDeclarationReflectionsFromPackages([ + { + tsconfigPath: 'tsconfig', + projectRoot: 'projectRoot', + }, + ])).toEqual(['foo', 'bar']); + }); + + it('throws if receiving an empty children array', () => { + vi.mocked(getPackageAsTree).mockImplementation(() => { + return { + children: [], + } as unknown as ProjectReflection; + }); + expect(() => getDeclarationReflectionsFromPackages([ + { + tsconfigPath: 'tsconfig', + projectRoot: 'projectRoot', + }, + ])).toThrow(); + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-declaration-reflections-from-packages.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-declaration-reflections-from-packages.ts new file mode 100644 index 000000000..6322c23b3 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-declaration-reflections-from-packages.ts @@ -0,0 +1,22 @@ +import { getPackageAsTree } from "./get-package-as-tree.js"; +import { DeclarationReflection } from "typedoc"; +import path from "path"; + +export interface ProjectDefinition { + tsconfigPath: string; + projectRoot: string; +} + +export const getDeclarationReflectionsFromPackages = (projectDefinitions: ProjectDefinition[]): DeclarationReflection[] => [ + ...projectDefinitions, +].reduce((arr, { tsconfigPath, projectRoot }) => { + const { children } = getPackageAsTree( + path.join(projectRoot, 'src'), + tsconfigPath, + projectRoot, + ); + if (children === undefined || children.length === 0) { + throw new Error(`No children were found for ${projectRoot}. Indicates an error in the returned structure from getPackageAsTree`); + } + return arr.concat(children); +}, []); diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-definitions.test.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-definitions.test.ts new file mode 100644 index 000000000..09940877a --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-definitions.test.ts @@ -0,0 +1,103 @@ +import { getDefinitions } from "./get-definitions.js"; +import { getAllDeclarationReflections } from "./get-all-declaration-reflections.js"; +import { DeclarationReflection, ReflectionKind } from "typedoc"; +import { PlatformSpecificFileDeclarationReflection } from "../types.js"; + +vi.mock('./get-all-declaration-reflections.js', () => ({ + getAllDeclarationReflections: vi.fn(), +})); + +describe('getDefinitions()', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('throws if given a bad "kind"', async () => { + vi.mocked(getAllDeclarationReflections).mockImplementation(() => { + return Promise.resolve([ + { + kind: 'foo', + } + ] as unknown as (DeclarationReflection | PlatformSpecificFileDeclarationReflection)[]); + }); + + await expect(() => getDefinitions()).rejects.toThrow(); + }); + + it('gets definitions', async () => { + const Constructor = { + name: 'Constructor', + kind: ReflectionKind.Constructor, + }; + const Method = { + name: 'Method', + kind: ReflectionKind.Method, + }; + const Interface = { + name: 'Interface', + kind: ReflectionKind.Interface, + }; + const TypeAlias = { + name: 'TypeAlias', + kind: ReflectionKind.TypeAlias, + }; + const Class = { + name: 'Class', + kind: ReflectionKind.Class, + }; + const Function = { + name: 'Function', + kind: ReflectionKind.Function, + }; + const Enum = { + name: 'Enum', + kind: ReflectionKind.Enum, + }; + const PlatformSpecific = { + declarationReflection: { + name: 'PlatformSpecific', + kind: ReflectionKind.Constructor, + }, + browser: {}, + node: {}, + }; + vi.mocked(getAllDeclarationReflections).mockImplementation(() => { + return Promise.resolve([ + PlatformSpecific, + Constructor, + Method, + Interface, + TypeAlias, + Class, + Function, + Enum, + ] as (DeclarationReflection | PlatformSpecificFileDeclarationReflection)[]); + }); + + const result = await getDefinitions(); + expect(result).toEqual({ + methods: { + Method, + }, + constructors: { + Constructor, + PlatformSpecific, + }, + functions: { + Function, + }, + types: { + TypeAlias, + }, + interfaces: { + Interface, + }, + classes: { + Class, + }, + enums: { + Enum, + }, + }); + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-definitions.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-definitions.ts new file mode 100644 index 000000000..550d1f7f3 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-definitions.ts @@ -0,0 +1,41 @@ +import { ReflectionKind } from "typedoc"; +import { Definitions, isPlatformSpecificFileDeclarationReflection } from "../types.js"; +import { getAllDeclarationReflections } from "./get-all-declaration-reflections.js"; +import { } from "./get-types-from-platform-specific-upscaler-files.js"; + +export const KindStringKey: Partial> = { + [ReflectionKind.Constructor]: 'constructors', + [ReflectionKind.Method]: 'methods', + [ReflectionKind.Interface]: 'interfaces', + [ReflectionKind.TypeAlias]: 'types', + [ReflectionKind.Class]: 'classes', + [ReflectionKind.Function]: 'functions', + [ReflectionKind.Enum]: 'enums', +} + +const getKindStringKey = (kindString: ReflectionKind): keyof Definitions => { + const nameOfKind = KindStringKey[kindString]; + if (!nameOfKind) { + throw new Error(`Unexpected kind string: ${kindString}`); + } + return nameOfKind; +}; + +export const getDefinitions = async (): Promise => { + const children = await getAllDeclarationReflections(); + const definitions: Definitions = { + constructors: {}, + methods: {}, + functions: {}, + interfaces: {}, + types: {}, + classes: {}, + enums: {}, + }; + for (const child of children) { + const { name, kind } = isPlatformSpecificFileDeclarationReflection(child) ? child.declarationReflection : child; + definitions[getKindStringKey(kind)][name] = child; + } + return definitions; +} + diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-package-as-tree.test.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-package-as-tree.test.ts new file mode 100644 index 000000000..be060da5e --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-package-as-tree.test.ts @@ -0,0 +1,61 @@ +import * as typedoc from 'typedoc'; +import { vi } from 'vitest'; +import { getPackageAsTree } from './get-package-as-tree.js'; + +vi.mock('typedoc', async () => { + const actual = await vi.importActual('typedoc') as typeof typedoc; + return { + ...actual, + Application: vi.fn().mockImplementation(() => ({ + options: { + addReader: vi.fn(), + }, + bootstrap: vi.fn(), + convert: vi.fn(), + serializer: { + projectToObject: vi.fn(), + }, + })), + TSConfigReader: vi.fn(), + TypeDocReader: vi.fn(), + } +}); + +describe('getPackageAsTree', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('throws if project is not converted', async () => { + vi.mocked(typedoc.Application).mockImplementation(() => { + return { + options: { + addReader: vi.fn(), + }, + bootstrap: vi.fn(), + convert: vi.fn(), + } as unknown as typedoc.Application; + }); + await expect(async () => { + await getPackageAsTree('entryPoint', 'tsconfig', 'projectRoot') + }).rejects.toThrow(); + }); + + it('returns project if it is converted', async () => { + const projectToObject = vi.fn().mockImplementation(() => 'projectToObject'); + vi.mocked(typedoc.Application).mockImplementation(() => { + return { + options: { + addReader: vi.fn(), + }, + bootstrap: vi.fn(), + convert: vi.fn().mockImplementation(() => 'foo'), + serializer: { + projectToObject, + } + } as unknown as typedoc.Application; + }); + const result = await getPackageAsTree('entryPoint', 'tsconfig', 'projectRoot'); + expect(result).toEqual('projectToObject'); + expect(projectToObject).toHaveBeenCalledWith('foo', 'projectRoot'); + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-package-as-tree.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-package-as-tree.ts new file mode 100644 index 000000000..a44867593 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-package-as-tree.ts @@ -0,0 +1,20 @@ +import { Application, ProjectReflection, TSConfigReader, TypeDocReader } from "typedoc"; + +export const getPackageAsTree = (entryPoint: string, tsconfig: string, projectRoot: string): ProjectReflection => { + const app = new Application(); + + app.options.addReader(new TSConfigReader()); + app.options.addReader(new TypeDocReader()); + + app.bootstrap({ + entryPoints: [entryPoint], + tsconfig, + }); + + const project = app.convert(); + + if (!project) { + throw new Error('No project was converted.') + } + return app.serializer.projectToObject(project, projectRoot) as unknown as ProjectReflection; +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-types-from-platform-specific-upscaler-files.test.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-types-from-platform-specific-upscaler-files.test.ts new file mode 100644 index 000000000..742b3e8d1 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-types-from-platform-specific-upscaler-files.test.ts @@ -0,0 +1,134 @@ +import { DeclarationReflection, ProjectReflection, ReflectionKind, SomeType } from "typedoc"; +import { + getPlatformSpecificUpscalerDeclarationReflections, + getTypesFromPlatformSpecificUpscalerFile, + getTypesFromPlatformSpecificUpscalerFiles, + makeDeclarationReflection, +} from "./get-types-from-platform-specific-upscaler-files.js"; +import { getPackageAsTree } from "./get-package-as-tree.js"; + +vi.mock('./get-package-as-tree.js', () => { + return { + getPackageAsTree: vi.fn(), + }; +}); + +describe('makeDeclarationReflection', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('gets a declaration reflection', () => { + const decRef = makeDeclarationReflection('foo', { + type: 'functions', + } as unknown as SomeType); + expect(decRef.name).toEqual('foo'); + expect(decRef.kind).toEqual(ReflectionKind.Function); + expect(decRef.type).toEqual({ + type: 'functions', + }); + }) +}); + +describe('getPlatformSpecificUpscalerDeclarationReflections', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('throws if it cannot find a matching type', () => { + vi.mocked(getPackageAsTree).mockImplementation(() => { + return { + children: [ + 'foo', + 'bar', + ], + } as unknown as ProjectReflection; + }); + + expect(() => getPlatformSpecificUpscalerDeclarationReflections('browser', { + fileName: 'fileName', + typeName: 'typeName', + })).toThrow(); + }); + + it('throws if it cannot find a matching type', () => { + const child = { + name: 'foo', + }; + vi.mocked(getPackageAsTree).mockImplementation(() => { + return { + children: [child], + } as unknown as ProjectReflection; + }); + + expect(getPlatformSpecificUpscalerDeclarationReflections('browser', { + fileName: 'fileName', + typeName: child.name, + })).toEqual(child); + }); +}); + +describe('getTypesFromPlatformSpecificUpscalerFile', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('gets types from platform specific upscaler file', () => { + const typeName = 'typeName'; + const child = { + name: typeName, + type: { + type: 'functions', + }, + }; + vi.mocked(getPackageAsTree).mockImplementation(() => { + return { + children: [child], + } as unknown as ProjectReflection; + }); + const result = getTypesFromPlatformSpecificUpscalerFile({ + fileName: 'fileName', + typeName, + }); + expect(result.declarationReflection.name).toEqual(typeName); + expect(result.browser).toEqual(child); + expect(result.node).toEqual(child); + }); +}); + +describe('getTypesFromPlatformSpecificFiles', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('iterates through files array', async () => { + const typeName = 'typeName'; + const child = { + name: typeName, + type: { + type: 'functions', + }, + }; + vi.mocked(getPackageAsTree).mockImplementation(() => { + return { + children: [child], + } as unknown as ProjectReflection; + }); + const result = await getTypesFromPlatformSpecificUpscalerFiles([{ + fileName: 'file1', + typeName, + }, { + fileName: 'file2', + typeName, + }]); + + expect(result).toEqual([ + expect.objectContaining({ + declarationReflection: expect.any(DeclarationReflection), + browser: child, + node: child, + }), + expect.objectContaining({ + declarationReflection: expect.any(DeclarationReflection), + browser: child, + node: child, + }), + ]); + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-types-from-platform-specific-upscaler-files.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-types-from-platform-specific-upscaler-files.ts new file mode 100644 index 000000000..030972be1 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/get-types-from-platform-specific-upscaler-files.ts @@ -0,0 +1,94 @@ +import { TFJSLibrary } from "@internals/common/tfjs-library"; +import { DeclarationReflection, ReflectionKind, SomeType } from "typedoc"; +import { getPackageAsTree } from "./get-package-as-tree.js"; +import { UPSCALER_DIR } from "@internals/common/constants"; +import path from "path"; +import { PlatformSpecificFileDeclarationReflection } from "../types.js"; + +export interface PlatformSpecificFileDefinition { + fileName: string; + typeName: string; +} + +const tfjsLibraries: TFJSLibrary[] = ['browser', 'node']; + +const reverseKindStringKey: Record = { + constructors: ReflectionKind.Constructor, + methods: ReflectionKind.Method, + interfaces: ReflectionKind.Interface, + types: ReflectionKind.TypeAlias, + classes: ReflectionKind.Class, + functions: ReflectionKind.Function, + enums: ReflectionKind.Enum, +}; + +export const makeDeclarationReflection = (typeName: string, type: SomeType): DeclarationReflection => { + if (type.type === 'union') { + // const childType = type.types?.[0]; + // console.log(typeName, type.types); + // if (!childType) { + // throw new Error('No child type for union'); + // } + // return makeDeclarationReflection(typeName, childType); + const declarationReflection = new DeclarationReflection(typeName, ReflectionKind.Interface); + declarationReflection.type = type; + return declarationReflection; + } + const kind = reverseKindStringKey[type.type]; + if (kind === undefined) { + throw new Error(`Kind is undefined for type ${type.type}`); + } + const declarationReflection = new DeclarationReflection(typeName, kind); + declarationReflection.type = type; + return declarationReflection; +}; + +export const getPlatformSpecificUpscalerDeclarationReflections = ( + tfjsLibrary: TFJSLibrary, { + fileName, + typeName, + }: PlatformSpecificFileDefinition +): DeclarationReflection => { + // await scaffoldUpscaler(tfjsLibrary); + const { children } = getPackageAsTree( + path.resolve(UPSCALER_DIR, 'src', `${fileName}.${tfjsLibrary}.ts`), + path.resolve(UPSCALER_DIR, `tsconfig.docs.${tfjsLibrary}.json`), + UPSCALER_DIR, + ); + const matchingType = children?.filter(child => child.name === typeName).pop(); + if (!matchingType) { + throw new Error(`Could not find input from ${fileName}.${tfjsLibrary}.ts`); + } + return matchingType; +}; + +export const getTypesFromPlatformSpecificUpscalerFile = ({ fileName, typeName }: PlatformSpecificFileDefinition) => { + const [browser, node] = tfjsLibraries.map(tfjsLibrary => getPlatformSpecificUpscalerDeclarationReflections(tfjsLibrary, { fileName, typeName })); + if (browser.type?.type !== node.type?.type) { + throw new Error([ + 'Some mismatch for file name', + fileName, + 'and type name', + typeName, + 'between browser type:', + `\n\n${JSON.stringify(browser.type)}\n\n`, + 'and node type:', + `\n\n${JSON.stringify(node.type)}`, + ].join(' ')); + } + + const type = browser.type; + if (!type) { + throw new Error('No type defined on browser type'); + } + + return { + declarationReflection: makeDeclarationReflection(typeName, type), + browser, + node, + }; +}; + +export const getTypesFromPlatformSpecificUpscalerFiles = ( + fileNames: PlatformSpecificFileDefinition[] +): Promise => Promise.all(fileNames.map(getTypesFromPlatformSpecificUpscalerFile)); diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-definitions/index.ts b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/index.ts new file mode 100644 index 000000000..f2d6815bf --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-definitions/index.ts @@ -0,0 +1 @@ +export { getDefinitions } from "./get-definitions.js"; diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-sorted-methods-for-writing.test.ts b/internals/scripts/src/bin/write/docs/api/lib/get-sorted-methods-for-writing.test.ts new file mode 100644 index 000000000..d8cb2216a --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-sorted-methods-for-writing.test.ts @@ -0,0 +1,48 @@ +import { ReflectionKind } from "typedoc"; +import { getSortedMethodsForWriting } from "./get-sorted-methods-for-writing.js"; +import { DecRef, Definitions } from "./types.js"; + +describe('getSortedMethodsForWriting()', () => { + it('ignores a non-default class', () => { + const Class = { + name: 'class', + kind: ReflectionKind.Class, + children: [], + } as unknown as DecRef; + const definitions = { + classes: { + Class, + }, + } as unknown as Definitions; + expect(getSortedMethodsForWriting(definitions)).toEqual([ ]) + }); + + it('sorts children by line number', () => { + const srcLine1 = { + name: 'upscale', + sources: { + line: 1, + } + }; + const srcLine0 = { + name: 'constructor', + sources: { + line: 0, + } + }; + const Class = { + name: 'default', + kind: ReflectionKind.Class, + children: [srcLine1, srcLine0], + } as unknown as DecRef; + const definitions = { + classes: { + Class, + }, + } as unknown as Definitions; + expect(getSortedMethodsForWriting(definitions)).toEqual([ + srcLine1, + srcLine0, + ]) + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/get-sorted-methods-for-writing.ts b/internals/scripts/src/bin/write/docs/api/lib/get-sorted-methods-for-writing.ts new file mode 100644 index 000000000..8c18652f8 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/get-sorted-methods-for-writing.ts @@ -0,0 +1,24 @@ +import { DeclarationReflection } from "typedoc"; +import { DecRef, Definitions, isPlatformSpecificFileDeclarationReflection } from "./types.js"; +import { VALID_EXPORTS_FOR_WRITING_DOCS, VALID_METHODS_FOR_WRITING_DOCS } from "./constants.js"; +import { info } from "@internals/common/logger"; +import { sortChildrenByLineNumber } from "./sort-children-by-line-number.js"; + +const getDecRef = (decRef: DecRef) => isPlatformSpecificFileDeclarationReflection(decRef) ? decRef.declarationReflection : decRef; + +export const getSortedMethodsForWriting = (definitions: Definitions): DeclarationReflection[] => { + const decRefs = Object.values(definitions.classes); + const methods: DeclarationReflection[] = []; + for (const { name, children = [] } of decRefs.map(getDecRef)) { + if (VALID_EXPORTS_FOR_WRITING_DOCS.includes(name)) { + sortChildrenByLineNumber(children).forEach(method => { + if (VALID_METHODS_FOR_WRITING_DOCS.includes(method.name)) { + methods.push(method); + } else { + info(`** Ignoring method ${method.name}`); + } + }); + } + } + return methods; +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/sort-children-by-line-number.test.ts b/internals/scripts/src/bin/write/docs/api/lib/sort-children-by-line-number.test.ts new file mode 100644 index 000000000..2efcffc71 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/sort-children-by-line-number.test.ts @@ -0,0 +1,46 @@ +import { vi, } from 'vitest'; +import { DeclarationReflection, SourceReference } from "typedoc"; +import { sortChildrenByLineNumber } from "./sort-children-by-line-number.js"; + +const getMockDeclarationReflection = (...sources: Partial[]): DeclarationReflection => { + return { + sources, + } as unknown as DeclarationReflection; +}; + +describe('sortChildrenByLineNumber', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('sorts children', () => { + const line1 = getMockDeclarationReflection({ + line: 1, + }); + const line0 = getMockDeclarationReflection({ + line: 0, + }); + const line2 = getMockDeclarationReflection({ + line: 2, + }); + expect(sortChildrenByLineNumber([ + line1, + line2, + line0, + ])).toEqual([line0, line1, line2]); + }); + + it('returns sources before no sources', () => { + const linefoo = getMockDeclarationReflection(); + const linebar = getMockDeclarationReflection(); + const line2 = getMockDeclarationReflection({ + line: 2, + }); + expect(sortChildrenByLineNumber([ + linefoo, + line2, + linebar, + ])).toEqual([line2, linefoo, linebar]); + }); +}); + diff --git a/internals/scripts/src/bin/write/docs/api/lib/sort-children-by-line-number.ts b/internals/scripts/src/bin/write/docs/api/lib/sort-children-by-line-number.ts new file mode 100644 index 000000000..ba35a5a0a --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/sort-children-by-line-number.ts @@ -0,0 +1,13 @@ +import { DeclarationReflection } from "typedoc"; + +export function sortChildrenByLineNumber(children: DeclarationReflection[]) { + return children.sort(({ sources: aSrc }, { sources: bSrc }) => { + if (!aSrc?.length) { + return 1; + } + if (!bSrc?.length) { + return -1; + } + return aSrc[0].line - bSrc[0].line; + }); +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/types.ts b/internals/scripts/src/bin/write/docs/api/lib/types.ts new file mode 100644 index 000000000..f88beaafa --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/types.ts @@ -0,0 +1,49 @@ +import { + ArrayType, + DeclarationReflection, + IntersectionType, + IntrinsicType, + LiteralType, + ReferenceType, + SomeType, + UnionType, +} from "typedoc"; + +export interface PlatformSpecificFileDeclarationReflection { + declarationReflection: DeclarationReflection; + browser: DeclarationReflection; + node: DeclarationReflection; +} + +export type DecRef = DeclarationReflection | PlatformSpecificFileDeclarationReflection; +export interface Definitions { + constructors: Record; + methods: Record; + interfaces: Record; + types: Record; + classes: Record; + functions: Record; + enums: Record; +} + +export const isPlatformSpecificFileDeclarationReflection = ( + child: DeclarationReflection | PlatformSpecificFileDeclarationReflection +): child is PlatformSpecificFileDeclarationReflection => 'browser' in child; + +export const isDeclarationReflection = (reflection?: DecRef): reflection is DeclarationReflection => reflection !== undefined && !isPlatformSpecificFileDeclarationReflection(reflection); +export const isArrayType = (type: SomeType): type is ArrayType => type.type === 'array'; +export const isReferenceType = (type: SomeType): type is ReferenceType => type.type === 'reference'; +export const isLiteralType = (type: SomeType): type is LiteralType => type.type === 'literal'; +export const isInstrinsicType = (type: SomeType): type is IntrinsicType => type.type === 'intrinsic'; +export const isUnionType = (type: SomeType): type is UnionType => type.type === 'union'; +export const isIntersectionType = (type: SomeType): type is IntersectionType => type.type === 'intersection'; +export const getLiteralTypeValue = (type: LiteralType): string => { + const { value } = type; + if (typeof value === 'number') { + return `${value}`; + } else if (typeof value === 'string') { + return value; + } + + throw new Error('Not yet implemented for literal'); +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-docs.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-docs.ts new file mode 100644 index 000000000..4e9febe51 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-docs.ts @@ -0,0 +1,15 @@ +import { getDefinitions } from './get-definitions/index.js'; +import { getSortedMethodsForWriting } from './get-sorted-methods-for-writing.js'; +import { writeIndexFile } from './write-index-file.js'; +import { writeAPIDocumentationFiles } from './write-api-documentation-files/index.js'; + +export async function writeAPIDocs(dest: string) { + return; + const definitions = await getDefinitions(); + const methods = getSortedMethodsForWriting(definitions); + + await Promise.all([ + writeAPIDocumentationFiles(dest, methods, definitions), + writeIndexFile(dest, methods), + ]); +} diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-content-for-method.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-content-for-method.ts new file mode 100644 index 000000000..56cdc75ce --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-content-for-method.ts @@ -0,0 +1,85 @@ +import { DeclarationReflection, SignatureReflection, TypeParameterReflection } from 'typedoc'; +import { getSource } from './get-source.js'; +import { getTextSummary } from './get-text-summary.js'; +import { getParameters } from './write-parameter.js'; +import { getReturnType } from './get-return-type.js'; +import { EXPANDED_TYPE_CONTENT, TYPES_TO_EXPAND } from '../constants.js'; +import { Definitions } from '../types.js'; + +function getAsObj (arr: T[], getKey: (item: T) => string) { + return arr.reduce((obj, item) => ({ + ...obj, + [getKey(item)]: item, + }), {} as Record); +} + +const writeExpandedTypeDefinitions = (methodName: string, definitions: Definitions, typeParameters: Record = {}): string => { + // this method is for writing out additional information on the types, below the parameters + const typesToExpand: string[] = TYPES_TO_EXPAND[methodName === 'constructor' ? '_constructor' : methodName] || []; + return typesToExpand.map(type => [ + `### \`${type}\``, + EXPANDED_TYPE_CONTENT[type](definitions, typeParameters), + ].join('\n')).join('\n'); +} + +export const getContentForMethod = async (method: DeclarationReflection, definitions: Definitions, i: number) => { + const { + name, + signatures, + sources, + } = method; + + if (name === 'upscale') { + return [ + [ + '---', + `title: ${name}`, + `sidebar_position: ${i}`, + `sidebar_label: ${name}`, + '---', + ].join('\n'), + + `# ${name}`, + 'Alias for [`execute`](execute)', + ].filter(Boolean).join('\n\n'); + + } + + if (!sources?.length) { + throw new Error(`No sources found for ${name}`); + } + if (!signatures?.length) { + const { type: _type } = method; + throw new Error(`No signatures found in ${name}`); + } + const signature = signatures[0] as SignatureReflection & { typeParameter?: TypeParameterReflection[] }; + const { comment, parameters, typeParameter: typeParameters } = signature; + + const { description, codeSnippet, blockTags } = getTextSummary(name, comment); + const source = await getSource(sources); + + const content = [ + [ + '---', + `title: ${name}`, + `sidebar_position: ${i}`, + `sidebar_label: ${name}`, + '---', + ].join('\n'), +`# \`${name}\``, + description, + ...(codeSnippet ? [ + '## Example', + codeSnippet, + ] : []), + source, + ...(parameters ? [ + '## Parameters', + getParameters(name, parameters, definitions, getAsObj(typeParameters || [], t => t.name)), + ] : []), + writeExpandedTypeDefinitions(name, definitions, getAsObj(typeParameters || [], t => t.name)), + '## Returns', + getReturnType(signatures, blockTags), + ].filter(Boolean).join('\n\n'); + return content; +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-matching-type.test.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-matching-type.test.ts new file mode 100644 index 000000000..80851162a --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-matching-type.test.ts @@ -0,0 +1,189 @@ +import { ArrayType, DeclarationReflection, ReflectionKind } from "typedoc"; +import { getMatchingType } from "./get-matching-type.js"; +import { getReferenceTypeOfParameter } from "./get-reference-type-of-parameter.js"; +import * as constants from "../constants.js"; +import { Definitions, isLiteralType } from "../types.js"; + +vi.mock('../types.js', async () => { + const actualTypes = await import('../types.js'); + return { + ...actualTypes, + isLiteralType: vi.fn().mockImplementation(() => false), + } +}); + +vi.mock('../constants.js', () => ({ + INTRINSIC_TYPES: [], + EXTERNALLY_DEFINED_TYPES: [], +})); + +vi.mock('./get-reference-type-of-parameter.js', () => ({ + getReferenceTypeOfParameter: vi.fn(), +})); + +const makeNewExternalType = (name: string, _url: string): DeclarationReflection => { + const type = new DeclarationReflection(name, ReflectionKind['SomeType']); + type.sources = []; + return type; +}; + +const Interface = { + name: 'Interface', + kind: ReflectionKind.Interface, +}; +const TypeAlias = { + name: 'TypeAlias', + kind: ReflectionKind.TypeAlias, +}; +const mockDefinitions = (): Definitions => { + const Constructor = { + name: 'Constructor', + kind: ReflectionKind.Constructor, + }; + const Method = { + name: 'Method', + kind: ReflectionKind.Method, + }; + const Class = { + name: 'Class', + kind: ReflectionKind.Class, + }; + const Function = { + name: 'Function', + kind: ReflectionKind.Function, + }; + const Enum = { + name: 'Enum', + kind: ReflectionKind.Enum, + }; + const PlatformSpecific = { + declarationReflection: { + name: 'PlatformSpecific', + kind: ReflectionKind.Constructor, + }, + browser: {}, + node: {}, + }; + return { + methods: { + Method, + }, + constructors: { + Constructor, + PlatformSpecific, + }, + functions: { + Function, + }, + types: { + TypeAlias, + }, + interfaces: { + Interface, + }, + classes: { + Class, + }, + enums: { + Enum, + }, + } as unknown as Definitions; +} + +describe('getMatchingType', () => { + beforeEach(() => { + vi.spyOn(constants, 'INTRINSIC_TYPES', 'get').mockReturnValue([]); + vi.spyOn(constants, 'EXTERNALLY_DEFINED_TYPES', 'get').mockReturnValue({}); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('returns undefined if intrinsic types includes the name of the type definition', () => { + const definitions = mockDefinitions(); + const parameter = new DeclarationReflection('foo', ReflectionKind.Parameter); + constants.INTRINSIC_TYPES.push('foo'); + vi.mocked(getReferenceTypeOfParameter).mockImplementation(() => ({ + name: 'foo', + type: 'literal', + }) as ReturnType); + expect(getMatchingType(parameter, definitions, {})).toEqual(undefined); + }); + + it('returns undefined if parameter type is undefined', () => { + const definitions = mockDefinitions(); + const parameter = new DeclarationReflection('foo', ReflectionKind.Parameter); + parameter.type = undefined; + vi.mocked(getReferenceTypeOfParameter).mockImplementation(() => ({ + name: 'foo', + type: 'literal' + }) as ReturnType); + expect(getMatchingType(parameter, definitions, {})).toEqual(undefined); + }); + + it('returns undefined if it is a literal type', () => { + const definitions = mockDefinitions(); + const parameter = new DeclarationReflection('foo', ReflectionKind.Parameter); + parameter.type = { type: 'array' } as ArrayType; + vi.mocked(getReferenceTypeOfParameter).mockImplementation(() => ({ + name: 'foo', + type: 'literal' + }) as ReturnType); + vi.mocked(isLiteralType).mockImplementation(() => true) + expect(getMatchingType(parameter, definitions, {})).toEqual(undefined); + }); + + it('returns an externally defined type if defined', () => { + const definitions = mockDefinitions(); + const parameter = new DeclarationReflection('foo', ReflectionKind.Parameter); + parameter.type = { type: 'array' } as ArrayType; + const externalType = makeNewExternalType('Foo', 'https://foo.com'); + vi.spyOn(constants, 'EXTERNALLY_DEFINED_TYPES', 'get').mockReturnValue({ + Foo: externalType, + }); + vi.mocked(getReferenceTypeOfParameter).mockImplementation(() => ({ + name: 'Foo', + type: 'literal' + }) as ReturnType); + expect(getMatchingType(parameter, definitions, {})).toEqual(externalType); + }); + + it('returns a type defined on an interface', () => { + const definitions = mockDefinitions(); + const parameter = new DeclarationReflection('foo', ReflectionKind.Parameter); + parameter.type = { type: 'array' } as ArrayType; + vi.mocked(getReferenceTypeOfParameter).mockImplementation(() => ({ + name: Interface.name, + type: 'literal' + }) as ReturnType); + expect(getMatchingType(parameter, definitions, {})).toEqual(Interface); + }); + + it('returns a type defined on a types', () => { + const definitions = mockDefinitions(); + const parameter = new DeclarationReflection('foo', ReflectionKind.Parameter); + parameter.type = { type: 'array' } as ArrayType; + vi.mocked(getReferenceTypeOfParameter).mockImplementation(() => ({ + name: TypeAlias.name, + type: 'literal' + }) as ReturnType); + expect(getMatchingType(parameter, definitions, {})).toEqual(TypeAlias); + }); + + // it('returns a type parameter if defined', () => { + // const definitions = mockDefinitions(); + // const parameter = new DeclarationReflection('parameter', ReflectionKind.Parameter); + // // parameter.type = 'foo'; + // vi.mocked(getReferenceTypeOfParameter).mockImplementation(() => ({ + // name: 'foo', + // type: 'literal' as 'literal', + // })); + // const reflection = new DeclarationReflection('bar', ReflectionKind.Class); + // const typeReflection = new TypeParameterReflection('foo', reflection, undefined); + // expect(getMatchingType(parameter, definitions, { + // foo: typeReflection, + // })).toEqual(); + // }); +}); + diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-matching-type.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-matching-type.ts new file mode 100644 index 000000000..c632dd74a --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-matching-type.ts @@ -0,0 +1,64 @@ +import { DeclarationReflection, ParameterReflection, TypeParameterReflection } from "typedoc"; +import { DecRef, Definitions, isLiteralType, isPlatformSpecificFileDeclarationReflection, isUnionType } from "../types.js"; +import { EXTERNALLY_DEFINED_TYPES, INTRINSIC_TYPES } from "../constants.js"; +import { warn } from "@internals/common/logger"; +import { getReferenceTypeOfParameter } from "./get-reference-type-of-parameter.js"; + +/** + * getMatchingType returns the matching type for a given parameter. + * + * It looks through the definitions tree, along with externally defined types + */ + +export const getMatchingType = ( + parameter: ParameterReflection | DeclarationReflection, + definitions: Definitions, + typeParameters: Record = {} +): undefined | DecRef => { + const { classes, interfaces, types } = definitions; + let { name: nameOfTypeDefinition } = getReferenceTypeOfParameter(parameter.type, definitions); + if (INTRINSIC_TYPES.includes(nameOfTypeDefinition)) { + return undefined; + } + if (parameter.type === undefined) { + return undefined; + } + if (isLiteralType(parameter.type)) { + return undefined; + } + // first, check if it is a specially defined external type + const externallyDefinedType = EXTERNALLY_DEFINED_TYPES[nameOfTypeDefinition] || interfaces[nameOfTypeDefinition] || types[nameOfTypeDefinition]; + if (externallyDefinedType) { + return externallyDefinedType; + } + + // it's possible that this type is a generic type; in which case, replace the generic with the actual type it's extending + // this is _UGLY_ + const typeParameterType = typeParameters[nameOfTypeDefinition]; + if (typeParameterType && typeParameterType.type !== undefined && 'name' in typeParameterType.type) { + nameOfTypeDefinition = typeParameterType.type.name; + const matchingType = interfaces[nameOfTypeDefinition] || types[nameOfTypeDefinition]; + parameter.type = isPlatformSpecificFileDeclarationReflection(matchingType) ? matchingType.declarationReflection.type : matchingType.type; + return matchingType; + } + if (!isUnionType(parameter.type)) { + let matchingDefKey: string | undefined; + for (const [definitionKey, defs] of Object.entries(definitions)) { + for (const key of Object.keys(defs)) { + if (key === nameOfTypeDefinition) { + matchingDefKey = definitionKey; + } + } + } + warn([ + `No matching type could be found for ${nameOfTypeDefinition} in interfaces, types, or classes.`, + matchingDefKey ? `However, it was found in ${matchingDefKey}.` : undefined, + `- Available interfaces: ${Object.keys(interfaces).join(', ')}`, + `- Available types: ${Object.keys(types).join(', ')}`, + `- Available classes: ${Object.keys(classes).join(', ')}`, + 'Parameter type:', + JSON.stringify(parameter.type), + ].join('\n')); + } + return undefined; +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-reference-type-of-parameter.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-reference-type-of-parameter.ts new file mode 100644 index 000000000..bdb892b2a --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-reference-type-of-parameter.ts @@ -0,0 +1,157 @@ +import { SomeType, UnionType } from "typedoc"; +import { Definitions, getLiteralTypeValue, isArrayType, isDeclarationReflection, isInstrinsicType, isIntersectionType, isLiteralType, isReferenceType, isUnionType } from "../types.js"; +import { info, warn } from "@internals/common/logger"; + +export const getReferenceTypeOfParameter = (_type?: SomeType, definitions?: Definitions): { + type: 'reference' | 'array' | 'literal' | 'intrinsic' | 'union', + name: string; + includeURL?: boolean; +} => { + if (!_type) { + throw new Error('Define a type'); + } + if (isArrayType(_type)) { + const { elementType } = _type; + if (isReferenceType(elementType)) { + return { + type: _type.type, + name: elementType.name, + } + } else if (isUnionType(elementType)) { + return { + type: 'union', + name: elementType.types.map(t => { + if ('name' in t) { + return t.name; + } + throw new Error('unimplemented'); + }).join(' | '), + }; + } + + throw new Error('Not yet implemented'); + } + + if (isReferenceType(_type)) { + const { name } = _type; + if (name === 'ModelDefinitionObjectOrFn') { + return { + type: _type.type, + name: "ModelDefinition", + }; + } + return { + type: _type.type, + name, + }; + } + + if (isLiteralType(_type)) { + return { + type: 'literal', + name: getLiteralTypeValue(_type), + }; + } + + if (isInstrinsicType(_type)) { + return { + type: 'intrinsic', + name: _type.name, + } + } + + if (isIntersectionType(_type)) { + const refType = _type.types.filter(t => t.type === 'reference').pop(); + if (!refType || !isReferenceType(refType)) { + throw new Error('No reference type found on intersection type.'); + } + // if (definitions === undefined) { + // throw new Error('Intersection type was provided and a reference type was found in the union, but no definitions are present.') + // } + const intersectionType = refType.typeArguments?.filter(t => t.type === 'reference').pop(); + if (!intersectionType || !('name' in intersectionType)) { + throw new Error('No type arguments found on intersection type.'); + } + return { + type: 'literal', + name: intersectionType.name, + }; + } + + if (isUnionType(_type)) { + let includeURL = true; + + const getNameFromUnionType = (type: UnionType): string => type.types.map(t => { + if (isReferenceType(t)) { + if (definitions === undefined) { + warn('Union type was provided and a reference type was found in the union, but no definitions are present.'); + return t.name; + } + const { interfaces, types } = definitions; + const matchingType = interfaces[t.name] || types[t.name]; + if (!isDeclarationReflection(matchingType)) { + throw new Error('Is a platform specific type'); + } + if (!matchingType?.type) { + return t.name; + // throw new Error(`No matching type found for literal ${t.name} in union`); + } + const matchingTypeType = matchingType.type; + if (isLiteralType(matchingTypeType)) { + // if any literal types are included, don't include the URL + includeURL = false; + return JSON.stringify(matchingTypeType.value); + } + if (matchingTypeType.type === 'reflection') { + // Ignore reflection types + return t.name; + } + if (matchingTypeType.type === 'union') { + return getNameFromUnionType(matchingTypeType); + } + if (matchingTypeType.type === 'tuple') { + info('matchingTypeType tuple', matchingTypeType); + return `[${matchingTypeType.elements?.map(e => { + if ('name' in e) { + return e.name; + } + throw new Error('Array type not yet implemented'); + }).join(',')}]`; + } + throw new Error(`Unsupported type of matching type ${matchingTypeType.type} in reference type of union type ${t.name}.`); + } else if (isInstrinsicType(t)) { + if (t.name === 'undefined') { + // ignore an explicit undefined type; this should be better represented to the user as an optional flag. + return undefined; + } + return t.name; + } else if (isLiteralType(t)) { + return `${t.value}`; + } else if (t.type === 'indexedAccess') { + const objectType = t.objectType; + if ('name' in objectType) { + return objectType.name; + } + return ''; + } else if (t.type === 'array') { + if ('name' in t.elementType) { + return `${t.elementType.name}[]`; + } + warn('Unknown element type', t); + // throw new Error('Unknown element type'); + return ''; + } + throw new Error(`Unsupported type in union type: ${t.type}`); + }).filter(Boolean).join(' | '); + + const name = getNameFromUnionType(_type); + + return { + type: 'literal', + includeURL, + name, + }; + } + + throw new Error(`Unsupported type: ${_type.type}`) +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-return-type.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-return-type.ts new file mode 100644 index 000000000..9c75fcb5f --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-return-type.ts @@ -0,0 +1,80 @@ +import { CommentTag, SignatureReflection, TypeParameterReflection } from "typedoc"; +import { isInstrinsicType, isReferenceType, isUnionType } from "../types.js"; +import { getReferenceTypeOfParameter } from "./get-reference-type-of-parameter.js"; + +export const getReturnType = (signatures: (SignatureReflection & { typeParameter?: TypeParameterReflection[] })[], blockTags?: Record) => { + if (signatures.length === 1) { + const { type } = signatures[0]; + if (type === undefined) { + return 'void'; + } + + if (isReferenceType(type)) { + const { name, typeArguments } = type; + let nameOfType = name; + if (typeArguments?.length) { + nameOfType = `${nameOfType}<${typeArguments.map(t => getReferenceTypeOfParameter(t)).map(({ name }) => name).join(', ')}>`; + } + const returnDescription = blockTags?.['@returns']?.map(({ text }) => text).join(''); + return `\`${nameOfType}\`${returnDescription ? ` - ${returnDescription}` : ''}`; + } + + if (isInstrinsicType(type)) { + const nameOfType = type.name; + const returnDescription = blockTags?.['@returns']?.map(({ text }) => text).join(''); + return `\`${nameOfType}\`${returnDescription ? ` - ${returnDescription}` : ''}`; + } + + throw new Error(`Return Type function not yet implemented for type ${type.type}`) + } + + // let comment: Comment; + let commentSeen = false; + const validReturnTypes = new Set(); + let returnType = ''; + signatures.forEach(signature => { + if (signature.comment) { + if (commentSeen) { + throw new Error('Multiple comments defined for return signatures'); + } + commentSeen = true; + // comment = signature.comment; + } + const { type } = signature; + if (type === undefined) { + throw new Error('No type defined for signature'); + } + if (!isReferenceType(type)) { + throw new Error(`Unsupported type: ${type.type}`); + } + if (returnType !== '' && returnType !== type.name) { + throw new Error(`Conflicting return types in signatures: ${returnType} vs ${type.name}}`) + } + returnType = type.name; + if (!('typeArguments' in type)) { + throw new Error('No type arguments defined for type'); + } + const { typeArguments } = type; + typeArguments?.forEach(type => { + if (isUnionType(type)) { + type.types.forEach(t => { + if (isInstrinsicType(t) || isReferenceType(t)) { + validReturnTypes.add(t.name); + } else { + throw new Error(`Unsupported type when trying to handle union type while collecting valid signatures: ${type.type} ${t.type}`); + } + }); + } else if (isInstrinsicType(type)) { + validReturnTypes.add(type.name); + } else if (isReferenceType(type)) { + validReturnTypes.add(type.name); + } else { + throw new Error(`Unsupported type when trying to collect valid signatures: ${type.type}`); + } + }); + }) + + const nameOfType = `${returnType}<${Array.from(validReturnTypes).join(' | ')}>`; + const returnDescription = blockTags?.['@returns']?.map(({ text }) => text).join(''); + return `\`${nameOfType}\`${returnDescription ? ` - ${returnDescription}` : ''}`; +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-source.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-source.ts new file mode 100644 index 000000000..16b8ab26c --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-source.ts @@ -0,0 +1,20 @@ +import { SourceReference } from "typedoc"; +import { REPO_ROOT, TEMPLATES_DIR } from "../constants.js"; +import { getTemplate } from "@internals/common/get-template"; +import { rewriteURL } from "./get-url-from-sources.js"; +import path from "path"; + +export const getSource = ([source]: SourceReference[]) => { + let { + fileName, + line, + url, + } = source; + url = `${REPO_ROOT}/blob/main/${fileName}#L${line}`; + const prettyFileName = fileName.split('packages/upscalerjs/src/').pop(); + return getTemplate(path.resolve(TEMPLATES_DIR, 'source.md.t'), { + prettyFileName, + line, + url: rewriteURL(url), + }); +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-text-summary.test.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-text-summary.test.ts new file mode 100644 index 000000000..5dc3606d0 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-text-summary.test.ts @@ -0,0 +1,47 @@ +import { Comment } from "typedoc"; +import { getTextSummary } from "./get-text-summary.js"; + +describe('getTextSummary', () => { + it('returns an empty object for an undefined comment', () => { + expect(getTextSummary('foo')).toEqual({}); + }); + + it('throws an error if receiving an empty summary', () => { + expect(() => getTextSummary('foo', { + summary: [], + blockTags: [], + } as unknown as Comment)).toThrow('Expected code snippet not found for foo'); + }); + + it('throws an error if receiving a summary with kind not code', () => { + expect(() => getTextSummary('foo', { + summary: [{ + kind: 'foo', + }], + blockTags: [], + } as unknown as Comment)).toThrow('Expected code snippet not found for foo'); + }); + + it('returns a text summary', () => { + expect(getTextSummary('foo', { + summary: [{ + kind: 'code', + text: 'foo', + }, { + kind: 'code', + text: 'bar', + }], + blockTags: [{ + tag: 'baz', + content: 'qux', + }], + } as unknown as Comment)).toEqual({ + blockTags: { + baz: 'qux', + }, + description: 'foo', + codeSnippet: 'bar', + + }); + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-text-summary.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-text-summary.ts new file mode 100644 index 000000000..db7835354 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-text-summary.ts @@ -0,0 +1,25 @@ +import { Comment, CommentDisplayPart } from "typedoc"; + +export const getTextSummary = (name: string, comment?: Comment): { + codeSnippet?: string; + description?: string; + blockTags?: Record; +} => { + if (comment === undefined) { + return {}; + } + const { summary, blockTags } = comment; + const expectedCodeSnippet = summary.pop(); + if (expectedCodeSnippet?.kind !== 'code') { + throw new Error(`Expected code snippet not found for ${name}`); + } + const text = summary.map(({ text }) => text).join(''); + return { + blockTags: blockTags?.reduce>((obj, { tag, content }) => ({ + ...obj, + [tag]: content, + }), {}), + description: text.trim(), + codeSnippet: expectedCodeSnippet.text.trim(), + } +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-url-from-sources.test.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-url-from-sources.test.ts new file mode 100644 index 000000000..97d765b4e --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-url-from-sources.test.ts @@ -0,0 +1,53 @@ +import { DeclarationReflection, SourceReference } from "typedoc"; +import { DecRef } from "../types.js"; +import { getURLFromSources, rewriteURL } from "./get-url-from-sources.js"; + +describe('rewriteURL', () => { + it('rewrites the URL', () => { + const URL = 'https://github.com/thekevinscott/UpscalerJS/blob/foobabaz/packages/upscalerjs/foo.setup.ts'; + expect(rewriteURL(URL)).toEqual([ + "https://github.com/thekevinscott/UpscalerJS/", + "tree/main", + "/packages/upscalerjs/foo.setup.ts", + ].join('')); + }); + + it('throws if given a non-matching url', () => { + const URL = 'https://github.com/thekevinscott/UpscalerJS/foobabaz/packages/upscalerjs/foo.setup.ts'; + expect(() => rewriteURL(URL)).toThrow(); + }); +}); + +describe('getURLFromSources', () => { + it('returns undefined if no matching type is provided', () => { + expect(getURLFromSources(undefined)).toEqual(undefined); + }); + + it('returns undefined if sources is not available', () => { + expect(getURLFromSources({} as DecRef)).toEqual(undefined); + }); + + it('returns undefined if sources is empty', () => { + expect(getURLFromSources({ + sources: [] as SourceReference[], + } as DeclarationReflection)).toEqual(undefined); + }); + + it('returns rewritten URL if beginning with repo root', () => { + const url = 'https://github.com/thekevinscott/UpscalerJS/blob/foobabaz/packages/upscalerjs/foo.setup.ts'; + expect(getURLFromSources({ + sources: [{ + url, + }] as SourceReference[], + } as DecRef)).toEqual(rewriteURL(url)); + }); + + it('returns unrewritten URL if not beginning with repo root', () => { + const url = 'foo.com'; + expect(getURLFromSources({ + sources: [{ + url, + }] as SourceReference[], + } as DecRef)).toEqual(url); + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-url-from-sources.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-url-from-sources.ts new file mode 100644 index 000000000..3e83e6344 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/get-url-from-sources.ts @@ -0,0 +1,30 @@ +import { TypeParameterReflection } from "typedoc"; +import { DecRef } from "../types.js"; +import { REPO_ROOT } from "../constants.js"; + +export const rewriteURL = (url: string) => { + const parts = url.split(/blob\/(?[^/]+)/) + if (parts.length !== 3) { + throw new Error(`Error with the regex: ${url}`); + } + return [ + parts[0], + 'tree/main', + parts[2], + ].join(''); +}; + +export const getURLFromSources = (matchingType: undefined | DecRef | TypeParameterReflection) => { + if (!matchingType) { + return undefined; + } + if ('sources' in matchingType) { + const sources = matchingType.sources; + if (sources?.length) { + const { url } = sources[0]; + return url?.startsWith(REPO_ROOT) ? rewriteURL(url) : url; + } + } + + return undefined; +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/index.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/index.ts new file mode 100644 index 000000000..bcc7c0983 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/index.ts @@ -0,0 +1 @@ +export { writeAPIDocumentationFiles } from "./write-api-documentation-files.js"; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-api-documentation-files.test.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-api-documentation-files.test.ts new file mode 100644 index 000000000..090a8e599 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-api-documentation-files.test.ts @@ -0,0 +1,51 @@ +import { writeAPIDocumentationFiles } from "./write-api-documentation-files.js"; +import { mkdirp, writeFile } from "@internals/common/fs"; +import { getContentForMethod } from './get-content-for-method.js'; +import { Definitions } from "../types.js"; +import { DeclarationReflection } from "typedoc"; + +vi.mock('@internals/common/fs', () => ({ + writeFile: vi.fn(), + mkdirp: vi.fn(), +})); + +vi.mock('./get-content-for-method.js', () => ({ + getContentForMethod: vi.fn(), +})); + +describe('writeAPIDocumentationFiles', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('it to write API documentation files', async () => { + vi.mocked(getContentForMethod).mockResolvedValue('foobarbaz'); + await writeAPIDocumentationFiles('/out', [{ + name: 'foo' + } as DeclarationReflection], {} as unknown as Definitions); + expect(getContentForMethod).toHaveBeenCalled(); + expect(mkdirp).toHaveBeenCalledWith('/out'); + expect(writeFile).toHaveBeenCalledWith('/out/foo.md', 'foobarbaz'); + }); + + it('it to write API documentation files for multiple methods', async () => { + vi.mocked(getContentForMethod).mockResolvedValue('foobarbaz'); + await writeAPIDocumentationFiles('/out', [{ + name: 'foo' + }, { + name: 'bar', + }] as DeclarationReflection[], {} as unknown as Definitions); + expect(getContentForMethod).toHaveBeenCalledTimes(2); + expect(writeFile).toHaveBeenCalledWith('/out/foo.md', 'foobarbaz'); + expect(writeFile).toHaveBeenCalledWith('/out/bar.md', 'foobarbaz'); + }); + + it('it to throw if no content is returned', async () => { + vi.mocked(getContentForMethod).mockResolvedValue(''); + await expect(() => writeAPIDocumentationFiles('/out', [{ + name: 'foo' + }, { + name: 'bar', + }] as DeclarationReflection[], {} as unknown as Definitions)).rejects.toThrow(); + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-api-documentation-files.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-api-documentation-files.ts new file mode 100644 index 000000000..c9183e8af --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-api-documentation-files.ts @@ -0,0 +1,22 @@ +import path from 'path'; +import { mkdirp, writeFile } from '@internals/common/fs'; +import { getContentForMethod } from './get-content-for-method.js'; +import { Definitions } from '../types.js'; +import { DeclarationReflection } from 'typedoc'; +import { verbose } from '@internals/common/logger'; + +export const writeAPIDocumentationFiles = async (dest: string, methods: DeclarationReflection[], definitions: Definitions) => { + await Promise.all(methods.map(async (method, i) => { + verbose('Getting content for method', method.name); + const content = await getContentForMethod(method, definitions, i); + verbose('Content for method', method.name, 'measures', content.length); + if (!content) { + throw new Error(`No content for method ${method.name}`); + } + const target = path.resolve(dest, `${method.name}.md`); + await mkdirp(path.dirname(target)); + await writeFile(target, content.trim()); + verbose('Wrote content for method', method.name, 'to', target); + })); +}; + diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-parameter.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-parameter.ts new file mode 100644 index 000000000..6bc3732aa --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-parameter.ts @@ -0,0 +1,90 @@ +import { Comment, DeclarationReflection, ParameterReflection, TypeParameterReflection } from "typedoc"; +import { DecRef, Definitions, PlatformSpecificFileDeclarationReflection, isDeclarationReflection, isPlatformSpecificFileDeclarationReflection } from "../types.js"; +import { getReferenceTypeOfParameter } from "./get-reference-type-of-parameter.js"; +import { getURLFromSources } from "./get-url-from-sources.js"; +import { sortChildrenByLineNumber } from "../sort-children-by-line-number.js"; +import { getMatchingType } from "./get-matching-type.js"; +import { TYPES_TO_EXPAND } from "../constants.js"; + +export const getSummary = (comment?: Comment) => comment?.summary.map(({ text }) => text).join(''); + +type MatchingType = undefined | DecRef | TypeParameterReflection; +type Parameter = ParameterReflection | DeclarationReflection; + +const writeParameter = ( + methodName: string, + parameter: Parameter, + matchingType: MatchingType, + definitions: Definitions, + childParameters: string +) => { + const comment = getSummary(parameter.comment); + const { type, name, includeURL = true } = getReferenceTypeOfParameter(parameter.type, definitions); + const parsedName = `${name}${type === 'array' ? '[]' : ''}`; + + let url: string | undefined; + const typesToExpand = TYPES_TO_EXPAND[methodName === 'constructor' ? '_constructor' : methodName] || []; + if (typesToExpand.includes(name)) { + url = `#${name.toLowerCase()}`; + } else if (includeURL) { + url = getURLFromSources(matchingType); + } + const linkedName = url ? `[\`${parsedName}\`](${url})` : `\`${parsedName}\``; + return [ + '-', + `**${parameter.name}${parameter.flags?.isOptional ? '?' : ''}**:`, + childParameters === '' ? linkedName : undefined, // only show the type information if we're not expanding it + comment ? ` - ${comment.split('\n').join(" ")}` : undefined, + ].filter(Boolean).join(' '); +}; + +const writePlatformSpecificParameter = (platform: string, parameter: DeclarationReflection, definitions: Definitions) => { + const comment = getSummary(parameter.comment); + const { type, name } = getReferenceTypeOfParameter(parameter.type, definitions); + const url = getURLFromSources(parameter); + const parsedName = `${name}${type === 'array' ? '[]' : ''}`; + return [ + '-', + `**[${platform}](${url})**:`, + `\`${parsedName}\``, + comment ? ` - ${comment}` : undefined, + ].filter(Boolean).join(' '); +}; + + +export const writePlatformSpecificDefinitions = (definitions: Definitions): string => { + const platformSpecificTypes: PlatformSpecificFileDeclarationReflection[] = []; + for (const type of Object.values(definitions.types)) { + if (!isDeclarationReflection(type)) { + platformSpecificTypes.push(type); + } + } + return platformSpecificTypes.map(parameter => [ + writePlatformSpecificParameter('Browser', parameter.browser, definitions), + writePlatformSpecificParameter('Node', parameter.node, definitions), + ].join('\n')).join('\n'); +}; + +export const getParameters = ( + methodName: string, + parameters: Parameter[], + definitions: Definitions, + typeParameters: Record = {}, + depth = 0 +): string => { + if (depth > 5) { + throw new Error('Too many levels of depth'); + } + return parameters.map((parameter) => { + const matchingType = getMatchingType(parameter, definitions, typeParameters); + if (matchingType) { + const { children } = isPlatformSpecificFileDeclarationReflection(matchingType) ? matchingType.declarationReflection : matchingType; + const childParameters = children ? getParameters(methodName, sortChildrenByLineNumber(children), definitions, typeParameters, depth + 1) : ''; + return [ + writeParameter(methodName, parameter, matchingType, definitions, childParameters), + childParameters, + ].filter(Boolean).map(line => Array(depth * 2).fill(' ').join('') + line).join('\n'); + } + return undefined; + }).filter(Boolean).join('\n'); +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-platform-specific-parameter.ts b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-platform-specific-parameter.ts new file mode 100644 index 000000000..e77fe420e --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-api-documentation-files/write-platform-specific-parameter.ts @@ -0,0 +1,18 @@ +import { DeclarationReflection } from "typedoc"; +import { Definitions } from "../types.js"; +import { getReferenceTypeOfParameter } from "./get-reference-type-of-parameter.js"; +import { getURLFromSources } from "./get-url-from-sources.js"; +import { getSummary } from "./write-parameter.js"; + +export const writePlatformSpecificParameter = (platform: string, parameter: DeclarationReflection, definitions: Definitions) => { + const comment = getSummary(parameter.comment); + const { type, name } = getReferenceTypeOfParameter(parameter.type, definitions); + const url = getURLFromSources(parameter); + const parsedName = `${name}${type === 'array' ? '[]' : ''}`; + return [ + '-', + `**[${platform}](${url})**:`, + `\`${parsedName}\``, + comment ? ` - ${comment}` : undefined, + ].filter(Boolean).join(' '); +}; diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-index-file.test.ts b/internals/scripts/src/bin/write/docs/api/lib/write-index-file.test.ts new file mode 100644 index 000000000..3174fa25e --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-index-file.test.ts @@ -0,0 +1,27 @@ +import { DeclarationReflection } from "typedoc"; +import { writeIndexFile } from "./write-index-file.js"; +import { getTemplate } from "@internals/common/get-template"; +import { writeFile } from "@internals/common/fs"; + +vi.mock('@internals/common/fs', () => ({ + writeFile: vi.fn(), +})); + +vi.mock('@internals/common/get-template', () => ({ + getTemplate: vi.fn(), +})); + +describe('writeIndexFile', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('writes an index file', async () => { + vi.mocked(getTemplate).mockResolvedValue('foobarbaz'); + await writeIndexFile('/out', [{ + name: 'foo', + }] as unknown as DeclarationReflection[]); + expect(getTemplate).toHaveBeenCalled(); + expect(writeFile).toHaveBeenCalledWith('/out/index.md', 'foobarbaz'); + }); +}); diff --git a/internals/scripts/src/bin/write/docs/api/lib/write-index-file.ts b/internals/scripts/src/bin/write/docs/api/lib/write-index-file.ts new file mode 100644 index 000000000..901e2ec34 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/api/lib/write-index-file.ts @@ -0,0 +1,12 @@ +import { writeFile } from "@internals/common/fs"; +import { getTemplate } from "@internals/common/get-template"; +import path from "path"; +import { DeclarationReflection } from "typedoc"; +import { TEMPLATES_DIR } from "./constants.js"; + +export const writeIndexFile = async (dest: string, methods: DeclarationReflection[]) => { + const contents = await getTemplate(path.resolve(TEMPLATES_DIR, 'index.md.t'), { + methods, + }); + await writeFile(path.resolve(dest, 'index.md'), contents); +} diff --git a/internals/scripts/src/bin/write/docs/check-tense.ts b/internals/scripts/src/bin/write/docs/check-tense.ts new file mode 100644 index 000000000..094f2141d --- /dev/null +++ b/internals/scripts/src/bin/write/docs/check-tense.ts @@ -0,0 +1,106 @@ + +/***** + * Script for checking tense in docs markdown files + */ +import { sync } from 'glob'; +import path from 'path'; +import { readFile } from '@internals/common/fs'; +import { DOCS_DIR } from '@internals/common/constants'; + +/**** + * Constants + */ + +const EXCLUDED_DIRECTORIES = [ + 'node_modules', + 'blog', +]; + +/**** + * Utility functions + */ + +const getDocumentationFiles = (): string[] => { + return sync(path.resolve(DOCS_DIR, '**/*.{md,mdx}')).filter(file => { + return EXCLUDED_DIRECTORIES.reduce((include, dir) => { + return !include ? false : !file.includes(dir); + }, true); + }); +}; + +// split a markdown file's contents into two concatenated strings, +// one containing the main content of the file, the other containing +// just the asides +const splitFileContents = (contents: string): [string, string] => { + const nonAsides = []; + const asides = []; + let isAside = false; + for (const line of contents.split('\n')) { + if (line.startsWith(':::')) { + isAside = !isAside; + } else { + if (isAside) { + asides.push(line); + } else { + nonAsides.push(line); + } + } + } + return [nonAsides.join('\n'), asides.join('\n')]; +}; + +// check that a chunk of text matches a specific tense +const checkTense = (contents: string, expectedTense: 'third' | 'second') => { + if (expectedTense === 'third') { + // const matches = contents.match(/(Y|y)ou|(Y|y)our|(M|m)ine|(M|m)y/g); + return contents.match(/\b(I |I'm|me|my|mine|you|your|yours|yourself|yourselves)\b/g); + } else if (expectedTense === 'second') { + return contents.match(/\b(I |I'm|me|my|mine|we|us|our|ours|ourselves)\b/g); + } + throw new Error(`Unexpected tense: ${expectedTense}`); +} + +const checkFileForTense = async (file: string) => { + const contents = await readFile(file); + if (file.includes('documentation/api') || file.includes('troubleshooting')) { + const matches = checkTense(contents, 'second'); + if (matches !== null) { + return [ + `Found inconsistent tenses in file ${file}:`, + '', + `Main content should be second person, found following keywords: ${matches.join('|')}`, + ].join('\n'); + } + } else { + const [mainContents, asides] = splitFileContents(contents); + const mainMatches = checkTense(mainContents, 'third'); + const asidesMatches = checkTense(asides, 'second'); + if (mainMatches !== null || asidesMatches !== null) { + return [ + `Found inconsistent tenses in file ${file}:`, + '', + ...(mainMatches !== null ? [ + `Main content should be third person, found following keywords: ${mainMatches.join('|')}`, + ] : []), + ...(asidesMatches !== null ? [ + `Asides content should be second person, found following keywords: ${asidesMatches.join('|')}`, + ] : []), + ].join('\n'); + } + } + return undefined; +} + +/**** + * Main function + */ +export const main = async () => { + const files = getDocumentationFiles(); + const errors = (await Promise.all(files.map(checkFileForTense))).filter(Boolean); + + if (errors.length) { + throw new Error(errors.join('\n\n\n')); + } +} + +main(); diff --git a/internals/scripts/src/bin/write/docs/guides/index.ts b/internals/scripts/src/bin/write/docs/guides/index.ts new file mode 100644 index 000000000..7eba7f4d2 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/guides/index.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { DOCS_DIR } from '@internals/common/constants'; +import { mkdirp } from '@internals/common/fs'; +import { writeGuideDocs } from './write-guide-docs.js'; +import { info, verbose } from '@internals/common/logger'; +import { getSharedArgs } from '../shared/get-shared-args.js'; +import { clearOutMarkdownFiles } from '../shared/clear-out-markdown-files.js'; + +const EXAMPLES_DOCS_DEST = path.resolve(DOCS_DIR, 'docs/documentation/guides'); + +const writeGuideDocumentation = async ({ shouldClearMarkdown }: { shouldClearMarkdown: boolean }) => { + info('Writing guides documentation'); + await mkdirp(EXAMPLES_DOCS_DEST); + if (shouldClearMarkdown) { + verbose(`Clearing out markdown files in ${EXAMPLES_DOCS_DEST}`) + await clearOutMarkdownFiles(EXAMPLES_DOCS_DEST); + } + + return writeGuideDocs(EXAMPLES_DOCS_DEST); +}; + +const main = async () => { + return writeGuideDocumentation(getSharedArgs()); +}; + +main(); diff --git a/internals/scripts/src/bin/write/docs/guides/write-guide-docs.ts b/internals/scripts/src/bin/write/docs/guides/write-guide-docs.ts new file mode 100644 index 000000000..8e140acdb --- /dev/null +++ b/internals/scripts/src/bin/write/docs/guides/write-guide-docs.ts @@ -0,0 +1,278 @@ +import { EXAMPLES_DIR } from "@internals/common/constants"; +import { copyFile, exists, mkdirp, readFile, readdir, stat, writeFile } from "@internals/common/fs"; +import { getPackageJSON } from "@internals/common/package-json"; +import path from "path"; + +/**** + * Types + */ +interface FrontMatter { + [index: string]: string | number | FrontMatter; +} +interface ExampleContent { + title: string; + frontmatter: FrontMatter; +} +type Category = 'browser' | 'node' | 'other'; + +/**** + * Utility functions + */ +const DEFAULT_EMBED_FOR_NODE = 'codesandbox'; +const DEFAULT_EMBED_FOR_BROWSER = 'codesandbox'; +const isCategory = (category: unknown): category is Category => typeof category === 'string' && ['browser', 'node', 'other'].includes(category); +const getGuideFolders = async (root: string): Promise => { + const files = await readdir(root); + const includedFiles: string[] = []; + await Promise.all(files.map(async file => { + const isDirectory = (await stat(path.resolve(root, file))).isDirectory(); + if (isDirectory) { + includedFiles.push(file); + } + })); + if (includedFiles.length === 0) { + throw new Error('No guides found') + } + return includedFiles; +}; + +const getBody = (contents: string) => contents.split('---').pop() ?? ''; + +const getDefaultCodeEmbedParameters = (category: Category, params: Record = {}) => { + if (category === 'node') { + return 'view=split,preview&module=index.js&hidenavigation=1'; + }; + return Object.entries({ + embed: 1, + file: 'index.js', + hideExplorer: 1, + ...params, + }).map(([key, val]) => `${key}=${val}`).join('&'); +} + +const getFrontmatter = async (key: string): Promise => { + const packageJSON = await getPackageJSON(path.resolve(EXAMPLES_DIR, key, 'package.json')); + const readmePath = path.resolve(EXAMPLES_DIR, key, 'README.md'); + const readmeContents = await readFile(readmePath); + const body = getBody(readmeContents); + const bodyParts = body.split('\n'); + let title: undefined | string; + for (const line of bodyParts) { + if (line.startsWith('#')) { + title = line.split('#')?.pop()?.trim() ?? ''; + break; + } + } + + if (!title) { + throw new Error(`No title found in file ${readmePath}`); + } + + const { + category = 'browser', + code_embed, + ...frontmatter + } = packageJSON['@upscalerjs']?.guide?.frontmatter || {}; + + const codeEmbed = code_embed !== false ? { + params: getDefaultCodeEmbedParameters(category, frontmatter.params), + type: category ? DEFAULT_EMBED_FOR_NODE : DEFAULT_EMBED_FOR_BROWSER, + url: `/examples/${key}`, + ...code_embed, + } : {}; + + return { + frontmatter: { + category, + hide_table_of_contents: true, + ...frontmatter, + code_embed: codeEmbed, + }, + title, + } +}; + +const getGuidesWithFrontmatter = async (): Promise<({ key: string; } & ExampleContent)[]> => { + const folders = await getGuideFolders(EXAMPLES_DIR); + const includedFolders: string[] = []; + await Promise.all(folders.map(async folder => { + const readmePath = path.resolve(EXAMPLES_DIR, folder, 'README.md'); + if (await exists(readmePath)) { + includedFolders.push(folder); + } + })) + if (includedFolders.length === 0) { + throw new Error('No guides found including a readme') + } + + return Promise.all(includedFolders.map(async key => ({ + key, + ...(await getFrontmatter(key)), + }))); +}; + +const getExampleOrder = (examples: ({ key: string; } & ExampleContent)[]) => { + return examples.sort((a, b) => { + const aPos = Number(a.frontmatter.sidebar_position); + const bPos = Number(b.frontmatter.sidebar_position); + if (Number.isNaN(aPos)) { + return 1; + } + if (Number.isNaN(bPos)) { + return -1; + } + return aPos - bPos; + }).map(({ key }) => key); +} + +const getExamplesByName = async () => { + const examplesWithFrontmatter = await getGuidesWithFrontmatter(); + const exampleOrder = getExampleOrder(examplesWithFrontmatter); + + return { + examplesByName: examplesWithFrontmatter.reduce((obj, { key, ...rest }) => { + if (obj[key]) { + throw new Error(`Example already exists for key ${key}`); + } + return { + ...obj, + [key]: rest, + }; + }, {} as Record), + exampleOrder, + }; +} + +const indent = (str: string, depth = 0) => [...Array(depth * 2).fill(''), str].join(' '); +const uppercase = (str: string) => str[0].toUpperCase() + str.slice(1); + +const buildFrontmatter = (frontmatter: FrontMatter = {}, depth = 0): string[] => Object.entries(frontmatter).reduce((arr, [key, val]) => { + if (typeof val === 'object') { + return arr.concat(...[ + `${key}:`, + ...buildFrontmatter(val, depth + 1), + ].map(str => indent(str, depth))); + } + return arr.concat(indent(`${key}: ${val}`, depth)); +}, [] as string[]); + +const parseContents = async (key: string, frontmatter: FrontMatter = {}) => { + const readmePath = path.resolve(EXAMPLES_DIR, key, 'README.md'); + const contents = await readFile(readmePath); + const frontmatterContents = [ + ...buildFrontmatter(frontmatter), + ]; + return [ + '---', + ...frontmatterContents, + '---', + '', + contents, + ].filter(Boolean).join('\n'); +} + +const copyAssets = async (targetDir: string, key: string) => { + const srcAssetsDir = path.resolve(EXAMPLES_DIR, key, 'assets'); + if (await exists(srcAssetsDir)) { + const targetAssetsDir = path.resolve(targetDir, 'assets'); + await mkdirp(targetAssetsDir); + const assets = await readdir(srcAssetsDir); + await Promise.all(assets.map(async asset => { + const assetPath = path.resolve(srcAssetsDir, asset); + await copyFile(assetPath, path.resolve(targetAssetsDir, asset)); + })); + } +} + +const copyReadmesToDocs = async (exampleOrder: string[], examplesByName: Record, dest: string) => { + await Promise.all(exampleOrder.map(async (key) => { + const example = examplesByName[key]; + if (!example) { + throw new Error(`No example found for key ${key}`); + } + const { + frontmatter, + } = example; + + const { + parent, + category, + } = frontmatter; + if (!isCategory(category)) { + throw new Error(`Category is not valid: ${category}, for key ${key}`); + } + if (parent !== undefined && typeof parent !== 'string') { + throw new Error(`Parent is not of type string: ${parent}`); + } + const targetDir = path.resolve(...[dest, category, parent].filter(Boolean)); + + // copy assets + await copyAssets(targetDir, key); + + // write readme + const targetPath = path.resolve(targetDir, `${key}.md`); + await mkdirp(path.dirname(targetPath)); + const fileContents = await parseContents(key, frontmatter); + await writeFile(targetPath, fileContents); + })); +} + +const writeIndexFile = async (exampleOrder: string[], examplesByName: Record, dest: string) => { + const examplesByCategory = exampleOrder.reduce((obj, example) => { + const { frontmatter: { parent, category } } = examplesByName[example]; + if (!isCategory(category)) { + throw new Error(`Category is not valid: ${category}, for key ${example}`); + } + if (parent !== undefined && typeof parent !== 'string') { + throw new Error(`Parent is not of type string: ${parent}`); + } + return { + ...obj, + [category]: (obj[category] || []).concat([[parent ? uppercase(parent) : undefined, example]]), + } + }, {} as Record>); + + const content = [ + '---', + 'hide_table_of_contents: true', + '---', + '# Guides', + 'This page contains a list of guides and examples for using various features of UpscalerJS.', + '', + 'The first two guides discuss the basics of UpscalerJS and how to use it in a project. The [Models](browser/models) and [Working with Tensors](browser/tensors) guides discuss useful configuration options of UpscalerJS.', + '', + 'There are also guides on [improving the performance](#performance) of UpscalerJS, [specific examples of implementations](#implementations), and [Node.js-specific](#node) guides.', + '', + ...Object.entries(examplesByCategory).map(([category, examples]) => { + let activeParent: undefined | string; + return `\n## ${uppercase(category)}\n\n${examples.map(([parent, example]) => { + const { title } = examplesByName[example]; + const url = [ + '/documentation', + 'guides', + category, + parent, + example + ].filter(Boolean).join('/'); + const strings: string[] = []; + if (activeParent !== parent) { + activeParent = parent; + strings.push(`- ### ${parent}`); + } + strings.push(indent(`- [${title}](${url})`, activeParent ? 1 : 0)); + return strings.join('\n'); + }).join('\n')}`; + }), + ].join('\n'); + + await writeFile(path.resolve(dest, 'index.md'), content); +} + +export const writeGuideDocs = async (dest: string) => { + const { exampleOrder, examplesByName } = await getExamplesByName(); + + await Promise.all([ + copyReadmesToDocs(exampleOrder, examplesByName, dest), + writeIndexFile(exampleOrder, examplesByName, dest), + ]); +} diff --git a/internals/scripts/src/bin/write/docs/models/index.ts b/internals/scripts/src/bin/write/docs/models/index.ts new file mode 100644 index 000000000..55c68e068 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/models/index.ts @@ -0,0 +1,27 @@ +import path from 'path'; +import { DOCS_DIR } from '@internals/common/constants'; +import { mkdirp } from '@internals/common/fs'; +import { writeModelReadmes } from './write-model-readmes.js'; +import { verbose } from '@internals/common/logger'; +import { info } from 'console'; +import { getSharedArgs } from '../shared/get-shared-args.js'; +import { clearOutMarkdownFiles } from '../shared/clear-out-markdown-files.js'; + +const targetDocDir = path.resolve(DOCS_DIR, 'docs/models/available'); + +const writeModelsDocumentation = async ({ shouldClearMarkdown }: { shouldClearMarkdown: boolean }) => { + info('Writing models documentation'); + await mkdirp(targetDocDir); + if (shouldClearMarkdown) { + verbose(`Clearing out markdown files in ${targetDocDir}`) + await clearOutMarkdownFiles(targetDocDir); + } + + return writeModelReadmes(targetDocDir); +}; + +const main = async () => { + return writeModelsDocumentation(getSharedArgs()); +}; + +main(); diff --git a/internals/scripts/src/bin/write/docs/models/write-model-readmes.ts b/internals/scripts/src/bin/write/docs/models/write-model-readmes.ts new file mode 100644 index 000000000..457107426 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/models/write-model-readmes.ts @@ -0,0 +1,214 @@ +/***** + * Script for linking model readmes locally in docs folder + */ +import path from 'path'; +import { verbose } from '@internals/common/logger'; +import { DOCS_DIR, MODELS_DIR } from '@internals/common/constants'; +import { copy, exists, mkdirp, readFile, writeFile } from '@internals/common/fs'; +import { ALL_MODEL_PACKAGE_DIRECTORY_NAMES, PRIVATE_MODEL_PACKAGE_NAMES } from '@internals/common/models'; + +/**** + * Types + */ + +interface PackageWithMetadata { + description: string; + sidebarPosition: number; + enhancedSrc: string; + unenhancedSrc: string; + category: string; + packageName: string; +} + +/**** + * Utility functions + */ + +const copyAssets = async (packageName: string, targetDir: string) => { + const packagePath = path.resolve(MODELS_DIR, packageName, 'assets'); + const targetPath = path.resolve(targetDir, packageName); + await copy(packagePath, targetPath); +}; + +const createMarkdown = (contents: string, targetPath: string) => writeFile(targetPath, contents); + +const getCategory = (packageName: string, readmeContents: string) => { + const lines = readmeContents.split('\n'); + for (const line of lines) { + if (line.startsWith('category: ')) { + return line.split('category: ').pop() ?? ''; + } + } + + throw new Error(`Could not find category for package name ${packageName}`); +}; + +const linkAllModelReadmes = async (packages: string[], targetAssetDir: string, targetDocDir: string) => { + for (const packageName of packages) { + const packagePath = path.resolve(MODELS_DIR, packageName); + const docMdxPath = path.resolve(packagePath, 'DOC.mdx'); + + if (await exists(docMdxPath)) { + const docMdxContents = await readFile(docMdxPath); + const category = getCategory(packageName, docMdxContents); + + const targetPath = path.resolve(targetDocDir, category, `${packageName}.mdx`); + await mkdirp(path.dirname(targetPath)); + // try { + // unlinkSync(targetPath); + // } catch (err) { } + await copyAssets(packageName, targetAssetDir); + await createMarkdown(await readFile(docMdxPath), targetPath); + verbose(`** Linked: ${packageName}`); + } else { + verbose(`** Does not have a DOC.mdx file: ${packageName}`) + } + } +}; + +const getDescription = (readmeContents: string) => { + const lines = readmeContents.split('\n'); + let description = ''; + let startedDescription = false; + for (const line of lines) { + if (line.startsWith('# ')) { + startedDescription = true; + } else if (line.startsWith('## ')) { + startedDescription = false; + break; + } else if (!line.startsWith(' `${part[0].toUpperCase()}${part.slice(1)}`; + +const getSidebarPosition = (packageName: string, readmeContents: string) => { + const lines = readmeContents.split('\n'); + for (const line of lines) { + if (line.startsWith('sidebar_position: ')) { + const pos = line.split('sidebar_position: ').pop() ?? ''; + return parseInt(pos, 10); + } + } + throw new Error(`Could not find sidebar position for package name ${packageName}`); +}; + +const getEnhancedSrc = (packageName: string, readmeContents: string) => { + const lines = readmeContents.split('\n'); + for (const line of lines) { + if (line.startsWith('enhanced_src: ')) { + return line.split('enhanced_src: ').pop() ?? ''; + } + } + + throw new Error(`Could not find enhanced_src for package name ${packageName}`); +}; + +const getPackageMetadata = async (packageName: string) => { + const packagePath = path.resolve(MODELS_DIR, packageName); + const docMdxPath = path.resolve(packagePath, 'DOC.mdx'); + const docMdxContents = await readFile(docMdxPath); + return { + description: getDescription(docMdxContents), + sidebarPosition: getSidebarPosition(packageName, docMdxContents), + enhancedSrc: getEnhancedSrc(packageName, docMdxContents), + unenhancedSrc: `${packageName}/fixture.png`, + category: getCategory(packageName, docMdxContents), + }; +}; + +const getAllPackagesWithMetadata = async (packageNames: string[]): Promise => { + const packagesWithValidReadme = packageNames.filter(packageName => { + const packagePath = path.resolve(MODELS_DIR, packageName); + const readmePath = path.resolve(packagePath, 'DOC.mdx'); + return exists(readmePath); + }); + const packagesWithMetadata = await Promise.all(packagesWithValidReadme.map(async (packageName) => ({ + packageName, + ...(await getPackageMetadata(packageName)), + }))); + + return packagesWithMetadata; +}; + +const getAllPackagesOrganizedByCategory = async (packageNames: string[]): Promise<{ category: string, packages: PackageWithMetadata[] }[]> => { + const packages = await getAllPackagesWithMetadata(packageNames); + + const packagesByCategory = packages.reduce>>((obj, pkg) => { + const { category, sidebarPosition } = pkg; + if (!obj[category]) { + obj[category] = {}; + } + obj[category][sidebarPosition] = pkg; + return obj; + }, {}); + + return Object.keys(packagesByCategory).map(category => { + const packageSidebarPositions = Object.keys(packagesByCategory[category]).sort(); + const packages = packagesByCategory[category]; + + return { + category, + packages: packageSidebarPositions.map(position => packages[position]), + } + }); +}; + +const writeModelIndexFile = async (packageNames: string[], _targetAssetDir: string) => { + const packagesByCategory = getAllPackagesOrganizedByCategory(packageNames); + const contents = ` +--- +title: Models +description: An overview of available UpscalerJS Models +sidebar_position: 1 +sidebar_label: Overview +pagination_next: null +pagination_prev: null +hide_title: true +--- +View this page on the UpscalerJS website + +# Models + +UpscalerJS offers a number of available models. With the exception of \`default-model\`, these models must be explicitly installed alongside UpscalerJS. + +import ModelCard from '@site/src/components/modelCards/modelCard/modelCard'; +import ModelCards from '@site/src/components/modelCards/modelCards'; + +${(await packagesByCategory).map(({ category, packages }) => ` +## ${category.split('-').map(uppercase)} + + + ${packages.map(({ packageName, description, unenhancedSrc, enhancedSrc } ) => ` + + `).join('\n')} + +`).join('\n')} + + `; + await writeFile(path.resolve(DOCS_DIR, 'docs', 'models', 'index.md'), contents.trim()); +}; + +const isExcluded = (folder: string) => !PRIVATE_MODEL_PACKAGE_NAMES.includes(folder); + +export const writeModelReadmes = async (targetDocDir: string) => { + const packages = (await ALL_MODEL_PACKAGE_DIRECTORY_NAMES).filter(isExcluded); + const targetAssetDir = path.resolve(DOCS_DIR, 'assets/assets/sample-images'); + + await writeModelIndexFile(packages, targetAssetDir); + verbose('Wrote model index file'); + await linkAllModelReadmes(packages, targetAssetDir, targetDocDir); + verbose(`Linked ${packages.length} model readmes`) +}; diff --git a/internals/scripts/src/bin/write/docs/shared/clear-out-markdown-files.ts b/internals/scripts/src/bin/write/docs/shared/clear-out-markdown-files.ts new file mode 100644 index 000000000..a06ab704a --- /dev/null +++ b/internals/scripts/src/bin/write/docs/shared/clear-out-markdown-files.ts @@ -0,0 +1,18 @@ +import { unlink } from '@internals/common/fs'; +import { glob } from 'glob'; + +const getAllMarkdownFiles = (target: string) => glob(`${target}/**/*.md?(x)`); + +export const clearOutMarkdownFiles = async (target: string, verbose?: boolean) => { + const files = await getAllMarkdownFiles(target); + if (files.length > 0) { + await Promise.all(files.map(file => unlink(file))); + if (verbose) { + console.log([ + `Cleared out ${files.length} markdown files, including:`, + ...files.map(file => file.split(/docs\/documentation\//gi).pop()).map(file => `- ${file}`), + ].join('\n')); + } + } +}; + diff --git a/internals/scripts/src/bin/write/docs/shared/get-shared-args.ts b/internals/scripts/src/bin/write/docs/shared/get-shared-args.ts new file mode 100644 index 000000000..32dd3a1c3 --- /dev/null +++ b/internals/scripts/src/bin/write/docs/shared/get-shared-args.ts @@ -0,0 +1,18 @@ +import { parseArgs } from "node:util"; + +export const getSharedArgs = () => { + const { + values: { + ['should-clear-markdown']: shouldClearMarkdown = false, + }, + } = parseArgs({ + options: { + 'should-clear-markdown': { + type: "boolean", + short: "c", + }, + }, + }); + + return { shouldClearMarkdown }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42d139ed3..e21be3fab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -665,6 +665,9 @@ importers: '@internals/common': specifier: workspace:* version: link:../common + typedoc: + specifier: ^0.24.8 + version: 0.24.8(typescript@5.1.6) devDependencies: vitest: specifier: ^0.34.2 @@ -12089,6 +12092,7 @@ packages: /marked@4.3.0: resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} engines: {node: '>= 12'} + hasBin: true /matchit@1.1.0: resolution: {integrity: sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==} diff --git a/scripts/package.json b/scripts/package.json index e642887a7..d891cb79f 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -6,11 +6,6 @@ }, "scripts": { "__run_command": "ts-node --esm --project ./tsconfig.json", - "docs:build-api": "pnpm __run_command ./package-scripts/docs/build-api.ts", - "docs:build-guides": "pnpm __run_command ./package-scripts/docs/build-guides.ts", - "docs:link-model-readmes": "pnpm __run_command ./package-scripts/docs/link-model-readmes.ts", - "docs:tense-checks": "pnpm __run_command ./package-scripts/docs/tense-checks.ts", - "model:write-docs": "pnpm __run_command ./package-scripts/write-model-docs.ts", "test:integration:browserstack": "pnpm __run_command ./test.ts --kind integration --platform browser --runner browserstack", "test:integration:clientside": "pnpm __run_command ./test.ts --kind integration --platform browser", "test:integration:serverside": "pnpm __run_command ./test.ts --kind integration --platform node",