Skip to content

Commit b795ac5

Browse files
committed
TypeDoc now supports a browser bundle
Resolves #2528
1 parent e758771 commit b795ac5

File tree

25 files changed

+222
-33
lines changed

25 files changed

+222
-33
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"Combinatorially",
4646
"deconfliction",
4747
"deserializers",
48+
"dprint",
4849
"frontmatter",
4950
"githubprivate",
5051
"hideconstructor",

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ title: Changelog
2323
- File name references in `intentionallyNotExported` now use a package name/package relative path instead of an absolute path for matching.
2424
- Introduced `packagesRequiringDocumentation` option for `validation.notDocumented`, TypeDoc will expect comments to be present for symbols in the specified packages.
2525
- TypeDoc's `--entryPointStrategy merge` mode now requires JSON from at least version 0.28.0.
26+
- TypeDoc now exports a `typedoc/browser` entrypoint for parsing and using serialized JSON files, #2528.
2627

2728
TODO:
2829

29-
- Generate locale bundles for strings used in models/utils-common/serializer/deserializer
30-
- Finish supporting models/serde as browser import
3130
- Clean up Internationalization class, it probably doesn't make sense anymore.
3231

3332
## Unreleased

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"./tsdoc.json": "./tsdoc.json",
1010
"./package.json": "./package.json",
1111
"./models": "./dist/lib/models/index.js",
12+
"./browser": "./dist/browser-utils.js",
13+
"./browser/*": "./dist/browser-locales/*.js",
1214
"./debug": "./dist/lib/debug/index.js",
1315
"./debug/*": "./dist/lib/debug/*.js"
1416
},
@@ -75,10 +77,11 @@
7577
"example": "cd example && node ../bin/typedoc",
7678
"test:full": "c8 -r lcov -r text-summary mocha --config .config/mocha.full.json",
7779
"rebuild_specs": "node scripts/rebuild_specs.js",
78-
"build": "npm run build:tsc && npm run build:themes",
80+
"build": "npm run build:tsc && npm run build:locales && npm run build:themes",
7981
"build:tsc": "tsc --project .",
8082
"build:themes": "node scripts/build_themes.js",
81-
"build:prod": "npm run build:prod:tsc && npm run build:themes",
83+
"build:locales": "tsx scripts/build_browser_translations.js",
84+
"build:prod": "npm run build:prod:tsc && npm run build:locales && npm run build:themes",
8285
"build:prod:tsc": "tsc --project . --sourceMap false --declarationMap false",
8386
"lint": "eslint . --max-warnings 0 && dprint check",
8487
"prepack": "node scripts/set_strict.js false && npm run build:prod",
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Expects to be run with tsx
2+
// @ts-check
3+
4+
import ts from "typescript";
5+
import { join } from "node:path";
6+
import { fileURLToPath } from "node:url";
7+
import { Logger, Options, TSConfigReader } from "typedoc";
8+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9+
import { ok } from "node:assert";
10+
11+
const browserBundleFolders = [
12+
"/utils-common/",
13+
"/models/",
14+
"/serialization/",
15+
];
16+
17+
const localesDir = join(
18+
fileURLToPath(import.meta.url),
19+
"../../src/lib/internationalization/locales",
20+
);
21+
22+
const distDir = join(
23+
fileURLToPath(import.meta.url),
24+
"../../dist/browser-locales",
25+
);
26+
27+
const options = new Options();
28+
const tsconfigReader = new TSConfigReader();
29+
tsconfigReader.read(options, new Logger(), process.cwd());
30+
31+
/** @type {ts.LanguageServiceHost} */
32+
const host = {
33+
getScriptFileNames: () => options.getFileNames().slice(),
34+
getScriptVersion: () => "unused",
35+
getScriptSnapshot: (fileName) => {
36+
if (!existsSync(fileName)) return undefined;
37+
return ts.ScriptSnapshot.fromString(
38+
readFileSync(fileName, "utf-8"),
39+
);
40+
},
41+
getCurrentDirectory: () => process.cwd(),
42+
getCompilationSettings: () => options.getCompilerOptions(),
43+
getDefaultLibFileName: (opts) => ts.getDefaultLibFilePath(opts),
44+
fileExists: ts.sys.fileExists,
45+
readFile: ts.sys.readFile,
46+
readDirectory: ts.sys.readDirectory,
47+
directoryExists: ts.sys.directoryExists,
48+
getDirectories: ts.sys.getDirectories,
49+
};
50+
51+
const service = ts.createLanguageService(
52+
host,
53+
ts.createDocumentRegistry(),
54+
);
55+
56+
const program = service.getProgram();
57+
ok(program, "Failed to get program for i18n analysis");
58+
59+
const sf = program.getSourceFile(join(localesDir, "en.cts"));
60+
ok(sf, "Failed to get source file");
61+
62+
const moduleSymbol = program.getTypeChecker().getSymbolAtLocation(sf);
63+
const translatable = moduleSymbol?.exports?.get(
64+
"export=" as ts.__String,
65+
);
66+
ok(translatable, "Failed to get translatable symbol");
67+
68+
ok(translatable.valueDeclaration && ts.isExportAssignment(translatable.valueDeclaration));
69+
ok(ts.isAsExpression(translatable.valueDeclaration.expression));
70+
ok(
71+
ts.isObjectLiteralExpression(
72+
translatable.valueDeclaration.expression.expression,
73+
),
74+
);
75+
const translatableObj = translatable.valueDeclaration.expression.expression;
76+
77+
const bundleUsedTranslationKeys: string[] = [];
78+
79+
translatableObj.forEachChild((child) => {
80+
ok(ts.isPropertyAssignment(child));
81+
const refs = service.getReferencesAtPosition(
82+
sf.fileName,
83+
child.getStart(),
84+
);
85+
86+
if (refs?.some(ref => browserBundleFolders.some(f => ref.fileName.includes(f)))) {
87+
bundleUsedTranslationKeys.push(child.name.getText());
88+
}
89+
});
90+
91+
service.dispose();
92+
93+
const enLocale = (await import(join(localesDir, "en.cts"))).default;
94+
95+
rmSync(distDir, { recursive: true, force: true });
96+
mkdirSync(distDir, { recursive: true });
97+
98+
for (const locale of readdirSync(localesDir)) {
99+
console.log(`Processing ${locale}`);
100+
101+
const browserTranslations = {};
102+
const translations = (await import(join(localesDir, locale))).default;
103+
for (const key of bundleUsedTranslationKeys) {
104+
browserTranslations[key] = translations[key] || enLocale[key];
105+
}
106+
107+
writeFileSync(
108+
join(distDir, locale.replace(".cts", ".js")),
109+
`export default ${JSON.stringify(browserTranslations, null, 4)}\n`,
110+
);
111+
112+
writeFileSync(
113+
join(distDir, locale.replace(".cts", ".d.ts")),
114+
`const translations: Record<string, string>;\nexport default translations;\n`,
115+
);
116+
}

