1033 lines
28 KiB
TypeScript
1033 lines
28 KiB
TypeScript
import { computeJSDocInputHash, extractJSDocsSync, parseJSDoc } from "@effect/jsdocs"
|
|
import { assert, describe, it } from "@effect/vitest"
|
|
import * as fs from "node:fs"
|
|
import * as os from "node:os"
|
|
import * as path from "node:path"
|
|
import * as ts from "typescript"
|
|
|
|
interface SourceFileWithParseDiagnostics extends ts.SourceFile {
|
|
readonly parseDiagnostics: ReadonlyArray<ts.Diagnostic>
|
|
}
|
|
|
|
const signatureParseDiagnostics = (signature: string): ReadonlyArray<string> =>
|
|
(ts.createSourceFile(
|
|
"signature.ts",
|
|
signature,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.TS
|
|
) as SourceFileWithParseDiagnostics)
|
|
.parseDiagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"))
|
|
|
|
describe("jsdocs", () => {
|
|
it("parses a raw JSDoc block", () => {
|
|
const result = parseJSDoc(`/**
|
|
* Creates a value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/`)
|
|
assert.strictEqual(result._tag, "Success")
|
|
if (result._tag === "Success") {
|
|
assert.strictEqual(result.value.description.short, "Creates a value.")
|
|
}
|
|
})
|
|
|
|
it("accepts practical When to use forms", () => {
|
|
const result = parseJSDoc(`/**
|
|
* Creates a value.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to create a value when construction should be explicit.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/`)
|
|
assert.strictEqual(result._tag, "Success")
|
|
if (result._tag === "Success") {
|
|
assert.strictEqual(
|
|
result.value.description.whenToUse,
|
|
"Use to create a value when construction should be explicit."
|
|
)
|
|
}
|
|
})
|
|
|
|
it("flags unsupported When to use forms", () => {
|
|
const result = parseJSDoc(`/**
|
|
* Creates a value.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* - Creating a value when construction should be explicit.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/`)
|
|
assert.strictEqual(result._tag, "Failure")
|
|
if (result._tag === "Failure") {
|
|
assert.deepStrictEqual(
|
|
result.error.diagnostics.map((diagnostic) => diagnostic.code),
|
|
["when-to-use-format"]
|
|
)
|
|
}
|
|
})
|
|
|
|
it("extracts docs with TypeScript", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* Creates a value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const makeValue = () => 1
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
assert.strictEqual(model.version, 2)
|
|
assert.strictEqual(model.files.length, 1)
|
|
assert.strictEqual(model.files[0]?.declarations[0]?.name, "makeValue")
|
|
assert.strictEqual(model.apis[0]?.apiFqn, "@effect/sample/Foo.makeValue")
|
|
assert.deepStrictEqual(model.apis[0]?.importGuidance, {
|
|
style: "namespace-barrel",
|
|
importDeclaration: "import { Foo } from \"@effect/sample\"",
|
|
usage: "Foo.makeValue"
|
|
})
|
|
})
|
|
|
|
it("stores a stable input hash for cache checks", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "jsdocs.config.json"),
|
|
JSON.stringify({
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* Creates a value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const makeValue = () => 1
|
|
`
|
|
)
|
|
const options = {
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
}
|
|
const inputHash = computeJSDocInputHash(options)
|
|
const model = extractJSDocsSync(options)
|
|
assert.strictEqual(model.inputHash, inputHash)
|
|
assert.strictEqual(computeJSDocInputHash(options), inputHash)
|
|
|
|
fs.appendFileSync(path.join(cwd, "src/Foo.ts"), "\n")
|
|
assert.notStrictEqual(computeJSDocInputHash(options), inputHash)
|
|
})
|
|
|
|
it("extracts typechecker signatures for top-level functions", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/External.ts"),
|
|
`/**
|
|
* External value type.
|
|
*
|
|
* @category models
|
|
* @since 1.0.0
|
|
*/
|
|
export interface External {
|
|
readonly value: string
|
|
}
|
|
|
|
/**
|
|
* Makes an external value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const makeExternal = (value: string): External => ({ value })
|
|
`
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* Parses a value.
|
|
*
|
|
* @category parsing
|
|
* @since 1.0.0
|
|
*/
|
|
export function parse(value: string): string
|
|
export function parse(value: number): number
|
|
export function parse(value: string | number) {
|
|
return value
|
|
}
|
|
|
|
/**
|
|
* Converts a value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const inferred = (value: number) => String(value)
|
|
|
|
const aliased = (value: boolean) => !value
|
|
|
|
export {
|
|
/**
|
|
* Negates a value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
aliased as renamed
|
|
}
|
|
|
|
export {
|
|
/**
|
|
* Makes an external value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
makeExternal as external
|
|
} from "./External.ts"
|
|
|
|
const attempt = (value: string) => value.length
|
|
|
|
export {
|
|
/**
|
|
* Attempts a value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
attempt as try
|
|
}
|
|
|
|
/**
|
|
* Handles many overloads.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export function many(value: 1): 1
|
|
export function many(value: 2): 2
|
|
export function many(value: 3): 3
|
|
export function many(value: 4): 4
|
|
export function many(value: 5): 5
|
|
export function many(value: 6): 6
|
|
export function many(value: 7): 7
|
|
export function many(value: 8): 8
|
|
export function many(value: 9): 9
|
|
export function many(value: 10): 10
|
|
export function many(value: 11): 11
|
|
export function many(value: 12): 12
|
|
export function many(value: 13): 13
|
|
export function many(value: number) {
|
|
return value
|
|
}
|
|
|
|
/**
|
|
* Runs a value through a transform chain.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export function pipeline<A>(a: A): A
|
|
export function pipeline<A, B>(a: A, ab: (a: A) => B): B
|
|
export function pipeline<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C
|
|
export function pipeline<A, B, C, D>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D
|
|
export function pipeline<A, B, C, D, E>(
|
|
a: A,
|
|
ab: (a: A) => B,
|
|
bc: (b: B) => C,
|
|
cd: (c: C) => D,
|
|
de: (d: D) => E
|
|
): E
|
|
export function pipeline<A, B, C, D, E, F>(
|
|
a: A,
|
|
ab: (a: A) => B,
|
|
bc: (b: B) => C,
|
|
cd: (c: C) => D,
|
|
de: (d: D) => E,
|
|
ef: (e: E) => F
|
|
): F
|
|
export function pipeline<A, B, C, D, E, F, G>(
|
|
a: A,
|
|
ab: (a: A) => B,
|
|
bc: (b: B) => C,
|
|
cd: (c: C) => D,
|
|
de: (d: D) => E,
|
|
ef: (e: E) => F,
|
|
fg: (f: F) => G
|
|
): G
|
|
export function pipeline<A, B, C, D, E, F, G, H>(
|
|
a: A,
|
|
ab: (a: A) => B,
|
|
bc: (b: B) => C,
|
|
cd: (c: C) => D,
|
|
de: (d: D) => E,
|
|
ef: (e: E) => F,
|
|
fg: (f: F) => G,
|
|
gh: (g: G) => H
|
|
): H
|
|
export function pipeline<A, B, C, D, E, F, G, H, I>(
|
|
a: A,
|
|
ab: (a: A) => B,
|
|
bc: (b: B) => C,
|
|
cd: (c: C) => D,
|
|
de: (d: D) => E,
|
|
ef: (e: E) => F,
|
|
fg: (f: F) => G,
|
|
gh: (g: G) => H,
|
|
hi: (h: H) => I
|
|
): I
|
|
export function pipeline(...args: Array<any>) {
|
|
return args.length === 1 ? args[0] : args.slice(1).reduce((value, f) => f(value), args[0])
|
|
}
|
|
|
|
interface Box<in out A> {}
|
|
|
|
declare namespace Box {
|
|
export type Inner<A extends Box<any>> = A extends Box<infer T> ? T : never
|
|
}
|
|
|
|
interface Check<A> {}
|
|
|
|
interface Ctor<A extends Box<any>> {
|
|
(value: Box.Inner<A>): A
|
|
}
|
|
|
|
/**
|
|
* Builds a boxed constructor.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export function boxed<A extends Box<any>>(
|
|
...value: readonly [Check<Box.Inner<A>>, ...Array<Check<Box.Inner<A>>>]
|
|
): Ctor<A> {
|
|
return null as any
|
|
}
|
|
|
|
/**
|
|
* Handles distinct overload modes.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export function modes(body: () => string): string
|
|
export function modes(body: (value: number) => string): string
|
|
export function modes(body: (this: string) => string): string
|
|
export function modes(body: () => string, map: (value: string) => number): number
|
|
export function modes(body: () => string, map: (value: string) => number, next: (value: number) => boolean): boolean
|
|
export function modes(options: { readonly self: string }, body: (this: string) => string): string
|
|
export function modes(name: string): (body: () => string) => string
|
|
export function modes(self: string, body: () => string): string
|
|
export function modes(predicate: (value: number) => boolean, body: () => number): number
|
|
export function modes(refinement: (value: string | number) => value is string, body: () => string): string
|
|
export function modes(...args: Array<any>) {
|
|
return args[0]
|
|
}
|
|
|
|
/**
|
|
* Service for sample values.
|
|
*
|
|
* @category services
|
|
* @since 1.0.0
|
|
*/
|
|
export class Service {
|
|
readonly value: string
|
|
/**
|
|
* Runs the service.
|
|
*/
|
|
run(input: number): string {
|
|
return \`\${this.value}:\${input}\`
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default count.
|
|
*
|
|
* @category constants
|
|
* @since 1.0.0
|
|
*/
|
|
export const count = 1
|
|
|
|
/**
|
|
* Sample value type.
|
|
*
|
|
* @category models
|
|
* @since 1.0.0
|
|
*/
|
|
export interface Sample {
|
|
readonly value: string
|
|
}
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
const signatureByName = new Map(model.apis.map((api) => [api.apiName, api.signature]))
|
|
assert.deepStrictEqual(model.files.flatMap((file) => file.diagnostics), [])
|
|
for (const api of model.apis) {
|
|
if (api.signature !== null) {
|
|
const diagnostics = signatureParseDiagnostics(api.signature)
|
|
if (diagnostics.length > 0) {
|
|
assert.fail(`${api.apiName}: ${diagnostics.join(", ")}`)
|
|
}
|
|
}
|
|
}
|
|
assert.strictEqual(
|
|
signatureByName.get("parse"),
|
|
"declare function parse(value: string): string\ndeclare function parse(value: number): number"
|
|
)
|
|
assert.strictEqual(signatureByName.get("inferred"), "declare function inferred(value: number): string")
|
|
assert.strictEqual(signatureByName.get("renamed"), "declare function renamed(value: boolean): boolean")
|
|
assert.strictEqual(
|
|
signatureByName.get("try"),
|
|
`declare const _try: {
|
|
(value: string): number;
|
|
}
|
|
export { _try as try }`
|
|
)
|
|
assert.strictEqual(signatureByName.get("external"), "declare function external(value: string): External")
|
|
const manyLines = signatureByName.get("many")?.split("\n")
|
|
assert.strictEqual(manyLines?.length, 1)
|
|
assert.strictEqual(manyLines?.[0], "declare function many(value: 1): 1")
|
|
const pipelineLines = signatureByName.get("pipeline")?.split("\n") ?? []
|
|
assert.strictEqual(pipelineLines.length, 6)
|
|
assert.deepStrictEqual(pipelineLines.map((line) => line.match(/=>/g)?.length ?? 0), [0, 1, 2, 3, 4, 8])
|
|
const boxedSignature = signatureByName.get("boxed")
|
|
assert.strictEqual(
|
|
boxedSignature,
|
|
"declare const boxed: <A extends Box<any>>(value_0: Check<Box.Inner<A>>, ...value: Check<Box.Inner<A>>[]) => Ctor<A>"
|
|
)
|
|
const modeLines = signatureByName.get("modes")?.split("\n")
|
|
assert.deepStrictEqual(modeLines, [
|
|
"declare function modes(body: () => string): string",
|
|
"declare function modes(body: (this: string) => string): string",
|
|
"declare function modes(body: () => string, map: (value: string) => number): number",
|
|
"declare function modes(options: { readonly self: string; }, body: (this: string) => string): string",
|
|
"declare function modes(name: string): (body: () => string) => string",
|
|
"declare function modes(self: string, body: () => string): string",
|
|
"declare function modes(predicate: (value: number) => boolean, body: () => number): number",
|
|
"declare function modes(refinement: (value: string | number) => value is string, body: () => string): string"
|
|
])
|
|
assert.strictEqual(signatureByName.get("Service"), null)
|
|
assert.strictEqual(signatureByName.get("Service.run"), null)
|
|
assert.strictEqual(signatureByName.get("count"), null)
|
|
assert.strictEqual(signatureByName.get("Sample"), null)
|
|
})
|
|
|
|
it("stores valid top-of-file module JSDoc", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* The Foo module provides helpers for sample values. Use {@link makeValue} to
|
|
* create the default value.
|
|
*
|
|
* **Example** (Using the module)
|
|
*
|
|
* \`\`\`ts
|
|
* import { Foo } from "@effect/sample"
|
|
*
|
|
* Foo.makeValue()
|
|
* \`\`\`
|
|
*
|
|
* @see {@link makeValue}
|
|
* @since 1.0.0
|
|
*/
|
|
import type { Buffer } from "node:buffer"
|
|
|
|
/**
|
|
* Creates a value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const makeValue = () => 1
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
assert.strictEqual(model.files[0]?.moduleJSDoc?.raw.includes("The Foo module provides helpers"), true)
|
|
assert.deepStrictEqual(model.files[0]?.diagnostics, [])
|
|
})
|
|
|
|
it("does not treat the first exported declaration JSDoc as module JSDoc", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* Creates a value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const makeValue = () => 1
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
assert.strictEqual(model.files[0]?.moduleJSDoc, undefined)
|
|
assert.deepStrictEqual(model.files[0]?.diagnostics, [])
|
|
})
|
|
|
|
it("flags inline links that TypeScript does not bind", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(path.join(cwd, "src/Schema.ts"), `export {}\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* Creates a value with the {@link Schema} module.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const makeValue = () => 1
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
assert.deepStrictEqual(
|
|
model.files[0]?.diagnostics.map((diagnostic) => diagnostic.code),
|
|
["unresolved-link"]
|
|
)
|
|
assert.deepStrictEqual(
|
|
model.files[0]?.diagnostics.map((diagnostic) => diagnostic.message),
|
|
["Unresolved JSDoc inline link: {@link Schema}"]
|
|
)
|
|
})
|
|
|
|
it("flags module JSDoc tag, example, and public @see diagnostics", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* The Foo module provides helpers.
|
|
*
|
|
* \`\`\`ts
|
|
* const value = 1
|
|
* \`\`\`
|
|
*
|
|
* @see {@link Hidden}
|
|
*/
|
|
import type { Buffer } from "node:buffer"
|
|
|
|
class Hidden {}
|
|
|
|
/**
|
|
* Creates a value.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const makeValue = () => 1
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
assert.deepStrictEqual(
|
|
model.files[0]?.diagnostics.map((diagnostic) => diagnostic.code).sort(),
|
|
["loose-ts-fence", "missing-tag", "public-see-target"].sort()
|
|
)
|
|
assert.strictEqual(model.files[0]?.moduleJSDoc?.raw.includes("The Foo module provides helpers"), true)
|
|
})
|
|
|
|
it("flags @see links to targets without public JSDoc", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* A documented container.
|
|
*
|
|
* @category models
|
|
* @since 1.0.0
|
|
*/
|
|
export interface Box {
|
|
documented: string
|
|
hidden: string
|
|
}
|
|
|
|
/**
|
|
* Uses the hidden member.
|
|
*
|
|
* @see {@link Box.hidden}
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const useHidden = () => undefined
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
assert.deepStrictEqual(
|
|
model.files[0]?.diagnostics.map((diagnostic) => diagnostic.code),
|
|
["undocumented-see-target"]
|
|
)
|
|
assert.deepStrictEqual(model.files[0]?.imports?.barrel, {
|
|
type: "namespace",
|
|
module: "@effect/sample",
|
|
name: "Foo"
|
|
})
|
|
})
|
|
|
|
it("resolves @see links through TypeScript symbols before local-name lookup", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/index.ts"),
|
|
`export * as Eq from "./Eq.ts"\nexport * as Ordering from "./Ordering.ts"\nexport * as Reducer from "./Reducer.ts"\n`
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Reducer.ts"),
|
|
`/**
|
|
* A reusable reducer.
|
|
*
|
|
* @category models
|
|
* @since 1.0.0
|
|
*/
|
|
export interface Reducer {
|
|
/**
|
|
* Combines two values.
|
|
*/
|
|
combine: string
|
|
}
|
|
`
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Ordering.ts"),
|
|
`/**
|
|
* Ordering reducer value.
|
|
*
|
|
* @category constants
|
|
* @since 1.0.0
|
|
*/
|
|
export const Reducer = "ordering"
|
|
`
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Eq.ts"),
|
|
`import * as Reducer from "./Reducer.ts"
|
|
|
|
/**
|
|
* Creates an equivalence reducer.
|
|
*
|
|
* @see {@link Reducer}
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const makeReducer = () => Reducer
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
const makeReducer = model.apis.find((api) => api.apiFqn === "@effect/sample/Eq.makeReducer")
|
|
const link = makeReducer?.see[0]?.links[0]
|
|
assert.deepStrictEqual(model.files.flatMap((file) => file.diagnostics), [])
|
|
assert.deepStrictEqual(link?.resolution, {
|
|
_tag: "Resolved",
|
|
apiId: "root-declaration:type:@effect/sample/Reducer.Reducer",
|
|
apiFqn: "@effect/sample/Reducer.Reducer"
|
|
})
|
|
})
|
|
|
|
it("flags @see links to targets outside the public JSDoc API model", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* Private helper.
|
|
*
|
|
* @category models
|
|
* @since 1.0.0
|
|
*/
|
|
class Hidden {}
|
|
|
|
/**
|
|
* Uses a hidden helper.
|
|
*
|
|
* @see {@link Hidden}
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const useHidden = () => Hidden
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
assert.deepStrictEqual(
|
|
model.files[0]?.diagnostics.map((diagnostic) => diagnostic.code),
|
|
["public-see-target"]
|
|
)
|
|
assert.deepStrictEqual(
|
|
model.files[0]?.diagnostics.map((diagnostic) => diagnostic.message),
|
|
[
|
|
"@see link {@link Hidden} does not resolve to a public JSDoc API. Check that the target is exported and has valid public JSDoc."
|
|
]
|
|
)
|
|
})
|
|
|
|
it("keeps valid APIs from files with diagnostics for @see resolution", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/index.ts"),
|
|
`export * as Bar from "./Bar.ts"\nexport * as Foo from "./Foo.ts"\n`
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* Target value.
|
|
*
|
|
* @category constants
|
|
* @since 1.0.0
|
|
*/
|
|
export const Target = "target"
|
|
|
|
/**
|
|
* Broken value.
|
|
*
|
|
* **Example**
|
|
*
|
|
* \`\`\`ts
|
|
* const value = "broken"
|
|
* \`\`\`
|
|
*
|
|
* @category constants
|
|
* @since 1.0.0
|
|
*/
|
|
export const Broken = "broken"
|
|
`
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Bar.ts"),
|
|
`import * as Foo from "./Foo.ts"
|
|
|
|
/**
|
|
* Uses the target value.
|
|
*
|
|
* @see {@link Foo.Target} for the target value
|
|
* @category constants
|
|
* @since 1.0.0
|
|
*/
|
|
export const useTarget = () => Foo.Target
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
const foo = model.files.find((file) => file.file === "src/Foo.ts")
|
|
const bar = model.files.find((file) => file.file === "src/Bar.ts")
|
|
assert.deepStrictEqual(
|
|
foo?.diagnostics.map((diagnostic) => diagnostic.code),
|
|
["malformed-example"]
|
|
)
|
|
assert.deepStrictEqual(bar?.diagnostics, [])
|
|
assert.strictEqual(
|
|
model.apis.some((api) => api.apiFqn === "@effect/sample/Foo.Target"),
|
|
true
|
|
)
|
|
})
|
|
|
|
it("resolves @see links to aliased export specifiers", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-"))
|
|
fs.mkdirSync(path.join(cwd, "src"), { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(cwd, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" },
|
|
include: ["src/**/*.ts"]
|
|
})
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "package.json"),
|
|
JSON.stringify({
|
|
name: "@effect/sample",
|
|
type: "module",
|
|
exports: { ".": "./src/index.ts", "./*": "./src/*.ts" }
|
|
})
|
|
)
|
|
fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`)
|
|
fs.writeFileSync(
|
|
path.join(cwd, "src/Foo.ts"),
|
|
`/**
|
|
* @since 1.0.0
|
|
*/
|
|
const Array_ = "array"
|
|
|
|
export {
|
|
/**
|
|
* Public array helper.
|
|
*
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
Array_ as Array
|
|
}
|
|
|
|
/**
|
|
* Public tuple helper.
|
|
*
|
|
* @see {@link Array_}
|
|
* @see {@link Array}
|
|
* @category constructors
|
|
* @since 1.0.0
|
|
*/
|
|
export const Tuple = Array
|
|
`
|
|
)
|
|
const model = extractJSDocsSync({
|
|
cwd,
|
|
tsconfig: "tsconfig.json",
|
|
include: ["src/**/*.ts"],
|
|
output: ".data/jsdocs.json"
|
|
})
|
|
const tuple = model.apis.find((api) => api.apiFqn === "@effect/sample/Foo.Tuple")
|
|
const links = tuple?.see.flatMap((tag) => tag.links) ?? []
|
|
assert.deepStrictEqual(model.files.flatMap((file) => file.diagnostics), [])
|
|
assert.deepStrictEqual(links.map((link) => link.resolution), [{
|
|
_tag: "Resolved",
|
|
apiId: "root-declaration:value:@effect/sample/Foo.Array",
|
|
apiFqn: "@effect/sample/Foo.Array"
|
|
}, {
|
|
_tag: "Resolved",
|
|
apiId: "root-declaration:value:@effect/sample/Foo.Array",
|
|
apiFqn: "@effect/sample/Foo.Array"
|
|
}])
|
|
})
|
|
})
|