build: add rules for generating block/element API data (#52480)

Adds build rules for "artificially" generating `DocEntry` collections for block and element APIs. The two rules are very similar, but _just_ different enough that it's worth having two separate implementations.

PR Close #52480
This commit is contained in:
Jeremy Elbourn 2023-10-31 18:02:22 -07:00 committed by Alex Rickabaugh
parent a3abe1671c
commit 64db486edc
18 changed files with 613 additions and 3 deletions

View file

@ -1168,10 +1168,11 @@ groups:
'tools/esm-interop/**/{*,.*}',
'tools/gulp-tasks/**/{*,.*}',
'tools/legacy-saucelabs/**/{*,.*}',
'tools/manual_api_docs/**/{*,.*}',
'tools/npm-patches/**/{*,.*}',
'tools/rxjs/**/{*,.*}',
'tools/saucelabs/**/{*,.*}',
'tools/saucelabs-daemon/**/{*,.*}',
'tools/saucelabs/**/{*,.*}',
'tools/symbol-extractor/**/{*,.*}',
'tools/testing/**/{*,.*}',
'tools/tslint/**/{*,.*}',

View file

@ -0,0 +1,37 @@
load("//tools:defaults.bzl", "nodejs_binary", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "generate_element_api_json_lib",
srcs = ["generate_element_api_json.ts"],
deps = [
"//packages/compiler-cli",
"@npm//@types/node",
],
)
nodejs_binary(
name = "generate_element_api_json",
data = [
":generate_element_api_json_lib",
],
entry_point = ":generate_element_api_json.ts",
)
ts_library(
name = "generate_block_api_json_lib",
srcs = ["generate_block_api_json.ts"],
deps = [
"//packages/compiler-cli",
"@npm//@types/node",
],
)
nodejs_binary(
name = "generate_block_api_json",
data = [
":generate_block_api_json_lib",
],
entry_point = ":generate_block_api_json.ts",
)

View file

@ -0,0 +1,6 @@
load("//tools/manual_api_docs:generate_block_api_json.bzl", "generate_block_api_json")
generate_block_api_json(
name = "blocks",
srcs = glob(["*.md"]),
)

View file

@ -0,0 +1,63 @@
A type of [block](api/core/defer) that can be used to defer load the JavaScript for components,
directives and pipes used inside a component template.
## Syntax
```html
@defer ( on <trigger>; when <condition>; prefetch on <trigger>; prefetch when <condition> ) {
<!-- deferred template fragment -->
<calendar-cmp />
} @placeholder ( minimum? <duration> ) {
<!-- placeholder template fragment -->
<p>Placeholder</p>
} @loading ( minimum? <duration>; after? <duration> ) {
<!-- loading template fragment -->
<img alt="loading image" src="loading.gif" />
} @error {
<!-- error template fragment -->
<p>An loading error occured</p>
}
```
## Description
### Blocks
Supported sections of a defer block. Note: only the @defer block template fragment is deferred
loaded. The remaining optional blocks are eagerly loaded.
| block | Description |
|----------------|----------------------------------------------------------|
| `@defer` | The defer loaded block of content |
| `@placeholder` | Content shown prior to defer loading (Optional) |
| `@loading` | Content shown during defer loading (Optional) |
| `@error` | Content shown when defer loading errors occur (Optional) |
<h3>Triggers</h3>
Triggers provide conditions for when defer loading occurs. Some allow a template reference variable
as an optional parameter. Separate multiple triggers with a semicolon.
| trigger | Triggers... |
|---------------------------------|-----------------------------------------------|
| `on idle` | when the browser reports idle state (default) |
| `on viewport(<elementRef>?)` | when the element enters the viewport |
| `on interaction(<elementRef>?)` | when clicked, touched, or focused |
| `on hover(<elementRef>?)` | when element has been hovered |
| `on immediate` | when the page finishes rendering |
| `on timer(<duration>)` | after a specific timeout |
| `when <condition>` | on a custom condition |
<h2>Prefetch</h2>
Configures prefetching of the defer block used in the `@defer` parameters, but does not affect
rendering. Rendering is handled by the standard `on` and `when` conditions. Separate multiple
prefetch configurations with a semicolon.
```html
@defer (prefetch on <trigger>; prefetch when <condition>) {
<!-- deferred template fragment -->
}
```
Learn more in the [defer loading guide](guide/defer).

View file

@ -0,0 +1,54 @@
The `@for` block repeatedly renders content of a block for each item in a collection.
## Syntax
```html
@for (item of items; track item.name) {
<li> {{ item.name }} </li>
} @empty {
<li> There are no items. </li>
}
```
## Description
The `@for` block renders its content in response to changes in a collection. Collections can be any
JavaScript [iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols),
but there are performance advantages of using a regular `Array`.
You can optionally include an `@empty` section immediately after the `@for` block content. The
content of the `@empty` block displays when there are no items.
<h3> track and objects identity </h3>
The value of the `track` expression determines a key used to associate array items with the views in
the DOM. Having clear indication of the item identity allows Angular to execute a minimal set of DOM
operations as items are added, removed or moved in a collection.
Loops over immutable data without `trackBy` as one of the most common causes for performance issues
across Angular applications. Because of the potential for poor performance, the `track` expression
is required for the `@for` loops. When in doubt, using `track $index` is a good default.
<h3> `$index` and other contextual variables </h3>
Inside `@for` contents, several implicit variables are always available:
| Variable | Meaning |
| -------- | ------- |
| `$count` | Number of items in a collection iterated over |
| `$index` | Index of the current row |
| `$first` | Whether the current row is the first row |
| `$last` | Whether the current row is the last row |
| `$even` | Whether the current row index is even |
| `$odd` | Whether the current row index is odd |
These variables are always available with these names, but can be aliased via a `let` segment:
```html
@for (item of items; track item.id; let idx = $index, e = $even) {
Item #{{ idx }}: {{ item.name }}
}
```
The aliasing is especially useful in case of using nested `@for` blocks where contextual variable
names could collide.

View file

@ -0,0 +1,27 @@
The `@if` block conditionally displays its content when its condition expression is truthy.
## Syntax
```html
@if (a > b) {
{{a}} is greater than {{b}}
} @else if (b > a) {
{{a}} is less than {{b}}
} @else {
{{a}} is equal to {{b}}
}
```
## Description
Content is added and removed from the DOM based on the evaluation of conditional expressions in
the `@if` and `@else` blocks.
The built-in `@if` supports referencing of expression results to keep a solution for common coding
patterns:
```html
@if (users$ | async; as users) {
{{ users.length }}
}
```

View file

@ -0,0 +1,29 @@
The `@switch` block is inspired by the JavaScript `switch` statement:
## Syntax
```html
@switch (condition) {
@case (caseA) {
Case A.
}
@case (caseB) {
Case B.
}
@default {
Default case.
}
}
```
## Description
The `@switch` blocks displays content selected by one of the cases matching against the conditional
expression. The value of the conditional expression is compared to the case expression using
the `===` operator.
The `@default` block is optional and can be omitted. If no `@case` matches the expression and there
is no `@default` block, nothing is shown.
**`@switch` does not have fallthrough**, so you do not need an equivalent to a `break` or `return`
statement.

View file

@ -0,0 +1,6 @@
load("//tools/manual_api_docs:generate_element_api_json.bzl", "generate_element_api_json")
generate_element_api_json(
name = "elements",
srcs = glob(["*.md"]),
)

View file

@ -0,0 +1,119 @@
A special element that can hold structural directives without adding new elements to the DOM.
The `<ng-container>` allows us to use structural directives without any extra element, making sure
that the only DOM changes being applied are those dictated by the directives themselves.
This not only increases performance \(even so slightly\) since the browser ends up rendering less
elements but can also be a valuable asset in having cleaner DOMs and styles alike.
It can for example enable us to use structural directives without breaking styling dependent on a
precise DOM structure \(as for example the ones we get when using flex containers, margins, the
child combinator selector, etc.\).
## Usage notes
### With `*NgIf`s
One common use case of `<ng-container>` is alongside the `*ngIf` structural directive. By using the
special element we can produce very clean templates easy to understand and work with.
For example, we may want to have a number of elements shown conditionally but they do not need to be
all under the same root element. That can be easily done by wrapping them in such a block:
<code-example format="html" language="html">
&lt;ng-container *ngIf="condition"&gt;
&hellip;
&lt;/ng-container&gt;
</code-example>
This can also be augmented with an `else` statement alongside an `<ng-template>` as:
<code-example format="html" language="html">
&lt;ng-container *ngIf="condition; else templateA"&gt;
&hellip;
&lt;/ng-container&gt;
&lt;ng-template #templateA&gt;
&hellip;
&lt;/ng-template&gt;
</code-example>
### Combination of multiple structural directives
Multiple structural directives cannot be used on the same element; if you need to take advantage of
more than one structural directive, it is advised to use an `<ng-container>` per structural
directive.
The most common scenario is with `*ngIf` and `*ngFor`. For example, let's imagine that we have a
list of items but each item needs to be displayed only if a certain condition is true. We could be
tempted to try something like:
<code-example format="html" language="html">
&lt;ul&gt;
&lt;li *ngFor="let item of items" *ngIf="item.isValid"&gt;
{{ item.name }}
&lt;/li&gt;
&lt;/ul&gt;
</code-example>
As we said that would not work, what we can do is to simply move one of the structural directives to
an `<ng-container>` element, which would then wrap the other one, like so:
<code-example format="html" language="html">
&lt;ul&gt;
&lt;ng-container *ngFor="let item of items"&gt;
&lt;li *ngIf="item.isValid"&gt;
{{ item.name }}
&lt;/li&gt;
&lt;/ng-container&gt;
&lt;/ul&gt;
</code-example>
This would work as intended without introducing any new unnecessary elements in the DOM.
For more information see [one structural directive per element](guide/structural-directives#one-per-element).
### Use alongside ngTemplateOutlet
The `NgTemplateOutlet` directive can be applied to any element but most of the time it's applied
to `<ng-container>` ones. By combining the two, we get a very clear and easy to follow HTML and DOM
structure in which no extra elements are necessary and template views are instantiated where
requested.
For example, imagine a situation in which we have a large HTML, in which a small portion needs to be
repeated in different places. A simple solution is to define an `<ng-template>` containing our
repeating HTML and render that where necessary by using `<ng-container>` alongside
an `NgTemplateOutlet`.
Like so:
<code-example format="html" language="html">
&lt;!-- &hellip; --&gt;
&lt;ng-container *ngTemplateOutlet="tmpl; context: {&dollar;implicit: 'Hello'}"&gt;
&lt;/ng-container&gt;
&lt;!-- &hellip; --&gt;
&lt;ng-container *ngTemplateOutlet="tmpl; context: {&dollar;implicit: 'World'}"&gt;
&lt;/ng-container&gt;
&lt;!-- &hellip; --&gt;
&lt;ng-template #tmpl let-text&gt;
&lt;h1&gt;{{ text }}&lt;/h1&gt;
&lt;/ng-template&gt;
</code-example>
For more information regarding `NgTemplateOutlet`, see
the [`NgTemplateOutlet`s api documentation page](api/common/NgTemplateOutlet).

View file

@ -0,0 +1,12 @@
The `<ng-content>` element specifies where to project content inside a component template.
## Attributes
| Attribute | Description |
|---------------|-------------------------------------------------------------------------|
| `select` | CSS selector. Matching elements are projected into this `<ng-content>`. |
Only select elements from the projected content that match the given CSS `selector`.
Angular supports [selectors](https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors) for any
combination of tag name, attribute, CSS class, and the `:not` pseudo-class.

View file

@ -0,0 +1,66 @@
Angular's `<ng-template>` element defines a template that is not rendered by default.
With `<ng-template>`, you can define template content that is only being rendered by Angular when
you, whether directly or indirectly, specifically instruct it to do so, allowing you to have full
control over how and when the content is displayed.
<div class="alter is-helpful">
Note that if you wrap content inside an `<ng-template>` without instructing Angular to render it,
such content will not appear on a page. For example, see the following HTML code, when handling it
Angular won't render the middle "Hip!" in the phrase "Hip! Hip! Hooray!" because of the
surrounding `<ng-template>`.
```html
<p>Hip!</p>
<ng-template>
<p>Hip!</p>
</ng-template>
<p>Hooray!</p>
```
</div>
## Usage notes
### Structural Directives
One of the main uses for `<ng-template>` is to hold template content that will be used
by [Structural directives](guide/structural-directives). Those directives can add and remove copies
of the template content based on their own logic.
When using
the [structural directive shorthand](guide/structural-directives#structural-directive-shorthand),
Angular creates an `<ng-template>` element behind the scenes.
### TemplateRef
`<ng-template>` elements are represented as instances of the `TemplateRef` class.
To add copies of the template to the DOM, pass this object to the `ViewContainerRef`
method `createEmbeddedView()`.
### Template Variables
`<ng-template>` elements can be referenced in templates
using [standard template variables](guide/template-reference-variables#how-angular-assigns-values-to-template-variables).
*This is how `<ng-template>` elements are used as `ngIf` else clauses.*
Such template variables can be used in conjunction with `ngTemplateOutlet` directives to render the
content defined inside `<ng-template>` tags.
### Querying
A [Query](api/core/Query) \(such as `ViewChild`\) can find the `TemplateRef` associated to
an `<ng-template>` element so that it can be used programmatically; for instance, to pass it to
the `ViewContainerRef` method `createEmbeddedView()`.
### Context
Inside the `<ng-template>` tags you can reference variables present in the surrounding outer
template.
Additionally, a context object can be associated with `<ng-template>` elements.
Such an object contains variables that can be accessed from within the template contents via
template \(`let` and `as`\) declarations.

View file

@ -0,0 +1,54 @@
load("@build_bazel_rules_nodejs//:providers.bzl", "run_node")
def _generate_block_api_json(ctx):
"""Implementation of the generate_block_api_json rule"""
# Define arguments that will be passed to the underlying nodejs program.
args = ctx.actions.args()
# Use a param file for consistency with other doc generation rules.
args.set_param_file_format("multiline")
args.use_param_file("%s", use_always = True)
# Pass the set of source files from which the API data will be generated.
args.add_joined(ctx.files.srcs, join_with = ",")
# Pass the name of the output JSON file.
manifest = ctx.actions.declare_file("blocks.json")
args.add(manifest.path)
# 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),
executable = "_generate_block_api_json",
outputs = [manifest],
arguments = [args],
)
# The return value describes what the rule is producing. In this case we need to specify
# the "DefaultInfo" with the output JSON manifest.
return [DefaultInfo(files = depset([manifest]))]
generate_block_api_json = rule(
# Point to the starlark function that will execute for this rule.
implementation = _generate_block_api_json,
doc = """Rule that generates an Angular API doc collection for hand-written block APIs""",
# The attributes that can be set to this rule.
attrs = {
"srcs": attr.label_list(
doc = """The source files for this rule, must include one or more markdown files.""",
allow_empty = False,
allow_files = True,
),
# The executable for this rule (private).
"_generate_block_api_json": attr.label(
default = Label("//tools/manual_api_docs:generate_block_api_json"),
executable = True,
cfg = "exec",
),
},
)

View file

@ -0,0 +1,33 @@
/**
* @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.io/license
*/
import {DocEntry, EntryType} from '@angular/compiler-cli';
import {readFileSync, writeFileSync} from 'fs';
import {basename} from 'path';
function main() {
const [paramFilePath] = process.argv.slice(2);
const rawParamLines = readFileSync(paramFilePath, {encoding: 'utf8'}).split('\n');
const [srcs, outputFileExecRootRelativePath] = rawParamLines;
const entries: DocEntry[] = srcs.split(',').map(sourceFilePath => {
const fileContent = readFileSync(sourceFilePath, {encoding: 'utf8'});
return {
name: `@${basename(sourceFilePath, '.md')}`,
entryType: EntryType.Block,
description: fileContent,
rawComment: fileContent,
jsdocTags: [],
};
});
writeFileSync(outputFileExecRootRelativePath, JSON.stringify(entries), {encoding: 'utf8'});
}
main();

View file

@ -0,0 +1,54 @@
load("@build_bazel_rules_nodejs//:providers.bzl", "run_node")
def _generate_element_api_json(ctx):
"""Implementation of the generate_element_api_json rule"""
# Define arguments that will be passed to the underlying nodejs program.
args = ctx.actions.args()
# Use a param file for consistency with other doc generation rules.
args.set_param_file_format("multiline")
args.use_param_file("%s", use_always = True)
# Pass the set of source files from which the API data will be generated.
args.add_joined(ctx.files.srcs, join_with = ",")
# Pass the name of the output JSON file.
manifest = ctx.actions.declare_file("elements.json")
args.add(manifest.path)
# 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),
executable = "_generate_element_api_json",
outputs = [manifest],
arguments = [args],
)
# The return value describes what the rule is producing. In this case we need to specify
# the "DefaultInfo" with the output JSON manifest.
return [DefaultInfo(files = depset([manifest]))]
generate_element_api_json = rule(
# Point to the starlark function that will execute for this rule.
implementation = _generate_element_api_json,
doc = """Rule that generates an Angular API doc collection for hand-written element APIs""",
# The attributes that can be set to this rule.
attrs = {
"srcs": attr.label_list(
doc = """The source files for this rule, must include one or more markdown files.""",
allow_empty = False,
allow_files = True,
),
# The executable for this rule (private).
"_generate_element_api_json": attr.label(
default = Label("//tools/manual_api_docs:generate_element_api_json"),
executable = True,
cfg = "exec",
),
},
)

View file

@ -0,0 +1,33 @@
/**
* @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.io/license
*/
import {DocEntry, EntryType} from '@angular/compiler-cli';
import {readFileSync, writeFileSync} from 'fs';
import {basename} from 'path';
function main() {
const [paramFilePath] = process.argv.slice(2);
const rawParamLines = readFileSync(paramFilePath, {encoding: 'utf8'}).split('\n');
const [srcs, outputFileExecRootRelativePath] = rawParamLines;
const entries: DocEntry[] = srcs.split(',').map(sourceFilePath => {
const fileContent = readFileSync(sourceFilePath, {encoding: 'utf8'});
return {
name: basename(sourceFilePath, '.md'),
entryType: EntryType.Element,
description: fileContent,
rawComment: fileContent,
jsdocTags: [],
};
});
writeFileSync(outputFileExecRootRelativePath, JSON.stringify(entries), {encoding: 'utf8'});
}
main();

View file

@ -0,0 +1,12 @@
load("//tools/manual_api_docs:generate_element_api_json.bzl", "generate_element_api_json")
load("//tools/manual_api_docs:generate_block_api_json.bzl", "generate_block_api_json")
generate_element_api_json(
name = "element_test",
srcs = ["dummy.md"],
)
generate_block_api_json(
name = "block_test",
srcs = ["dummy.md"],
)

View file

@ -0,0 +1 @@
## This is the content of the file

View file

@ -1,4 +1,5 @@
{
// This tsconfig is only used by IDEs, no actual builds.
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
@ -10,8 +11,10 @@
"outDir": "../dist/tools/",
"noImplicitAny": true,
"noFallthroughCasesInSwitch": true,
"paths": {},
"rootDir": ".",
"paths": {
"@angular/*": ["../packages/*"]
},
"rootDirs": [".", ".."],
"sourceMap": true,
"inlineSources": true,
"lib": [