From f28044d2b982009194ad6cb4037015dbb6c984d3 Mon Sep 17 00:00:00 2001 From: hawkgs Date: Fri, 20 Dec 2024 13:44:55 +0200 Subject: [PATCH 1/2] docs(docs-infra): generate errors and extended-diagnostics route NavigationItem-s Generate the `NavigationItem`-s as part of adev/shared-docs pipeline and use the output JSON files instead of the hardcoded objects in the `sub-navigation-data.ts`. --- adev/shared-docs/index.bzl | 4 +- adev/shared-docs/pipeline/BUILD.bazel | 19 ++ adev/shared-docs/pipeline/_navigation.bzl | 61 +++++ .../pipeline/navigation/BUILD.bazel | 36 +++ adev/shared-docs/pipeline/navigation/index.ts | 26 ++ .../pipeline/navigation/nav-items-gen.ts | 59 ++++ .../pipeline/navigation/strategies.ts | 52 ++++ .../pipeline/navigation/test/BUILD.bazel | 17 ++ .../navigation/test/nav-items-gen.spec.ts | 39 +++ adev/shared-docs/pipeline/navigation/types.ts | 28 ++ .../pipeline/tutorials/BUILD.bazel | 1 - adev/shared-docs/utils/BUILD.bazel | 2 - adev/src/app/sub-navigation-data.ts | 259 +----------------- adev/src/assets/BUILD.bazel | 2 + adev/src/content/reference/errors/BUILD.bazel | 31 ++- .../extended-diagnostics/BUILD.bazel | 31 ++- 16 files changed, 398 insertions(+), 269 deletions(-) create mode 100644 adev/shared-docs/pipeline/_navigation.bzl create mode 100644 adev/shared-docs/pipeline/navigation/BUILD.bazel create mode 100644 adev/shared-docs/pipeline/navigation/index.ts create mode 100644 adev/shared-docs/pipeline/navigation/nav-items-gen.ts create mode 100644 adev/shared-docs/pipeline/navigation/strategies.ts create mode 100644 adev/shared-docs/pipeline/navigation/test/BUILD.bazel create mode 100644 adev/shared-docs/pipeline/navigation/test/nav-items-gen.spec.ts create mode 100644 adev/shared-docs/pipeline/navigation/types.ts diff --git a/adev/shared-docs/index.bzl b/adev/shared-docs/index.bzl index d5f9adecde90..f910c2911a11 100644 --- a/adev/shared-docs/index.bzl +++ b/adev/shared-docs/index.bzl @@ -1,9 +1,11 @@ load("//adev/shared-docs/pipeline:_guides.bzl", _generate_guides = "generate_guides") -load("//adev/shared-docs/pipeline:_stackblitz.bzl", _generate_stackblitz = "generate_stackblitz") +load("//adev/shared-docs/pipeline:_navigation.bzl", _generate_nav_items = "generate_nav_items") load("//adev/shared-docs/pipeline:_playground.bzl", _generate_playground = "generate_playground") +load("//adev/shared-docs/pipeline:_stackblitz.bzl", _generate_stackblitz = "generate_stackblitz") load("//adev/shared-docs/pipeline:_tutorial.bzl", _generate_tutorial = "generate_tutorial") generate_guides = _generate_guides generate_stackblitz = _generate_stackblitz generate_playground = _generate_playground generate_tutorial = _generate_tutorial +generate_nav_items = _generate_nav_items diff --git a/adev/shared-docs/pipeline/BUILD.bazel b/adev/shared-docs/pipeline/BUILD.bazel index 92accc4468f6..2473df567718 100644 --- a/adev/shared-docs/pipeline/BUILD.bazel +++ b/adev/shared-docs/pipeline/BUILD.bazel @@ -97,11 +97,24 @@ esbuild_esm_bundle( ], ) +esbuild_esm_bundle( + name = "navigation-bundle", + entry_point = "//adev/shared-docs/pipeline/navigation:index.ts", + output = "navigation.mjs", + platform = "node", + target = "es2022", + visibility = ["//visibility:public"], + deps = [ + "//adev/shared-docs/pipeline/navigation", + ], +) + exports_files([ "_guides.bzl", "_stackblitz.bzl", "_playground.bzl", "_tutorial.bzl", + "_navigation.bzl", "BUILD.bazel", ]) @@ -160,3 +173,9 @@ nodejs_binary( entry_point = "//adev/shared-docs/pipeline:tutorial.mjs", visibility = ["//visibility:public"], ) + +nodejs_binary( + name = "navigation", + entry_point = "//adev/shared-docs/pipeline:navigation.mjs", + visibility = ["//visibility:public"], +) diff --git a/adev/shared-docs/pipeline/_navigation.bzl b/adev/shared-docs/pipeline/_navigation.bzl new file mode 100644 index 000000000000..d61f0ce2c12a --- /dev/null +++ b/adev/shared-docs/pipeline/_navigation.bzl @@ -0,0 +1,61 @@ +load("@build_bazel_rules_nodejs//:providers.bzl", "run_node") + +def _generate_nav_items(ctx): + """Implementation of the navigation items data generator rule""" + + # Set the arguments for the actions inputs and output location. + 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 set of source files. + args.add_joined(ctx.files.srcs, join_with = ",") + + # Add BUILD file path to the arguments. + args.add(ctx.label.package) + + # Add the nav item generation strategy to thte arguments. + args.add(ctx.attr.strategy) + + # File declaration of the generated JSON file. + json_output = ctx.actions.declare_file("routes.json") + + # Add the path to the output file to the arguments. + args.add(json_output.path) + + run_node( + ctx = ctx, + inputs = depset(ctx.files.srcs), + executable = "_generate_nav_items", + 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 file. + return [DefaultInfo(files = depset([json_output]))] + +generate_nav_items = rule( + # Point to the starlark function that will execute for this rule. + implementation = _generate_nav_items, + doc = """Rule that generates navigation items data.""", + + # The attributes that can be set to this rule. + attrs = { + "srcs": attr.label_list( + doc = """Markdown files that represent the page contents.""", + allow_empty = False, + allow_files = True, + ), + "strategy": attr.string( + doc = """Represents the navigation items generation strategy.""", + ), + "_generate_nav_items": attr.label( + default = Label("//adev/shared-docs/pipeline:navigation"), + executable = True, + cfg = "exec", + ), + }, +) diff --git a/adev/shared-docs/pipeline/navigation/BUILD.bazel b/adev/shared-docs/pipeline/navigation/BUILD.bazel new file mode 100644 index 000000000000..aa01af2889b2 --- /dev/null +++ b/adev/shared-docs/pipeline/navigation/BUILD.bazel @@ -0,0 +1,36 @@ +load("//tools:defaults.bzl", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "lib", + srcs = glob( + [ + "*.ts", + ], + exclude = [ + "index.ts", + ], + ), + deps = [ + "//adev/shared-docs/interfaces", + "@npm//@types/node", + "@npm//@webcontainer/api", + "@npm//fast-glob", + ], +) + +ts_library( + name = "navigation", + srcs = [ + "index.ts", + ], + visibility = [ + "//adev/shared-docs:__subpackages__", + ], + deps = [ + ":lib", + "//adev/shared-docs/interfaces", + "@npm//@types/node", + ], +) diff --git a/adev/shared-docs/pipeline/navigation/index.ts b/adev/shared-docs/pipeline/navigation/index.ts new file mode 100644 index 000000000000..42ab3691985f --- /dev/null +++ b/adev/shared-docs/pipeline/navigation/index.ts @@ -0,0 +1,26 @@ +// /*! +// * @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 {generateNavItems} from './nav-items-gen'; +import {getNavItemGenStrategy} from './strategies'; + +async function main() { + const [paramFilePath] = process.argv.slice(2); + const rawParamLines = readFileSync(paramFilePath, {encoding: 'utf8'}).split('\n'); + const [joinedSrcs, packageDir, strategy, outputFilePath] = rawParamLines; + + const srcs = joinedSrcs.split(','); + + // Generate navigation data + const navData = await generateNavItems(srcs, getNavItemGenStrategy(strategy, packageDir)); + + writeFileSync(outputFilePath, JSON.stringify(navData)); +} + +await main(); diff --git a/adev/shared-docs/pipeline/navigation/nav-items-gen.ts b/adev/shared-docs/pipeline/navigation/nav-items-gen.ts new file mode 100644 index 000000000000..7380c9282ef8 --- /dev/null +++ b/adev/shared-docs/pipeline/navigation/nav-items-gen.ts @@ -0,0 +1,59 @@ +// /*! +// * @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 fs from 'fs'; +import readline from 'readline'; +import {basename, dirname, resolve} from 'path'; + +import {NavigationItem} from '../../interfaces'; +import {NavigationItemGenerationStrategy} from './types'; + +/** + * Generate navigations items by a provided strategy. + * + * @param mdFilesPaths Paths to the Markdown files that represent the page contents + * @param strategy Strategy + * @returns An array with navigation items + */ +export async function generateNavItems( + mdFilesPaths: string[], + strategy: NavigationItemGenerationStrategy, +): Promise { + const navItems: NavigationItem[] = []; + const {labelGeneratorFn, pathPrefix, contentPath} = strategy; + + for (const path of mdFilesPaths) { + const fullPath = resolve(dirname(path), basename(path)); + const name = path.split('/').pop()?.replace('.md', '')!; + const firstLine = await getTextfileFirstLine(fullPath); + + navItems.push({ + label: labelGeneratorFn(name, firstLine), + path: `${pathPrefix}/${name}`, + contentPath: `${contentPath}/${name}`, + }); + } + + return navItems; +} + +/** Extract the first line of a text file optimally. */ +async function getTextfileFirstLine(filePath: string): Promise { + const readStream = fs.createReadStream(filePath); + const rl = readline.createInterface({input: readStream}); + + const line = await new Promise((resolve) => + rl.on('line', (line) => { + rl.close(); + resolve(line); + }), + ); + readStream.destroy(); + + return line; +} diff --git a/adev/shared-docs/pipeline/navigation/strategies.ts b/adev/shared-docs/pipeline/navigation/strategies.ts new file mode 100644 index 000000000000..513cae7b9b36 --- /dev/null +++ b/adev/shared-docs/pipeline/navigation/strategies.ts @@ -0,0 +1,52 @@ +// /*! +// * @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 {NavigationItemGenerationStrategy, Strategy} from './types'; + +const CONTENT_FOLDER_PATH = 'adev/src/content/'; + +// Using an object for type safety (i.e. make sure SUPPORTED_STRATEGIES includes all strategies). +const strategiesObj: {[key in Strategy]: null} = {errors: null, 'extended-diagnostics': null}; +const SUPPORTED_STRATEGIES = Object.keys(strategiesObj); + +/** Get navigation item generation strategy by a provided strategy string. */ +export function getNavItemGenStrategy( + strategy: string, + packageDir: string, +): NavigationItemGenerationStrategy { + if (SUPPORTED_STRATEGIES.indexOf(strategy) === -1) { + throw new Error( + `Unsupported NavigationItem generation strategy "${strategy}". Supported: ${SUPPORTED_STRATEGIES.join(', ')}`, + ); + } + + switch (strategy as Strategy) { + case 'errors': + return errorsStrategy(packageDir); + case 'extended-diagnostics': + return extendedDiagnosticsStrategy(packageDir); + } +} + +// "Errors" navigation items generation strategy +function errorsStrategy(packageDir: string): NavigationItemGenerationStrategy { + return { + pathPrefix: 'errors', + contentPath: packageDir.replace(CONTENT_FOLDER_PATH, ''), + labelGeneratorFn: (fileName, firstLine) => firstLine.replace('#', fileName + ':'), + }; +} + +// "Extended diagnostics" items generation strategy +function extendedDiagnosticsStrategy(packageDir: string): NavigationItemGenerationStrategy { + return { + pathPrefix: 'extended-diagnostics', + contentPath: packageDir.replace(CONTENT_FOLDER_PATH, ''), + labelGeneratorFn: (fileName, firstLine) => firstLine.replace('#', fileName + ':'), + }; +} diff --git a/adev/shared-docs/pipeline/navigation/test/BUILD.bazel b/adev/shared-docs/pipeline/navigation/test/BUILD.bazel new file mode 100644 index 000000000000..b7472dbe0265 --- /dev/null +++ b/adev/shared-docs/pipeline/navigation/test/BUILD.bazel @@ -0,0 +1,17 @@ +load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") + +package(default_visibility = ["//adev/shared-docs/pipeline/navigation:__subpackages__"]) + +ts_library( + name = "unit_test_lib", + testonly = True, + srcs = glob(["*.spec.ts"]), + deps = [ + "//adev/shared-docs/pipeline/navigation:lib", + ], +) + +jasmine_node_test( + name = "unit_tests", + deps = [":unit_test_lib"], +) diff --git a/adev/shared-docs/pipeline/navigation/test/nav-items-gen.spec.ts b/adev/shared-docs/pipeline/navigation/test/nav-items-gen.spec.ts new file mode 100644 index 000000000000..918ceab74023 --- /dev/null +++ b/adev/shared-docs/pipeline/navigation/test/nav-items-gen.spec.ts @@ -0,0 +1,39 @@ +import {generateNavItems} from '../nav-items-gen'; +import {NavigationItemGenerationStrategy} from '../types'; +import fs from 'fs'; +import readline from 'readline'; + +const readlineMock = { + close: () => null, + on: (_: string, lineFn: (line: string) => void) => { + lineFn('Doc first line'); + }, +}; + +describe('generateNavItems', () => { + it('should test the default case', async () => { + spyOn(fs, 'createReadStream').and.returnValue({destroy: () => null} as any); + spyOn(readline, 'createInterface').and.returnValue(readlineMock as any); + + const strategy: NavigationItemGenerationStrategy = { + pathPrefix: 'page', + contentPath: 'content/directory', + labelGeneratorFn: (fileName, firstLine) => fileName + ' // ' + firstLine, + }; + + const navItems = await generateNavItems(['directory/home.md', 'directory/about.md'], strategy); + + expect(navItems).toEqual([ + { + label: 'home // Doc first line', + path: 'page/home', + contentPath: 'content/directory/home', + }, + { + label: 'about // Doc first line', + path: 'page/about', + contentPath: 'content/directory/about', + }, + ]); + }); +}); diff --git a/adev/shared-docs/pipeline/navigation/types.ts b/adev/shared-docs/pipeline/navigation/types.ts new file mode 100644 index 000000000000..a77d8d0ad9c3 --- /dev/null +++ b/adev/shared-docs/pipeline/navigation/types.ts @@ -0,0 +1,28 @@ +// /*! +// * @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 +// */ + +/** + * `NavigationItem` generation strategy + */ +export type NavigationItemGenerationStrategy = { + /** + * App route path prefix. + */ + pathPrefix: string; + /** + * Content path where the source files are kept. + */ + contentPath: string; + /** + * Page/route label generator function. + */ + labelGeneratorFn: (fileName: string, firstLine: string) => string; +}; + +// All strategies names +export type Strategy = 'errors' | 'extended-diagnostics'; diff --git a/adev/shared-docs/pipeline/tutorials/BUILD.bazel b/adev/shared-docs/pipeline/tutorials/BUILD.bazel index 40e1384f93c0..a7faa454b246 100644 --- a/adev/shared-docs/pipeline/tutorials/BUILD.bazel +++ b/adev/shared-docs/pipeline/tutorials/BUILD.bazel @@ -33,7 +33,6 @@ ts_library( ":editor", "//adev/shared-docs/interfaces", "@npm//@types/node", - "@npm//fast-glob", ], ) diff --git a/adev/shared-docs/utils/BUILD.bazel b/adev/shared-docs/utils/BUILD.bazel index 133d2d0b348f..3d277f55f488 100644 --- a/adev/shared-docs/utils/BUILD.bazel +++ b/adev/shared-docs/utils/BUILD.bazel @@ -29,8 +29,6 @@ ts_library( "//adev/shared-docs/providers", "//packages/core", "//packages/router", - "@npm//@types/node", - "@npm//@webcontainer/api", "@npm//fflate", ], ) diff --git a/adev/src/app/sub-navigation-data.ts b/adev/src/app/sub-navigation-data.ts index 9cdd596611ff..726214196441 100644 --- a/adev/src/app/sub-navigation-data.ts +++ b/adev/src/app/sub-navigation-data.ts @@ -12,6 +12,8 @@ import {NavigationItem} from '@angular/docs'; import FIRST_APP_TUTORIAL_NAV_DATA from '../../src/assets/tutorials/first-app/routes.json'; import LEARN_ANGULAR_TUTORIAL_NAV_DATA from '../../src/assets/tutorials/learn-angular/routes.json'; import DEFERRABLE_VIEWS_TUTORIAL_NAV_DATA from '../../src/assets/tutorials/deferrable-views/routes.json'; +import ERRORS_NAV_DATA from '../../src/assets/content/reference/errors/routes.json'; +import EXT_DIAGNOSTICS_NAV_DATA from '../../src/assets/content/reference/extended-diagnostics/routes.json'; import {DefaultPage} from './core/enums/pages'; import {getApiNavigationItems} from './features/references/helpers/manifest.helper'; @@ -1110,206 +1112,7 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'errors', contentPath: 'reference/errors/overview', }, - { - label: 'NG0100: Expression Changed After Checked', - path: 'errors/NG0100', - contentPath: 'reference/errors/NG0100', - }, - { - label: 'NG01101: Wrong Async Validator Return Type', - path: 'errors/NG01101', - contentPath: 'reference/errors/NG01101', - }, - { - label: 'NG01203: Missing value accessor', - path: 'errors/NG01203', - contentPath: 'reference/errors/NG01203', - }, - { - label: 'NG0200: Circular Dependency in DI', - path: 'errors/NG0200', - contentPath: 'reference/errors/NG0200', - }, - { - label: 'NG0201: No Provider Found', - path: 'errors/NG0201', - contentPath: 'reference/errors/NG0201', - }, - { - label: 'NG0203: `inject()` must be called from an injection context', - path: 'errors/NG0203', - contentPath: 'reference/errors/NG0203', - }, - { - label: 'NG0209: Invalid multi provider', - path: 'errors/NG0209', - contentPath: 'reference/errors/NG0209', - }, - { - label: 'NG02200: Missing Iterable Differ', - path: 'errors/NG02200', - contentPath: 'reference/errors/NG02200', - }, - { - label: 'NG02800: JSONP support in HttpClient configuration', - path: 'errors/NG02800', - contentPath: 'reference/errors/NG02800', - }, - { - label: 'NG0300: Selector Collision', - path: 'errors/NG0300', - contentPath: 'reference/errors/NG0300', - }, - { - label: 'NG0301: Export Not Found', - path: 'errors/NG0301', - contentPath: 'reference/errors/NG0301', - }, - { - label: 'NG0302: Pipe Not Found', - path: 'errors/NG0302', - contentPath: 'reference/errors/NG0302', - }, - { - label: `NG0403: Bootstrapped NgModule doesn't specify which component to initialize`, - path: 'errors/NG0403', - contentPath: 'reference/errors/NG0403', - }, - { - label: 'NG0500: Hydration Node Mismatch', - path: 'errors/NG0500', - contentPath: 'reference/errors/NG0500', - }, - { - label: 'NG0501: Hydration Missing Siblings', - path: 'errors/NG0501', - contentPath: 'reference/errors/NG0501', - }, - { - label: 'NG0502: Hydration Missing Node', - path: 'errors/NG0502', - contentPath: 'reference/errors/NG0502', - }, - { - label: 'NG0503: Hydration Unsupported Projection of DOM Nodes', - path: 'errors/NG0503', - contentPath: 'reference/errors/NG0503', - }, - { - label: 'NG0504: Skip hydration flag is applied to an invalid node', - path: 'errors/NG0504', - contentPath: 'reference/errors/NG0504', - }, - { - label: 'NG0505: No hydration info in server response', - path: 'errors/NG0505', - contentPath: 'reference/errors/NG0505', - }, - { - label: 'NG0506: NgZone remains unstable', - path: 'errors/NG0506', - contentPath: 'reference/errors/NG0506', - }, - { - label: 'NG0507: HTML content was altered after server-side rendering', - path: 'errors/NG0507', - contentPath: 'reference/errors/NG0507', - }, - { - label: 'NG0602: Disallowed function call inside reactive context', - path: 'errors/NG0602', - contentPath: 'reference/errors/NG0602', - }, - { - label: 'NG05104: Root element was not found', - path: 'errors/NG05104', - contentPath: 'reference/errors/NG05104', - }, - { - label: 'NG0910: Unsafe bindings on an iframe element', - path: 'errors/NG0910', - contentPath: 'reference/errors/NG0910', - }, - { - label: 'NG0912: Component ID generation collision', - path: 'errors/NG0912', - contentPath: 'reference/errors/NG0912', - }, - { - label: 'NG0913: Runtime Performance Warnings', - path: 'errors/NG0913', - contentPath: 'reference/errors/NG0913', - }, - { - label: 'NG0950: Required input is accessed before a value is set.', - path: 'errors/NG0950', - contentPath: 'reference/errors/NG0950', - }, - { - label: 'NG0951: Child query result is required but no value is available.', - path: 'errors/NG0951', - contentPath: 'reference/errors/NG0951', - }, - { - label: 'NG0955: Track expression resulted in duplicated keys for a given collection', - path: 'errors/NG0955', - contentPath: 'reference/errors/NG0955', - }, - { - label: 'NG0956: Tracking expression caused re-creation of the DOM structure', - path: 'errors/NG0956', - contentPath: 'reference/errors/NG0956', - }, - { - label: 'NG1001: Argument Not Literal', - path: 'errors/NG1001', - contentPath: 'reference/errors/NG1001', - }, - { - label: 'NG2003: Missing Token', - path: 'errors/NG2003', - contentPath: 'reference/errors/NG2003', - }, - { - label: 'NG2009: Invalid Shadow DOM selector', - path: 'errors/NG2009', - contentPath: 'reference/errors/NG2009', - }, - { - label: 'NG3003: Import Cycle Detected', - path: 'errors/NG3003', - contentPath: 'reference/errors/NG3003', - }, - { - label: 'NG05000: Hydration with unsupported Zone.js instance.', - path: 'errors/NG05000', - contentPath: 'reference/errors/NG05000', - }, - { - label: 'NG0750: @defer dependencies failed to load', - path: 'errors/NG0750', - contentPath: 'reference/errors/NG0750', - }, - { - label: 'NG6100: NgModule.id Set to module.id anti-pattern', - path: 'errors/NG6100', - contentPath: 'reference/errors/NG6100', - }, - { - label: 'NG8001: Invalid Element', - path: 'errors/NG8001', - contentPath: 'reference/errors/NG8001', - }, - { - label: 'NG8002: Invalid Attribute', - path: 'errors/NG8002', - contentPath: 'reference/errors/NG8002', - }, - { - label: 'NG8003: Missing Reference Target', - path: 'errors/NG8003', - contentPath: 'reference/errors/NG8003', - }, + ...ERRORS_NAV_DATA, ], }, { @@ -1320,61 +1123,7 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'extended-diagnostics', contentPath: 'reference/extended-diagnostics/overview', }, - { - label: 'NG8101: Invalid Banana-in-Box', - path: 'extended-diagnostics/NG8101', - contentPath: 'reference/extended-diagnostics/NG8101', - }, - { - label: 'NG8102: Nullish coalescing not nullable', - path: 'extended-diagnostics/NG8102', - contentPath: 'reference/extended-diagnostics/NG8102', - }, - { - label: 'NG8103: Missing control flow directive', - path: 'extended-diagnostics/NG8103', - contentPath: 'reference/extended-diagnostics/NG8103', - }, - { - label: 'NG8104: Text attribute not binding', - path: 'extended-diagnostics/NG8104', - contentPath: 'reference/extended-diagnostics/NG8104', - }, - { - label: 'NG8105: Missing `let` keyword in an *ngFor expression', - path: 'extended-diagnostics/NG8105', - contentPath: 'reference/extended-diagnostics/NG8105', - }, - { - label: 'NG8106: Suffix not supported', - path: 'extended-diagnostics/NG8106', - contentPath: 'reference/extended-diagnostics/NG8106', - }, - { - label: 'NG8107: Optional chain not nullable', - path: 'extended-diagnostics/NG8107', - contentPath: 'reference/extended-diagnostics/NG8107', - }, - { - label: 'NG8108: ngSkipHydration should be a static attribute', - path: 'extended-diagnostics/NG8108', - contentPath: 'reference/extended-diagnostics/NG8108', - }, - { - label: 'NG8109: Signals must be invoked in template interpolations', - path: 'extended-diagnostics/NG8109', - contentPath: 'reference/extended-diagnostics/NG8109', - }, - { - label: 'NG8111: Functions must be invoked in event bindings', - path: 'extended-diagnostics/NG8111', - contentPath: 'reference/extended-diagnostics/NG8111', - }, - { - label: 'NG8113: Unused Standalone Imports', - path: 'extended-diagnostics/NG8113', - contentPath: 'reference/extended-diagnostics/NG8113', - }, + ...EXT_DIAGNOSTICS_NAV_DATA, ], }, { diff --git a/adev/src/assets/BUILD.bazel b/adev/src/assets/BUILD.bazel index 99599093c287..9b64479b2659 100644 --- a/adev/src/assets/BUILD.bazel +++ b/adev/src/assets/BUILD.bazel @@ -32,7 +32,9 @@ copy_to_directory( "//adev/src/content/reference/concepts", "//adev/src/content/reference/configs", "//adev/src/content/reference/errors", + "//adev/src/content/reference/errors:route-nav-items", "//adev/src/content/reference/extended-diagnostics", + "//adev/src/content/reference/extended-diagnostics:route-nav-items", "//adev/src/content/reference/migrations", "//adev/src/content/tools", "//adev/src/content/tools/cli", diff --git a/adev/src/content/reference/errors/BUILD.bazel b/adev/src/content/reference/errors/BUILD.bazel index c347eea407fc..fdfbb6a529be 100644 --- a/adev/src/content/reference/errors/BUILD.bazel +++ b/adev/src/content/reference/errors/BUILD.bazel @@ -1,13 +1,34 @@ -load("//adev/shared-docs:index.bzl", "generate_guides") +load("//adev/shared-docs:index.bzl", "generate_guides", "generate_nav_items") + +package(default_visibility = ["//adev:__subpackages__"]) + +filegroup( + name = "files", + srcs = glob( + [ + "*.md", + ], + exclude = [ + "overview.md", + ], + ), + visibility = ["//visibility:private"], +) generate_guides( name = "errors", - srcs = glob([ - "*.md", - ]), + srcs = [ + "overview.md", + ":files", + ], data = [ "//adev/src/content/examples/errors:cyclic-imports/child.component.ts", "//adev/src/content/examples/errors:cyclic-imports/parent.component.ts", ], - visibility = ["//adev:__subpackages__"], +) + +generate_nav_items( + name = "route-nav-items", + srcs = [":files"], + strategy = "errors", ) diff --git a/adev/src/content/reference/extended-diagnostics/BUILD.bazel b/adev/src/content/reference/extended-diagnostics/BUILD.bazel index b5f6f83e522e..2ad047e0be5f 100644 --- a/adev/src/content/reference/extended-diagnostics/BUILD.bazel +++ b/adev/src/content/reference/extended-diagnostics/BUILD.bazel @@ -1,9 +1,30 @@ -load("//adev/shared-docs:index.bzl", "generate_guides") +load("//adev/shared-docs:index.bzl", "generate_guides", "generate_nav_items") + +package(default_visibility = ["//adev:__subpackages__"]) + +filegroup( + name = "files", + srcs = glob( + [ + "*.md", + ], + exclude = [ + "overview.md", + ], + ), + visibility = ["//visibility:private"], +) generate_guides( name = "extended-diagnostics", - srcs = glob([ - "*.md", - ]), - visibility = ["//adev:__subpackages__"], + srcs = [ + "overview.md", + ":files", + ], +) + +generate_nav_items( + name = "route-nav-items", + srcs = [":files"], + strategy = "extended-diagnostics", ) From 02d37b4b3a76db92960bf5b2b88d8eea29463aa3 Mon Sep 17 00:00:00 2001 From: hawkgs Date: Tue, 7 Jan 2025 12:10:25 +0200 Subject: [PATCH 2/2] fixup! docs(docs-infra): generate errors and extended-diagnostics route NavigationItem-s Update code comments and improve MD file heading extraction. --- adev/shared-docs/pipeline/navigation/index.ts | 14 ++++---- .../pipeline/navigation/nav-items-gen.ts | 34 +++++++++---------- .../pipeline/navigation/strategies.ts | 22 ++++++------ .../navigation/test/nav-items-gen.spec.ts | 25 ++++++++++---- adev/shared-docs/pipeline/navigation/types.ts | 28 ++++++--------- 5 files changed, 65 insertions(+), 58 deletions(-) diff --git a/adev/shared-docs/pipeline/navigation/index.ts b/adev/shared-docs/pipeline/navigation/index.ts index 42ab3691985f..38583b76d95b 100644 --- a/adev/shared-docs/pipeline/navigation/index.ts +++ b/adev/shared-docs/pipeline/navigation/index.ts @@ -1,10 +1,10 @@ -// /*! -// * @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 -// */ +/*! + * @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 {generateNavItems} from './nav-items-gen'; diff --git a/adev/shared-docs/pipeline/navigation/nav-items-gen.ts b/adev/shared-docs/pipeline/navigation/nav-items-gen.ts index 7380c9282ef8..0e6e58f23eaf 100644 --- a/adev/shared-docs/pipeline/navigation/nav-items-gen.ts +++ b/adev/shared-docs/pipeline/navigation/nav-items-gen.ts @@ -1,10 +1,10 @@ -// /*! -// * @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 -// */ +/*! + * @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 fs from 'fs'; import readline from 'readline'; @@ -30,7 +30,7 @@ export async function generateNavItems( for (const path of mdFilesPaths) { const fullPath = resolve(dirname(path), basename(path)); const name = path.split('/').pop()?.replace('.md', '')!; - const firstLine = await getTextfileFirstLine(fullPath); + const firstLine = await getMdFileHeading(fullPath); navItems.push({ label: labelGeneratorFn(name, firstLine), @@ -42,18 +42,18 @@ export async function generateNavItems( return navItems; } -/** Extract the first line of a text file optimally. */ -async function getTextfileFirstLine(filePath: string): Promise { +/** Extract the first heading from a Markdown file. */ +async function getMdFileHeading(filePath: string): Promise { const readStream = fs.createReadStream(filePath); const rl = readline.createInterface({input: readStream}); - const line = await new Promise((resolve) => - rl.on('line', (line) => { + for await (const line of rl) { + if (line.trim().startsWith('#')) { rl.close(); - resolve(line); - }), - ); - readStream.destroy(); + readStream.destroy(); + return line.replace(/^#+[ \t]+/, ''); + } + } - return line; + return ''; } diff --git a/adev/shared-docs/pipeline/navigation/strategies.ts b/adev/shared-docs/pipeline/navigation/strategies.ts index 513cae7b9b36..f817fcdf6778 100644 --- a/adev/shared-docs/pipeline/navigation/strategies.ts +++ b/adev/shared-docs/pipeline/navigation/strategies.ts @@ -1,16 +1,18 @@ -// /*! -// * @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 -// */ +/*! + * @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 {NavigationItemGenerationStrategy, Strategy} from './types'; +// Should point to the website content. +// Update, if the location is updated or shared-docs is extracted from `adev/`. const CONTENT_FOLDER_PATH = 'adev/src/content/'; -// Using an object for type safety (i.e. make sure SUPPORTED_STRATEGIES includes all strategies). +// Ensure that all Strategy-ies are part of SUPPORTED_STRATEGIES by using a key-typed object. const strategiesObj: {[key in Strategy]: null} = {errors: null, 'extended-diagnostics': null}; const SUPPORTED_STRATEGIES = Object.keys(strategiesObj); @@ -38,7 +40,7 @@ function errorsStrategy(packageDir: string): NavigationItemGenerationStrategy { return { pathPrefix: 'errors', contentPath: packageDir.replace(CONTENT_FOLDER_PATH, ''), - labelGeneratorFn: (fileName, firstLine) => firstLine.replace('#', fileName + ':'), + labelGeneratorFn: (fileName, firstLine) => fileName + ': ' + firstLine, }; } @@ -47,6 +49,6 @@ function extendedDiagnosticsStrategy(packageDir: string): NavigationItemGenerati return { pathPrefix: 'extended-diagnostics', contentPath: packageDir.replace(CONTENT_FOLDER_PATH, ''), - labelGeneratorFn: (fileName, firstLine) => firstLine.replace('#', fileName + ':'), + labelGeneratorFn: (fileName, firstLine) => fileName + ': ' + firstLine, }; } diff --git a/adev/shared-docs/pipeline/navigation/test/nav-items-gen.spec.ts b/adev/shared-docs/pipeline/navigation/test/nav-items-gen.spec.ts index 918ceab74023..0612521bf05a 100644 --- a/adev/shared-docs/pipeline/navigation/test/nav-items-gen.spec.ts +++ b/adev/shared-docs/pipeline/navigation/test/nav-items-gen.spec.ts @@ -1,19 +1,30 @@ +/** + * @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 {generateNavItems} from '../nav-items-gen'; import {NavigationItemGenerationStrategy} from '../types'; import fs from 'fs'; import readline from 'readline'; -const readlineMock = { - close: () => null, - on: (_: string, lineFn: (line: string) => void) => { - lineFn('Doc first line'); +const readlineInterfaceMock = { + close: () => {}, + async *[Symbol.asyncIterator]() { + yield ''; + yield 'Some random text'; + yield '## Heading'; + yield 'Some text'; }, }; describe('generateNavItems', () => { it('should test the default case', async () => { spyOn(fs, 'createReadStream').and.returnValue({destroy: () => null} as any); - spyOn(readline, 'createInterface').and.returnValue(readlineMock as any); + spyOn(readline, 'createInterface').and.returnValue(readlineInterfaceMock as any); const strategy: NavigationItemGenerationStrategy = { pathPrefix: 'page', @@ -25,12 +36,12 @@ describe('generateNavItems', () => { expect(navItems).toEqual([ { - label: 'home // Doc first line', + label: 'home // Heading', path: 'page/home', contentPath: 'content/directory/home', }, { - label: 'about // Doc first line', + label: 'about // Heading', path: 'page/about', contentPath: 'content/directory/about', }, diff --git a/adev/shared-docs/pipeline/navigation/types.ts b/adev/shared-docs/pipeline/navigation/types.ts index a77d8d0ad9c3..f92e5db9e005 100644 --- a/adev/shared-docs/pipeline/navigation/types.ts +++ b/adev/shared-docs/pipeline/navigation/types.ts @@ -1,28 +1,22 @@ -// /*! -// * @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 -// */ +/*! + * @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 + */ /** * `NavigationItem` generation strategy */ export type NavigationItemGenerationStrategy = { - /** - * App route path prefix. - */ + /** App route path prefix. */ pathPrefix: string; - /** - * Content path where the source files are kept. - */ + /** Content path where the source files are kept. */ contentPath: string; - /** - * Page/route label generator function. - */ + /** Page/route label generator function. */ labelGeneratorFn: (fileName: string, firstLine: string) => string; }; -// All strategies names +/** Strategy for navigation item generation. */ export type Strategy = 'errors' | 'extended-diagnostics';