site/overview.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,41 @@ if (project) {
7272
await app.generateJson(project, outputDir + "/docs.json");
7373
}
7474
```
75+
76+
## Browser Bundle
77+
78+
TypeDoc exports a limited portion of its API surface for users who want to process
79+
serialized JSON from TypeDoc within a browser via `typedoc/browser`. The browser
80+
entry point includes the following components:
81+
82+
- TypeDoc's models
83+
- `Serializer` and `Deserializer` classes
84+
- A small set of utility functions
85+
86+
```ts
87+
import {
88+
ConsoleLogger,
89+
Deserializer,
90+
FileRegistry,
91+
setTranslations,
92+
} from "typedoc/browser";
93+
94+
// Similar paths are available for ja, ko, zh
95+
import translations from "typedoc/browser/en";
96+
97+
// Before doing anything with TypeDoc, it should be configured with translations
98+
setTranslations(translations);
99+
100+
const projectJson = await fetch("...").then(r => r.json());
101+
102+
const logger = new ConsoleLogger();
103+
const deserializer = new Deserializer(logger);
104+
const project = deserializer.reviveProject("API Docs", projectJson, {
105+
projectRoot: "/",
106+
registry: new FileRegistry(),
107+
});
108+
109+
// Now we can use TypeDoc's models to more easily analyze the json
110+
console.log(project.getChildByName("SomeClass.property"));
111+
console.log(project.getChildByName("SomeClass.property").type.toString());
112+
```

src/browser-utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export * from "#models";
2+
3+
export {
4+
type ComponentPath,
5+
ConsoleLogger,
6+
type DeclarationReference,
7+
type EnumKeys,
8+
Logger,
9+
LogLevel,
10+
type Meaning,
11+
type MinimalNode,
12+
MinimalSourceFile,
13+
type NormalizedPath,
14+
setTranslations,
15+
type SymbolReference,
16+
type TranslatedString,
17+
translateTagName,
18+
} from "#utils";
19+
20+
export {
21+
type Deserializable,
22+
Deserializer,
23+
type DeserializerComponent,
24+
JSONOutput,
25+
SerializeEvent,
26+
Serializer,
27+
type SerializerComponent,
28+
type SerializerEvents,
29+
} from "#serialization";

src/lib/converter/comments/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import assert, { ok } from "assert";
22
import { parseDocument as parseYamlDoc } from "yaml";
33
import type { CommentParserConfig } from "./index.js";
44
import { Comment, type CommentDisplayPart, CommentTag, type InlineTagDisplayPart } from "../../models/index.js";
5-
import type { MinimalSourceFile } from "../../utils-common/minimalSourceFile.js";
5+
import type { MinimalSourceFile } from "#utils";
66
import { nicePath } from "../../utils/paths.js";
77
import { type Token, TokenSyntaxKind } from "./lexer.js";
88
import { extractTagName } from "./tagName.js";

src/lib/converter/comments/textParser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
import type { TranslatedString, TranslationProxy } from "../../internationalization/index.js";
99
import type { CommentDisplayPart, RelativeLinkDisplayPart } from "../../models/index.js";
1010
import type { FileRegistry } from "../../models/FileRegistry.js";
11-
import { HtmlAttributeParser, type NormalizedPath, ParserState } from "#utils";
11+
import { HtmlAttributeParser, ParserState } from "#node-utils";
1212
import { type Token, TokenSyntaxKind } from "./lexer.js";
1313

1414
import MarkdownIt from "markdown-it";
15+
import type { NormalizedPath } from "#utils";
1516
const MdHelpers = new MarkdownIt().helpers;
1617

1718
interface TextParserData {

src/lib/converter/plugins/IncludePlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fs from "fs";
44
import { ConverterComponent } from "../components.js";
55
import { ConverterEvents } from "../converter-events.js";
66
import type { CommentDisplayPart, Reflection } from "../../models/index.js";
7-
import { MinimalSourceFile } from "../../utils-common/minimalSourceFile.js";
7+
import { MinimalSourceFile } from "#utils";
88
import type { Converter } from "../converter.js";
99
import { isFile } from "../../utils/fs.js";
1010
import { dedent, escapeRegExp, i18n } from "#utils";

src/lib/models/ProjectReflection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export class ProjectReflection extends ContainerReflection {
9595
* Registers the given reflection so that it can be quickly looked up by helper methods.
9696
* Should be called for *every* reflection added to the project.
9797
*
98-
* Note: During conversion, {@link Context.registerReflection} should be used instead so
98+
* Note: During conversion, `Context.registerReflection` should be used instead so
9999
* that symbols can be saved for later use.
100100
*/
101101
registerReflection(

0 commit comments

Comments
 (0)