From 6d21dc82bf248dac79f028e650ee38b765cd644b Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Sun, 13 Apr 2025 04:02:48 +0000 Subject: [PATCH 1/5] docs: generate adev-compatible api json --- package.json | 2 +- src/cdk/BUILD.bazel | 18 ++ src/cdk/testing/BUILD.bazel | 12 ++ src/cdk/testing/protractor/BUILD.bazel | 12 ++ .../testing/selenium-webdriver/BUILD.bazel | 12 ++ src/cdk/testing/testbed/BUILD.bazel | 12 ++ tools/adev-api-extraction/BUILD.bazel | 70 +++++++ tools/adev-api-extraction/README.md | 3 + .../extract_api_to_json.bzl | 109 ++++++++++ tools/adev-api-extraction/index.ts | 129 ++++++++++++ .../interpolate-code-examples.ts | 190 ++++++++++++++++++ tools/adev-api-extraction/tsconfig.json | 42 ++++ tools/tsconfig.json | 2 +- 13 files changed, 611 insertions(+), 2 deletions(-) create mode 100644 tools/adev-api-extraction/BUILD.bazel create mode 100644 tools/adev-api-extraction/README.md create mode 100644 tools/adev-api-extraction/extract_api_to_json.bzl create mode 100644 tools/adev-api-extraction/index.ts create mode 100644 tools/adev-api-extraction/interpolate-code-examples.ts create mode 100644 tools/adev-api-extraction/tsconfig.json diff --git a/package.json b/package.json index c0b4badd4a59..027adfa0b908 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "integration-tests": "bazel test --test_tag_filters=-linker-integration-test --build_tests_only -- //integration/...", "test-linker-aot": "bazel test --partial_compilation --test_tag_filters=partial-compilation-integration,-firefox --build_tests_only -- //integration/... //src/...", "test-linker-jit": "bazel test --partial_compilation --test_tag_filters=partial-compilation-integration,-firefox --build_tests_only --//tools:force_partial_jit_compilation=True -- //integration/... //src/...", - "check-tooling-setup": "pnpm tsc --project tools/tsconfig.json --noEmit && pnpm tsc --project scripts/tsconfig.json --noEmit && pnpm tsc --project .ng-dev/tsconfig.json --noEmit", + "check-tooling-setup": "pnpm tsc --project tools/tsconfig.json --noEmit && pnpm tsc --project tools/adev-api-extraction/tsconfig.json --noEmit && pnpm tsc --project scripts/tsconfig.json --noEmit && pnpm tsc --project .ng-dev/tsconfig.json --noEmit", "tsc": "node ./node_modules/typescript/bin/tsc", "ci-push-deploy-docs-app": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only scripts/docs-deploy/deploy-ci-push.mts", "ci-docs-monitor-test": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only scripts/docs-deploy/monitoring/ci-test.mts", diff --git a/src/cdk/BUILD.bazel b/src/cdk/BUILD.bazel index 7f96065e4039..387948d75f9c 100644 --- a/src/cdk/BUILD.bazel +++ b/src/cdk/BUILD.bazel @@ -1,6 +1,7 @@ load("//src/cdk:config.bzl", "CDK_ENTRYPOINTS", "CDK_ENTRYPOINTS_WITH_STYLES", "CDK_SCSS_LIBS", "CDK_TARGETS") load("//tools:defaults.bzl", "ng_package", "sass_library", "ts_project") load("@npm//:defs.bzl", "npm_link_all_packages") +load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory") package(default_visibility = ["//visibility:public"]) @@ -59,7 +60,11 @@ ng_package( ] + prebuiltStyleTargets + CDK_SCSS_LIBS, nested_packages = [ "//src/cdk/schematics:npm_package", + ":adev_assets", ], + replace_prefixes = { + "adev_assets/": "_adev_assets/", + }, tags = ["release-package"], visibility = [ "//:__pkg__", @@ -75,3 +80,16 @@ filegroup( # which contain a slash are not in the top-level and do not have an overview. srcs = ["//src/cdk/%s:overview" % ep for ep in CDK_ENTRYPOINTS if not "/" in ep], ) + +copy_to_directory( + name = "adev_assets", + srcs = [ + "//src/cdk/testing:json_api", + "//src/cdk/testing/protractor:json_api", + "//src/cdk/testing/selenium-webdriver:json_api", + "//src/cdk/testing/testbed:json_api", + ], + replace_prefixes = { + "**/": "", + }, +) diff --git a/src/cdk/testing/BUILD.bazel b/src/cdk/testing/BUILD.bazel index ee78a4837991..6020eac3d830 100644 --- a/src/cdk/testing/BUILD.bazel +++ b/src/cdk/testing/BUILD.bazel @@ -1,6 +1,7 @@ load("//src/e2e-app:test_suite.bzl", "e2e_test_suite") load("//tools:defaults.bzl", "markdown_to_html", "ng_web_test_suite", "ts_project") load("//src/cdk/testing/tests:webdriver-test.bzl", "webdriver_test") +load("//tools/adev-api-extraction:extract_api_to_json.bzl", "extract_api_to_json") package(default_visibility = ["//visibility:public"]) @@ -47,3 +48,14 @@ webdriver_test( "//src/cdk/testing/tests:webdriver_test_sources", ], ) + +extract_api_to_json( + name = "json_api", + srcs = [ + ":source-files", + ], + entry_point = ":index.ts", + module_name = "@angular/cdk/testing", + output_name = "cdk_testing.json", + private_modules = [""], +) diff --git a/src/cdk/testing/protractor/BUILD.bazel b/src/cdk/testing/protractor/BUILD.bazel index 94c1d441ee83..fc5baddd99df 100644 --- a/src/cdk/testing/protractor/BUILD.bazel +++ b/src/cdk/testing/protractor/BUILD.bazel @@ -1,4 +1,5 @@ load("//tools:defaults.bzl", "ts_project") +load("//tools/adev-api-extraction:extract_api_to_json.bzl", "extract_api_to_json") package(default_visibility = ["//visibility:public"]) @@ -20,3 +21,14 @@ filegroup( name = "source-files", srcs = glob(["**/*.ts"]), ) + +extract_api_to_json( + name = "json_api", + srcs = [ + ":source-files", + ], + entry_point = ":index.ts", + module_name = "@angular/cdk/testing/protractor", + output_name = "cdk_testing_protractor.json", + private_modules = [""], +) diff --git a/src/cdk/testing/selenium-webdriver/BUILD.bazel b/src/cdk/testing/selenium-webdriver/BUILD.bazel index 72145766a41b..19cd8a48e63b 100644 --- a/src/cdk/testing/selenium-webdriver/BUILD.bazel +++ b/src/cdk/testing/selenium-webdriver/BUILD.bazel @@ -1,4 +1,5 @@ load("//tools:defaults.bzl", "ts_project") +load("//tools/adev-api-extraction:extract_api_to_json.bzl", "extract_api_to_json") package(default_visibility = ["//visibility:public"]) @@ -19,3 +20,14 @@ filegroup( name = "source-files", srcs = glob(["**/*.ts"]), ) + +extract_api_to_json( + name = "json_api", + srcs = [ + ":source-files", + ], + entry_point = ":index.ts", + module_name = "@angular/cdk//selenium-webdriver", + output_name = "cdk_testing_selenium_webdriver.json", + private_modules = [""], +) diff --git a/src/cdk/testing/testbed/BUILD.bazel b/src/cdk/testing/testbed/BUILD.bazel index eafb34509394..e91f3c7ac696 100644 --- a/src/cdk/testing/testbed/BUILD.bazel +++ b/src/cdk/testing/testbed/BUILD.bazel @@ -1,4 +1,5 @@ load("//tools:defaults.bzl", "ng_web_test_suite", "ts_project") +load("//tools/adev-api-extraction:extract_api_to_json.bzl", "extract_api_to_json") package(default_visibility = ["//visibility:public"]) @@ -35,3 +36,14 @@ ng_web_test_suite( name = "unit_tests", deps = [":unit_test_sources"], ) + +extract_api_to_json( + name = "json_api", + srcs = [ + ":source-files", + ], + entry_point = ":index.ts", + module_name = "@angular/cdk/testing/testbed", + output_name = "cdk_testing_testbed.json", + private_modules = [""], +) diff --git a/tools/adev-api-extraction/BUILD.bazel b/tools/adev-api-extraction/BUILD.bazel new file mode 100644 index 000000000000..7742d90e57d6 --- /dev/null +++ b/tools/adev-api-extraction/BUILD.bazel @@ -0,0 +1,70 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") +load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild") +load("//tools:defaults.bzl", "ts_project") +load("@aspect_rules_ts//ts:defs.bzl", rules_js_tsconfig = "ts_config") + +package(default_visibility = ["//visibility:public"]) + +esbuild( + name = "bin", + bundle = True, + entry_point = ":index.ts", + external = [ + "typescript", + ], + format = "esm", + output = "bin.mjs", + platform = "node", + target = "es2022", + deps = [ + ":extract_api_to_json_lib", + "//:node_modules/@angular/compiler-cli", + ], +) + +ts_project( + name = "extract_api_to_json_lib", + srcs = glob( + ["**/*.ts"], + exclude = [ + "**/*.spec.ts", + ], + ), + resolve_json_module = True, + tsconfig = ":tsconfig", + deps = [ + "//:node_modules/@angular/compiler", + "//:node_modules/@angular/compiler-cli", + "//:node_modules/@bazel/runfiles", + "//:node_modules/@types/node", + "//:node_modules/typescript", + ], +) + +# Action binary for the api_gen bazel rule. +nodejs_binary( + name = "extract_api_to_json", + data = [ + ":bin", + "//:node_modules/typescript", + ], + entry_point = "bin.mjs", + # Note: Using the linker here as we need it for ESM. The linker is not + # super reliably when running concurrently on Windows- but we have existing + # actions using the linker. An alternative would be to: + # - bundle the Angular compiler into a CommonJS bundle + # - use the patched resolution- but also patch the ESM imports (similar to how FW does it). + visibility = ["//visibility:public"], +) + +# Expose the sources in the dev-infra NPM package. +filegroup( + name = "files", + srcs = glob(["**/*"]), +) + +rules_js_tsconfig( + name = "tsconfig", + src = "tsconfig.json", + deps = ["//:node_modules/@types/node"], +) diff --git a/tools/adev-api-extraction/README.md b/tools/adev-api-extraction/README.md new file mode 100644 index 000000000000..bcc5d09e8552 --- /dev/null +++ b/tools/adev-api-extraction/README.md @@ -0,0 +1,3 @@ +Copied from https://github.com/angular/angular/tree/main/adev/shared-docs/pipeline/api-gen/extraction + +TODO: share this script between angular/angular & angular/components diff --git a/tools/adev-api-extraction/extract_api_to_json.bzl b/tools/adev-api-extraction/extract_api_to_json.bzl new file mode 100644 index 000000000000..fd5793fa15d2 --- /dev/null +++ b/tools/adev-api-extraction/extract_api_to_json.bzl @@ -0,0 +1,109 @@ +load("@build_bazel_rules_nodejs//:providers.bzl", "run_node") + +def _extract_api_to_json(ctx): + """Implementation of the extract_api_to_json rule""" + + # Define arguments that will be passed to the underlying nodejs program. + args = ctx.actions.args() + + # Use a param file because we may have a large number of inputs. + args.set_param_file_format("multiline") + args.use_param_file("%s", use_always = True) + + # Pass the module_name for the extracted APIs. This will be something like "@angular/core". + args.add(ctx.attr.module_name) + + # Pass the module_label for the extracted APIs, This is something like core for "@angular/core". + args.add(ctx.attr.module_label) + + # Pass the set of private modules that should not be included in the API reference. + args.add_joined(ctx.attr.private_modules, join_with = ",") + + # Pass the entry_point for from which to extract public symbols. + args.add(ctx.file.entry_point) + + # Pass the set of source files from which API reference data will be extracted. + args.add_joined(ctx.files.srcs, join_with = ",") + + # Pass the name of the output JSON file. + json_output = ctx.outputs.output_name + args.add(json_output.path) + + # Pass the import path map + # TODO: consider module_mappings_aspect to deal with path mappings instead of manually + # specifying them + # https://github.com/bazelbuild/rules_nodejs/blob/5.x/internal/linker/link_node_modules.bzl#L236 + path_map = {} + for target, path in ctx.attr.import_map.items(): + files = target.files.to_list() + if len(files) != 1: + fail("Expected a single file in import_map target %s" % target.label) + path_map[path] = files[0].path + args.add(json.encode(path_map)) + + # Pass the set of (optional) extra entries + args.add_joined(ctx.files.extra_entries, join_with = ",") + + # Define an action that runs the nodejs_binary executable. This is + # the main thing that this rule does. + run_node( + ctx = ctx, + inputs = depset(ctx.files.srcs + ctx.files.extra_entries), + executable = "_extract_api_to_json", + outputs = [json_output], + arguments = [args], + ) + + # The return value describes what the rule is producing. In this case we need to specify + # the "DefaultInfo" with the output JSON files. + return [DefaultInfo(files = depset([json_output]))] + +extract_api_to_json = rule( + # Point to the starlark function that will execute for this rule. + implementation = _extract_api_to_json, + doc = """Rule that extracts Angular API reference information from TypeScript + sources and write it to a JSON file""", + + # The attributes that can be set to this rule. + attrs = { + "srcs": attr.label_list( + doc = """The source files for this rule. This must include one or more + TypeScript files.""", + allow_empty = False, + allow_files = True, + ), + "output_name": attr.output( + doc = """Name of the JSON output file.""", + ), + "entry_point": attr.label( + doc = """Source file entry-point from which to extract public symbols""", + mandatory = True, + allow_single_file = True, + ), + "private_modules": attr.string_list( + doc = """List of private modules that should not be included in the API symbol linking""", + ), + "import_map": attr.label_keyed_string_dict( + doc = """Map of import path to the index.ts file for that import""", + allow_files = True, + ), + "module_name": attr.string( + doc = """JS Module name to be used for the extracted symbols""", + mandatory = True, + ), + "module_label": attr.string( + doc = """Module label to be used for the extracted symbols. To be used as display name, for example in API docs""", + ), + "extra_entries": attr.label_list( + doc = """JSON files that contain extra entries to append to the final collection.""", + allow_files = True, + ), + + # The executable for this rule (private). + "_extract_api_to_json": attr.label( + default = Label("//tools/adev-api-extraction:extract_api_to_json"), + executable = True, + cfg = "exec", + ), + }, +) diff --git a/tools/adev-api-extraction/index.ts b/tools/adev-api-extraction/index.ts new file mode 100644 index 000000000000..22c93c27043f --- /dev/null +++ b/tools/adev-api-extraction/index.ts @@ -0,0 +1,129 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {readFileSync, writeFileSync} from 'fs'; +import path from 'path'; +// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context. +import { + ClassEntry, + CompilerOptions, + createCompilerHost, + DocEntry, + EntryCollection, + InterfaceEntry, + NgtscProgram, +} from '@angular/compiler-cli'; +import ts from 'typescript'; +import {EXAMPLES_PATH, interpolateCodeExamples} from './interpolate-code-examples'; + +function main() { + const [paramFilePath] = process.argv.slice(2); + const rawParamLines = readFileSync(paramFilePath, {encoding: 'utf8'}).split('\n'); + + const [ + moduleName, + moduleLabel, + serializedPrivateModules, + entryPointExecRootRelativePath, + srcs, + outputFilenameExecRootRelativePath, + serializedPathMapWithExecRootRelativePaths, + extraEntriesSrcs, + ] = rawParamLines; + + const privateModules = new Set(serializedPrivateModules.split(',')); + + // The path map is a serialized JSON map of import path to index.ts file. + // For example, {'@angular/core': 'path/to/some/index.ts'} + const pathMap = JSON.parse(serializedPathMapWithExecRootRelativePaths) as Record; + + // The tsconfig expects the path map in the form of path -> array of actual locations. + // We also resolve the exec root relative paths to absolute paths to disambiguate. + const resolvedPathMap: {[key: string]: string[]} = {}; + for (const [importPath, filePath] of Object.entries(pathMap)) { + resolvedPathMap[importPath] = [path.resolve(filePath)]; + + // In addition to the exact import path, + // also add wildcard mappings for subdirectories. + const importPathWithWildcard = path.join(importPath, '*'); + resolvedPathMap[importPathWithWildcard] = [ + path.join(path.resolve(path.dirname(filePath)), '*'), + ]; + } + + const compilerOptions: CompilerOptions = { + paths: resolvedPathMap, + rootDir: '.', + skipLibCheck: true, + target: ts.ScriptTarget.ES2022, + // This is necessary because otherwise types that include `| null` are not included in the documentation. + strictNullChecks: true, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + experimentalDecorators: true, + }; + + // Code examples should not be fed to the compiler. + const filesWithoutExamples = srcs.split(',').filter(src => !src.startsWith(EXAMPLES_PATH)); + const compilerHost = createCompilerHost({options: compilerOptions}); + const program: NgtscProgram = new NgtscProgram( + filesWithoutExamples, + compilerOptions, + compilerHost, + ); + + const extraEntries: DocEntry[] = (extraEntriesSrcs ?? '') + .split(',') + .filter(path => !!path) + .reduce((result: DocEntry[], path) => { + return result.concat(JSON.parse(readFileSync(path, {encoding: 'utf8'})) as DocEntry[]); + }, []); + + const apiDoc = program.getApiDocumentation(entryPointExecRootRelativePath, privateModules); + const extractedEntries = apiDoc.entries; + const combinedEntries = extractedEntries.concat(extraEntries); + + interpolateCodeExamples(combinedEntries); + + const normalized = moduleName.replace('@', '').replace(/[\/]/g, '_'); + + const output = JSON.stringify({ + moduleLabel: moduleLabel || moduleName, + moduleName: moduleName, + normalizedModuleName: normalized, + entries: combinedEntries, + symbols: [ + // Symbols referenced, originating from other packages + ...apiDoc.symbols.entries(), + + // Exported symbols from the current package + ...apiDoc.entries.map(entry => [entry.name, moduleName]), + + // Also doing it for every member of classes/interfaces + ...apiDoc.entries.flatMap(entry => [ + [entry.name, moduleName], + ...getEntriesFromMembers(entry).map(member => [member, moduleName]), + ]), + ], + } as EntryCollection); + + writeFileSync(outputFilenameExecRootRelativePath, output, {encoding: 'utf8'}); +} + +function getEntriesFromMembers(entry: DocEntry): string[] { + if (!hasMembers(entry)) { + return []; + } + + return entry.members.map(member => `${entry.name}.${member.name}`); +} + +function hasMembers(entry: DocEntry): entry is InterfaceEntry | ClassEntry { + return 'members' in entry; +} + +main(); diff --git a/tools/adev-api-extraction/interpolate-code-examples.ts b/tools/adev-api-extraction/interpolate-code-examples.ts new file mode 100644 index 000000000000..81e9359d83fe --- /dev/null +++ b/tools/adev-api-extraction/interpolate-code-examples.ts @@ -0,0 +1,190 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {DocEntry} from '@angular/compiler-cli'; +import {basename, dirname, resolve} from 'path'; +import fs from 'fs'; + +export const EXAMPLES_PATH = 'packages/examples'; + +// It's assumed that all markers start with #. +const REGION_START_MARKER = '#docregion'; +const REGION_END_MARKER = '#enddocregion'; + +// Used only for clean up of leftovers comments +const TS_COMMENT_REGION_REGEX = /[ \t]*\/\/[ \t]*#(docregion|enddocregion)[ \t]*[\w-]*(\n|$)/g; +const HTML_COMMENT_REGION_REGEX = + /[ \t]*(\n|$)/g; + +const examplesCache = new Map>(); // > + +type FileType = 'ts' | 'js' | 'html'; + +type RegionStartToken = {name: string; startIdx: number}; + +const MD_CTYPE_MAP: {[key in FileType]: string} = { + 'ts': 'angular-ts', + 'js': 'javascript', + 'html': 'angular-html', +}; + +/** + * Interpolate code examples in the `DocEntry`-ies JSDocs content and raw comments in place. + * The examples are wrapped in a Markdown code block. + * + * @param entries Target `DocEntry`-ies array that has its examples substituted with the actual TS code. + * @param examplesFiles A set with all examples files sources. + */ +export function interpolateCodeExamples(entries: DocEntry[]): void { + for (const entry of entries) { + entry.rawComment = replaceExample(entry.rawComment); + entry.description = replaceExample(entry.description); + + for (const jsdocTag of entry.jsdocTags) { + jsdocTag.comment = replaceExample(jsdocTag.comment); + } + } +} + +function replaceExample(text: string): string { + // To generate a valid markdown code block, there should not be any leading spaces + // The regex includes the leading spaces to make sure to remove them. + // It shouldn't include line break because it create the code block at the end of the previous line + const examplesTagRegex = /[ \t]*{@example (\S+) region=(['"])([^'"]+)\2\s*}/g; + + return text.replace(examplesTagRegex, (_: string, path: string, __: string, region: string) => { + const example = getExample(path, region); + if (!example) { + throw new Error(`Missing code example ${EXAMPLES_PATH}/${path}#${region}`); + } + + return example; + }); +} + +/** Returns the example wrapped in a Markdown code block or an empty string, if the example doesn't exist. */ +function getExample(path: string, region: string): string { + let fileExamples = examplesCache.get(path); + const src = `${EXAMPLES_PATH}/${path}`; + const fullPath = resolve(dirname(src), basename(src)); + const fileType = path.split('.').pop() as FileType; + + if (!fileExamples) { + const contents = fs.readFileSync(fullPath, {encoding: 'utf8'}); + + fileExamples = extractExamplesFromContents(contents, fileType); + examplesCache.set(path, fileExamples); + } + + const example = fileExamples.get(region); + if (!example) { + return ''; + } + + return `\`\`\`${MD_CTYPE_MAP[fileType]}\n${example}\n\`\`\``; +} + +/** + * Extract `#docregion` examples from file contents represented as a string. + * + * @param contents File contents represented as a string + * @param fileType File type + * @returns A map with all available examples in a given file contents + */ +function extractExamplesFromContents(contents: string, fileType: FileType): Map { + let markerBuffer = ''; + let paramBuffer = ''; + let markerFound = false; + + const regionStack: RegionStartToken[] = []; + const examples = new Map(); + + // Iterate over the contents string and determine the start and end indices. + for (let i = 0; i < contents.length; i++) { + const char = contents[i]; + + // Build the marker string. + if (char === REGION_START_MARKER[0]) { + markerBuffer = char; + } else if (markerBuffer && !markerFound) { + if (!/\s/.test(char)) { + markerBuffer += char; + } else { + markerFound = true; + } + } + + if (markerFound && !/\s/.test(char)) { + // Build a param string. + paramBuffer += char; + } else if ((markerFound && char === '\n') || (paramBuffer && char === ' ')) { + // Resolve found marker. + switch (markerBuffer) { + case REGION_START_MARKER: + // Push the current index to the stack, if a start marker. + regionStack.push({ + name: paramBuffer, + startIdx: i + 1, + }); + break; + case REGION_END_MARKER: + if (regionStack.length) { + // Check whether the end marker has a parameter or not. + // If not, pop from the stack (it corresponds to the last inserted token). + // If yes, pull the corresponding token. + let tokenIdx = paramBuffer ? regionStack.findIndex(t => t.name === paramBuffer) : -1; + let token: RegionStartToken; + + if (tokenIdx > -1) { + token = regionStack.splice(tokenIdx, 1)[0]; + } else { + token = regionStack.pop()!; + } + + // Caclculate the end index (should represent the start of the marker). + const endIdx = + i - REGION_END_MARKER.length - (paramBuffer ? paramBuffer.length + 1 : 0); + + let example = contents.substring(token.startIdx, endIdx); + example = removeLeftoverCommentsFromExample(example, fileType); + + // A code example can be composed by multiple regions; + // hence, we check for an existing one. + const existing = examples.get(token.name); + example = (!!existing ? existing + '\n' : '') + example; + examples.set(token.name, example); + } + break; + } + + markerFound = false; + markerBuffer = ''; + paramBuffer = ''; + } + } + + return examples; +} + +function removeLeftoverCommentsFromExample(example: string, fileType: FileType): string { + example = example.trim(); + + switch (fileType) { + case 'ts': + case 'js': + return example + .replace(/\n[ \t]*?\/\/\s*$/, '') // We can have only a trailing TS comment leftover + .replace(TS_COMMENT_REGION_REGEX, ''); + case 'html': + return example + .replace(/(^\s*-->\n)|(\n[ \t]*?