diff --git a/eslint.config.mjs b/eslint.config.mjs index 9544f7862..1a7426546 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -182,6 +182,20 @@ const nodeModules = [ ]; export default tslint.config( + { + files: ["src/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + // Disallow importing the FS module everywhere to force all IO to go through + // the FileSystem class. + paths: ["fs", "node:fs", "fs/promises", "node:fs/promises"], + patterns: ["*/utils-common/*"], + }, + ], + }, + }, { files: ["src/**/*.ts"], rules: { diff --git a/src/lib/application.ts b/src/lib/application.ts index 6a432b5aa..97573e968 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -8,12 +8,13 @@ import { type ProjectReflection, ReflectionSymbolId } from "./models/index.js"; import { AbstractComponent, FancyConsoleLogger, + type FileSystem, loadPlugins, + NodeFileSystem, type OptionsReader, PackageJsonReader, TSConfigReader, TypeDocReader, - writeFile, } from "./utils/index.js"; import { Option, Options } from "./utils/index.js"; @@ -25,7 +26,6 @@ import { EntryPointStrategy, getEntryPoints, getPackageDirectories, - getWatchEntryPoints, inferEntryPoints, } from "./utils/entry-point.js"; import { nicePath, normalizePath } from "./utils/paths.js"; @@ -34,7 +34,7 @@ import { validateExports } from "./validation/exports.js"; import { validateDocumentation } from "./validation/documentation.js"; import { validateLinks } from "./validation/links.js"; import { ApplicationEvents } from "./application-events.js"; -import { deriveRootDir, findTsConfigFile, glob, readFile } from "#node-utils"; +import { deriveRootDir, findTsConfigFile, glob } from "#node-utils"; import { FileRegistry } from "./models/FileRegistry.js"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; @@ -151,6 +151,12 @@ export class Application extends AbstractComponent< files: FileRegistry = new ValidatingFileRegistry(); + /** + * Object which will be used by TypeDoc to interact with the user's + * filesystem. + */ + fs: FileSystem = new NodeFileSystem(); + /** @internal */ @Option("lang") accessor lang!: string; @@ -351,7 +357,8 @@ export class Application extends AbstractComponent< if (this.options.isSet("entryPoints")) { return this.getDefinedEntryPoints(); } - return inferEntryPoints(this.logger, this.options); + const host = ts.createCompilerHost({}); + return inferEntryPoints(this.logger, this.options, host); } /** @@ -359,7 +366,8 @@ export class Application extends AbstractComponent< * May return undefined if entry points fail to be expanded. */ public getDefinedEntryPoints(): DocumentationEntryPoint[] | undefined { - return getEntryPoints(this.logger, this.options); + const host = ts.createCompilerHost({}); + return getEntryPoints(this.logger, this.options, host); } /** @@ -431,17 +439,10 @@ export class Application extends AbstractComponent< return project; } - private watchers = new Map(); private _watchFile?: (path: string, shouldRestart?: boolean) => void; - private criticalFiles = new Set(); - - private clearWatches() { - this.watchers.forEach((w) => w.close()); - this.watchers.clear(); - } private watchConfigFile(path: string) { - this.criticalFiles.add(path); + this._watchFile?.(path, true); } /** diff --git a/src/lib/cli/watchHost.ts b/src/lib/cli/watchHost.ts new file mode 100644 index 000000000..af04f8a12 --- /dev/null +++ b/src/lib/cli/watchHost.ts @@ -0,0 +1,35 @@ +import type ts from "typescript"; +import { EventDispatcher } from "#utils"; + +export class WatchHost extends EventDispatcher<{ + fileChanged: [path: string, shouldRestart: boolean]; +}> { + private watchers = new Map(); + private criticalFiles = new Set(); + + private listener = (path: string) => { + this.trigger("fileChanged", path, this.criticalFiles.has(path)); + }; + + constructor(readonly host: ts.WatchCompilerHost) { + super(); + } + + watchFile(path: string, shouldRestart: boolean) { + if (!this.watchers.has(path)) { + this.watchers.set(path, this.host.watchFile(path, this.listener)); + } + if (shouldRestart) { + this.criticalFiles.add(path); + } + } + + clearWatchers() { + this.watchers.forEach((w) => w.close()); + this.watchers.clear(); + } + + [Symbol.dispose]() { + this.clearWatchers(); + } +} diff --git a/src/lib/cli/watchPackages.ts b/src/lib/cli/watchPackages.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/lib/converter/comments/blockLexer.ts b/src/lib/converter/comments/blockLexer.ts index 7bd0bb5af..974dfe3d5 100644 --- a/src/lib/converter/comments/blockLexer.ts +++ b/src/lib/converter/comments/blockLexer.ts @@ -2,8 +2,10 @@ import ts from "typescript"; import { type Token, TokenSyntaxKind } from "./lexer.js"; import { resolveAliasedSymbol } from "../utils/symbols.js"; import { createSymbolId } from "../factories/symbol-id.js"; +import type { FileSystem } from "#node-utils"; export function* lexBlockComment( + fs: FileSystem, file: string, pos = 0, end = file.length, @@ -14,6 +16,7 @@ export function* lexBlockComment( let textToken: Token | undefined; for ( const token of lexBlockComment2( + fs, file, pos, end, @@ -75,6 +78,7 @@ function getLinkTags( } function* lexBlockComment2( + fs: FileSystem, file: string, pos: number, end: number, @@ -357,6 +361,7 @@ function* lexBlockComment2( ); if (tsTarget) { token.tsLinkTarget = createSymbolId( + fs, resolveAliasedSymbol(tsTarget, checker!), ); token.tsLinkText = link.text.replace(/^\s*\|\s*/, ""); diff --git a/src/lib/converter/context.ts b/src/lib/converter/context.ts index f0a59f187..84ec67dfc 100644 --- a/src/lib/converter/context.ts +++ b/src/lib/converter/context.ts @@ -19,7 +19,7 @@ import { ConverterEvents } from "./converter-events.js"; import { resolveAliasedSymbol } from "./utils/symbols.js"; import { getComment, getFileComment, getJsDocComment, getNodeComment, getSignatureComment } from "./comments/index.js"; import { getHumanName, getQualifiedName } from "../utils/tsutils.js"; -import { findPackageForPath, normalizePath } from "#node-utils"; +import { type FileSystem, findPackageForPath, normalizePath } from "#node-utils"; import { createSymbolId } from "./factories/symbol-id.js"; import { type NormalizedPath, removeIf } from "#utils"; @@ -32,6 +32,13 @@ export class Context { */ readonly converter: Converter; + /** + * The filesystem object to interact with the filesystem. + */ + get fs(): FileSystem { + return this.converter.application.fs; + } + /** * The TypeChecker instance returned by the TypeScript compiler. */ @@ -254,7 +261,7 @@ export class Context { ): ReferenceType { const ref = ReferenceType.createUnresolvedReference( name ?? symbol.name, - createSymbolId(symbol), + createSymbolId(this.fs, symbol), context.project, getQualifiedName(symbol, name ?? symbol.name), ); @@ -265,7 +272,7 @@ export class Context { const symbolPath = symbol.declarations?.[0]?.getSourceFile().fileName; if (!symbolPath) return ref; - ref.package = findPackageForPath(symbolPath)?.[0]; + ref.package = findPackageForPath(symbolPath, this.fs)?.[0]; return ref; } @@ -289,7 +296,7 @@ export class Context { registerReflection(reflection: Reflection, symbol: ts.Symbol | undefined, filePath?: NormalizedPath) { if (symbol) { this.reflectionIdToSymbolMap.set(reflection.id, symbol); - const id = createSymbolId(symbol); + const id = createSymbolId(this.fs, symbol); // #2466 // If we just registered a member of a class or interface, then we need to check if @@ -326,7 +333,7 @@ export class Context { } getReflectionFromSymbol(symbol: ts.Symbol) { - return this.project.getReflectionFromSymbolId(createSymbolId(symbol)); + return this.project.getReflectionFromSymbolId(createSymbolId(this.fs, symbol)); } getSymbolFromReflection(reflection: Reflection) { diff --git a/src/lib/converter/converter.ts b/src/lib/converter/converter.ts index be7aee822..e7647e7f2 100644 --- a/src/lib/converter/converter.ts +++ b/src/lib/converter/converter.ts @@ -19,7 +19,7 @@ import { } from "../models/index.js"; import { Context } from "./context.js"; import { AbstractComponent } from "../utils/component.js"; -import { getDocumentEntryPoints, Option, readFile } from "../utils/index.js"; +import { getDocumentEntryPoints, Option } from "../utils/index.js"; import { convertType } from "./types.js"; import { ConverterEvents } from "./converter-events.js"; import { convertSymbol } from "./symbols.js"; @@ -340,11 +340,12 @@ export class Converter extends AbstractComponent { const projectDocuments = getDocumentEntryPoints( this.application.logger, this.application.options, + this.application.fs, ); for (const { displayName, path } of projectDocuments) { let file: MinimalSourceFile; try { - file = new MinimalSourceFile(readFile(path), path); + file = new MinimalSourceFile(this.application.fs.readFile(path), path); } catch (error: any) { this.application.logger.error( i18n.failed_to_read_0_when_processing_project_document( @@ -662,7 +663,7 @@ export class Converter extends AbstractComponent { let file: MinimalSourceFile; try { const resolved = normalizePath(resolve(relativeTo, path)); - file = new MinimalSourceFile(readFile(resolved), resolved); + file = new MinimalSourceFile(this.application.fs.readFile(resolved), resolved); } catch { this.application.logger.warn( i18n.failed_to_read_0_when_processing_document_tag_in_1( @@ -745,7 +746,7 @@ export class Converter extends AbstractComponent { const absPath = normalizePath(resolve(dirname(file.fileName), path)); let childFile: MinimalSourceFile; try { - childFile = new MinimalSourceFile(readFile(absPath), absPath); + childFile = new MinimalSourceFile(this.application.fs.readFile(absPath), absPath); } catch (error: any) { this.application.logger.error( i18n.failed_to_read_0_when_processing_document_child_in_1( diff --git a/src/lib/converter/factories/signature.ts b/src/lib/converter/factories/signature.ts index c2a976b6d..4dacf9afd 100644 --- a/src/lib/converter/factories/signature.ts +++ b/src/lib/converter/factories/signature.ts @@ -51,7 +51,7 @@ export function createSignature( if (symbol && declaration) { context.project.registerSymbolId( sigRef, - createSymbolId(symbol, declaration), + createSymbolId(context.fs, symbol, declaration), ); } diff --git a/src/lib/converter/factories/symbol-id.ts b/src/lib/converter/factories/symbol-id.ts index d74964250..104e6be59 100644 --- a/src/lib/converter/factories/symbol-id.ts +++ b/src/lib/converter/factories/symbol-id.ts @@ -1,5 +1,5 @@ import { ReflectionSymbolId } from "#models"; -import { findPackageForPath, getCommonDirectory, getQualifiedName, normalizePath, readFile } from "#node-utils"; +import { type FileSystem, findPackageForPath, getCommonDirectory, getQualifiedName, normalizePath } from "#node-utils"; import { type NormalizedPath, Validation } from "#utils"; import { existsSync } from "fs"; import { join, relative, resolve } from "node:path"; @@ -10,13 +10,13 @@ const declarationMapCache = new Map(); let transientCount = 0; const transientIds = new WeakMap(); -export function createSymbolId(symbol: ts.Symbol, declaration?: ts.Declaration) { +export function createSymbolId(fs: FileSystem, symbol: ts.Symbol, declaration?: ts.Declaration) { declaration ??= symbol.declarations?.[0]; const tsSource = declaration?.getSourceFile().fileName ?? ""; - const sourceFileName = resolveDeclarationMaps(tsSource); + const sourceFileName = resolveDeclarationMaps(fs, tsSource); let packageName: string; let packagePath: NormalizedPath; - const packageInfo = findPackageForPath(tsSource); + const packageInfo = findPackageForPath(tsSource, fs); if (packageInfo) { let packageDir: string; [packageName, packageDir] = packageInfo; @@ -51,7 +51,7 @@ export function createSymbolId(symbol: ts.Symbol, declaration?: ts.Declaration) return id; } -function resolveDeclarationMaps(file: string): string { +function resolveDeclarationMaps(fs: FileSystem, file: string): string { if (!/\.d\.[cm]?ts$/.test(file)) return file; if (declarationMapCache.has(file)) return declarationMapCache.get(file)!; @@ -60,7 +60,7 @@ function resolveDeclarationMaps(file: string): string { let sourceMap: unknown; try { - sourceMap = JSON.parse(readFile(mapFile)) as unknown; + sourceMap = JSON.parse(fs.readFile(mapFile)) as unknown; } catch { return file; } diff --git a/src/lib/converter/jsdoc.ts b/src/lib/converter/jsdoc.ts index 53836f906..c4624520f 100644 --- a/src/lib/converter/jsdoc.ts +++ b/src/lib/converter/jsdoc.ts @@ -134,7 +134,7 @@ function convertJsDocSignature(context: Context, node: ts.JSDocSignature) { ); context.project.registerSymbolId( signature, - createSymbolId(symbol, node), + createSymbolId(context.fs, symbol, node), ); context.registerReflection(signature, void 0); const signatureCtx = rc.withScope(signature); diff --git a/src/lib/converter/plugins/IncludePlugin.ts b/src/lib/converter/plugins/IncludePlugin.ts index 29a4cfea3..3c12edbf3 100644 --- a/src/lib/converter/plugins/IncludePlugin.ts +++ b/src/lib/converter/plugins/IncludePlugin.ts @@ -5,7 +5,6 @@ import { ConverterEvents } from "../converter-events.js"; import type { CommentDisplayPart, Reflection } from "../../models/index.js"; import { MinimalSourceFile } from "#utils"; import type { Converter } from "../converter.js"; -import { isFile, readFile } from "../../utils/fs.js"; import { dedent, escapeRegExp, i18n } from "#utils"; import { normalizePath } from "#node-utils"; @@ -53,6 +52,7 @@ export class IncludePlugin extends ConverterComponent { parts: CommentDisplayPart[], included: string[] = [], ) { + const fs = this.application.fs; for (let i = 0; i < parts.length; ++i) { const part = parts[i]; @@ -76,8 +76,8 @@ export class IncludePlugin extends ConverterComponent { included.join("\n\t"), ), ); - } else if (isFile(file)) { - const text = readFile(file).replaceAll("\r\n", "\n"); + } else if (fs.isFile(file)) { + const text = fs.readFile(file).replaceAll("\r\n", "\n"); const ext = path.extname(file).substring(1); const includedText = regionTarget diff --git a/src/lib/converter/plugins/PackagePlugin.ts b/src/lib/converter/plugins/PackagePlugin.ts index d8f4414d6..a37458c81 100644 --- a/src/lib/converter/plugins/PackagePlugin.ts +++ b/src/lib/converter/plugins/PackagePlugin.ts @@ -14,7 +14,6 @@ import { nicePath, normalizePath, Option, - readFile, } from "#node-utils"; import { existsSync } from "fs"; @@ -85,7 +84,8 @@ export class PackagePlugin extends ConverterComponent { `Begin package.json search at ${nicePath(dirName)}`, ); - const packageJson = discoverPackageJson(dirName); + const fs = this.application.fs; + const packageJson = discoverPackageJson(dirName, fs); this.packageJson = packageJson?.content; // Path will be resolved already. This is kind of ugly, but... @@ -97,7 +97,7 @@ export class PackagePlugin extends ConverterComponent { // Readme path provided, read only that file. this.application.watchFile(this.readme); try { - this.readmeContents = readFile(this.readme); + this.readmeContents = fs.readFile(this.readme); this.readmeFile = normalizePath(this.readme); } catch { this.application.logger.error( @@ -121,7 +121,7 @@ export class PackagePlugin extends ConverterComponent { if (readmePath) { this.readmeFile = normalizePath(readmePath); - this.readmeContents = readFile(readmePath); + this.readmeContents = fs.readFile(readmePath); this.application.watchFile(this.readmeFile); } } diff --git a/src/lib/converter/types.ts b/src/lib/converter/types.ts index 3203f2084..865c8451f 100644 --- a/src/lib/converter/types.ts +++ b/src/lib/converter/types.ts @@ -276,7 +276,7 @@ const constructorConverter: TypeConverter = { } context.project.registerSymbolId( signature, - createSymbolId(symbol, node), + createSymbolId(context.fs, symbol, node), ); context.registerReflection(signature, void 0); const signatureCtx = rc.withScope(signature); @@ -379,7 +379,7 @@ const functionTypeConverter: TypeConverter = { ); context.project.registerSymbolId( signature, - createSymbolId(symbol, node), + createSymbolId(context.fs, symbol, node), ); context.registerReflection(signature, undefined); const signatureCtx = rc.withScope(signature); diff --git a/src/lib/output/plugins/AssetsPlugin.ts b/src/lib/output/plugins/AssetsPlugin.ts index f477677a7..b5baed2e6 100644 --- a/src/lib/output/plugins/AssetsPlugin.ts +++ b/src/lib/output/plugins/AssetsPlugin.ts @@ -1,6 +1,5 @@ import { RendererComponent } from "../components.js"; import { RendererEvent } from "../events.js"; -import { copySync, readFile, writeFileSync } from "../../utils/fs.js"; import { DefaultTheme } from "../themes/default/DefaultTheme.js"; import { getStyles } from "../../utils/highlighter.js"; import { type EnumKeys, getEnumKeys, i18n, type NormalizedPath } from "#utils"; @@ -56,12 +55,13 @@ export class AssetsPlugin extends RendererComponent { private onRenderBegin(event: RendererEvent) { const dest = join(event.outputDirectory, "assets"); + const fs = this.application.fs; if ( !/^https?:\/\//i.test(this.favicon) && [".ico", ".png", ".svg"].includes(extname(this.favicon)) ) { - copySync( + fs.copy( this.favicon, join(dest, "favicon" + extname(this.favicon)), ); @@ -70,7 +70,7 @@ export class AssetsPlugin extends RendererComponent { if (this.customCss) { this.application.watchFile(this.customCss); if (existsSync(this.customCss)) { - copySync(this.customCss, join(dest, "custom.css")); + fs.copy(this.customCss, join(dest, "custom.css")); } else { this.application.logger.error( i18n.custom_css_file_0_does_not_exist( @@ -83,7 +83,7 @@ export class AssetsPlugin extends RendererComponent { if (this.customJs) { this.application.watchFile(this.customJs); if (existsSync(this.customJs)) { - copySync(this.customJs, join(dest, "custom.js")); + fs.copy(this.customJs, join(dest, "custom.js")); } else { this.application.logger.error( i18n.custom_js_file_0_does_not_exist( @@ -100,16 +100,18 @@ export class AssetsPlugin extends RendererComponent { * @param event An event object describing the current render operation. */ private onRenderEnd(event: RendererEvent) { + const fs = this.application.fs; + if (this.owner.theme instanceof DefaultTheme) { const src = join( fileURLToPath(import.meta.url), "../../../../../static", ); const dest = join(event.outputDirectory, "assets"); - copySync(join(src, "style.css"), join(dest, "style.css")); + fs.copy(join(src, "style.css"), join(dest, "style.css")); - const mainJs = readFile(join(src, "main.js")); - writeFileSync( + const mainJs = fs.readFile(join(src, "main.js")); + fs.writeFile( join(dest, "main.js"), [ '"use strict";', @@ -118,12 +120,12 @@ export class AssetsPlugin extends RendererComponent { ].join("\n"), ); - writeFileSync(join(dest, "highlight.css"), getStyles()); + fs.writeFile(join(dest, "highlight.css"), getStyles()); const media = join(event.outputDirectory, "media"); const toCopy = event.project.files.getNameToAbsoluteMap(); for (const [fileName, absolute] of toCopy.entries()) { - copySync(absolute, join(media, fileName)); + fs.copy(absolute, join(media, fileName)); } } } diff --git a/src/lib/output/plugins/HierarchyPlugin.ts b/src/lib/output/plugins/HierarchyPlugin.ts index 18a2be0f1..ca187d82d 100644 --- a/src/lib/output/plugins/HierarchyPlugin.ts +++ b/src/lib/output/plugins/HierarchyPlugin.ts @@ -1,7 +1,6 @@ import * as Path from "path"; import { RendererComponent } from "../components.js"; import { RendererEvent } from "../events.js"; -import { writeFile } from "../../utils/index.js"; import { DefaultTheme } from "../themes/default/DefaultTheme.js"; import type { Renderer } from "../index.js"; @@ -95,7 +94,7 @@ export class HierarchyPlugin extends RendererComponent { "hierarchy.js", ); - await writeFile( + this.application.fs.writeFile( hierarchyJs, `window.hierarchyData = "${await compressJson(hierarchy)}"`, ); diff --git a/src/lib/output/plugins/IconsPlugin.tsx b/src/lib/output/plugins/IconsPlugin.tsx index 5b0e2b752..2d9ff7fb1 100644 --- a/src/lib/output/plugins/IconsPlugin.tsx +++ b/src/lib/output/plugins/IconsPlugin.tsx @@ -1,6 +1,5 @@ import { RendererComponent } from "../components.js"; import { RendererEvent } from "../events.js"; -import { writeFile } from "../../utils/fs.js"; import { DefaultTheme } from "../themes/default/DefaultTheme.js"; import { join } from "path"; import { JSX } from "#utils"; @@ -36,18 +35,15 @@ export class IconsPlugin extends RendererComponent { constructor(owner: Renderer) { super(owner); - this.owner.on(RendererEvent.BEGIN, this.onBeginRender.bind(this)); + this.owner.on(RendererEvent.END, this.onRenderEnd.bind(this)); } - private onBeginRender(_event: RendererEvent) { - if (this.owner.theme instanceof DefaultTheme) { - this.owner.postRenderAsyncJobs.push((event) => this.onRenderEnd(event)); + private onRenderEnd(event: RendererEvent) { + if (!(this.owner.theme instanceof DefaultTheme)) { + return; } - } - - private async onRenderEnd(event: RendererEvent) { const children: JSX.Element[] = []; - const icons = (this.owner.theme as DefaultTheme).icons; + const icons = this.owner.theme.icons; for (const [name, icon] of Object.entries(icons)) { children.push( @@ -58,11 +54,11 @@ export class IconsPlugin extends RendererComponent { } const svg = JSX.renderElement({children}); - const js = ICONS_JS.replace("SVG_HTML", JSX.renderElement(<>{children}).replaceAll("`", "\\`")); - const svgPath = join(event.outputDirectory, "assets/icons.svg"); - const jsPath = join(event.outputDirectory, "assets/icons.js"); + this.application.fs.writeFile(svgPath, svg); - await Promise.all([writeFile(svgPath, svg), writeFile(jsPath, js)]); + const js = ICONS_JS.replace("SVG_HTML", JSX.renderElement(<>{children}).replaceAll("`", "\\`")); + const jsPath = join(event.outputDirectory, "assets/icons.js"); + this.application.fs.writeFile(jsPath, js); } } diff --git a/src/lib/output/plugins/JavascriptIndexPlugin.ts b/src/lib/output/plugins/JavascriptIndexPlugin.ts index 694a592ea..6b134c972 100644 --- a/src/lib/output/plugins/JavascriptIndexPlugin.ts +++ b/src/lib/output/plugins/JavascriptIndexPlugin.ts @@ -4,7 +4,7 @@ import lunr from "lunr"; import { type Comment, type DeclarationReflection, type DocumentReflection, Reflection } from "../../models/index.js"; import { RendererComponent } from "../components.js"; import { IndexEvent, RendererEvent } from "../events.js"; -import { Option, writeFile } from "../../utils/index.js"; +import { Option } from "../../utils/index.js"; import { DefaultTheme } from "../themes/default/DefaultTheme.js"; import type { Renderer } from "../index.js"; import { GroupPlugin } from "../../converter/plugins/GroupPlugin.js"; @@ -148,7 +148,7 @@ export class JavascriptIndexPlugin extends RendererComponent { rows, index, }; - await writeFile( + this.application.fs.writeFile( jsonFileName, `window.searchData = "${await compressJson(data)}";`, ); diff --git a/src/lib/output/plugins/NavigationPlugin.ts b/src/lib/output/plugins/NavigationPlugin.ts index 6412aa458..9c7fe1a82 100644 --- a/src/lib/output/plugins/NavigationPlugin.ts +++ b/src/lib/output/plugins/NavigationPlugin.ts @@ -1,7 +1,6 @@ import * as Path from "path"; import { RendererComponent } from "../components.js"; import { RendererEvent } from "../events.js"; -import { writeFile } from "../../utils/index.js"; import { DefaultTheme } from "../themes/default/DefaultTheme.js"; import type { Renderer } from "../index.js"; import { compressJson } from "../../utils/compress.js"; @@ -30,7 +29,7 @@ export class NavigationPlugin extends RendererComponent { event.project, ); - await writeFile( + this.application.fs.writeFile( navigationJs, `window.navigationData = "${await compressJson(nav)}"`, ); diff --git a/src/lib/output/plugins/SitemapPlugin.ts b/src/lib/output/plugins/SitemapPlugin.ts index b9e9b4912..482e60749 100644 --- a/src/lib/output/plugins/SitemapPlugin.ts +++ b/src/lib/output/plugins/SitemapPlugin.ts @@ -2,7 +2,6 @@ import Path from "path"; import { RendererComponent } from "../components.js"; import { RendererEvent } from "../events.js"; import { DefaultTheme } from "../themes/default/DefaultTheme.js"; -import { writeFile } from "#node-utils"; import { escapeHtml, JSX } from "#utils"; import type { Renderer } from "../index.js"; @@ -17,7 +16,7 @@ export class SitemapPlugin extends RendererComponent { this.owner.on(RendererEvent.BEGIN, this.onRendererBegin.bind(this)); } - private onRendererBegin(_event: RendererEvent) { + private onRendererBegin(event: RendererEvent) { if (!(this.owner.theme instanceof DefaultTheme)) { return; } @@ -36,10 +35,10 @@ export class SitemapPlugin extends RendererComponent { return { tag: JSX.Fragment, props: null, children: [] }; }); - this.owner.preRenderAsyncJobs.push((event) => this.buildSitemap(event)); + this.buildSitemap(event); } - private async buildSitemap(event: RendererEvent) { + private buildSitemap(event: RendererEvent) { // cSpell:words lastmod urlset const sitemapXml = Path.join(event.outputDirectory, "sitemap.xml"); const lastmod = new Date(this.owner.renderStartTime).toISOString(); @@ -71,7 +70,7 @@ export class SitemapPlugin extends RendererComponent { }) + "\n"; - await writeFile(sitemapXml, sitemap); + this.application.fs.writeFile(sitemapXml, sitemap); } } diff --git a/src/lib/output/renderer.ts b/src/lib/output/renderer.ts index 008a9d916..288bc728a 100644 --- a/src/lib/output/renderer.ts +++ b/src/lib/output/renderer.ts @@ -13,7 +13,6 @@ import type { Application } from "../application.js"; import type { Theme } from "./theme.js"; import { IndexEvent, type MarkdownEvent, PageEvent, RendererEvent } from "./events.js"; import type { ProjectReflection } from "../models/ProjectReflection.js"; -import { writeFileSync } from "../utils/fs.js"; import { DefaultTheme } from "./themes/default/DefaultTheme.js"; import { AbstractComponent, Option } from "../utils/index.js"; import type { Comment, Reflection } from "../models/index.js"; @@ -382,7 +381,7 @@ export class Renderer extends AbstractComponent { this.hooks.restoreMomento(momento); try { - writeFileSync(event.filename, event.contents); + this.application.fs.writeFile(event.filename, event.contents); } catch (error) { this.application.logger.error( i18n.could_not_write_0(event.filename), diff --git a/src/lib/utils/ValidatingFileRegistry.ts b/src/lib/utils/ValidatingFileRegistry.ts index f16afad64..bc7cbb85c 100644 --- a/src/lib/utils/ValidatingFileRegistry.ts +++ b/src/lib/utils/ValidatingFileRegistry.ts @@ -1,16 +1,20 @@ import { FileRegistry } from "../models/FileRegistry.js"; -import { isFile } from "./fs.js"; +import type { FileSystem } from "./fs.js"; import type { Deserializer, JSONOutput } from "#serialization"; import { i18n, type NormalizedPath, NormalizedPathUtils } from "#utils"; export class ValidatingFileRegistry extends FileRegistry { + constructor(private fs: FileSystem) { + super(); + } + override register( sourcePath: NormalizedPath, relativePath: NormalizedPath, ): { target: number; anchor: string | undefined } | undefined { const absolute = NormalizedPathUtils.resolve(NormalizedPathUtils.dirname(sourcePath), relativePath); const absoluteWithoutAnchor = absolute.replace(/#.*/, ""); - if (!isFile(absoluteWithoutAnchor)) { + if (!this.fs.isFile(absoluteWithoutAnchor)) { return; } return this.registerAbsolute(absolute); @@ -19,7 +23,7 @@ export class ValidatingFileRegistry extends FileRegistry { override fromObject(de: Deserializer, obj: JSONOutput.FileRegistry) { for (const [key, val] of Object.entries(obj.entries)) { const absolute = NormalizedPathUtils.resolve(de.projectRoot, val); - if (!isFile(absolute)) { + if (!this.fs.isFile(absolute)) { de.logger.warn( i18n.saved_relative_path_0_resolved_from_1_is_not_a_file( val, diff --git a/src/lib/utils/entry-point.ts b/src/lib/utils/entry-point.ts index a9e341505..679f63ae0 100644 --- a/src/lib/utils/entry-point.ts +++ b/src/lib/utils/entry-point.ts @@ -4,7 +4,7 @@ import * as FS from "fs"; import { expandPackages } from "./package-manifest.js"; import { deriveRootDir, getCommonDirectory, MinimatchSet, nicePath, normalizePath } from "./paths.js"; import type { Options } from "./options/index.js"; -import { discoverPackageJson, glob, inferPackageEntryPointPaths, isDir } from "./fs.js"; +import { discoverPackageJson, type FileSystem, glob, inferPackageEntryPointPaths } from "./fs.js"; import { assertNever, type GlobString, i18n, type Logger, type NormalizedPath } from "#utils"; /** @@ -46,28 +46,32 @@ export interface DocumentEntryPoint { path: NormalizedPath; } -export function inferEntryPoints(logger: Logger, options: Options, programs?: ts.Program[]) { +export function inferEntryPoints( + logger: Logger, + options: Options, + hostOrPrograms: ts.CompilerHost | ts.Program[], + fs: FileSystem, +) { const packageJson = discoverPackageJson( options.packageDir ?? process.cwd(), + fs, ); if (!packageJson) { logger.warn(i18n.no_entry_points_provided()); return []; } - const pathEntries = inferPackageEntryPointPaths(packageJson.file); + const pathEntries = inferPackageEntryPointPaths(packageJson.file, fs); const entryPoints: DocumentationEntryPoint[] = []; - programs ||= getEntryPrograms( - pathEntries.map((p) => p[1]), - logger, - options, - ); + if (!Array.isArray(hostOrPrograms)) { + hostOrPrograms = getEntryPrograms(pathEntries.map(p => p[1]), logger, options, hostOrPrograms); + } // See also: addInferredDeclarationMapPaths in ReflectionSymbolId const jsToTsSource = new Map(); - for (const program of programs) { + for (const program of hostOrPrograms) { const opts = program.getCompilerOptions(); const rootDir = opts.rootDir || getCommonDirectory(program.getRootFileNames()); const outDir = opts.outDir || rootDir; @@ -88,7 +92,7 @@ export function inferEntryPoints(logger: Logger, options: Options, programs?: ts const displayName = name.replace(/^\.\/?/, ""); const targetPath = jsToTsSource.get(path) || path; - const program = programs.find((p) => p.getSourceFile(targetPath)); + const program = hostOrPrograms.find((p) => p.getSourceFile(targetPath)); if (program) { entryPoints.push({ displayName, @@ -113,6 +117,8 @@ export function inferEntryPoints(logger: Logger, options: Options, programs?: ts export function getEntryPoints( logger: Logger, options: Options, + host: ts.CompilerHost, + fs: FileSystem, ): DocumentationEntryPoint[] | undefined { if (!options.isSet("entryPoints")) { logger.warn(i18n.no_entry_points_provided()); @@ -134,16 +140,20 @@ export function getEntryPoints( case EntryPointStrategy.Resolve: result = getEntryPointsForPaths( logger, - expandGlobs(entryPoints, exclude, logger), + expandGlobs(entryPoints, exclude, logger, fs), options, + host, + fs, ); break; case EntryPointStrategy.Expand: result = getExpandedEntryPointsForPaths( logger, - expandGlobs(entryPoints, exclude, logger), + expandGlobs(entryPoints, exclude, logger, fs), options, + host, + fs, ); break; @@ -173,13 +183,14 @@ export function getEntryPoints( export function getDocumentEntryPoints( logger: Logger, options: Options, + fs: FileSystem, ): DocumentEntryPoint[] { const docGlobs = options.getValue("projectDocuments"); if (docGlobs.length === 0) { return []; } - const docPaths = expandGlobs(docGlobs, [], logger); + const docPaths = expandGlobs(docGlobs, [], logger, fs); // We might want to expand this in the future, there are quite a lot of extensions // that have at some point or another been used for markdown: https://superuser.com/a/285878 @@ -200,75 +211,18 @@ export function getDocumentEntryPoints( }); } -export function getWatchEntryPoints( - logger: Logger, - options: Options, - program: ts.Program, -): DocumentationEntryPoint[] | undefined { - let result: DocumentationEntryPoint[] | undefined; - - const entryPoints = options.getValue("entryPoints"); - const exclude = options.getValue("exclude"); - const strategy = options.getValue("entryPointStrategy"); - - switch (strategy) { - case EntryPointStrategy.Resolve: - if (options.isSet("entryPoints")) { - result = getEntryPointsForPaths( - logger, - expandGlobs(entryPoints, exclude, logger), - options, - [program], - ); - } else { - result = inferEntryPoints(logger, options, [program]); - } - break; - - case EntryPointStrategy.Expand: - if (options.isSet("entryPoints")) { - result = getExpandedEntryPointsForPaths( - logger, - expandGlobs(entryPoints, exclude, logger), - options, - [program], - ); - } else { - result = inferEntryPoints(logger, options, [program]); - } - break; - - case EntryPointStrategy.Packages: - logger.error(i18n.watch_does_not_support_packages_mode()); - break; - - case EntryPointStrategy.Merge: - logger.error(i18n.watch_does_not_support_merge_mode()); - break; - - default: - assertNever(strategy); - } - - if (result && result.length === 0) { - logger.error(i18n.unable_to_find_any_entry_points()); - return; - } - - return result; -} - export function getPackageDirectories( logger: Logger, options: Options, packageGlobPaths: GlobString[], + fs: FileSystem, ) { const exclude = new MinimatchSet(options.getValue("exclude")); const rootDir = deriveRootDir(packageGlobPaths); // packages arguments are workspace tree roots, or glob patterns // This expands them to leave only leaf packages - return expandPackages(logger, rootDir, packageGlobPaths, exclude); + return expandPackages(logger, rootDir, packageGlobPaths, exclude, fs); } function getModuleName(fileName: string, baseDir: string) { @@ -286,8 +240,12 @@ function getEntryPointsForPaths( logger: Logger, inputFiles: string[], options: Options, - programs = getEntryPrograms(inputFiles, logger, options), + hostOrPrograms: ts.CompilerHost | ts.Program[], + fs: FileSystem, ): DocumentationEntryPoint[] { + if (!Array.isArray(hostOrPrograms)) { + hostOrPrograms = getEntryPrograms(inputFiles, logger, options, hostOrPrograms); + } const baseDir = options.getValue("basePath") || getCommonDirectory(inputFiles); const entryPoints: DocumentationEntryPoint[] = []; let expandSuggestion = true; @@ -307,7 +265,7 @@ function getEntryPointsForPaths( ); } - for (const program of programs) { + for (const program of hostOrPrograms) { for (const check of toCheck) { const sourceFile = program.getSourceFile(check); if (sourceFile) { @@ -324,7 +282,7 @@ function getEntryPointsForPaths( logger.warn( i18n.entry_point_0_not_in_program(nicePath(fileOrDir)), ); - if (expandSuggestion && isDir(fileOrDir)) { + if (expandSuggestion && fs.isDir(fileOrDir)) { expandSuggestion = false; logger.info(i18n.use_expand_or_glob_for_files_in_dir()); } @@ -337,7 +295,8 @@ export function getExpandedEntryPointsForPaths( logger: Logger, inputFiles: string[], options: Options, - programs = getEntryPrograms(inputFiles, logger, options), + hostOrPrograms: ts.CompilerHost | ts.Program[], + fs: FileSystem, ): DocumentationEntryPoint[] { const compilerOptions = options.getCompilerOptions(); const supportedFileRegex = compilerOptions.allowJs || compilerOptions.checkJs @@ -348,16 +307,17 @@ export function getExpandedEntryPointsForPaths( logger, expandInputFiles(logger, inputFiles, options, supportedFileRegex), options, - programs, + hostOrPrograms, + fs, ); } -function expandGlobs(globs: GlobString[], exclude: GlobString[], logger: Logger) { +function expandGlobs(globs: GlobString[], exclude: GlobString[], logger: Logger, fs: FileSystem) { const excludePatterns = new MinimatchSet(exclude); const base = deriveRootDir(globs); const result = globs.flatMap((entry) => { - const result = glob(entry, base, { + const result = glob(entry, base, fs, { includeDirectories: true, followSymlinks: true, }); @@ -399,16 +359,19 @@ function getEntryPrograms( inputFiles: string[], logger: Logger, options: Options, + host: ts.CompilerHost, ) { const noTsConfigFound = options.getFileNames().length === 0 && options.getProjectReferences().length === 0; const rootProgram = noTsConfigFound ? ts.createProgram({ + host, rootNames: inputFiles, options: options.getCompilerOptions(), }) : ts.createProgram({ + host, rootNames: options.getFileNames(), options: options.getCompilerOptions(), projectReferences: options.getProjectReferences(), @@ -427,6 +390,7 @@ function getEntryPrograms( programs.push( ts.createProgram({ + host, options: options.fixCompilerOptions( ref.commandLine.options, ), diff --git a/src/lib/utils/fs.ts b/src/lib/utils/fs.ts index 00b4a7db1..9945079b0 100644 --- a/src/lib/utils/fs.ts +++ b/src/lib/utils/fs.ts @@ -1,120 +1,100 @@ import * as fs from "fs"; -import { promises as fsp } from "fs"; import { Minimatch } from "minimatch"; import { dirname, join, relative, resolve } from "path"; -import { escapeRegExp, type GlobString, type NormalizedPath, Validation } from "#utils"; +import { DefaultMap, escapeRegExp, type GlobString, type NormalizedPath, Validation } from "#utils"; import { normalizePath } from "./paths.js"; import { ok } from "assert"; -export function isFile(file: string) { - try { - return fs.statSync(file).isFile(); - } catch { - return false; - } +// cache of fs.realpathSync results to avoid extra I/O +const REALPATH_CACHE = new DefaultMap(path => fs.realpathSync(path)); + +export interface Stats { + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; } -export function isDir(path: string) { - try { - return fs.statSync(path).isDirectory(); - } catch { - return false; +export interface FileSystem extends NodeFileSystem {} + +export class NodeFileSystem { + isFile(file: string) { + try { + return fs.statSync(file).isFile(); + } catch { + return false; + } } -} -/** - * Load the given file and return its contents. - * - * @param file The path of the file to read. - * @returns The files contents. - */ -export function readFile(file: string): string { - const buffer = fs.readFileSync(file); - switch (buffer[0]) { - case 0xfe: - if (buffer[1] === 0xff) { - let i = 0; - while (i + 1 < buffer.length) { - const temp = buffer[i]; - buffer[i] = buffer[i + 1]; - buffer[i + 1] = temp; - i += 2; - } - return buffer.toString("ucs2", 2); - } - break; - case 0xff: - if (buffer[1] === 0xfe) { - return buffer.toString("ucs2", 2); - } - break; - case 0xef: - if (buffer[1] === 0xbb) { - return buffer.toString("utf8", 3); - } + isDir(path: string) { + try { + return fs.statSync(path).isDirectory(); + } catch { + return false; + } } - return buffer.toString("utf8", 0); -} + readFile(file: string): string { + const buffer = fs.readFileSync(file); + switch (buffer[0]) { + case 0xfe: + if (buffer[1] === 0xff) { + let i = 0; + while (i + 1 < buffer.length) { + const temp = buffer[i]; + buffer[i] = buffer[i + 1]; + buffer[i + 1] = temp; + i += 2; + } + return buffer.toString("ucs2", 2); + } + break; + case 0xff: + if (buffer[1] === 0xfe) { + return buffer.toString("ucs2", 2); + } + break; + case 0xef: + if (buffer[1] === 0xbb) { + return buffer.toString("utf8", 3); + } + } -/** - * Write a file to disc. - * - * If the containing directory does not exist it will be created. - * - * @param fileName The name of the file that should be written. - * @param data The contents of the file. - */ -export function writeFileSync(fileName: string, data: string) { - fs.mkdirSync(dirname(normalizePath(fileName)), { recursive: true }); - fs.writeFileSync(normalizePath(fileName), data); -} + return buffer.toString("utf8", 0); + } -/** - * Write a file to disc. - * - * If the containing directory does not exist it will be created. - * - * @param fileName The name of the file that should be written. - * @param data The contents of the file. - */ -export async function writeFile(fileName: string, data: string) { - await fsp.mkdir(dirname(normalizePath(fileName)), { - recursive: true, - }); - await fsp.writeFile(normalizePath(fileName), data); -} + readDir(path: string): string[] { + return fs.readdirSync(path); + } -/** - * Copy a file or directory recursively. - */ -export async function copy(src: string, dest: string): Promise { - const stat = await fsp.stat(src); + readDirTypes(path: string): Array { + return fs.readdirSync(path, { withFileTypes: true }); + } - if (stat.isDirectory()) { - const contained = await fsp.readdir(src); - await Promise.all( - contained.map((file) => copy(join(src, file), join(dest, file))), - ); - } else if (stat.isFile()) { - await fsp.mkdir(dirname(dest), { recursive: true }); - await fsp.copyFile(src, dest); - } else { - // Do nothing for FIFO, special devices. + writeFile(path: string, data: string) { + fs.mkdirSync(dirname(path), { recursive: true }); + fs.writeFileSync(path, data); } -} -export function copySync(src: string, dest: string): void { - const stat = fs.statSync(src); + copy(src: string, dest: string) { + const stat = fs.statSync(src); + + if (stat.isDirectory()) { + const contained = fs.readdirSync(src); + contained.forEach((file) => this.copy(join(src, file), join(dest, file))); + } else if (stat.isFile()) { + fs.mkdirSync(dirname(dest), { recursive: true }); + fs.copyFileSync(src, dest); + } else { + // Do nothing for FIFO, special devices. + } + } - if (stat.isDirectory()) { - const contained = fs.readdirSync(src); - contained.forEach((file) => copySync(join(src, file), join(dest, file))); - } else if (stat.isFile()) { - fs.mkdirSync(dirname(dest), { recursive: true }); - fs.copyFileSync(src, dest); - } else { - // Do nothing for FIFO, special devices. + realpath(path: string): string { + return REALPATH_CACHE.get(path); + } + + stat(path: string): Stats { + return fs.statSync(path); } } @@ -128,15 +108,13 @@ export interface DiscoverFilesController { followSymlinks?: boolean; } -// cache of fs.realpathSync results to avoid extra I/O -const realpathCache: Map = new Map(); - export function discoverFiles( + fs: FileSystem, rootDir: NormalizedPath, controller: DiscoverFilesController, ): NormalizedPath[] { const result: NormalizedPath[] = []; - const dirs: string[][] = [normalizePath(rootDir).split("/")]; + const dirs: string[][] = [rootDir.split("/")]; // cache of real paths to avoid infinite recursion const symlinkTargetsSeen: Set = new Set(); const { matchDirectories = false, followSymlinks = false } = controller; @@ -160,8 +138,7 @@ export function discoverFiles( const childPath = [...dir!, path].join("/"); let realpath: string; try { - realpath = realpathCache.get(childPath) ?? fs.realpathSync(childPath); - realpathCache.set(childPath, realpath); + realpath = fs.realpath(childPath); } catch { return; } @@ -172,7 +149,7 @@ export function discoverFiles( symlinkTargetsSeen.add(realpath); try { - const stats = fs.statSync(realpath); + const stats = fs.stat(realpath); if (stats.isDirectory()) { handleDirectory(path); } else if (stats.isFile()) { @@ -196,11 +173,7 @@ export function discoverFiles( result.push(dir.join("/") as NormalizedPath); } - for ( - const child of fs.readdirSync(dir.join("/"), { - withFileTypes: true, - }) - ) { + for (const child of fs.readDirTypes(dir.join("/"))) { if (child.isFile()) { handleFile(child.name); } else if (child.isDirectory()) { @@ -222,6 +195,7 @@ export function discoverFiles( export function glob( pattern: GlobString, root: NormalizedPath, + fs: FileSystem, options: { includeDirectories?: boolean; followSymlinks?: boolean } = {}, ): NormalizedPath[] { const mini = new Minimatch(pattern); @@ -249,7 +223,7 @@ export function glob( followSymlinks: options.followSymlinks, }; - return discoverFiles(root, controller); + return discoverFiles(fs, root, controller); } export function hasTsExtension(path: string): boolean { @@ -264,16 +238,15 @@ export function discoverInParentDirExactMatch( name: string, dir: string, read: (content: string) => T | undefined, - usedFile?: (path: string) => void, + fs: FileSystem, ): { file: string; content: T } | undefined { - if (!isDir(dir)) return; + if (!fs.isDir(dir)) return; const reachedTopDirectory = (dirName: string) => dirName === resolve(join(dirName, "..")); while (!reachedTopDirectory(dir)) { - usedFile?.(join(dir, name)); try { - const content = read(readFile(join(dir, name))); + const content = read(fs.readFile(join(dir, name))); if (content != null) { return { file: join(dir, name), content }; } @@ -284,10 +257,7 @@ export function discoverInParentDirExactMatch( } } -export function discoverPackageJson( - dir: string, - usedFile?: (path: string) => void, -) { +export function discoverPackageJson(dir: string, fs: FileSystem) { return discoverInParentDirExactMatch( "package.json", dir, @@ -302,14 +272,17 @@ export function discoverPackageJson( return pkg; } }, - usedFile, + fs, ); } // dir -> package info const packageCache = new Map(); -export function findPackageForPath(sourcePath: string): readonly [packageName: string, packageDir: string] | undefined { +export function findPackageForPath( + sourcePath: string, + fs: FileSystem, +): readonly [packageName: string, packageDir: string] | undefined { // Attempt to decide package name from path if it contains "node_modules" let startIndex = sourcePath.lastIndexOf("node_modules/"); if (startIndex !== -1) { @@ -329,7 +302,7 @@ export function findPackageForPath(sourcePath: string): readonly [packageName: s return cache; } - const packageJson = discoverPackageJson(dir); + const packageJson = discoverPackageJson(dir, fs); if (packageJson) { packageCache.set(dir, [packageJson.content.name, dirname(packageJson.file)]); return [packageJson.content.name, dirname(packageJson.file)]; @@ -338,12 +311,13 @@ export function findPackageForPath(sourcePath: string): readonly [packageName: s export function inferPackageEntryPointPaths( packagePath: string, + fs: FileSystem, ): [importPath: string, resolvedPath: string][] { const packageDir = normalizePath(dirname(packagePath)); - const packageJson = JSON.parse(readFile(packagePath)); + const packageJson = JSON.parse(fs.readFile(packagePath)); const exports: unknown = packageJson.exports; if (typeof exports === "string") { - return resolveExport(packageDir, ".", exports, false); + return resolveExport(packageDir, ".", exports, false, fs); } if (!exports || typeof exports !== "object") { @@ -357,10 +331,10 @@ export function inferPackageEntryPointPaths( const results: [string, string][] = []; if (Array.isArray(exports)) { - results.push(...resolveExport(packageDir, ".", exports, true)); + results.push(...resolveExport(packageDir, ".", exports, true, fs)); } else { for (const [importPath, exp] of Object.entries(exports)) { - results.push(...resolveExport(packageDir, importPath, exp, false)); + results.push(...resolveExport(packageDir, importPath, exp, false, fs)); } } @@ -372,6 +346,7 @@ function resolveExport( name: string, exportDeclaration: string | string[] | Record, validatePath: boolean, + fs: FileSystem, ): [string, string][] { if (typeof exportDeclaration === "string") { return resolveStarredExport( @@ -379,12 +354,13 @@ function resolveExport( name, exportDeclaration, validatePath, + fs, ); } if (Array.isArray(exportDeclaration)) { for (const item of exportDeclaration) { - const result = resolveExport(packageDir, name, item, true); + const result = resolveExport(packageDir, name, item, true, fs); if (result.length) { return result; } @@ -401,6 +377,7 @@ function resolveExport( name, exportDeclaration[cond], false, + fs, ); } } @@ -424,6 +401,7 @@ function resolveStarredExport( name: string, exportDeclaration: string, validatePath: boolean, + fs: FileSystem, ): [string, string][] { // Wildcards only do something if there is exactly one star in the name // If there isn't any star in the destination, all entries map to one file @@ -450,7 +428,7 @@ function resolveStarredExport( }) + "$", ); - const matchedFiles = discoverFiles(packageDir, { + const matchedFiles = discoverFiles(fs, packageDir, { matches(path) { return matcher.test(path); }, @@ -468,7 +446,7 @@ function resolveStarredExport( } const exportPath = resolve(packageDir, exportDeclaration); - if (validatePath && !fs.existsSync(exportPath)) { + if (validatePath && !fs.isFile(exportPath)) { return []; } diff --git a/src/lib/utils/options/options.ts b/src/lib/utils/options/options.ts index 1668d218d..d11095de8 100644 --- a/src/lib/utils/options/options.ts +++ b/src/lib/utils/options/options.ts @@ -1,7 +1,7 @@ import type ts from "typescript"; import { resolve } from "path"; import { ParameterType } from "./declaration.js"; -import type { OutputSpecification } from "../index.js"; +import type { FileSystem, OutputSpecification } from "../index.js"; import { normalizePath } from "../paths.js"; import type { Application } from "../../../index.js"; import { @@ -54,13 +54,13 @@ export interface OptionsReader { * @param container the options container that provides declarations * @param logger logger to be used to report errors * @param cwd the directory which should be treated as the current working directory for option file discovery - * @param usedFile a callback to track files that were read or whose existence was checked, for purposes of restarting a build when watching files + * @param fs file system object that should be used to read files */ read( container: Options, logger: Logger, + fs: FileSystem, cwd: string, - usedFile: (file: string) => void, ): void | Promise; } @@ -200,11 +200,11 @@ export class Options { async read( logger: Logger, + fs: FileSystem, cwd = process.cwd(), - usedFile: (path: string) => void = () => {}, ) { for (const reader of this._readers) { - await reader.read(this, logger, cwd, usedFile); + await reader.read(this, logger, fs, cwd); } } diff --git a/src/lib/utils/options/readers/package-json.ts b/src/lib/utils/options/readers/package-json.ts index 1fb6aba37..0ab9fd6cb 100644 --- a/src/lib/utils/options/readers/package-json.ts +++ b/src/lib/utils/options/readers/package-json.ts @@ -2,7 +2,7 @@ import type { OptionsReader } from "../index.js"; import type { Options } from "../options.js"; import { ok } from "assert"; import { nicePath } from "../../paths.js"; -import { discoverPackageJson } from "../../fs.js"; +import { discoverPackageJson, type FileSystem } from "../../fs.js"; import { dirname } from "path"; import { i18n, type Logger, type TranslatedString } from "#utils"; @@ -18,10 +18,10 @@ export class PackageJsonReader implements OptionsReader { read( container: Options, logger: Logger, + fs: FileSystem, cwd: string, - usedFile: (path: string) => void, ): void { - const result = discoverPackageJson(cwd, usedFile); + const result = discoverPackageJson(cwd, fs); if (!result) { return; diff --git a/src/lib/utils/options/readers/tsconfig.ts b/src/lib/utils/options/readers/tsconfig.ts index 45add3be1..293d9a330 100644 --- a/src/lib/utils/options/readers/tsconfig.ts +++ b/src/lib/utils/options/readers/tsconfig.ts @@ -3,7 +3,7 @@ import { dirname, join, resolve } from "path"; import ts from "typescript"; import type { Options, OptionsReader } from "../options.js"; -import { isFile } from "../../fs.js"; +import type { FileSystem } from "../../fs.js"; import { ok } from "assert"; import { i18n, type Logger, type TranslatedString, unique, Validation } from "#utils"; import { nicePath, normalizePath } from "../../paths.js"; @@ -62,12 +62,12 @@ export class TSConfigReader implements OptionsReader { read( container: Options, logger: Logger, + fs: FileSystem, cwd: string, - usedFile?: (path: string) => void, ): void { const file = container.getValue("tsconfig") || cwd; - let fileToRead = findTsConfigFile(file, usedFile); + let fileToRead = findTsConfigFile(fs, file); if (!fileToRead) { // If the user didn't give us this option, we shouldn't complain about not being able to find it. @@ -81,7 +81,7 @@ export class TSConfigReader implements OptionsReader { fileToRead = normalizePath(resolve(fileToRead)); logger.verbose(`Reading tsconfig at ${nicePath(fileToRead)}`); - this.addTagsFromTsdocJson(container, logger, resolve(fileToRead)); + this.addTagsFromTsdocJson(container, logger, resolve(fileToRead), fs); const parsed = readTsConfig(fileToRead, logger); if (!parsed) { @@ -93,7 +93,7 @@ export class TSConfigReader implements OptionsReader { return; } - const typedocOptions = getTypeDocOptionsFromTsConfig(fileToRead); + const typedocOptions = getTypeDocOptionsFromTsConfig(fs, fileToRead); if (typedocOptions.options) { logger.error(i18n.tsconfig_file_specifies_options_file()); delete typedocOptions.options; @@ -127,10 +127,11 @@ export class TSConfigReader implements OptionsReader { container: Options, logger: Logger, tsconfig: string, + fs: FileSystem, ) { this.seenTsdocPaths.clear(); const tsdoc = join(dirname(tsconfig), "tsdoc.json"); - if (!isFile(tsdoc)) { + if (!fs.isFile(tsdoc)) { return; } diff --git a/src/lib/utils/options/readers/typedoc.ts b/src/lib/utils/options/readers/typedoc.ts index b9e24d286..d6e5dd55f 100644 --- a/src/lib/utils/options/readers/typedoc.ts +++ b/src/lib/utils/options/readers/typedoc.ts @@ -6,7 +6,7 @@ import type { OptionsReader } from "../options.js"; import type { Options } from "../options.js"; import { ok } from "assert"; import { nicePath, normalizePath } from "../../paths.js"; -import { isFile } from "../../fs.js"; +import type { FileSystem } from "../../fs.js"; import { createRequire } from "module"; import { pathToFileURL } from "url"; import { i18n, type Logger, type TranslatedString } from "#utils"; @@ -30,11 +30,11 @@ export class TypeDocReader implements OptionsReader { async read( container: Options, logger: Logger, + fs: FileSystem, cwd: string, - usedFile: (path: string) => void, ): Promise { const path = container.getValue("options") || cwd; - const file = this.findTypedocFile(path, usedFile); + const file = this.findTypedocFile(path, fs); if (!file) { if (container.isSet("options")) { @@ -155,7 +155,7 @@ export class TypeDocReader implements OptionsReader { */ private findTypedocFile( path: string, - usedFile?: (path: string) => void, + fs: FileSystem, ): string | undefined { path = resolve(path); @@ -177,7 +177,7 @@ export class TypeDocReader implements OptionsReader { join(path, ".config/typedoc.js"), join(path, ".config/typedoc.cjs"), join(path, ".config/typedoc.mjs"), - ].find((file) => (usedFile?.(file), isFile(file))); + ].find((file) => fs.isFile(file)); } } diff --git a/src/lib/utils/package-manifest.ts b/src/lib/utils/package-manifest.ts index d920fde29..6f1ed72f5 100644 --- a/src/lib/utils/package-manifest.ts +++ b/src/lib/utils/package-manifest.ts @@ -2,7 +2,7 @@ import { dirname } from "path"; -import { glob, readFile } from "./fs.js"; +import { type FileSystem, glob } from "./fs.js"; import { createGlobString, type MinimatchSet, nicePath, normalizePath } from "./paths.js"; import { type GlobString, i18n, type Logger, type NormalizedPath } from "#utils"; @@ -25,8 +25,9 @@ function hasOwnProperty( export function loadPackageManifest( logger: Logger, packageJsonPath: string, + fs: FileSystem, ): Record | undefined { - const packageJson: unknown = JSON.parse(readFile(packageJsonPath)); + const packageJson: unknown = JSON.parse(fs.readFile(packageJsonPath)); if (typeof packageJson !== "object" || !packageJson) { logger.error( i18n.file_0_not_an_object(nicePath(packageJsonPath)), @@ -75,6 +76,7 @@ export function expandPackages( packageJsonDir: NormalizedPath, workspaces: GlobString[], exclude: MinimatchSet, + fs: FileSystem, ): string[] { // Technically npm and Yarn workspaces don't support recursive nesting, // however we support the passing of paths to either packages or @@ -85,6 +87,7 @@ export function expandPackages( const expandedPackageJsonPaths = glob( createGlobString(packageJsonDir, `${workspace}/package.json`), packageJsonDir, + fs, ); if (expandedPackageJsonPaths.length === 0) { @@ -108,7 +111,7 @@ export function expandPackages( return []; } - const packageJson = loadPackageManifest(logger, packageJsonPath); + const packageJson = loadPackageManifest(logger, packageJsonPath, fs); if (packageJson === undefined) { return []; } @@ -123,6 +126,7 @@ export function expandPackages( normalizePath(dirname(packageJsonPath)), packagePaths.map(p => createGlobString(normalizePath(dirname(packageJsonPath)), p)), exclude, + fs, ); }); }); diff --git a/src/lib/utils/tsconfig.ts b/src/lib/utils/tsconfig.ts index 2f29d7cfa..e3d0f7ae4 100644 --- a/src/lib/utils/tsconfig.ts +++ b/src/lib/utils/tsconfig.ts @@ -1,22 +1,22 @@ import ts from "typescript"; -import { isDir, isFile, readFile } from "./fs.js"; import { createRequire } from "module"; import type { Logger } from "#utils"; import { diagnostic, diagnostics } from "./loggers.js"; +import type { FileSystem } from "./fs.js"; export function findTsConfigFile( + fs: FileSystem, path: string, - usedFile?: (path: string) => void, ): string | undefined { let fileToRead: string | undefined = path; - if (isDir(fileToRead)) { + if (fs.isDir(fileToRead)) { fileToRead = ts.findConfigFile( path, - (file) => (usedFile?.(file), isFile(file)), + (file) => fs.isFile(file), ); } - if (!fileToRead || !isFile(fileToRead)) { + if (!fileToRead || !fs.isFile(fileToRead)) { return; } @@ -26,8 +26,8 @@ export function findTsConfigFile( // We don't need recursive read checks because that would cause a diagnostic // when reading the tsconfig for compiler options, which happens first, and we bail before // doing this in that case. -export function getTypeDocOptionsFromTsConfig(file: string): any { - const readResult = ts.readConfigFile(file, readFile); +export function getTypeDocOptionsFromTsConfig(fs: FileSystem, file: string): any { + const readResult = ts.readConfigFile(file, path => fs.readFile(path)); const result = {}; @@ -50,7 +50,7 @@ export function getTypeDocOptionsFromTsConfig(file: string): any { } Object.assign( result, - getTypeDocOptionsFromTsConfig(resolvedParent), + getTypeDocOptionsFromTsConfig(fs, resolvedParent), ); } } diff --git a/src/test/converter.test.ts b/src/test/converter.test.ts index bad427dc4..a151cc36a 100644 --- a/src/test/converter.test.ts +++ b/src/test/converter.test.ts @@ -18,11 +18,13 @@ import { SourceReference, } from "../index.js"; import type { ModelToObject } from "../lib/serialization/schema.js"; -import { getExpandedEntryPointsForPaths, normalizePath } from "../lib/utils/index.js"; +import { getExpandedEntryPointsForPaths, NodeFileSystem, normalizePath } from "../lib/utils/index.js"; import { getConverterApp, getConverterBase, getConverterProgram } from "./programs.js"; import { FileRegistry } from "../lib/models/FileRegistry.js"; import { ValidatingFileRegistry } from "../lib/utils/ValidatingFileRegistry.js"; +const fs = new NodeFileSystem(); + const comparisonSerializer = new Serializer(); comparisonSerializer.addSerializer({ priority: 0, @@ -201,12 +203,13 @@ describe("Converter", function () { it(`[${file}] converts fixtures`, function () { before(); resetReflectionID(); - app.files = new ValidatingFileRegistry(); + app.files = new ValidatingFileRegistry(fs); const entryPoints = getExpandedEntryPointsForPaths( app.logger, [path], app.options, [getConverterProgram()], + fs, ); ok(entryPoints, "Failed to get entry points"); result = app.converter.convert(entryPoints); diff --git a/src/test/packages.test.ts b/src/test/packages.test.ts index a27c01f9d..24bc16be6 100644 --- a/src/test/packages.test.ts +++ b/src/test/packages.test.ts @@ -1,12 +1,14 @@ import { deepStrictEqual as equal } from "assert"; import { join } from "path"; -import { normalizePath } from "../lib/utils/index.js"; +import { NodeFileSystem, normalizePath } from "../lib/utils/index.js"; import { expandPackages } from "../lib/utils/package-manifest.js"; import { tempdirProject } from "@typestrong/fs-fixture-builder"; import { TestLogger } from "./TestLogger.js"; import { createGlobString, MinimatchSet } from "../lib/utils/paths.js"; +const fs = new NodeFileSystem(); + describe("Packages support", () => { using project = tempdirProject(); @@ -85,6 +87,7 @@ describe("Packages support", () => { normalizePath(project.cwd), [createGlobString(normalizePath(project.cwd), ".")], new MinimatchSet([createGlobString(normalizePath(project.cwd), "**/ign")]), + fs, ); equal( @@ -134,6 +137,7 @@ describe("Packages support", () => { normalizePath(project.cwd), [createGlobString(normalizePath(project.cwd), ".")], new MinimatchSet([]), + fs, ); logger.expectNoOtherMessages(); @@ -175,6 +179,7 @@ describe("Packages support", () => { normalizePath(project.cwd), [createGlobString(normalizePath(project.cwd), ".")], new MinimatchSet([]), + fs, ); logger.expectNoOtherMessages(); diff --git a/src/test/programs.ts b/src/test/programs.ts index 9cc6dadb8..57628c3d2 100644 --- a/src/test/programs.ts +++ b/src/test/programs.ts @@ -15,6 +15,9 @@ import { createAppForTesting } from "../lib/application.js"; import { existsSync } from "fs"; import { clearCommentCache } from "../lib/converter/comments/index.js"; import { diagnostics } from "../lib/utils/loggers.js"; +import { NodeFileSystem } from "#node-utils"; + +const nodeFs = new NodeFileSystem(); let converterApp: Application | undefined; let converterProgram: ts.Program | undefined; @@ -50,6 +53,7 @@ export function getConverterApp() { new TSConfigReader().read( converterApp.options, converterApp.logger, + nodeFs, process.cwd(), ); @@ -127,6 +131,7 @@ export function getConverter2App() { new TSConfigReader().read( converter2App.options, converter2App.logger, + nodeFs, process.cwd(), ); } diff --git a/src/test/renderer/DefaultTheme.test.ts b/src/test/renderer/DefaultTheme.test.ts index cc6f0a013..79367e5ef 100644 --- a/src/test/renderer/DefaultTheme.test.ts +++ b/src/test/renderer/DefaultTheme.test.ts @@ -2,7 +2,7 @@ import { rm } from "fs/promises"; import { getConverter2App, getConverter2Project } from "../programs.js"; import { TestRouter, TestTheme } from "./testRendererUtils.js"; import { deepEqual as equal } from "assert/strict"; -import { glob, readFile } from "#node-utils"; +import { glob, NodeFileSystem } from "#node-utils"; import type { GlobString, NormalizedPath } from "#utils"; import { join, relative } from "path"; import { resetReflectionID } from "#models"; @@ -33,8 +33,9 @@ describe("DefaultTheme", () => { await rm(outPath + "/assets", { recursive: true }); await rm(outPath + "/.nojekyll"); - const expectedFiles = glob("**/*" as GlobString, SPEC_PATH); - const actualFiles = glob("**/*" as GlobString, outPath); + const fs = new NodeFileSystem(); + const expectedFiles = glob("**/*" as GlobString, SPEC_PATH, fs); + const actualFiles = glob("**/*" as GlobString, outPath, fs); const relPaths = expectedFiles.map(p => relative(SPEC_PATH, p)).sort(); equal( @@ -43,8 +44,8 @@ describe("DefaultTheme", () => { ); for (const rel of relPaths) { - const expected = JSON.parse(readFile(join(SPEC_PATH, rel))); - const actual = JSON.parse(readFile(join(outPath, rel))); + const expected = JSON.parse(fs.readFile(join(SPEC_PATH, rel))); + const actual = JSON.parse(fs.readFile(join(outPath, rel))); equal(actual, expected); } diff --git a/src/test/slow/internationalization-usage.test.ts b/src/test/slow/internationalization-usage.test.ts index d1e4efd38..7df768910 100644 --- a/src/test/slow/internationalization-usage.test.ts +++ b/src/test/slow/internationalization-usage.test.ts @@ -4,12 +4,15 @@ import { Logger, Options, TSConfigReader } from "../../index.js"; import { join } from "path"; import { existsSync, readFileSync } from "fs"; import { fileURLToPath } from "url"; +import { NodeFileSystem } from "#node-utils"; + +const fs = new NodeFileSystem(); describe("Internationalization", () => { it("Does not include strings in translatable object which are unused", () => { const options = new Options(); const tsconfigReader = new TSConfigReader(); - tsconfigReader.read(options, new Logger(), process.cwd()); + tsconfigReader.read(options, new Logger(), fs, process.cwd()); const defaultLocaleTs = join( fileURLToPath(import.meta.url), diff --git a/src/test/utils/fs.test.ts b/src/test/utils/fs.test.ts index 5e57313d7..1a602f0c9 100644 --- a/src/test/utils/fs.test.ts +++ b/src/test/utils/fs.test.ts @@ -1,11 +1,13 @@ -import * as fs from "fs"; +import * as FS from "fs"; import { createServer } from "net"; import { type Project, tempdirProject } from "@typestrong/fs-fixture-builder"; import { type AssertionError, deepStrictEqual as equal } from "assert"; import { basename, dirname, join, normalize, resolve } from "path"; -import { createGlobString, glob, inferPackageEntryPointPaths, normalizePath } from "#node-utils"; +import { createGlobString, glob, inferPackageEntryPointPaths, NodeFileSystem, normalizePath } from "#node-utils"; import type { NormalizedPath } from "#utils"; +const fs = new NodeFileSystem(); + describe("fs.ts", () => { describe("glob", () => { let fix: Project; @@ -21,7 +23,7 @@ describe("fs.ts", () => { it("handles root match", () => { fix.write(); - const result = glob(createGlobString("", cwd), cwd, { + const result = glob(createGlobString("", cwd), cwd, fs, { includeDirectories: true, }); equal(result.map(normalize), [fix.cwd].map(normalize)); @@ -35,11 +37,11 @@ describe("fs.ts", () => { fix.write(); equal( - glob(createGlobString(cwd, `*.ts`), cwd).map((f) => basename(f)), + glob(createGlobString(cwd, `*.ts`), cwd, fs).map((f) => basename(f)), ["a.ts", "test.ts", "test2.ts"], ); equal( - glob(createGlobString(cwd, `**/test*.ts`), cwd).map((f) => basename(f)), + glob(createGlobString(cwd, `**/test*.ts`), cwd, fs).map((f) => basename(f)), ["test.ts", "test2.ts"], ); }); @@ -48,9 +50,9 @@ describe("fs.ts", () => { it("should navigate symlinked directories", () => { const target = dirname(fix.dir("a").addFile("test.ts").path); fix.write(); - fs.symlinkSync(target, resolve(fix.cwd, "b"), "junction"); + FS.symlinkSync(target, resolve(fix.cwd, "b"), "junction"); equal( - glob(createGlobString(cwd, `b/*.ts`), cwd, { + glob(createGlobString(cwd, `b/*.ts`), cwd, fs, { followSymlinks: true, }).map((f) => basename(f)), ["test.ts"], @@ -60,13 +62,13 @@ describe("fs.ts", () => { it("should navigate recursive symlinked directories only once", () => { fix.addFile("test.ts"); fix.write(); - fs.symlinkSync( + FS.symlinkSync( fix.cwd, resolve(fix.cwd, "recursive"), "junction", ); equal( - glob(createGlobString(cwd, `**/*.ts`), cwd, { + glob(createGlobString(cwd, `**/*.ts`), cwd, fs, { followSymlinks: true, }).map((f) => basename(f)), ["test.ts", "test.ts"], @@ -77,7 +79,7 @@ describe("fs.ts", () => { const { path } = fix.addFile("test.ts"); fix.write(); try { - fs.symlinkSync( + FS.symlinkSync( path, resolve(dirname(path), "test-2.ts"), "file", @@ -93,7 +95,7 @@ describe("fs.ts", () => { } } equal( - glob(createGlobString(cwd, `**/*.ts`), cwd, { + glob(createGlobString(cwd, `**/*.ts`), cwd, fs, { followSymlinks: true, }).map((f) => basename(f)), ["test-2.ts", "test.ts"], @@ -106,7 +108,7 @@ describe("fs.ts", () => { fix.dir("node_modules").addFile("test.ts"); fix.write(); equal( - glob(createGlobString(cwd, `node_modules/test.ts`), cwd).map((f) => basename(f)), + glob(createGlobString(cwd, `node_modules/test.ts`), cwd, fs).map((f) => basename(f)), ["test.ts"], ); }); @@ -117,7 +119,7 @@ describe("fs.ts", () => { fix.dir("node_modules").addFile("test.ts"); fix.write(); equal( - glob(createGlobString(cwd, `**/test.ts`), cwd).map((f) => basename(f)), + glob(createGlobString(cwd, `**/test.ts`), cwd, fs).map((f) => basename(f)), [], ); }); @@ -137,7 +139,7 @@ describe("fs.ts", () => { .once("listening", () => { let err: AssertionError | null = null; try { - equal(glob(createGlobString(cwd, `*.sock`), cwd), []); + equal(glob(createGlobString(cwd, `*.sock`), cwd, fs), []); } catch (e) { err = e as AssertionError; } finally { @@ -155,7 +157,7 @@ describe("fs.ts", () => { const packagePath = (path: string) => normalizePath(join(fixture.cwd, path)); const inferExports = () => - inferPackageEntryPointPaths(fixture.cwd + "/package.json").map( + inferPackageEntryPointPaths(fixture.cwd + "/package.json", fs).map( (s) => [s[0], normalizePath(s[1])], ); diff --git a/src/test/utils/options/readers/arguments.test.ts b/src/test/utils/options/readers/arguments.test.ts index c29b0930f..76cbaad8a 100644 --- a/src/test/utils/options/readers/arguments.test.ts +++ b/src/test/utils/options/readers/arguments.test.ts @@ -1,6 +1,6 @@ import { deepStrictEqual as equal } from "assert"; -import { createGlobString, normalizePath, Options } from "../../../../lib/utils/index.js"; +import { createGlobString, NodeFileSystem, normalizePath, Options } from "../../../../lib/utils/index.js"; import { ArgumentsReader } from "../../../../lib/utils/options/readers/index.js"; import { type MapDeclarationOption, @@ -10,6 +10,7 @@ import { import { join } from "path"; import { TestLogger } from "../../../TestLogger.js"; +const fs = new NodeFileSystem(); const emptyHelp = () => ""; describe("Options - ArgumentsReader", () => { @@ -54,7 +55,7 @@ describe("Options - ArgumentsReader", () => { const reader = new ArgumentsReader(1, args); options.reset(); options.addReader(reader); - await options.read(logger); + await options.read(logger, fs); cb(); }); } @@ -131,7 +132,7 @@ describe("Options - ArgumentsReader", () => { const reader = new ArgumentsReader(1, ["--badOption"]); options.reset(); options.addReader(reader); - await options.read(logger); + await options.read(logger, fs); logger.expectMessage( `error: Unknown option: --badOption, you may have meant:\n\t${ similarOptions.join( @@ -146,7 +147,7 @@ describe("Options - ArgumentsReader", () => { options.reset(); options.addReader(reader); const logger = new TestLogger(); - await options.read(logger); + await options.read(logger, fs); logger.expectMessage( "warn: --out expected a value, but none was given as an argument", ); diff --git a/src/test/utils/options/readers/package-json.test.ts b/src/test/utils/options/readers/package-json.test.ts index e663d3d24..757a0cc6e 100644 --- a/src/test/utils/options/readers/package-json.test.ts +++ b/src/test/utils/options/readers/package-json.test.ts @@ -1,9 +1,11 @@ import { project } from "@typestrong/fs-fixture-builder"; import { PackageJsonReader } from "../../../../lib/utils/options/readers/index.js"; -import { Options } from "../../../../lib/utils/index.js"; +import { NodeFileSystem, Options } from "../../../../lib/utils/index.js"; import { TestLogger } from "../../../TestLogger.js"; +const fs = new NodeFileSystem(); + describe("Options - PackageJsonReader", () => { let optsContainer: Options; let testLogger: TestLogger; @@ -16,7 +18,7 @@ describe("Options - PackageJsonReader", () => { }); it("Does not error if no package.json file is found", async () => { - await optsContainer.read(testLogger, "/does-not-exist"); + await optsContainer.read(testLogger, fs, "/does-not-exist"); testLogger.expectNoOtherMessages(); }); @@ -31,7 +33,7 @@ describe("Options - PackageJsonReader", () => { proj.write(); after(() => proj.rm()); - await optsContainer.read(testLogger, proj.cwd); + await optsContainer.read(testLogger, fs, proj.cwd); test(testLogger); testLogger.expectNoOtherMessages(); diff --git a/src/test/utils/options/readers/tsconfig.test.ts b/src/test/utils/options/readers/tsconfig.test.ts index 5bd666e81..1a4d6a976 100644 --- a/src/test/utils/options/readers/tsconfig.test.ts +++ b/src/test/utils/options/readers/tsconfig.test.ts @@ -1,12 +1,14 @@ import { basename, join } from "path"; import { deepStrictEqual as equal } from "assert"; -import { normalizePath, Options, TSConfigReader } from "#node-utils"; +import { NodeFileSystem, normalizePath, Options, TSConfigReader } from "#node-utils"; import { TestLogger } from "../../../TestLogger.js"; import { type Project, tempdirProject } from "@typestrong/fs-fixture-builder"; import { tmpdir } from "os"; import { fileURLToPath } from "url"; +const fs = new NodeFileSystem(); + describe("Options - TSConfigReader", () => { const options = new Options(); options.addReader(new TSConfigReader()); @@ -24,7 +26,7 @@ describe("Options - TSConfigReader", () => { options.setValue("tsconfig", normalizePath(project.cwd)); project.addFile("temp.ts", "export {}"); project.write(); - await options.read(logger); + await options.read(logger, fs); if (noErrors) { logger.expectNoOtherMessages(); } @@ -38,7 +40,7 @@ describe("Options - TSConfigReader", () => { "tsconfig", normalizePath(join(tmpdir(), "typedoc/does-not-exist.json")), ); - await options.read(logger); + await options.read(logger, fs); logger.expectMessage("error: *"); }); @@ -94,7 +96,7 @@ describe("Options - TSConfigReader", () => { normalizePath(join(fileURLToPath(import.meta.url), "../data/does_not_exist.json")), ); options.addReader(new TSConfigReader()); - await options.read(logger); + await options.read(logger, fs); equal(logger.hasErrors(), false); }); diff --git a/src/test/utils/options/readers/typedoc.test.ts b/src/test/utils/options/readers/typedoc.test.ts index 24b5a5c9d..e7a1aa27b 100644 --- a/src/test/utils/options/readers/typedoc.test.ts +++ b/src/test/utils/options/readers/typedoc.test.ts @@ -1,10 +1,12 @@ import { deepStrictEqual as equal } from "assert"; import { project as fsProject } from "@typestrong/fs-fixture-builder"; -import { normalizePath, Options, TypeDocReader } from "#node-utils"; +import { NodeFileSystem, normalizePath, Options, TypeDocReader } from "#node-utils"; import { TestLogger } from "../../../TestLogger.js"; import { join } from "path"; +const fs = new NodeFileSystem(); + describe("Options - TypeDocReader", () => { const options = new Options(); options.addReader(new TypeDocReader()); @@ -19,7 +21,7 @@ describe("Options - TypeDocReader", () => { project.write(); options.reset(); options.setValue("options", normalizePath(project.cwd)); - await options.read(logger); + await options.read(logger, fs); logger.expectNoOtherMessages(); equal(options.getValue("name"), "comment"); @@ -40,7 +42,7 @@ describe("Options - TypeDocReader", () => { after(() => project.rm()); options.reset(); options.setValue("options", normalizePath(project.cwd)); - await options.read(logger); + await options.read(logger, fs); logger.expectNoOtherMessages(); equal(options.getValue("name"), "extends"); @@ -55,7 +57,7 @@ describe("Options - TypeDocReader", () => { project.write(); options.reset(); options.setValue("options", normalizePath(project.cwd)); - await options.read(logger); + await options.read(logger, fs); project.rm(); logger.expectNoOtherMessages(); @@ -71,7 +73,7 @@ describe("Options - TypeDocReader", () => { after(() => project.rm()); options.reset(); options.setValue("options", normalizePath(project.cwd)); - await options.read(logger); + await options.read(logger, fs); logger.expectNoOtherMessages(); equal(options.getValue("name"), "js"); @@ -81,7 +83,7 @@ describe("Options - TypeDocReader", () => { options.reset(); options.setValue("options", normalizePath("./non-existent-file.json")); const logger = new TestLogger(); - await options.read(logger); + await options.read(logger, fs); logger.expectMessage( "error: The options file */non-existent-file.json does not exist", ); @@ -108,7 +110,7 @@ describe("Options - TypeDocReader", () => { const logger = new TestLogger(); project.write(); after(() => project.rm()); - await options.read(logger); + await options.read(logger, fs); logger.expectMessage(message); }); } @@ -160,7 +162,7 @@ describe("Options - TypeDocReader", () => { options.addReader(new TypeDocReader()); const logger = new TestLogger(); - await options.read(logger); + await options.read(logger, fs); equal(logger.hasErrors(), false); }); @@ -177,7 +179,7 @@ describe("Options - TypeDocReader", () => { const options = new Options(); options.setValue("options", normalizePath(join(project.cwd, "typedoc.config.mjs"))); options.addReader(new TypeDocReader()); - await options.read(logger); + await options.read(logger, fs); equal(logger.hasErrors(), false); }); @@ -191,7 +193,7 @@ describe("Options - TypeDocReader", () => { const options = new Options(); options.setValue("options", normalizePath(join(project.cwd, "typedoc.config.mjs"))); options.addReader(new TypeDocReader()); - await options.read(logger); + await options.read(logger, fs); logger.expectMessage( "error: Failed to parse */typedoc.config.mjs, ensure it exists and exports an object", @@ -209,7 +211,7 @@ describe("Options - TypeDocReader", () => { const options = new Options(); options.setValue("options", normalizePath(join(project.cwd, "typedoc.config.cjs"))); options.addReader(new TypeDocReader()); - await options.read(logger); + await options.read(logger, fs); logger.expectMessage( "error: Failed to parse */typedoc.config.cjs, ensure it exists and exports an object", diff --git a/src/test/utils/options/tsdoc-defaults.test.ts b/src/test/utils/options/tsdoc-defaults.test.ts index 2be5f2015..4cafea2ed 100644 --- a/src/test/utils/options/tsdoc-defaults.test.ts +++ b/src/test/utils/options/tsdoc-defaults.test.ts @@ -4,7 +4,7 @@ import ts from "typescript"; import * as defaults from "../../../lib/utils/options/tsdoc-defaults.js"; import { fileURLToPath } from "url"; import { TYPEDOC_ROOT } from "../../../lib/utils/general.js"; -import { readFile } from "../../../lib/utils/fs.js"; +import { readFileSync } from "fs"; describe("tsdoc-defaults.ts", () => { const tsdoc = ts.readConfigFile( @@ -90,7 +90,7 @@ describe("tsdoc-defaults.ts", () => { }); function getDocumentedTags() { - const text = readFile(TYPEDOC_ROOT + "/site/tags.md"); + const text = readFileSync(TYPEDOC_ROOT + "/site/tags.md", "utf-8"); const tags: string[] = []; for (const line of text.split("\n")) {