Compare commits

...

137 Commits

Author SHA1 Message Date
Andrew Kushnir f75429d8a8 release: cut the v18.2.3 release 2024-09-04 08:25:39 -07:00
Doug Parker c91c37728f release: bump Angular DevTools version to 1.0.18 (#57585)
PR Close #57585
2024-09-03 10:43:22 -07:00
Matthieu Riegler de68e049e4 fix(http): Dynamicaly call the global fetch implementation (#57531)
Instead of using the reference that existing when `FetchBackend` is setup.

fixes #57527

PR Close #57531
2024-09-03 09:10:23 -07:00
Ilia Brahinets 2324d9b15a docs: Fix 'EnvironmentInjector' description (#57582)
PR Close #57582
2024-09-03 08:50:05 -07:00
Angular Robot b494e9b5f7 build: update babel dependencies to v7.25.6 (#57634)
See associated pull request for more information.

PR Close #57634
2024-09-03 07:52:13 -07:00
Angular Robot 5d7e446ce2 build: update scorecard action dependencies (#57636)
See associated pull request for more information.

PR Close #57636
2024-09-03 07:51:22 -07:00
Alfredo González 00c0876626 docs(upgrade): change example wording on Input tutorial (#57625)
PR Close #57625
2024-09-03 07:48:16 -07:00
Matthieu Riegler 9c3aeb99bb docs(docs-infra): add support for extends/implements on API entries (#57588)
PR Close #57588
2024-09-03 07:47:25 -07:00
Matthieu Riegler 582aac49a7 refactor(compiler-cli): Add support for inheritance in API extraction (#57588)
This commit adds the `extends` and `implements` properties to the `ClassEntry` & `InterfaceEntry`

PR Close #57588
2024-09-03 07:47:25 -07:00
Matthieu Riegler 5256016695 docs(docs-infra): fix navigation between API pages (#57492)
PR Close #57492
2024-08-30 11:12:27 -07:00
Matthieu Riegler d2d72e369c docs(docs-infra): Add set of unit tests to the API markdown parsing (#57492)
PR Close #57492
2024-08-30 11:12:26 -07:00
Matthieu Riegler 641808fa72 docs(docs-infra): Improve styling around API pages (#57492)
PR Close #57492
2024-08-30 11:12:26 -07:00
Matthieu Riegler f859d5c156 docs(docs-infra): Improve support for deprecated methods (#57492)
PR Close #57492
2024-08-30 11:12:26 -07:00
Matthieu Riegler cbb36fc1ab docs: ensure the all _ are replaced by slash for API entry urls. (#57600)
fixes #57599

PR Close #57600
2024-08-30 07:39:37 -07:00
Andrew Kushnir 4d8da40574 Revert "refactor(compiler-cli): extract function overload separatly (#56489)" (#57594)
This reverts commit 37b88a5a98.

PR Close #57594
2024-08-29 16:21:47 -07:00
David Valentine 614c311a01 docs: update common-router-tasks to include sample dynamic fragment (#57590)
Remove specific reference to "Tour of Heroes" tutorial and add sample route for use in the example for reference.

Co-authored-by: Andrew Scott <atscott01@gmail.com>

PR Close #57590
2024-08-29 15:20:06 -07:00
Matthieu Riegler ed91b03e0f docs: api sub-entries should have / in urls. (#57593)
`@angular/animations/browser` => `api/animations/browser/xyz`

PR Close #57593
2024-08-29 15:19:00 -07:00
Matthieu Riegler 5007970c3d docs: revert changes on API entries urls (#57589)
The name `angular_` shouldn't be included in the URL.

fixes #57587

PR Close #57589
2024-08-29 14:25:27 -07:00
Matthieu Riegler c068045d69 docs(docs-infra): Add support for cross-links on API pages (#57346)
PR Close #57346
2024-08-29 13:39:59 -07:00
Matthieu Riegler 9814767d34 refactor(compiler-cli): Add a map of every symbols used inside a package (#57346)
This commit changes the structure of the API extraction files to include all symbols used inside a package.

The structure is a `Map`, Symbol => package
eg: 'ApplicationRef' => '@angular/core'

PR Close #57346
2024-08-29 13:39:59 -07:00
Matthieu Riegler 93bdbbc812 docs(docs-infra): Add dev-mode only mention for core/global (#57365)
PR Close #57365
2024-08-29 10:17:35 -07:00
Joey Perrott 07607716d4 ci: use version 13.15.1 of firebase-tools for deployment of doc sites (#57583)
Use 13.15.1 instead of tracking to latest to prevent unexpected and unbisectable changes.

PR Close #57583
2024-08-29 10:14:43 -07:00
Paul Gschwendtner d9c1004a35 refactor(migrations): ensure control flow analysis of signal input migration works with all TS versions (#57566)
Ensures that the control flow analysis version of the signal input works
with all TS versions currently supported.

PR Close #57566
2024-08-29 07:51:39 -07:00
Paul Gschwendtner bb29c8bbd7 refactor(migrations): cleanup TODOs in signal input migration (#57566)
Cleans up remaining TODOs in the signal input migration, and other
small clean ups.

PR Close #57566
2024-08-29 07:51:38 -07:00
Matthieu Riegler 8e1d6c71fc refactor(compiler-cli): extract function overload separatly (#56489)
in order for the docs to process function entry, this commit refactor function extraction by keeping the implementation as a the default entry and adds all the overloads into a separate array of entries.

PR Close #56489
2024-08-29 07:49:38 -07:00
Romain 98c6c05c6f docs(docs-infra): fix small mistake in inject-function.md (#57517)
PR Close #57517
2024-08-29 07:44:29 -07:00
Matthieu Riegler 3b7162d03c docs: Add mention to ENVIRONMENT_INITIALIZER (#57464)
Explicitly mention that `ENVIRONMENT_INITIALIZER` should not be async.

PR Close #57464
2024-08-29 07:43:48 -07:00
Vlad Boisa 3bc28678fa docs(docs-infra): remove hash before link (#57351)
Remove hash in link for correct view

Fixes #57349

docs(docs-infra): change the link

Change the link with name of class and method

docs: fix the link name

PR Close #57351
2024-08-29 07:42:13 -07:00
Matthieu Riegler 97fd311b6a docs(docs-infra): show usageNotes only for methods. (#57343)
Prior to this change, function and methods showed their usage notes which resulted in duplicate displays for functions.

Fixes #57339

PR Close #57343
2024-08-29 07:39:51 -07:00
Alex Rickabaugh 1853bbb061 release: cut the v18.2.2 release 2024-08-28 14:32:26 -07:00
Alex Rickabaugh 3ecf620cc5 Revert "fix(http): Dynamicaly call the global fetch implementation (#57531)" (#57571)
This reverts commit 21445a2932.

Reason: failing test

PR Close #57571
2024-08-28 12:42:27 -07:00
Paul Gschwendtner 36aa3af77b refactor(migrations): forward-fix conflicts with generic of ProgramInfo. (#57565)
One PR removed the generic of `ProgramInfo`, while another PR,
separately merged, introduced new instanes of `ProgramInfo`.

PR Close #57565
2024-08-28 09:17:34 -07:00
AleksanderBodurri adf32746c2 fix(devtools): correctly set environment injector path in the case where there are no element injectors (#57442)
Previously, when "Hide injectors with no providers" was toggled, it is possible for the injector tree visualizer to have no Element injectors to visualize. This caused a bug in the slicing logic that splits apart the environment and element injectors from DI resolution paths within the injector tree component in Angular DevTools.

Now, this logic is correctly handled when there are no element injectors to visualize.

PR Close #57442
2024-08-28 08:49:34 -07:00
Matthieu Riegler 5501e9c8ed docs: remove @searchKeywords (#57560)
This tag is not supported.

fixes #57540

PR Close #57560
2024-08-28 08:49:05 -07:00
Paul Gschwendtner 389933f079 refactor(migrations): allow tsurge migrations to run with just NgCompiler (#57562)
This is important so that migrations can easily be wired up in the
language service where only `NgCompiler` is available.

PR Close #57562
2024-08-28 08:47:46 -07:00
Jeremy Elbourn 674aed5984 docs: cover time-based statements in docs authoring guide (#57536)
Adds some additional guidance on writing content that is tied to a
specific version or point in time.

PR Close #57536
2024-08-28 08:45:10 -07:00
Paul Gschwendtner b14c864170 refactor(migrations): properly handle multi query migration (#57556)
Properly handles queries with multiple results, by extracting the
type from the `QueryList`.

Also adds more tests and handles imports.

PR Close #57556
2024-08-28 08:43:39 -07:00
Paul Gschwendtner 10fce207e0 refactor(migrations): initial migration logic for converting to signal queries (#57556)
Adds initial migration logic to convert decorator query declarations to
signal queries.

We will re-use more part of the signal input migration in follow-ups, to
properly migrate, and e.g. even handle control flow

PR Close #57556
2024-08-28 08:43:38 -07:00
Alex Rickabaugh eba3a0ade0 Revert "refactor(migrations): initial migration logic for converting to signal queries (#57525)" (#57555)
This reverts commit 6f5b435a69.

Reason: breaks g3

PR Close #57555
2024-08-27 14:03:06 -07:00
Alex Rickabaugh 3d48a72193 Revert "refactor(migrations): properly handle multi query migration (#57525)" (#57555)
This reverts commit f454ad3bcf.

Reason: breaks g3

PR Close #57555
2024-08-27 14:03:06 -07:00
Matthieu Riegler 5d2e243c76 fix(http): Dynamicaly call the global fetch implementation (#57531)
Instead of using the reference that existing when `FetchBackend` is setup.

fixes #57527

PR Close #57531
2024-08-27 13:34:19 -07:00
Andrew Scott 5e9661d69c fix(docs-infra): Fix scrolling in application (#57554)
Since the change to the after render hooks, the "this" context must be
correct for the destroy to work.

fixes #57552

PR Close #57554
2024-08-27 13:32:14 -07:00
Kristiyan Kostadinov df42d2be16 test: avoid leaking some LViews in tests (#57546)
We had some tests that were leaking LViews, because we were testing things like `createComponent`, but not destroying them afterwards. These changes clean up most of them, although there are a handful still left that I didn't have time to fully track down.

PR Close #57546
2024-08-27 13:29:09 -07:00
Kristiyan Kostadinov 106917af87 fix(core): avoid leaking memory if component throws during creation (#57546)
When we create the LView for a component, we track it in the `TRACKED_LVIEWS` map. It gets untracked when it is destroy, but if it throws during creation, the user won't have access to a `ComponentRef` in order to clean it up.

These changes automatically untrack the related LViews if the component couldn't be created.

PR Close #57546
2024-08-27 13:29:09 -07:00
Alan Agius b74b5f0c06 build: remove Sourcemaps from Schematics bundles (#57545)
This commit reduces the bundle size of `@angular/core` by 19.5 MB by excluding sourcemaps from the schematics.

Since the `esbuild` rule doesn't allow disabling sourcemaps directly, we work around this by setting the sourcemaps to `external`. Afterward, we filter the output to include only the `.js` files.

PR Close #57545
2024-08-27 13:27:33 -07:00
Angular Robot 4231404065 build: update github/codeql-action action to v3.26.5 (#57543)
See associated pull request for more information.

PR Close #57543
2024-08-27 13:25:58 -07:00
Angular Robot adcdfeca16 build: update dependency @babel/generator to v7.25.5 (#57542)
See associated pull request for more information.

PR Close #57542
2024-08-27 13:24:59 -07:00
Andrew Kushnir 4f8c406664 refactor(compiler): extend directive mock to avoid failing at matching logic (#57537)
This commit updates a directive mock instance to include an extra field that a compiler code was expecting, which caused issues while processing elements with local refs and exported directives.

PR Close #57537
2024-08-27 13:23:30 -07:00
Paul Gschwendtner 206b7be7f7 ci: fix fw-testing group triggering for schematics testing changes (#57526)
The `fw-testing` group should not trigger for schematic changes.

PR Close #57526
2024-08-27 13:21:48 -07:00
Andrew Scott 804925b114 fix(router): Do not unnecessarily run matcher twice on route matching (#57530)
This commit makes a small update to the route matching algorithm to
avoid running the matcher function of a route twice.

fixes #57511

PR Close #57530
2024-08-27 13:20:58 -07:00
Paul Gschwendtner 6a953c6ed4 refactor(migrations): properly handle multi query migration (#57525)
Properly handles queries with multiple results, by extracting the
type from the `QueryList`.

Also adds more tests and handles imports.

PR Close #57525
2024-08-27 13:20:19 -07:00
Paul Gschwendtner 01cae95596 refactor(migrations): initial migration logic for converting to signal queries (#57525)
Adds initial migration logic to convert decorator query declarations to
signal queries.

We will re-use more part of the signal input migration in follow-ups, to
properly migrate, and e.g. even handle control flow

PR Close #57525
2024-08-27 13:20:19 -07:00
AleksanderBodurri 371ad098d5 fix(devtools): ignore DOM Nodes from other frames when performing render tree detection (#57518)
Previously, if an application had DOM Nodes injected into it from other frames, DevTools would fail to parse component trees with the render tree strategy properly because of an instanceof Node check that the framework performs.

Now we check for instanceof Node before even calling framework debug APIs on DOM nodes so that we can skip nodes that come from other frames entirely.

PR Close #57518
2024-08-27 13:19:35 -07:00
Angular Robot 73ad6ea647 build: update dependency jsdom to v25 (#57514)
See associated pull request for more information.

PR Close #57514
2024-08-27 13:17:39 -07:00
Matthieu Riegler d3366815ee refactor(animations): Add loading strategy for the Async Animations (#57493)
In some cases apps need to schedule a bit later the loading of the animation module. This private token will allow to investigate which other strategy could be useful.

PR Close #57493
2024-08-27 13:15:51 -07:00
Doug Parker 76ec60d73c refactor(compiler): ensure context is always provided for WhitespaceVisitor (#56507)
When disabling `i18nPreserveSignificantWhitespaceForLegacyExtraction` I was looking at a test case with ICU messages containing leading and trailing whitespace:

```angular
<div i18n>
  {apples, plural, =other {I have many apples.}}
</div>
```

This would historically generate two messages:

```javascript
const MSG_TMP = goog.getMsg('{apples, plural, =other {I have many apples.}}');
const MSG_FOO = goog.getMsg(' {$ICU} ', { 'ICU': MSG_TMP });
```

But I found that I was getting just one message:

```javascript
const MSG_TMP = goog.getMsg(' {apples, plural, =other {I have many apples.}} ');
```

This is arguably an improvement, but changed the messages and message IDs, which isn't desirable with this option. I eventually traced this back to the `isIcu` initialization in [`i18n_parser.ts`](/packages/compiler/src/i18n/i18n_parser.ts):

```typescript
const context: I18nMessageVisitorContext = {
  isIcu: nodes.length == 1 && nodes[0] instanceof html.Expansion,
  // ...
};
```

[`_I18nVisitor.prototype.visitExpansion`](/packages/compiler/src/i18n/i18n_parser.ts) uses this to decide whether or not to generate a sub-message for a given ICU expansion:

```typescript
if (context.isIcu || context.icuDepth > 0) {
  // Returns an ICU node when:
  // - the message (vs a part of the message) is an ICU message, or
  // - the ICU message is nested.
  const expPh = context.placeholderRegistry.getUniquePlaceholder(`VAR_${icu.type}`);
  i18nIcu.expressionPlaceholder = expPh;
  context.placeholderToContent[expPh] = {
    text: icu.switchValue,
    sourceSpan: icu.switchValueSourceSpan,
  };
  return context.visitNodeFn(icu, i18nIcu);
}

// Else returns a placeholder
// ICU placeholders should not be replaced with their original content but with the their
// translations.
// TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg
const phName = context.placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
context.placeholderToMessage[phName] = this.toI18nMessage([icu], '', '', '', undefined);
const node = new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
return context.visitNodeFn(icu, node);
```

Note that `isIcu` is the key condition between these two cases and depends on whether or not the ICU expansion has any siblings. The introduction of `WhitespaceVisitor` to `I18nMetaVisitor` trims insignificant whitespace, including empty text nodes not adjacent to an ICU expansion (from [`WhitespaceVisitor.prototype.visitText`](/packages/compiler/src/ml_parser/html_whitespaces.ts)):

```typescript
const isNotBlank = text.value.match(NO_WS_REGEXP);
const hasExpansionSibling =
  context && (context.prev instanceof html.Expansion || context.next instanceof html.Expansion);

if (isNotBlank || hasExpansionSibling) {
  // Transform node by trimming it...
  return trimmedNode;
}

return null; // Drop node which is empty and has no ICU expansion sibling.
```

`hasExpansionSibling` was intended to retain empty text nodes leading or trailing an ICU expansion, however `context` was `undefined`, so this check failed and the leading / trailing text nodes were dropped. This resulted in trimming the ICU text by dropping the leading / trailing whitespace nodes. Having only a single ICU expansion with no leading / trailing text nodes caused `_I18nVisitor` to initialize `isIcu` incorrectly and caused it to generate one message instead of two.

`WhitespaceVisitor` is supposed to get this context from `visitAllWithSiblings`. So the fix here is to make sure `WhitespaceVisitor` is always visited via this function which provides the required context. I updated all usage sites to make sure this context is use consistently and implemented the `WhitespaceVisitor.prototype.visit` method to throw when the context is missing to make sure we don't encounter a similar mistake in the future.

Unfortunately this broke one compliance test. Specifically the [`icu_logic/icu_only.js`](/home/douglasparker/Source/ng/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/icu_only.js) test which changed from generating:

```javascript
function MyComponent_Template(rf, ctx) {
  if (rf & 1) {
    i0.ɵɵi18n(0, 0);
  }
  // ...
}
```

To now generating:

```javascript
function MyComponent_Template(rf, ctx) {
  if (rf & 1) {
    i0.ɵɵtext(0, " ");
    i0.ɵɵi18n(1, 0);
    i0.ɵɵtext(2, "\n");
  }
  // ...
}
```

This test uses the default value `preserveWhitespaces: false` (`i18nPreserveSignificantWhitespaceForLegacyExtraction` should not affect compiled JS output, we already retain significant whitespace there). So what this indicates to me is that ICU logic is already broken because it's not preserving significant whitespace in this case. My change is probably a bug fix, but one which would affect the compiled runtime, which is not in scope here. The root cause is because using `visitAllWithSiblings` everywhere means the context is retained correctly in this case and the whitespace is leading/trailing an ICU message, therefore it is retained per the logic of `WhitespaceVisitor.prototype.visitText` I mentioned eariler.

To address this, I left one usage of `WhitespaceVisitor` using `html.visitAll` instead of `visitAllWithSiblings` to retain this bug. I has to lossen the assertion I put in `WhitespaceVisitor.prototype.visit` to make this possible, but it should still throw by default when misused, which is the important part.

PR Close #56507
2024-08-27 13:13:57 -07:00
Doug Parker 7bfe81700b refactor(compiler): add i18nPreserveWhitespaceForLegacyExtraction (#56507)
This configures whether or not to preserve whitespace content when extracting messages from Angular templates in the legacy (View Engine) extraction pipeline.

This includes several bug fixes which unfortunately cannot be landed without changing message IDs in a breaking fashion and are necessary to properly trim whitespace. Instead these bug fixes are included only when the new flag is disabled.

PR Close #56507
2024-08-27 13:13:57 -07:00
Michael van der Luit 3067633bc7 docs(docs-infra): Add query as an url query param to the api reference (#57062)
add an url query param to perform a search on api references directly via the url

PR Close #57062
2024-08-27 12:58:00 -07:00
AleksanderBodurri be82f282a4 fix(devtools): catch invalidated extension error to prevent devtools from spamming console (#55697)
When a browser extension is updated it becomes invalidated on currently open pages. If that extension then tries to send a message to those pages through `chrome.runtime.sendMessage(..)` then an error is thrown in the console

For Angular DevTools, this results in spamming the console with "Uncaught Error: Extension context invalidated." errors.

This commit catches that error and removes the event listener that triggers the `chrome.runtime.sendMessage(...)` call.

PR Close #55697
2024-08-27 12:54:44 -07:00
realstealthninja 03b35270b0 docs: fix typo in recommendations.ts (#57521)
PR Close #57521
2024-08-26 09:17:44 -07:00
Matthieu Riegler fe9005ce8e docs: fix formating in class-binding (#57447)
fixes #57430

PR Close #57447
2024-08-26 09:12:55 -07:00
Bjarki 03ec620e31 fix(upgrade): Address Trusted Types violations in @angular/upgrade (#57454)
Angular applications that are AngularJS hybrids are currently unable to
adopt Trusted Types due to violations eminating from an innerHTML
assignment in the @angular/upgrade package. This commit allows
developers of such applications to optionally ignore this class of
violations by configuring the Trusted Types header to allow the new
angular#unsafe-upgrade policy.

Note that the policy is explicitly labeled as unsafe as it does not in
any way mitigate the security risk of using AngularJS in an Angular
application, but does unblock Trusted Types adoption enabling XSS
protection for other parts of the application.

The implementation follows the approach taken in @angular/core;
see packages/core/src/util/security.

PR Close #57454
2024-08-26 09:04:48 -07:00
Thomas Nguyen 6d3a2af146 fix(core): Do not bubble capture events. (#57476)
These should only fire if the target is the same as the targetElement. Also, delete an out of date test since capture/non-capture tests are separately covered.

PR Close #57476
2024-08-23 14:46:55 -07:00
Paul Gschwendtner 223b7857ef refactor(migrations): replacements in tsurge are always serializable (#57501)
This removes an unnecessary type wrapping that can be removed because
`Replacements` is expected to be serializable by default.

Similar to how it's done in `TsurgeComplexMigration`.

PR Close #57501
2024-08-23 13:49:31 -07:00
Angular Robot 5bdae0387f build: update dependency mermaid to v11 (#57499)
See associated pull request for more information.

PR Close #57499
2024-08-23 13:00:59 -07:00
Peter 034b32c133 docs: fix typo of ChangeDetectionStrategy in the zoneless guide (#57482)
PR Close #57482
2024-08-23 12:59:09 -07:00
Kristiyan Kostadinov 2a89e184e0 fix(zone.js): more robust check for promise-like objects (#57388)
Fixes that Zone.js wasn't checking properly if an object is promise-like.

Fixes #57385.

PR Close #57388
2024-08-23 12:58:29 -07:00
Santosh 9b6865ec03 docs: add test for i18n code block and fix rendering (#57210)
PR Close #57210
2024-08-23 12:57:15 -07:00
Alex Rickabaugh 6059ca8f1f refactor(core): restructure AfterRenderManager to connect related phases (#57453) (#57504)
The `afterRender` infrastructure was first implemented around the idea of
independent, singular hooks. It was later updated to support a spec of
multiple hooks that pass values from one to another as they execute, but the
implementation still worked in terms of singular hooks under the hood. This
creates a number of maintenance issues, and a few bugs. For example, when
one hook fails, further hooks in the pipeline should no longer execute, but
this was hard to ensure under the old design.

This refactoring restructures `afterRender` infrastructure significantly to
introduce the concept of a "sequence", a collection of hooks of different
phases that execute together. Overall, the implementation is simplified
while making it more resilient to issues and future use cases, such as the
upcoming `afterRenderEffect`.

As part of this refactoring, the `internalAfterNextRender` concept is
removed, as well as the unused `queueStateUpdate` concept which used it.

PR Close #57453

PR Close #57504
2024-08-23 12:48:01 -07:00
Alex Rickabaugh 800f6c8ca3 refactor(core): track dirtiness bits in ApplicationRef (#57453) (#57504)
Previously the zoneless scheduler had a concept of whether views needed to
be refreshed or not, based on the notification type that was received. It
tracked this information as a boolean.

This commit refactors things to track dirtiness in `ApplicationRef` itself,
as a `dirtyFlags` field with bits corresponding to either view tree
dirtiness or after-render hooks.

PR Close #57453

PR Close #57504
2024-08-23 12:48:01 -07:00
Paul Gschwendtner 87c594b90b refactor(migrations): add support for simpler variant of tsurge migration (#57484)
Introduces a simpler, smaller variant of the current `Tsurge` migration
class. The difference is simply that for the migration phase (the third
stage), some migrations do not need a full set of workers re-analyzing
every compilation unit again to compute the "final migration
replacements".

This can be the case, for example, if a migration eagerly computes all
replacements in the analyze stage, visiting every unit, and then after
deriving the global metadata, problematic replacements are simply
filtered (e.g. via some unique IDs again).

PR Close #57484
2024-08-23 12:01:59 +00:00
Paul Gschwendtner f133489a16 refactor(migrations): clean up unused google3 code from signal input migration (#57484)
The code was replaced by the automatic Tsurge batch runner. This commit
cleans up the now unused code.

PR Close #57484
2024-08-23 12:01:59 +00:00
Otmane 09deb24d74 docs: fix indentation of file tree in app-shell (#57436)
This commit adds indentations to the file tree section in `app-shell.md`, it makes the tree easier to read. The indentation is now two spaces instead of no spaces.

PR Close #57436
2024-08-22 09:11:50 -07:00
Paweł Kubiak f1bc6f895b docs(docs-infra): add top level banner component (#57458)
docs(docs-infra): add top level banner component

- create top level banner component
- write unit tests
- close banner and keep state in the local storage
- fix: support screens of tablets and phones

PR Close #57458
2024-08-22 09:06:47 -07:00
Matthieu Riegler 5f95afb331 docs: new date for v19 (#57461)
PR Close #57461
2024-08-22 09:04:41 -07:00
Andrew Kushnir b30bc4531a refactor(compiler): create an internal util method to detect matching directives and pipes (#57466)
This commit adds an internal util method that allows to detect:

* which selectors are matching nodes in a template
* which pipes are present in a template

Both directives and pipes are split into 2 buckets: eagerly used and the ones that might potentially be defer-loaded.

PR Close #57466
2024-08-22 09:04:04 -07:00
Dylan Hunn a6225a6cdd release: cut the v18.2.1 release 2024-08-22 09:00:21 -07:00
Matthieu Riegler 69b20d8391 docs(docs-infra): fix regressions around members & deprecations (#57465)
This commit fixes regressions introduced by #57255

PR Close #57465
2024-08-21 11:26:27 -07:00
steciuk 6089772fe8 docs: remove extra space in DI example code (#57472)
PR Close #57472
2024-08-21 11:25:54 -07:00
Angular Robot 1a5f5ee5ce build: update github/codeql-action action to v3.26.3 (#57455)
See associated pull request for more information.

PR Close #57455
2024-08-20 13:20:01 -07:00
Angular Robot 76808cc328 build: update dependency saucelabs to v8 (#57413)
See associated pull request for more information.

PR Close #57413
2024-08-20 13:19:11 -07:00
Adam Koch 980c64abaa docs: remove incorrect documentation around the use of timezone abbreviations (#57425)
The ability to use an abbreviated timezone was removed in v5 (issue https://github.com/angular/angular/issues/20225 discusses this) and no alternative has been merged. See issue https://github.com/angular/angular/issues/40865 and PR https://github.com/angular/angular/pull/48482 The text suggests there is a way to pass in a timezone other than by an offset but there isn't.

PR Close #57425
2024-08-20 10:51:37 -07:00
Paul Gschwendtner df3e9c1661 refactor(migrations): leverage tsurge for signal input migration (#57451)
This commit simplifies the batching support for the signal input
migration by using the new tsurge framework we've built.

This allows for consistent setup across all possible entry-points and
also simplifies the 1P setup given that we can simply use the Tsurge
macros, instead of having to maintain our own Go-based runner.

PR Close #57451
2024-08-19 22:44:43 -07:00
Paul Gschwendtner cb7d817edd refactor(migrations): improve generic assignability in tsurge and pass more info (#57451)
* Improves some of the generic assignability for tsurge. Anything is
  allowed to be returned from an overridden `prepareProgram` method.
  This is useful for the signal input migration.
* Passes the absolute root paths to migrations. This is helpful for the
  signal input migration and there is no other way to access it. It's
  better to pass specifically, compared to passing the whole unsafe
  `ParsedConfiguration` object.

PR Close #57451
2024-08-19 22:44:43 -07:00
Angular Robot 8645152f8c build: update dependency ngx-progressbar to v12 (#57452)
See associated pull request for more information.

Closes #57443 as a pr takeover

PR Close #57452
2024-08-19 22:43:49 -07:00
Matthieu Riegler 0979ca0354 docs(docs-infra): Fix heading parsing (#57386)
fixes #57384

PR Close #57386
2024-08-19 09:24:08 -07:00
Matthieu Riegler af20b2a161 docs(docs-infra): remove remaining ngZone (#57229)
PR Close #57229
2024-08-19 09:22:01 -07:00
Matthieu Riegler b5719ba5eb docs(docs-infra): Add tests for marked rendering (#57344)
On top of #57338, to make sure we prevent similar regressions on marked rendering.

PR Close #57344
2024-08-19 09:21:17 -07:00
Michael Derfler c4901d4dfe docs: fix links to docs (#57391)
PR Close #57391
2024-08-19 09:20:16 -07:00
Andrew Kushnir 1682c60938 docs: add eager to lazy route migration (#57421)
PR Close #57421
2024-08-19 09:19:43 -07:00
Andrew Kushnir c65ab136bf docs: cleanup existing migration docs for consistency (#57421)
PR Close #57421
2024-08-19 09:19:43 -07:00
Andrew Kushnir c89be37193 docs: drop references to removed typed-forms migration (#57421)
PR Close #57421
2024-08-19 09:19:43 -07:00
Andrew Kushnir f723288adf docs: drop references to removed module-with-providers migration (#57421)
PR Close #57421
2024-08-19 09:19:42 -07:00
Joey Perrott 58e00deb34 fix(docs-infra): leverage http_server rule from @angular/build-tooling for adev local serving (#57428)
Use the http_server rule to serve adev locally to allow for slightly faster rebuilds and livereload on changes

PR Close #57428
2024-08-19 09:18:21 -07:00
Paul Gschwendtner 57db366522 refactor(migrations): framework to build batchable migrations (#57396)
Introduces a migration framework to build batchable migrations that
can run in Large Scale mode against e.g. all of Google, using workers.

This is the original signal input migration infrastructure extracted
into a more generic framework that we can use for writing additional
ones for output, signal queries etc, while making sure those are not
scoped to a single `ts.Program` that limits them to per-directory
execution in very large projects (e.g. G3).

The migration will be updated to use this, and in 1P we will add
helpers to easily integrate such migrations into a Go-based pipeline
runner.

PR Close #57396
2024-08-17 08:54:16 -05:00
Andrew Kushnir 3e6ee7eaff docs: update CHANGELOG to drop a breaking change note from 18.2.0 version (#57420)
This commit updates the CHANGELOG to remove an entry about a breaking change in Zone.js, since the change was not actually released. It will be released separately in Zone.js package.

PR Close #57420
2024-08-16 11:20:46 -04:00
Andrew Kushnir 086b754cef docs: add withEventReplay and withI18nSupport links to provideClientHydration docs (#57382)
PR Close #57382
2024-08-15 16:10:28 -04:00
vladboisa 4bd9ba714c docs(docs-infra): move link tag for correct view (#57395)
Move the link tag to the down, for correctly parsing of '@link'

Fixes #57332

PR Close #57395
2024-08-15 15:51:53 -04:00
cexbrayat 3b63082384 fix(migrations): avoid migrating route component in tests (#57317)
The migration was migrating all files in a project (like most migrations).
As there is no gain in migrating components used in test files. Excluding the test files reduces the migration noise.

PR Close #57317
2024-08-15 15:51:22 -04:00
David LJ 25bc810f83 docs: update version table for 18.2 (#57414)
PR Close #57414
2024-08-15 15:50:54 -04:00
Andrew Scott 9de30a7b1c fix(core): Allow zoneless scheduler to run inside fakeAsync (#56932)
The zoneless scheduler callback was executed in the root zone rather
than simply in `runOutsideAngular` to allow us to land the hybrid mode
change detection (scheduler always enabled, even for zones) without
breaking a ton of existing `fakeAsync` tests that could/would fail with
the "timer(s) still in queue" error. However, this caused another
problem: when a test executes inside `fakeAsync`, it cannot flush the
scheduled time. A similar problem exists with event and run coalescing (#56767).
This change would allow `fakeAsync` to flush the zoneless-scheduled
change detections and minimize breaking existing tests
by flushing pending timers at the end of the test, which actually now
matches what's done internally.

PR Close #56932
2024-08-15 12:32:24 -04:00
Matthieu Riegler b1ed7e2b5f docs(docs-infra): Add support for function/method overloads (#57379)
PR Close #57379
2024-08-15 12:12:26 -04:00
Kristiyan Kostadinov 6b4357fae4 fix(migrations): preserve type when using inject decorator (#57389)
Updates the migration so that it passes the type as a generic in the case of `@Inject(SOME_TOKEN) foo: SomeType`. This is done for two reasons:
1. It's a fairly common pattern and it ensures that the code can still be compiled.
2. It avoids leaving behind unused imports.

PR Close #57389
2024-08-15 12:11:59 -04:00
Kristiyan Kostadinov 0bb649b8fa fix(migrations): account for members with doc strings and no modifiers (#57389)
Fixes that the migration was duplicating the doc strings of members that don't have modifiers.

PR Close #57389
2024-08-15 12:11:59 -04:00
Andrew Kushnir 286012fb89 fix(core): handle hydration of components that project content conditionally (#57383)
This commit fixes an issue when hydration serialization tries to calculate DOM path to a content projection node (`<ng-content>`), but such nodes do not have DOM representation.

Resolves #56750.

PR Close #57383
2024-08-15 11:22:05 -04:00
Erik Wegner 9155f75e36 docs(common): fix example code (#57406)
Fix example code for interceptor.

PR Close #57406
2024-08-15 10:46:03 -04:00
Jeremy Elbourn 93410eda0a docs: add small clarification to ng-content (#57408)
This expands the recommendation to avoid conditionally showing `<ng-content>` with additional context

PR Close #57408
2024-08-15 10:45:16 -04:00
Andrew Kushnir d49e083999 release: cut the v18.2.0 release 2024-08-14 09:40:36 -07:00
Matthieu Riegler e50d239e80 docs(docs-infra): fix markdown link rendering (#57377)
fixes #57376

PR Close #57377
2024-08-13 15:53:56 -07:00
Thomas Nguyen e30c60e89f refactor(core): Add experimental support to have one event contract when there are multiple apps on the page. (#57355)
This may be removed if this turns out not to work out so well...

PR Close #57355
2024-08-13 12:10:35 -07:00
Thomas Nguyen 8104fc2126 refactor(core): Call stopPropagation and preventDefault unconditionally within the patched methods. (#57354)
This fixes a few tests in g3 and is a bug fix for the event dispatcher. Otherwise, bubbling might
continue.

PR Close #57354
2024-08-13 12:09:57 -07:00
Thomas Nguyen 7accd9d885 fix(core): Account for addEventListener to be passed a Window or Document. (#57354)
This happened to work for other event listeners since both had a
addEventListener method.

PR Close #57354
2024-08-13 12:09:57 -07:00
Andrew Scott 334c99f968 refactor(docs-infra): Update search results to display content when it is matched (#57298)
This commit updates the search results to query for the content as well
as a snippet of the content for display when it's the content that
matches the query rather than any of the headers.

PR Close #57298
2024-08-13 12:07:49 -07:00
Kristiyan Kostadinov b1a9d0f4de fix(migrations): avoid duplicating comments when generating properties (#57367)
Updates the logic that generates new component properties to avoid duplicating their doc strings.

PR Close #57367
2024-08-13 09:54:25 -07:00
Kristiyan Kostadinov 5d76401ff5 fix(migrations): preserve optional parameters (#57367)
Makes it so the inject migration preserves the optional token when declaring a parameter. This came up in some testing as something that can be potentially breaking for classes that implement interfaces.

PR Close #57367
2024-08-13 09:54:25 -07:00
Andrew Kushnir de85979648 fix(core): skip hydration for i18n nodes that were not projected (#57356)
This commit fixes an issue that happens when an i18n block is defined as a projectable content, but a parent component doesn't project it. With an extra check added in this commit, the code will be taking a regular "creation" pass instead of attempting hydration.

Resolves #57301.

PR Close #57356
2024-08-13 09:42:42 -07:00
Paul Gschwendtner bbc970bb0b refactor(migrations): always add readonly to migrated signal inputs (#57368)
Signal inputs are no longer updated by assignment, unlike `@Input()`, so
a good practice is adding `readonly` for the `InputSignal`— which should
never be swapped out.

This is a safe operation because the migration skips all inputs that are
being written anyway.

PR Close #57368
2024-08-13 09:41:09 -07:00
Paul Gschwendtner 87d00d26ff refactor(migrations): use input() shorthand if possible in input migration (#57368)
In some cases, the migration can detect when `input()` as a shorthand
may be usable. This commit adds such detection and migrates inputs to
this form when possible.

PR Close #57368
2024-08-13 09:41:08 -07:00
Matthieu Riegler 84752069f2 docs(docs-infra): Update marked to 14 (#57363)
This is a backport of the marked update and the fix that went with it (#57338)

PR Close #57363
2024-08-13 09:33:04 -07:00
Angular Robot d3b46ade94 build: update scorecard action dependencies (#57358)
See associated pull request for more information.

PR Close #57358
2024-08-13 09:30:16 -07:00
Angular Robot 1828d11d84 build: update dependency @babel/core to v7.25.2 (#57191)
See associated pull request for more information.

PR Close #57191
2024-08-13 09:29:35 -07:00
Paul Gschwendtner b8c82fa2f7 refactor(migrations): handle jit: true component templates in signal input migration (#57347)
Components with `jit: true` are not processed by the Angular compiler,
so we cannot ask the template checker for the parsed template; simply
because the template wasn't attempted to be parsed.

We still can migrate simple cases of such components, commonly seen in
unit tests. We do this by manually parsing the template and making use
of the reference fallback resolution that is also used for host bindings
(where we don't have any type check block information).

PR Close #57347
2024-08-12 15:17:41 -07:00
Paul Gschwendtner fa77c9e5b4 refactor(migrations): expose angular compiler options to signal input migration (#57347)
Instead of exposing just the `ts.CompilerOptions`, we should expose the
actual Angular compiler options throuhgout the signal input migration.

This will be useful for parsing templates, in cases of JIT-opted
components.

PR Close #57347
2024-08-12 15:17:40 -07:00
Paul Gschwendtner 5d16c286bf refactor(migrations): handle safe property reads in signal input migration (#57318)
As of this commit, the migration will also inspect safe property reads
and migrate them, if they reference an input that is being migrated.

PR Close #57318
2024-08-12 12:12:23 -07:00
Paul Gschwendtner 604270619a perf(migrations): speed up signal input migration by combining two analyze phases (#57318)
Instead of revisiting each source file, and each of its child nodes
twice, we now visit them together using a grouped AST visitor that only
traverses each source file once.

This seemed to speed up migration by 6-8% locally, but is likely
noticable better with large compilation scopes.

PR Close #57318
2024-08-12 12:12:23 -07:00
Paul Gschwendtner 1b546975f0 refactor(migrations): use import manager in signal input migration (#57318)
Instead of fiddling manually with the imports, which worked well, but
comes at a cost of complexity— we are now using the canonical import
manager. This simplifies deletion, insertion and updating of imports.

Notably, our import manager is not super great at preserving whitespaces
right now, but we assume a formatter runs over migrated code anyway.

PR Close #57318
2024-08-12 12:12:23 -07:00
Paul Gschwendtner 16ae748257 refactor(migrations): add best effort mode to signal input migration (#57318)
Introduces a best effort mode for the signal input migration. This mode
can be used to aggresively migrate as much as possible, ignoring most
of the incompatibility reasons, like "writes to the input".

PR Close #57318
2024-08-12 12:12:22 -07:00
Paul Gschwendtner ecb0f8f161 refactor(migrations): add initial docs for signal input migration incompatibility reasons (#57318)
Adds a markdown document capturing some of the incompatibilty reasons
on why the input wasn't migrated.

PR Close #57318
2024-08-12 12:12:22 -07:00
Gilad Mautner cea3e4b594 docs: reorder afterView and afterContent related lifecycle hooks (#57307)
PR Close #57307
2024-08-09 13:55:39 -07:00
Paul Gschwendtner 448279f57e refactor(migrations): detect if an input is not narrowed and can be migrated (#57308) (#57323)
By default, we don't migrate inputs if they are part of e.g. `@if` for
now. That is because we don't have the template narrowing feature
available yet.

To improve impact of the migration until we have the narrowing, we add
some additional checks that allow us to migrate instances of inputs that
are part of e.g. `@if` but are actually not used inside (and hence are
guaranteed to be _not_ narrowed).

PR Close #57308

PR Close #57323
2024-08-09 12:17:34 -07:00
Andrew Kushnir 5558e275ee fix(core): take skip hydration flag into account while hydrating i18n blocks (#57299)
This commit updates serialization and hydration i18n logic to take into account situations when i18n blocks are located within "skip hydration" blocks.

Resolves #57105.

PR Close #57299
2024-08-09 08:07:49 -07:00
Andrew Kushnir 86216792fd fix(core): complete post-hydration cleanup in components that use ViewContainerRef (#57300)
Previously, if a component injects a `ViewContainerRef`, the post-hydration cleanup process doesn't visit inner views to cleanup dehydrated views in nested LContainers. This commit updates the logic to recognize this situation and enter host LView to complete cleanup.

Resolves #56989.

PR Close #57300
2024-08-09 08:07:13 -07:00
Kristiyan Kostadinov cab6c23602 refactor(migrations): add internal cleanup logic (#57315)
Expands the `inject` migration to add some cleanups that are only relevant internally. Externally this isn't exposed to users.

PR Close #57315
2024-08-09 08:02:34 -07:00
Andrew Scott 296216cbe1 fix(core): Allow hybrid CD scheduling to support multiple "Angular zones" (#57267)
This commit updates the inside/outside NgZone detection of the hybrid CD
scheduling to track the actual instance of the NgZone being used rather
than the name "Angular" (how `isInsideAngularZone` works). This allows
the scheduling to work correctly when there are multiple versions of
Angular running on the page.

fixes #57261

PR Close #57267
2024-08-08 10:46:26 -07:00
Thomas Nguyen 9af760eb51 fix(core): Account for addEventListener to be passed a Window or Document. (#57282)
This happened to work for other event listeners since both had a
addEventListener method.

PR Close #57282
2024-08-08 08:32:11 -07:00
Kristiyan Kostadinov b16dd6d67f fix(compiler-cli): generate valid TS 5.6 type checking code (#57303)
Currently in some scenarios the compiler generates code like `null as any ? foo : bar` which will be invalid with [an upcoming TypeScript change](https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/#disallowed-nullish-and-truthy-checks). These changes switch to generating `0 as any` which is exempt from the change.

**Note:** I'm not starting the work to fully get us on TS 5.6 until the 18.2 release comes out, but this change is necessary to unblock an internal team.

PR Close #57303
2024-08-08 08:30:13 -07:00
Jessica Janiuk 02d613ef9c release: cut the v18.2.0-rc.0 release 2024-08-07 12:27:07 -07:00
366 changed files with 16158 additions and 5880 deletions
+2 -3
View File
@@ -158,9 +158,8 @@ test:saucelabs --flaky_test_attempts=1
# --ng_perf will ask the Ivy compiler to produce performance results for each build.
build --flag_alias=ng_perf=//packages/compiler-cli:ng_perf
# --adev_fast will run adev build/serve in a faster mode, skipping things like prerendering
# for local development.
build --flag_alias=fast_adev=//adev:fast_build_mode
# --prerender_adev will run adev build/serve in a full mode, performing prerendering
build --flag_alias=prerender_adev=//adev:prerender_adev
####################################################
# User bazel configuration
@@ -103,7 +103,7 @@ export async function setupRedirect(deployment: Deployment) {
}
function firebase(cmd: string, cwd?: string) {
spawnSync('npx', `-y firebase-tools@latest ${cmd}`.split(' '), {
spawnSync('npx', `-y firebase-tools@13.15.1 ${cmd}`.split(' '), {
cwd,
encoding: 'utf-8',
shell: true,
+1 -1
View File
@@ -11415,7 +11415,7 @@ async function setupRedirect(deployment) {
await rm(tmpRedirectDir, { recursive: true });
}
function firebase(cmd, cwd) {
spawnSync("npx", `-y firebase-tools@latest ${cmd}`.split(" "), {
spawnSync("npx", `-y firebase-tools@13.15.1 ${cmd}`.split(" "), {
cwd,
encoding: "utf-8",
shell: true,
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel build //adev:build --config=release
run: yarn bazel build //adev:build --prerender_adev --config=release
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@03b8a7dffd1205e061f0bee949024ebefc2a6592
with:
workflow-artifact-name: 'adev-preview'
+2 -2
View File
@@ -84,7 +84,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev in fast mode to ensure it continues to work
run: yarn bazel build //adev:build --fast_adev --config=release
run: yarn bazel build //adev:build --config=release
- name: Run tests
run: yarn bazel test //adev:test
@@ -192,7 +192,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel build //adev:build --config=release
run: yarn bazel build //adev:build --prerender_adev --config=release
- name: Deploy to firebase
uses: ./.github/actions/deploy-docs-site
with:
+1 -1
View File
@@ -113,7 +113,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev in fast mode to ensure it continues to work
run: yarn bazel build //adev:build --fast_adev --config=release
run: yarn bazel build //adev:build --config=release
- name: Run tests
run: yarn bazel test //adev:test
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
# Upload the results as artifacts.
- name: 'Upload artifact'
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: SARIF file
path: results.sarif
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
with:
sarif_file: results.sarif
+1 -1
View File
@@ -372,7 +372,7 @@ groups:
<<: *defaults
conditions:
- >
contains_any_globs(files.exclude('packages/compiler-cli/*').exclude('packages/language-service/*').exclude('packages/service-worker/*'), [
contains_any_globs(files.exclude('packages/compiler-cli/*').exclude('packages/language-service/*').exclude('packages/service-worker/*').exclude('packages/core/schematics/*'), [
'packages/**/testing/**/{*,.*}',
])
reviewers:
+3278 -2137
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -181,3 +181,15 @@ http_archive(
strip_prefix = "sc-4.8.2-osx",
url = "https://saucelabs.com/downloads/sc-4.8.2-osx.zip",
)
yarn_install(
name = "npm_ts_versions",
data = [
YARN_LABEL,
"//:.yarnrc",
],
exports_directories_only = False,
package_json = "//packages/core/schematics/migrations/signal-migration/test/ts-versions:package.json",
yarn = YARN_LABEL,
yarn_lock = "//packages/core/schematics/migrations/signal-migration/test/ts-versions:yarn.lock",
)
+13 -20
View File
@@ -1,4 +1,5 @@
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/http-server:index.bzl", "http_server")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
@@ -113,27 +114,27 @@ copy_to_bin(
)
bool_flag(
name = "fast_build_mode",
name = "prerender_adev",
build_setting_default = False,
)
config_setting(
name = "fast",
name = "no_prerender",
flag_values = {
":fast_build_mode": "true",
":prerender_adev": "false",
},
)
config_setting(
name = "full",
name = "prerender",
flag_values = {
":fast_build_mode": "false",
":prerender_adev": "true",
},
)
config_based_architect_flags = select({
":fast": ["--no-prerender"],
":full": ["--prerender"],
":no_prerender": ["--no-prerender"],
":prerender": ["--prerender"],
})
architect(
@@ -154,21 +155,13 @@ architect(
],
)
architect(
http_server(
name = "serve",
args = [
"angular-dev:serve",
"--poll=1000",
"--live-reload",
"--watch",
],
chdir = package_name(),
data = ensure_local_package_deps(APPLICATION_DEPS) + APPLICATION_ASSETS + [
":application_files_bin",
],
tags = [
"no-remote-exec",
additional_root_paths = [
"angular/adev/build/browser",
],
enable_dev_ui = True,
deps = [":build"],
)
architect_test(
+1
View File
@@ -22,6 +22,7 @@ ts_library(
"//adev/shared-docs/components/slide-toggle",
"//adev/shared-docs/components/table-of-contents",
"//adev/shared-docs/components/text-field",
"//adev/shared-docs/components/top-level-banner",
"//adev/shared-docs/components/viewers",
],
)
@@ -18,6 +18,7 @@ ng_module(
"//adev/shared-docs/components/navigation-list:__pkg__",
"//adev/shared-docs/components/table-of-contents:__pkg__",
"//adev/shared-docs/components/text-field:__pkg__",
"//adev/shared-docs/components/top-level-banner:__pkg__",
"//adev/shared-docs/components/viewers:__pkg__",
],
deps = [
+1
View File
@@ -15,3 +15,4 @@ export * from './table-of-contents/table-of-contents.component';
export * from './text-field/text-field.component';
export * from './icon/icon.component';
export * from './search-dialog/search-dialog.component';
export * from './top-level-banner/top-level-banner.component';
@@ -22,6 +22,7 @@ ng_module(
"//adev/shared-docs/interfaces",
"//adev/shared-docs/pipes",
"//adev/shared-docs/services",
"//packages/common",
"//packages/core",
"//packages/forms",
"//packages/router",
@@ -10,65 +10,62 @@
></docs-text-field>
@if (searchResults() && searchResults()!.length > 0) {
<ul class="docs-search-results docs-mini-scroll-track">
@for (result of searchResults(); track result.objectID) {
<li docsSearchItem [item]="result">
@if (result.url) {
<a [routerLink]="'/' + result.url | relativeLink: 'pathname'" [fragment]="result.url | relativeLink: 'hash'">
<div>
<div class="docs-result-icon-and-type">
<!-- Icon -->
<span class="docs-search-result-icon" aria-hidden="true">
@if (result.hierarchy?.lvl0 === 'Docs') {
<i role="presentation" class="material-symbols-outlined docs-icon-small">
description
</i>
} @else if (result.hierarchy?.lvl0 === 'Tutorials') {
<i role="presentation" class="material-symbols-outlined docs-icon-small">code</i>
} @else if (result.hierarchy?.lvl0 === 'Reference') {
<i role="presentation" class="material-symbols-outlined docs-icon-small">
description
</i>
}
</span>
<!-- Results type -->
<span class="docs-search-results__type">{{ result.hierarchy?.lvl1 }}</span>
<ul class="docs-search-results docs-mini-scroll-track">
@for (result of searchResults(); track result.objectID) {
<li docsSearchItem [item]="result">
<a
[routerLink]="'/' + result.url | relativeLink: 'pathname'"
[fragment]="result.url | relativeLink: 'hash'"
>
<div>
<div class="docs-result-icon-and-type">
<!-- Icon -->
<span class="docs-search-result-icon" aria-hidden="true">
<i role="presentation" class="material-symbols-outlined docs-icon-small">
{{ result.hierarchy.lvl0 === 'Tutorials' ? 'code' : 'description'}}
</i>
</span>
<!-- Results type -->
<span class="docs-search-results__type">
@let snippet = result._snippetResult.hierarchy?.lvl1?.value ?? '';
<ng-container
[ngTemplateOutlet]="highlightSnippet"
[ngTemplateOutletContext]="{snippet}"
></ng-container>
</span>
</div>
@let content = result._snippetResult.content;
@let hierarchy = result._snippetResult.hierarchy;
@if (content || hierarchy?.lvl2 || hierarchy?.lvl3 || hierarchy?.lvl4) {
<span class="docs-search-results__type docs-search-results__lvl2">
@let snippet = getBestSnippetForMatch(result);
<ng-container
[ngTemplateOutlet]="highlightSnippet"
[ngTemplateOutletContext]="{snippet}"
></ng-container>
</span>
}
</div>
<!-- Hide level 2 if level 3 exists -->
<!-- Level 2 -->
@if (result.hierarchy?.lvl2 && !result.hierarchy?.lvl3) {
<span class="docs-search-results__type docs-search-results__lvl2">
{{ result.hierarchy?.lvl2 }}
</span>
}
<!-- Level 3 -->
@if (result.hierarchy?.lvl3) {
<span class="docs-search-results__type docs-search-results__lvl3">
{{ result.hierarchy?.lvl3 }}
</span>
}
</div>
<!-- Page title -->
<span class="docs-result-page-title">{{ result.hierarchy?.lvl0 }}</span>
</a>
<!-- Page title -->
<span class="docs-result-page-title">{{ result.hierarchy?.lvl0 }}</span>
</a>
</li>
}
</li>
}
</ul>
</ul>
} @else {
<div class="docs-search-results docs-mini-scroll-track">
@if (searchResults() === undefined) {
<div class="docs-search-results__start-typing">
<span>Start typing to see results</span>
<div class="docs-search-results docs-mini-scroll-track">
@if (searchResults() === undefined) {
<div class="docs-search-results__start-typing">
<span>Start typing to see results</span>
</div>
} @else if (searchResults()?.length === 0) {
<div class="docs-search-results__no-results">
<span>No results found</span>
</div>
}
</div>
} @else if (searchResults()?.length === 0) {
<div class="docs-search-results__no-results">
<span>No results found</span>
</div>
}
</div>
}
<div class="docs-algolia">
@@ -79,3 +76,14 @@
</div>
</div>
</dialog>
<ng-template #highlightSnippet let-snippet="snippet">
@let parts = splitMarkedText(snippet);
@for (part of parts; track $index) {
@if (part.highlight) {
<mark>{{part.text}}</mark>
} @else {
<span>{{part.text}}</span>
}
}
</ng-template>
@@ -48,6 +48,14 @@ dialog {
padding-inline-end: 1rem;
padding-block: 0.25rem;
mark {
background: #e62600;
background: var(--red-to-orange-horizontal-gradient);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
a {
color: var(--secondary-contrast);
display: flex;
@@ -20,6 +20,7 @@ import {
viewChild,
viewChildren,
} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {WINDOW} from '../../providers/index';
import {ClickOutside} from '../../directives/index';
@@ -34,6 +35,7 @@ import {Router, RouterLink} from '@angular/router';
import {filter, fromEvent} from 'rxjs';
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
import {RelativeLink} from '../../pipes/relative-link.pipe';
import {SearchResult, SnippetResult} from '../../interfaces';
@Component({
selector: 'docs-search-dialog',
@@ -47,6 +49,7 @@ import {RelativeLink} from '../../pipes/relative-link.pipe';
AlgoliaIcon,
RelativeLink,
RouterLink,
NgTemplateOutlet,
],
templateUrl: './search-dialog.component.html',
styleUrls: ['./search-dialog.component.scss'],
@@ -104,6 +107,46 @@ export class SearchDialog implements OnDestroy {
});
}
splitMarkedText(snippet: string): Array<{highlight: boolean; text: string}> {
const parts: Array<{highlight: boolean; text: string}> = [];
while (snippet.indexOf('<ɵ>') !== -1) {
const beforeMatch = snippet.substring(0, snippet.indexOf('<ɵ>'));
const match = snippet.substring(snippet.indexOf('<ɵ>') + 3, snippet.indexOf('</ɵ>'));
parts.push({highlight: false, text: beforeMatch});
parts.push({highlight: true, text: match});
snippet = snippet.substring(snippet.indexOf('</ɵ>') + 4);
}
parts.push({highlight: false, text: snippet});
return parts;
}
getBestSnippetForMatch(result: SearchResult): string {
// if there is content, return it
if (result._snippetResult.content !== undefined) {
return result._snippetResult.content.value;
}
const hierarchy = result._snippetResult.hierarchy;
if (hierarchy === undefined) {
return '';
}
function matched(snippet: SnippetResult | undefined) {
return snippet?.matchLevel !== undefined && snippet.matchLevel !== 'none';
}
// return the most specific subheader match
if (matched(hierarchy.lvl4)) {
return hierarchy.lvl4!.value;
}
if (matched(hierarchy.lvl3)) {
return hierarchy.lvl3!.value;
}
if (matched(hierarchy.lvl2)) {
return hierarchy.lvl2!.value;
}
// if no subheader matched the query, fall back to just returning the most specific one
return hierarchy.lvl3?.value ?? hierarchy.lvl2?.value ?? '';
}
ngOnDestroy(): void {
this.keyManager.destroy();
}
@@ -0,0 +1,49 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "top-level-banner",
srcs = [
"top-level-banner.component.ts",
],
assets = [
":top-level-banner.component.css",
"top-level-banner.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
],
deps = [
"//adev/shared-docs/components/icon",
"//adev/shared-docs/directives",
"//adev/shared-docs/providers",
"//packages/common",
"//packages/core",
],
)
sass_binary(
name = "style",
src = "top-level-banner.component.scss",
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":top-level-banner",
"//adev/shared-docs/providers",
"//packages/core",
"//packages/core/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,15 @@
@if (!hasClosed()) {
@if (link()) {
<a [href]="link()" class="docs-top-level-banner">
<h1 tabindex="-1" class="docs-top-level-banner-cta">{{ text() }}</h1>
</a>
} @else {
<div class="docs-top-level-banner">
<h1 tabindex="-1" class="docs-top-level-banner-cta">{{ text() }}</h1>
</div>
}
<button class="docs-top-level-banner-close" type="button" (click)="close()">
<docs-icon class="docs-icon_high-contrast">close</docs-icon>
</button>
}
@@ -0,0 +1,76 @@
:host {
&:not(:empty) {
z-index: 50;
position: fixed;
height: 2rem;
width: 100vw;
border-bottom: 1px solid var(--septenary-contrast);
text-align: center;
align-content: center;
backdrop-filter: blur(16px);
background-color: color-mix(in srgb, var(--page-background) 70%, transparent);
}
a.docs-top-level-banner {
width: 100%;
display: inherit;
}
h1.docs-top-level-banner-cta {
display: inline;
position: relative;
font-size: 0.875rem;
margin: 0;
background-image: var(--red-to-pink-to-purple-horizontal-gradient);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
width: fit-content;
font-weight: 500;
&::after {
content: '';
position: absolute;
width: 100%;
transform: scaleX(0);
height: 1px;
bottom: -2px;
left: 0;
background: var(--tertiary-contrast);
animation-name: shimmer;
-webkit-animation-duration: 5s;
-moz-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
}
&:hover {
h1.docs-top-level-banner-cta {
&::after {
transform: scaleX(1);
transform-origin: bottom left;
}
}
}
.docs-top-level-banner-close {
position: absolute;
top: 0.25rem;
right: 0.5rem;
color: var(--primary-contrast);
}
}
@keyframes shimmer {
0% {
transform: scaleX(0);
transform-origin: bottom right;
}
100% {
transform: scaleX(1);
transform-origin: bottom left;
}
}
@@ -0,0 +1,99 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {STORAGE_KEY_PREFIX, TopLevelBannerComponent} from './top-level-banner.component';
import {LOCAL_STORAGE} from '../../providers';
describe('TopLevelBannerComponent', () => {
let component: TopLevelBannerComponent;
let fixture: ComponentFixture<TopLevelBannerComponent>;
let mockLocalStorage: jasmine.SpyObj<Storage>;
const EXAMPLE_TEXT = 'Click Here';
const EXAMPLE_LINK = 'https://example.com';
const EXAMPLE_ID = 'banner-id';
beforeEach(async () => {
mockLocalStorage = jasmine.createSpyObj('Storage', ['getItem', 'setItem']);
await TestBed.configureTestingModule({
imports: [TopLevelBannerComponent],
providers: [{provide: LOCAL_STORAGE, useValue: mockLocalStorage}],
}).compileComponents();
fixture = TestBed.createComponent(TopLevelBannerComponent);
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.componentRef.setInput('id', EXAMPLE_ID);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should render an anchor element when link is provided', () => {
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.componentRef.setInput('link', EXAMPLE_LINK);
fixture.detectChanges();
const bannerElement = fixture.nativeElement.querySelector('a.adev-top-level-banner');
expect(bannerElement).toBeTruthy();
expect(bannerElement.getAttribute('href')).toBe(EXAMPLE_LINK);
expect(bannerElement.textContent).toContain(EXAMPLE_TEXT);
});
it('should render a div element when link is not provided', () => {
const EXAMPLE_TEXT = 'No Link Available';
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.detectChanges();
const bannerElement = fixture.nativeElement.querySelector('div.adev-top-level-banner');
expect(bannerElement).toBeTruthy();
expect(bannerElement.textContent).toContain(EXAMPLE_TEXT);
});
it('should correctly render the text input', () => {
const EXAMPLE_TEXT = 'Lorem ipsum dolor...';
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.detectChanges();
const bannerElement = fixture.nativeElement.querySelector('.adev-top-level-banner-cta');
expect(bannerElement).toBeTruthy();
expect(bannerElement.textContent).toBe(EXAMPLE_TEXT);
});
it('should set hasClosed to true if the banner was closed before', () => {
mockLocalStorage.getItem.and.returnValue('true');
component.ngOnInit();
expect(component.hasClosed()).toBeTrue();
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`);
});
it('should set hasClosed to false if the banner was not closed before', () => {
mockLocalStorage.getItem.and.returnValue('false');
component.ngOnInit();
expect(component.hasClosed()).toBeFalse();
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`);
});
it('should set hasClosed to false if accessing localStorage throws an error', () => {
mockLocalStorage.getItem.and.throwError('Local storage error');
component.ngOnInit();
expect(component.hasClosed()).toBeFalse();
});
it('should set the banner as closed in localStorage and update hasClosed', () => {
component.close();
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`,
'true',
);
expect(component.hasClosed()).toBeTrue();
});
});
@@ -0,0 +1,52 @@
import {ChangeDetectionStrategy, Component, inject, input, OnInit, signal} from '@angular/core';
import {ExternalLink} from '../../directives';
import {LOCAL_STORAGE} from '../../providers';
import {IconComponent} from '../icon/icon.component';
export const STORAGE_KEY_PREFIX = 'docs-was-closed-top-banner-';
@Component({
selector: 'docs-top-level-banner',
standalone: true,
imports: [ExternalLink, IconComponent],
templateUrl: './top-level-banner.component.html',
styleUrl: './top-level-banner.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TopLevelBannerComponent implements OnInit {
private readonly localStorage = inject(LOCAL_STORAGE);
/**
* Unique identifier for the banner. This ID is required to ensure that
* the state of the banner (e.g., whether it has been closed) is tracked
* separately for different events or instances. Without a unique ID,
* closing one banner could inadvertently hide other banners for different events.
*/
id = input.required<string>();
// Optional URL link that the banner should navigate to when clicked.
link = input<string>();
// Text content to be displayed in the banner.
text = input.required<string>();
// Whether the user has closed the banner.
hasClosed = signal<boolean>(false);
ngOnInit(): void {
// Needs to be in a try/catch, because some browsers will
// throw when using `localStorage` in private mode.
try {
this.hasClosed.set(this.localStorage?.getItem(this.getBannerStorageKey()) === 'true');
} catch {
this.hasClosed.set(false);
}
}
close(): void {
this.localStorage?.setItem(this.getBannerStorageKey(), 'true');
this.hasClosed.set(true);
}
private getBannerStorageKey(): string {
return `${STORAGE_KEY_PREFIX}${this.id()}`;
}
}
+27 -2
View File
@@ -6,14 +6,39 @@
* found in the LICENSE file at https://angular.dev/license
*/
export interface SnippetResult {
value: string;
matchLevel: 'none' | 'full' | string;
}
/* The interface represents Algolia search result item. */
export interface SearchResult {
/* The url link to the search result page */
url?: string;
url: string;
/* The hierarchy of the item */
hierarchy?: Hierarchy;
hierarchy: Hierarchy;
/* The unique id of the search result item */
objectID: string;
/**
* The type of the result. A content result will have
* matched the content. A result of type 'lvl#' may have i
* matched a lvl above it. For example, a type 'lvl3' may be
* included in results because its 'lvl2' header matched the query.
*/
type: string;
/** Documentation content (not headers) */
content: string | null;
/** Snippets of the matched text */
_snippetResult: {
hierarchy?: {
lvl0?: SnippetResult;
lvl1?: SnippetResult;
lvl2?: SnippetResult;
lvl3?: SnippetResult;
lvl4?: SnippetResult;
};
content?: SnippetResult;
};
}
/* The hierarchy of the item */
+3 -3
View File
@@ -19,9 +19,9 @@
"fast-glob": "~3.3.2",
"fflate": "^0.8.2",
"html-entities": "~2.5.2",
"jsdom": "~24.1.0",
"marked": "~12.0.2",
"mermaid": "^10.8.0",
"jsdom": "~25.0.0",
"marked": "~14.0.0",
"mermaid": "^11.0.0",
"shiki": "^1.10.3"
},
"exports": {
@@ -13,6 +13,9 @@ def _extract_api_to_json(ctx):
# 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 entry_point for from which to extract public symbols.
args.add(ctx.file.entry_point)
@@ -82,6 +85,9 @@ extract_api_to_json = rule(
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,
@@ -1,7 +1,13 @@
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 {NgtscProgram, CompilerOptions, createCompilerHost, DocEntry} from '@angular/compiler-cli';
import {
NgtscProgram,
CompilerOptions,
createCompilerHost,
DocEntry,
EntryCollection,
} from '@angular/compiler-cli';
import ts from 'typescript';
function main() {
@@ -10,6 +16,7 @@ function main() {
const [
moduleName,
moduleLabel,
entryPointExecRootRelativePath,
srcs,
outputFilenameExecRootRelativePath,
@@ -54,13 +61,25 @@ function main() {
return result.concat(JSON.parse(readFileSync(path, {encoding: 'utf8'})) as DocEntry[]);
}, []);
const extractedEntries = program.getApiDocumentation(entryPointExecRootRelativePath);
const apiDoc = program.getApiDocumentation(entryPointExecRootRelativePath);
const extractedEntries = apiDoc.entries;
const combinedEntries = extractedEntries.concat(extraEntries);
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]),
],
} as EntryCollection);
writeFileSync(outputFilenameExecRootRelativePath, output, {encoding: 'utf8'});
}
@@ -1,13 +1,14 @@
load("//adev/shared-docs/pipeline/api-gen/extraction:extract_api_to_json.bzl", "extract_api_to_json")
load("//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html.bzl", "render_api_to_html")
def generate_api_docs(name, module_name, entry_point, srcs, import_map = {}, extra_entries = []):
def generate_api_docs(name, module_name, entry_point, srcs, module_label = None, import_map = {}, extra_entries = []):
"""Generates API documentation reference pages for the given sources."""
json_outfile = name + "_api.json"
extract_api_to_json(
name = name + "_extraction",
module_name = module_name,
module_label = module_label,
entry_point = entry_point,
srcs = srcs,
output_name = json_outfile,
@@ -1,11 +1,5 @@
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
import type {DocEntry, JsDocTagEntry} from '@angular/compiler-cli';
/** The JSON data file format for extracted API reference info. */
export interface EntryCollection {
moduleName: string;
entries: DocEntry[];
}
import type {DocEntry, EntryCollection, JsDocTagEntry} from '@angular/compiler-cli';
export interface ManifestEntry {
name: string;
@@ -16,7 +10,12 @@ export interface ManifestEntry {
}
/** Manifest that maps each module name to a list of API symbols. */
export type Manifest = Record<string, ManifestEntry[]>;
export type Manifest = {
moduleName: string;
normalizedModuleName: string;
moduleLabel: string;
entries: ManifestEntry[];
}[];
/** Gets a unique lookup key for an API, e.g. "@angular/core/ElementRef". */
function getApiLookupKey(moduleName: string, name: string) {
@@ -114,22 +113,39 @@ export function generateManifest(apiCollections: EntryCollection[]): Manifest {
});
}
const manifest: Manifest = {};
const manifest: Manifest = [];
for (const collection of apiCollections) {
if (!manifest[collection.moduleName]) {
manifest[collection.moduleName] = [];
const entries = collection.entries.map((entry) => ({
name: entry.name,
type: entry.entryType,
isDeprecated: isDeprecated(entryLookup, collection.moduleName, entry),
isDeveloperPreview: isDeveloperPreview(entryLookup, collection.moduleName, entry),
isExperimental: isExperimental(entryLookup, collection.moduleName, entry),
}));
const existingEntry = manifest.find((entry) => entry.moduleName === collection.moduleName);
if (existingEntry) {
existingEntry.entries.push(...entries);
} else {
manifest.push({
moduleName: collection.moduleName,
normalizedModuleName: collection.normalizedModuleName,
moduleLabel: collection.moduleLabel ?? collection.moduleName,
entries,
});
}
}
manifest.sort((entry1, entry2) => {
// Ensure that labels that start with a `code` tag like `window.ng` are last
if (entry1.moduleLabel.startsWith('<')) {
return 1;
} else if (entry2.moduleLabel.startsWith('<')) {
return -1;
}
manifest[collection.moduleName].push(
...collection.entries.map((entry) => ({
name: entry.name,
type: entry.entryType,
isDeprecated: isDeprecated(entryLookup, collection.moduleName, entry),
isDeveloperPreview: isDeveloperPreview(entryLookup, collection.moduleName, entry),
isExperimental: isExperimental(entryLookup, collection.moduleName, entry),
})),
);
}
return entry1.moduleLabel.localeCompare(entry2.moduleLabel);
});
return manifest;
}
@@ -1,5 +1,6 @@
import {readFileSync, writeFileSync} from 'fs';
import {EntryCollection, generateManifest} from './generate_manifest';
import {generateManifest} from './generate_manifest';
import type {EntryCollection} from '@angular/compiler-cli';
function main() {
const [paramFilePath] = process.argv.slice(2);
@@ -101,6 +101,8 @@ export interface ClassEntry extends DocEntry {
isAbstract: boolean;
members: MemberEntry[];
generics: GenericEntry[];
extends?: string;
implements: string[];
}
// From an API doc perspective, class and interfaces are identical.
@@ -132,15 +134,25 @@ export interface PipeEntry extends ClassEntry {
// TODO: add `isPure`.
}
export interface FunctionEntry extends DocEntry {
export interface FunctionSignatureMetadata extends DocEntry {
params: ParameterEntry[];
returnType: string;
returnDescription?: string;
generics: GenericEntry[];
isNewType: boolean;
}
export interface FunctionWithOverloadsEntry extends FunctionEntry {
overloads: FunctionEntry[] | null;
export type FunctionEntry = FunctionDefinitionEntry &
DocEntry & {
implementation: FunctionSignatureMetadata;
};
/** Interface describing a function with overload signatures. */
export interface FunctionDefinitionEntry {
name: string;
signatures: FunctionSignatureMetadata[];
implementation: FunctionSignatureMetadata | null;
}
/** Sub-entry for a single class or enum member. */
@@ -178,15 +190,9 @@ export interface ParameterEntry {
isRestParam: boolean;
}
export interface FunctionWithOverloads {
name: string;
signatures: FunctionEntry[];
implementation: FunctionEntry | null;
}
export interface InitializerApiFunctionEntry extends DocEntry {
callFunction: FunctionWithOverloads;
subFunctions: FunctionWithOverloads[];
callFunction: FunctionDefinitionEntry;
subFunctions: FunctionDefinitionEntry[];
__docsMetadata__?: {
/**
@@ -204,7 +210,3 @@ export interface InitializerApiFunctionEntry extends DocEntry {
export function isDocEntryWithSourceInfo(entry: DocEntry): entry is DocEntryWithSourceInfo {
return 'source' in entry;
}
export function isFunctionEntryWithOverloads(entry: DocEntry): entry is FunctionWithOverloadsEntry {
return 'overloads' in entry;
}
@@ -12,6 +12,7 @@ import {
DocEntry,
EnumEntry,
FunctionEntry,
FunctionSignatureMetadata,
InitializerApiFunctionEntry,
JsDocTagEntry,
MemberEntry,
@@ -55,7 +56,7 @@ export type TypeAliasEntryRenderable = TypeAliasEntry & DocEntryRenderable & Has
export type ClassEntryRenderable = ClassEntry &
DocEntryRenderable &
HasRenderableToc & {
membersGroups: Map<string, MemberEntryRenderable[]>;
members: MemberEntryRenderable[];
};
/** Documentation entity for a TypeScript enum augmented transformed content for rendering. */
@@ -71,9 +72,12 @@ export type InterfaceEntryRenderable = ClassEntryRenderable;
export type FunctionEntryRenderable = FunctionEntry &
DocEntryRenderable &
HasRenderableToc & {
params: ParameterEntryRenderable[];
deprecationMessage: string | null;
overloads: FunctionEntryRenderable[] | null;
};
export type FunctionSignatureMetadataRenderable = FunctionSignatureMetadata &
DocEntryRenderable & {
params: ParameterEntryRenderable[];
};
/** Sub-entry for a single class or enum member augmented with transformed content for rendering. */
@@ -1,8 +1,7 @@
import {FunctionEntry, JsDocTagEntry, MemberEntry, ParameterEntry} from '../entities';
import {JsDocTagEntry, MemberEntry, ParameterEntry} from '../entities';
import {
CodeLineRenderable,
FunctionEntryRenderable,
JsDocTagRenderable,
LinkEntryRenderable,
MemberEntryRenderable,
@@ -81,14 +80,6 @@ export interface HasRenderableParams {
params: ParameterEntryRenderable[];
}
export interface HasOverloads {
overloads: FunctionEntry[] | null;
}
export interface HasRenderableOverloads {
overloads: FunctionEntryRenderable[] | null;
}
export interface HasDeprecatedFlag {
isDeprecated: boolean;
deprecationMessage: string | null;
@@ -7,20 +7,27 @@ import {configureMarkedGlobally} from './marked/configuration';
import {getRenderable} from './processing';
import {renderEntry} from './rendering';
import {initHighlighter} from './shiki/shiki';
import {setSymbols} from './symbol-context';
/** The JSON data file format for extracted API reference info. */
interface EntryCollection {
moduleName: string;
moduleLabel?: string;
normalizedModuleName: string;
entries: DocEntry[];
symbols: Map<string, string>;
}
/** Parse all JSON data source files into an array of collections. */
function parseEntryData(srcs: string[]): EntryCollection[] {
return srcs.flatMap((jsonDataFilePath) => {
return srcs.flatMap((jsonDataFilePath): EntryCollection | EntryCollection[] => {
const fileContent = readFileSync(jsonDataFilePath, {encoding: 'utf8'});
const fileContentJson = JSON.parse(fileContent) as unknown;
if ((fileContentJson as EntryCollection).entries) {
return fileContentJson as EntryCollection;
return {
...(fileContentJson as EntryCollection),
symbols: new Map((fileContentJson as any).symbols ?? []),
};
}
// CLI subcommands should generate a separate file for each subcommand.
@@ -30,12 +37,16 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
return [
{
moduleName: 'unknown',
normalizedModuleName: 'unknown',
entries: [fileContentJson as DocEntry],
symbols: new Map(),
},
...command.subcommands!.map((subCommand) => {
return {
moduleName: 'unknown',
normalizedModuleName: 'unknown',
entries: [{...subCommand, parentCommand: command} as any],
symbols: new Map(),
};
}),
];
@@ -43,13 +54,15 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
return {
moduleName: 'unknown',
normalizedModuleName: 'unknown',
entries: [fileContentJson as DocEntry], // TODO: fix the typing cli entries aren't DocEntry
symbols: new Map(),
};
});
}
/** Gets a normalized filename for a doc entry. */
function getNormalizedFilename(moduleName: string, entry: DocEntry | CliCommand): string {
function getNormalizedFilename(normalizedModuleName: string, entry: DocEntry | CliCommand): string {
if (isCliEntry(entry)) {
return entry.parentCommand
? `${entry.parentCommand.name}/${entry.name}.html`
@@ -57,9 +70,6 @@ function getNormalizedFilename(moduleName: string, entry: DocEntry | CliCommand)
}
entry = entry as DocEntry;
// Angular entry points all contain an "@" character, which we want to remove
// from the filename. We also swap `/` with an underscore.
const normalizedModuleName = moduleName.replace('@', '').replace(/\//g, '_');
// Append entry type as suffix to prevent writing to file that only differs in casing or query string from already written file.
// This will lead to a race-condition and corrupted files on case-insensitive file systems.
@@ -96,6 +106,10 @@ async function main() {
for (const collection of entryCollections) {
const extractedEntries = collection.entries;
// Setting the symbols are a global context for the rendering templates of this entry
setSymbols(collection.symbols);
const renderableEntries = extractedEntries.map((entry) =>
getRenderable(entry, collection.moduleName),
);
@@ -103,7 +117,10 @@ async function main() {
const htmlOutputs = renderableEntries.map(renderEntry);
for (let i = 0; i < htmlOutputs.length; i++) {
const filename = getNormalizedFilename(collection.moduleName, collection.entries[i]);
const filename = getNormalizedFilename(
collection.normalizedModuleName,
collection.entries[i],
);
const outputPath = path.join(outputFilenameExecRootRelativePath, filename);
// in case the output path is nested, ensure the directory exists
@@ -6,16 +6,18 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {Renderer as MarkedRenderer} from 'marked';
import {Renderer, Tokens} from 'marked';
import {codeToHtml} from '../shiki/shiki';
/**
* Custom renderer for marked that will be used to transform markdown files to HTML
* files that can be used in the Angular docs.
*/
export const renderer: Partial<MarkedRenderer> = {
code(code: string, language: string, isEscaped: boolean): string {
const highlightResult = codeToHtml(code, language).replace(/>\s+</g, '><');
export const renderer: Partial<Renderer> = {
code({lang, text}): string {
const highlightResult = codeToHtml(text, lang)
// remove spaces/line-breaks between elements to not mess-up `pre` style
.replace(/>\s+</g, '><');
return `
<div class="docs-code" role="group">
@@ -25,37 +27,46 @@ export const renderer: Partial<MarkedRenderer> = {
</div>
`;
},
image(href: string | null, title: string | null, text: string): string {
image({href, title, text}): string {
return `
<img src="${href}" alt="${text}" title="${title}" class="docs-image">
`;
},
link(href: string, title: string, text: string): string {
return `<a href="${href}">${text}</a>`;
link(this: Renderer, {href, tokens}): string {
return `<a href="${href}">${this.parser.parseInline(tokens)}</a>`;
},
list(body: string, ordered: boolean, start: number) {
list(this: Renderer, {items, ordered, start}) {
if (ordered) {
return `
<ol class="docs-ordered-list">
${body}
${items.map((item) => this.listitem(item)).join('')}
</ol>
`;
}
return `
<ul class="docs-list">
${body}
${items.map((item) => this.listitem(item)).join('')}
</ul>
`;
},
table(header: string, body: string): string {
table(this: Renderer, {header, rows}: Tokens.Table) {
return `
<div class="docs-table docs-scroll-track-transparent">
<table>
<thead>
${header}
${this.tablerow({
text: header.map((cell) => this.tablecell(cell)).join(''),
})}
</thead>
<tbody>
${body}
${rows
.map((row) =>
this.tablerow({
text: row.map((cell) => this.tablecell(cell)).join(''),
}),
)
.join('')}
</tbody>
</table>
</div>
@@ -21,8 +21,12 @@ export async function initHighlighter() {
});
}
export function codeToHtml(code: string, language: string | undefined): string {
return highlighter.codeToHtml(code, {
export function codeToHtml(
code: string,
language: string | undefined,
options?: {removeFunctionKeyword?: boolean},
): string {
const html = highlighter.codeToHtml(code, {
lang: language ?? 'text',
themes: {
light: 'github-light',
@@ -31,4 +35,20 @@ export function codeToHtml(code: string, language: string | undefined): string {
cssVariablePrefix: '--shiki-',
defaultColor: false,
});
if (options?.removeFunctionKeyword) {
return removeFunctionKeywordFromShikiHtml(html);
}
return html;
}
export function removeFunctionKeywordFromShikiHtml(shikiHtml: string): string {
return (
shikiHtml
// remove the leading space of the element after the "function" element
.replace(/(<[^>]*>function<\/\w+><[^>]*>)(\s)(\w+<\/\w+>)/g, '$1$3')
// Shiki requires the keyword function for highlighting functions signatures
// We don't want to display it so we remove elements with the keyword
.replace(/<[^>]*>function<\/\w+>/g, '')
);
}
@@ -16,6 +16,7 @@ export const REFERENCE_MEMBERS = 'docs-reference-members';
export const REFERENCE_DEPRECATED = 'docs-reference-deprecated';
export const REFERENCE_MEMBERS_CONTAINER = 'docs-reference-members-container';
export const REFERENCE_MEMBER_CARD = 'docs-reference-member-card';
export const REFERENCE_MEMBER_CARD_HEADER = 'docs-reference-card-header';
export const REFERENCE_MEMBER_CARD_BODY = 'docs-reference-card-body';
export const REFERENCE_MEMBER_CARD_ITEM = 'docs-reference-card-item';
@@ -0,0 +1,18 @@
/**
* API pages are generated each package at a time.
* This allows to use a global context to store the symbols and their corresponding module names.
*/
let symbols = new Map<string, string>();
export function setSymbols(newSymbols: Map<string, string>): void {
symbols = newSymbols;
}
/**
* Returns the module name of a symbol.
* eg: 'ApplicationRef' => 'core', 'FormControl' => 'forms'
*/
export function getModuleName(symbol: string): string | undefined {
return symbols.get(symbol)?.replace('@angular/', '');
}
@@ -10,11 +10,11 @@ import {h} from 'preact';
import {MemberEntryRenderable} from '../entities/renderables';
import {ClassMember} from './class-member';
export function ClassMemberList(props: {membersGroups: Map<string, MemberEntryRenderable[]>}) {
export function ClassMemberList(props: {members: MemberEntryRenderable[]}) {
return (
<div class="docs-reference-members">
{Array.from(props.membersGroups).map(([_, group]) => (
<ClassMember members={group} />
{props.members.map((member) => (
<ClassMember member={member} />
))}
</div>
);
@@ -13,55 +13,54 @@ import {
isPropertyEntry,
isSetterEntry,
} from '../entities/categorization';
import {MemberEntryRenderable} from '../entities/renderables';
import {
REFERENCE_HEADER,
FunctionSignatureMetadataRenderable,
MemberEntryRenderable,
MethodEntryRenderable,
} from '../entities/renderables';
import {
REFERENCE_MEMBER_CARD,
REFERENCE_MEMBER_CARD_BODY,
REFERENCE_MEMBER_CARD_HEADER,
REFERENCE_MEMBER_CARD_ITEM,
} from '../styling/css-classes';
import {ClassMethodInfo} from './class-method-info';
import {DeprecatedLabel} from './deprecated-label';
import {RawHtml} from './raw-html';
import {getFunctionMetadataRenderable} from '../transforms/function-transforms';
import {CodeSymbol} from './code-symbols';
export function ClassMember(props: {members: MemberEntryRenderable[]}) {
const memberName = props.members[0].name;
const returnType = getMemberType(props.members[0]);
// Do not create body element when there is no description
const body = props.members.every(
(member) => !member.htmlDescription && !isClassMethodEntry(member),
) ? (
<></>
) : (
export function ClassMember(props: {member: MemberEntryRenderable}) {
const body = (
<div className={REFERENCE_MEMBER_CARD_BODY}>
{props.members.map((member) => {
return isClassMethodEntry(member) ? (
<ClassMethodInfo entry={member} isOverloaded={props.members.length > 1} />
) : (
<div className={REFERENCE_MEMBER_CARD_ITEM}>
{props.members.every((member) => member.deprecationMessage !== null) ? (
<DeprecatedLabel entry={props.members[0]} />
) : (
<></>
)}
<RawHtml value={member.htmlDescription} />
</div>
);
})}
{isClassMethodEntry(props.member) ? (
props.member.signatures.map((sig, i, signatures) => {
const renderableMember = getFunctionMetadataRenderable(sig);
return <ClassMethodInfo entry={renderableMember} options={{showUsageNotes: true}} />;
})
) : props.member.htmlDescription || props.member.deprecationMessage ? (
<div className={REFERENCE_MEMBER_CARD_ITEM}>
<DeprecatedLabel entry={props.member} />
<RawHtml value={props.member.htmlDescription} />
</div>
) : (
<></>
)}
</div>
);
const memberName = props.member.name;
const returnType = getMemberType(props.member);
return (
<div id={memberName} className={REFERENCE_MEMBER_CARD} tabIndex={-1}>
<header>
<div className={REFERENCE_HEADER}>
<div className={REFERENCE_MEMBER_CARD_HEADER}>
<h3>{memberName}</h3>
<div>
{props.members.length > 1 ? (
<span>{props.members.length} overloads</span>
{isClassMethodEntry(props.member) && props.member.signatures.length > 1 ? (
<span>{props.member.signatures.length} overloads</span>
) : returnType ? (
<code>{returnType}</code>
<CodeSymbol code={returnType} />
) : (
<></>
)}
@@ -75,7 +74,7 @@ export function ClassMember(props: {members: MemberEntryRenderable[]}) {
function getMemberType(entry: MemberEntryRenderable): string | null {
if (isClassMethodEntry(entry)) {
return entry.returnType;
return entry.implementation.returnType;
} else if (isPropertyEntry(entry) || isGetterEntry(entry) || isSetterEntry(entry)) {
return entry.type;
}
@@ -9,6 +9,7 @@
import {Fragment, h} from 'preact';
import {
FunctionEntryRenderable,
FunctionSignatureMetadataRenderable,
MethodEntryRenderable,
ParameterEntryRenderable,
} from '../entities/renderables';
@@ -16,13 +17,16 @@ import {PARAM_KEYWORD_CLASS_NAME, REFERENCE_MEMBER_CARD_ITEM} from '../styling/c
import {DeprecatedLabel} from './deprecated-label';
import {Parameter} from './parameter';
import {RawHtml} from './raw-html';
import {CodeSymbol} from './code-symbols';
/**
* Component to render the method-specific parts of a class's API reference.
*/
export function ClassMethodInfo(props: {
entry: MethodEntryRenderable | FunctionEntryRenderable;
isOverloaded?: boolean;
entry: FunctionSignatureMetadataRenderable;
options?: {
showUsageNotes?: boolean;
};
}) {
const entry = props.entry;
@@ -32,21 +36,21 @@ export function ClassMethodInfo(props: {
>
<RawHtml value={entry.htmlDescription} className={'docs-function-definition'} />
{/* In case when method is overloaded we need to indicate which overload is deprecated */}
{!props.isOverloaded ? (
<></>
) : (
{entry.isDeprecated ? (
<div>
<DeprecatedLabel entry={entry} />
</div>
) : (
<></>
)}
{entry.params.map((param: ParameterEntryRenderable) => (
<Parameter param={param} />
))}
<div className={'docs-return-type'}>
<span className={PARAM_KEYWORD_CLASS_NAME}>@returns</span>
<code>{entry.returnType}</code>
<CodeSymbol code={entry.returnType} />
</div>
{entry.htmlUsageNotes ? (
{entry.htmlUsageNotes && props.options?.showUsageNotes ? (
<div className={'docs-usage-notes'}>
<span className={PARAM_KEYWORD_CLASS_NAME}>Usage notes</span>
<RawHtml value={entry.htmlUsageNotes} />
@@ -24,9 +24,9 @@ export function ClassReference(entry: ClassEntryRenderable) {
<TabDescription entry={entry} />
<TabUsageNotes entry={entry} />
{
entry.membersGroups.size > 0
entry.members.length > 0
? (<div class={REFERENCE_MEMBERS_CONTAINER}>
<ClassMemberList membersGroups={entry.membersGroups} />
<ClassMemberList members={entry.members} />
</div>)
: (<></>)
}
@@ -9,12 +9,13 @@
import {Fragment, h} from 'preact';
import {CliCardRenderable} from '../entities/renderables';
import {DeprecatedLabel} from './deprecated-label';
import { REFERENCE_MEMBER_CARD, REFERENCE_MEMBER_CARD_HEADER } from '../styling/css-classes';
export function CliCard(props: {card: CliCardRenderable}) {
return (
<div id={props.card.type} class="docs-reference-member-card" tabIndex={-1}>
<div id={props.card.type} class={REFERENCE_MEMBER_CARD} tabIndex={-1}>
<header>
<div class="docs-card-ref-header">
<div class={REFERENCE_MEMBER_CARD_HEADER}>
<h3>{props.card.type}</h3>
</div>
</header>
@@ -23,7 +23,7 @@ export function CliCommandReference(entry: CliCommandRenderable) {
<div class="docs-code docs-reference-cli-toc">
<pre class="docs-mini-scroll-track">
<code>
<div className={'shiki-ln-line'}>
<div className={'shiki line cli'}>
ng {commandName(entry, command)}
{entry.argumentsLabel ? <button member-id={'Arguments'} className="shiki-ln-line-argument">{entry.argumentsLabel}</button> : <></>}
{entry.hasOptions ? <button member-id={'Options'} className="shiki-ln-line-option">[options]</button> : <></>}
@@ -0,0 +1,30 @@
import {h} from 'preact';
import {getModuleName} from '../symbol-context';
import {getLinkToModule} from '../transforms/url-transforms';
const symbolRegex = /([a-zA-Z_$][a-zA-Z_$0-9\.]*)/;
/**
* Component that generates a code block with a link to a Symbol if it's known,
* else generates a string code block
*/
export function CodeSymbol(props: {code: string}) {
return (
<code>
{props.code.split(symbolRegex).map((rawSymbol, index) => {
// Every even index is a non-match when the regex has 1 capturing group
if (index % 2 === 0) return rawSymbol;
let [symbol, subSymbol] = rawSymbol.split('.'); // Also takes care of methods, enum value etc.
const moduleName = getModuleName(symbol);
if (moduleName) {
const url = getLinkToModule(moduleName, symbol, subSymbol);
return <a href={url}>{rawSymbol}</a>;
}
return rawSymbol;
})}
</code>
);
}
@@ -7,7 +7,7 @@
*/
import {h, Fragment} from 'preact';
import {EnumEntryRenderable} from '../entities/renderables';
import {EnumEntryRenderable, MemberEntryRenderable} from '../entities/renderables';
import {HeaderApi} from './header-api';
import {TabDescription} from './tab-description';
import {TabApi} from './tab-api';
@@ -26,7 +26,7 @@ export function EnumReference(entry: EnumEntryRenderable) {
? (
<div class={REFERENCE_MEMBERS_CONTAINER}>
<div class={REFERENCE_MEMBERS}>
{entry.members.map((member: any) => (<ClassMember members={[member]}/>))}
{entry.members.map((member: MemberEntryRenderable) => (<ClassMember member={member}/>))}
</div>
</div>
)
@@ -7,23 +7,66 @@
*/
import {h} from 'preact';
import {FunctionEntryRenderable} from '../entities/renderables';
import {FunctionEntryRenderable, FunctionSignatureMetadataRenderable} from '../entities/renderables';
import {
PARAM_KEYWORD_CLASS_NAME,
REFERENCE_HEADER,
REFERENCE_MEMBERS,
REFERENCE_MEMBERS_CONTAINER,
REFERENCE_MEMBER_CARD,
REFERENCE_MEMBER_CARD_BODY,
REFERENCE_MEMBER_CARD_HEADER,
} from '../styling/css-classes';
import {ClassMethodInfo} from './class-method-info';
import {HeaderApi} from './header-api';
import {TabApi} from './tab-api';
import {TabDescription} from './tab-description';
import {TabUsageNotes} from './tab-usage-notes';
import {HighlightTypeScript} from './highlight-ts';
import {printInitializerFunctionSignatureLine} from '../transforms/code-transforms';
import {getFunctionMetadataRenderable} from '../transforms/function-transforms';
import {CodeSymbol} from './code-symbols';
export const signatureCard = (
name: string,
signature: FunctionSignatureMetadataRenderable,
opts: {id: string},
printSignaturesAsHeader: boolean,
) => {
return (
<div class={REFERENCE_MEMBER_CARD} id={opts.id} tabIndex={-1}>
<header>
{printSignaturesAsHeader ? (
<code>
<HighlightTypeScript
code={printInitializerFunctionSignatureLine(
name,
signature,
// Always omit types in signature headers, to keep them short.
true,
)}
removeFunctionKeyword={true}
/>
</code>
) : (
<div className={REFERENCE_MEMBER_CARD_HEADER}>
<h3>{name}</h3>
<div>
<CodeSymbol code={signature.returnType} />
</div>
</div>
)}
</header>
<div class={REFERENCE_MEMBER_CARD_BODY}>
<ClassMethodInfo entry={signature} />
</div>
</div>
);
};
/** Component to render a function API reference document. */
export function FunctionReference(entry: FunctionEntryRenderable) {
// Use signatures as header if there are multiple signatures.
const printSignaturesAsHeader = entry.signatures.length > 1;
return (
<div class="api">
<HeaderApi entry={entry} />
@@ -32,26 +75,16 @@ export function FunctionReference(entry: FunctionEntryRenderable) {
<TabUsageNotes entry={entry} />
<div className={REFERENCE_MEMBERS_CONTAINER}>
<div className={REFERENCE_MEMBERS}>
<div className={REFERENCE_MEMBER_CARD}>
<header>
<div className={REFERENCE_HEADER}>
<h3>{entry.name}</h3>
<div>
<code>{entry.returnType}</code>
</div>
</div>
{entry.isDeprecated && (
<span className={`${PARAM_KEYWORD_CLASS_NAME} docs-deprecated`}>@deprecated</span>
)}
</header>
<div className={REFERENCE_MEMBER_CARD_BODY}>
{entry.overloads ? (
entry.overloads.map((overload) => <ClassMethodInfo entry={overload} />)
) : (
<ClassMethodInfo entry={entry} isOverloaded={true} />
)}
</div>
</div>
{entry.signatures.map((s, i) =>
signatureCard(
s.name,
getFunctionMetadataRenderable(s, entry.moduleName),
{
id: `${s.name}_${i}`,
},
printSignaturesAsHeader,
),
)}
</div>
</div>
</div>
@@ -74,12 +74,12 @@ export function HeaderApi(props: {entry: DocEntryRenderable; showFullDescription
)}
</div>
<p
<section
className={'docs-reference-description'}
dangerouslySetInnerHTML={{
__html: props.showFullDescription ? entry.htmlDescription : entry.shortHtmlDescription,
}}
></p>
></section>
<DocsPillRow links={entry.additionalLinks} />
</header>
@@ -6,15 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import { h } from 'preact';
import { RawHtml } from './raw-html';
import { codeToHtml } from '../shiki/shiki';
import {h} from 'preact';
import {RawHtml} from './raw-html';
import {codeToHtml} from '../shiki/shiki';
/** Component to render a header of the CLI page. */
export function HighlightTypeScript(props: {code: string}) {
const result = codeToHtml(props.code, 'typescript');
export function HighlightTypeScript(props: {code: string; removeFunctionKeyword?: boolean}) {
const result = codeToHtml(props.code, 'typescript', {
removeFunctionKeyword: props.removeFunctionKeyword,
});
return (
<RawHtml value={result} />
);
return <RawHtml value={result} />;
}
@@ -7,20 +7,13 @@
*/
import {h, JSX} from 'preact';
import {FunctionEntryRenderable, InitializerApiFunctionRenderable} from '../entities/renderables';
import {InitializerApiFunctionRenderable} from '../entities/renderables';
import {HeaderApi} from './header-api';
import {TabApi} from './tab-api';
import {TabUsageNotes} from './tab-usage-notes';
import {
REFERENCE_MEMBERS,
REFERENCE_MEMBERS_CONTAINER,
REFERENCE_MEMBER_CARD,
REFERENCE_MEMBER_CARD_BODY,
} from '../styling/css-classes';
import {printInitializerFunctionSignatureLine} from '../transforms/code-transforms';
import {HighlightTypeScript} from './highlight-ts';
import {ClassMethodInfo} from './class-method-info';
import {getFunctionRenderable} from '../transforms/function-transforms';
import {REFERENCE_MEMBERS, REFERENCE_MEMBERS_CONTAINER} from '../styling/css-classes';
import {getFunctionMetadataRenderable} from '../transforms/function-transforms';
import {signatureCard} from './function-reference';
/** Component to render a constant API reference document. */
export function InitializerApiFunction(entry: InitializerApiFunctionRenderable) {
@@ -40,32 +33,6 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
entry.callFunction.signatures[0].description = '';
}
const signatureCard = (name: string, signature: FunctionEntryRenderable, opts: {id: string}) => {
return (
<div class={REFERENCE_MEMBER_CARD} id={opts.id} tabIndex={-1}>
<header>
{printSignaturesAsHeader ? (
<code>
<HighlightTypeScript
code={printInitializerFunctionSignatureLine(
name,
signature,
// Always omit types in signature headers, to keep them short.
true,
)}
/>
</code>
) : (
<h3>{`${name}()`}</h3>
)}
</header>
<div class={REFERENCE_MEMBER_CARD_BODY}>
<ClassMethodInfo entry={signature} />
</div>
</div>
);
};
return (
<div class="api">
<HeaderApi entry={entry} showFullDescription={true} />
@@ -75,9 +42,14 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
<div class={REFERENCE_MEMBERS_CONTAINER}>
<div class={REFERENCE_MEMBERS}>
{entry.callFunction.signatures.map((s, i) =>
signatureCard(s.name, getFunctionRenderable(s, entry.moduleName), {
id: `${s.name}_${i}`,
}),
signatureCard(
s.name,
getFunctionMetadataRenderable(s, entry.moduleName),
{
id: `${s.name}_${i}`,
},
printSignaturesAsHeader,
),
)}
{entry.subFunctions.reduce(
@@ -86,10 +58,11 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
...subFunction.signatures.map((s, i) =>
signatureCard(
`${entry.name}.${s.name}`,
getFunctionRenderable(s, entry.moduleName),
getFunctionMetadataRenderable(s, entry.moduleName),
{
id: `${entry.name}_${s.name}_${i}`,
},
printSignaturesAsHeader,
),
),
],
@@ -10,7 +10,7 @@ import {h} from 'preact';
import {ParameterEntryRenderable} from '../entities/renderables';
import {RawHtml} from './raw-html';
import {PARAM_GROUP_CLASS_NAME} from '../styling/css-classes';
import {CodeSymbol} from './code-symbols';
/** Component to render a function or method parameter reference doc fragment. */
export function Parameter(props: {param: ParameterEntryRenderable}) {
@@ -21,7 +21,7 @@ export function Parameter(props: {param: ParameterEntryRenderable}) {
{/*TODO: isOptional, isRestParam*/}
<span class="docs-param-keyword">@param</span>
<span class="docs-param-name">{param.name}</span>
<code>{param.type}</code>
<CodeSymbol code={param.type} />
<RawHtml value={param.htmlDescription} className="docs-parameter-description" />
</div>
);
@@ -19,10 +19,18 @@ ts_library(
),
deps = [
"//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html_lib",
"@npm//@bazel/runfiles",
"@npm//@types/jsdom",
"@npm//jsdom",
],
)
jasmine_node_test(
name = "unit_tests",
data = [
"@npm//jsdom",
] + glob([
"**/*.json",
]),
deps = [":unit_test_lib"],
)
@@ -3,81 +3,671 @@
"entries": [
{
"name": "NgTemplateOutlet",
"description": "*one* directive",
"isAbstract": false,
"entryType": "directive",
"members": [],
"jsdocTags": [],
"rawComment": "",
"source": {
"fileName": "/packages/core/src/ng_template_outlet.ts",
"line": 7,
"character": 7
}
},
{
"name": "UserProfile",
"entryType": "undecorated_class",
"members": [
{
"name": "userId",
"type": "number",
"name": "ngTemplateOutletContext",
"type": "C",
"memberType": "property",
"memberTags": [],
"description": "A user identifier.",
"jsdocTags": []
"memberTags": ["input"],
"description": "A context object to attach to the {@link EmbeddedViewRef}. This should be an\nobject, the object's keys will be available for binding by the local template `let`\ndeclarations.\nUsing the key `$implicit` in the context object will set its value as default.",
"jsdocTags": [],
"inputAlias": "ngTemplateOutletContext",
"isRequiredInput": false
},
{
"name": "name",
"type": "string",
"memberType": "getter",
"memberTags": [],
"description": "Name of the user",
"jsdocTags": []
"name": "ngTemplateOutlet",
"type": "TemplateRef<C>",
"memberType": "property",
"memberTags": ["input"],
"description": "A string defining the template reference and optionally the context object for the template.",
"jsdocTags": [],
"inputAlias": "ngTemplateOutlet",
"isRequiredInput": false
},
{
"name": "name",
"type": "string",
"memberType": "setter",
"memberTags": [],
"description": "Name of the user",
"jsdocTags": []
"name": "ngTemplateOutletInjector",
"type": "Injector",
"memberType": "property",
"memberTags": ["input"],
"description": "Injector to be used within the embedded view.",
"jsdocTags": [],
"inputAlias": "ngTemplateOutletInjector",
"isRequiredInput": false
},
{
"params": [
"name": "ngOnChanges",
"signatures": [
{
"name": "config",
"description": "Setting for saving.",
"type": "object",
"isOptional": false,
"isRestParam": false
"name": "ngOnChanges",
"entryType": "function",
"description": "",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [
{
"name": "changes",
"description": "",
"type": "SimpleChanges",
"isOptional": false,
"isRestParam": false
}
],
"rawComment": "",
"returnType": "void"
}
],
"name": "save",
"returnType": "boolean",
"implementation": {
"params": [
{
"name": "changes",
"description": "",
"type": "SimpleChanges",
"isOptional": false,
"isRestParam": false
}
],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "ngOnChanges",
"description": "",
"entryType": "function",
"jsdocTags": [],
"rawComment": ""
},
"entryType": "function",
"description": "Save the user.",
"jsdocTags": [
{
"name": "param",
"comment": "Setting for saving."
},
{
"name": "returns",
"comment": "Whether it succeeded"
}
],
"rawComment": "/**\n * Save the user.\n * @param config Setting for saving.\n * @returns Whether it succeeded\n */",
"description": "",
"jsdocTags": [],
"rawComment": "",
"memberType": "method",
"memberTags": []
}
],
"generics": [{"name": "C", "default": "unknown"}],
"description": "",
"jsdocTags": [
{"name": "ngModule", "comment": "CommonModule"},
{
"name": "description",
"comment": "Inserts an embedded view from a prepared `TemplateRef`.\n\nYou can attach a context object to the `EmbeddedViewRef` by setting `[ngTemplateOutletContext]`.\n`[ngTemplateOutletContext]` should be an object, the object's keys will be available for binding\nby the local template `let` declarations."
},
{
"name": "usageNotes",
"comment": "```\n<ng-container *ngTemplateOutlet=\"templateRefExp; context: contextExp\"></ng-container>\n```\n\nUsing the key `$implicit` in the context object will set its value as default.\n\n### Example\n\n{@example common/ngTemplateOutlet/ts/module.ts region='NgTemplateOutlet'}"
},
{"name": "publicApi", "comment": ""}
],
"rawComment": "/**\n * @ngModule CommonModule\n *\n * @description\n *\n * Inserts an embedded view from a prepared `TemplateRef`.\n *\n * You can attach a context object to the `EmbeddedViewRef` by setting `[ngTemplateOutletContext]`.\n * `[ngTemplateOutletContext]` should be an object, the object's keys will be available for binding\n * by the local template `let` declarations.\n *\n * @usageNotes\n * ```\n * <ng-container *ngTemplateOutlet=\"templateRefExp; context: contextExp\"></ng-container>\n * ```\n *\n * Using the key `$implicit` in the context object will set its value as default.\n *\n * ### Example\n *\n * {@example common/ngTemplateOutlet/ts/module.ts region='NgTemplateOutlet'}\n *\n * @publicApi\n */",
"implements": ["OnChanges"],
"isStandalone": true,
"selector": "[ngTemplateOutlet]",
"exportAs": [],
"source": {
"filePath": "/packages/common/src/directives/ng_template_outlet.ts",
"startLine": 45,
"endLine": 126
}
},
{
"name": "ViewRef",
"isAbstract": true,
"entryType": "undecorated_class",
"members": [
{
"name": "destroy",
"signatures": [
{
"name": "destroy",
"entryType": "function",
"description": "Destroys this view and all of the data structures associated with it.",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * Destroys this view and all of the data structures associated with it.\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "destroy",
"description": "Destroys this view and all of the data structures associated with it.",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * Destroys this view and all of the data structures associated with it.\n */"
},
"entryType": "function",
"description": "Destroys this view and all of the data structures associated with it.",
"jsdocTags": [],
"rawComment": "/**\n * Destroys this view and all of the data structures associated with it.\n */",
"memberType": "method",
"memberTags": ["abstract"]
},
{
"name": "destroyed",
"type": "boolean",
"memberType": "getter",
"memberTags": ["abstract"],
"description": "Reports whether this view has been destroyed.",
"jsdocTags": [
{
"name": "returns",
"comment": "True after the `destroy()` method has been called, false otherwise."
}
]
},
{
"name": "onDestroy",
"signatures": [
{
"name": "onDestroy",
"entryType": "function",
"description": "A lifecycle hook that provides additional developer-defined cleanup\nfunctionality for views.",
"generics": [],
"isNewType": false,
"jsdocTags": [
{
"name": "param",
"comment": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked."
}
],
"params": [
{
"name": "callback",
"description": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked.",
"type": "Function",
"isOptional": false,
"isRestParam": false
}
],
"rawComment": "/**\n * A lifecycle hook that provides additional developer-defined cleanup\n * functionality for views.\n * @param callback A handler function that cleans up developer-defined data\n * associated with a view. Called when the `destroy()` method is invoked.\n */",
"returnType": "void"
}
],
"implementation": {
"params": [
{
"name": "callback",
"description": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked.",
"type": "Function",
"isOptional": false,
"isRestParam": false
}
],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "onDestroy",
"description": "A lifecycle hook that provides additional developer-defined cleanup\nfunctionality for views.",
"entryType": "function",
"jsdocTags": [
{
"name": "param",
"comment": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked."
}
],
"rawComment": "/**\n * A lifecycle hook that provides additional developer-defined cleanup\n * functionality for views.\n * @param callback A handler function that cleans up developer-defined data\n * associated with a view. Called when the `destroy()` method is invoked.\n */"
},
"entryType": "function",
"description": "A lifecycle hook that provides additional developer-defined cleanup\nfunctionality for views.",
"jsdocTags": [
{
"name": "param",
"comment": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked."
}
],
"rawComment": "/**\n * A lifecycle hook that provides additional developer-defined cleanup\n * functionality for views.\n * @param callback A handler function that cleans up developer-defined data\n * associated with a view. Called when the `destroy()` method is invoked.\n */",
"memberType": "method",
"memberTags": ["abstract"]
},
{
"name": "markForCheck",
"signatures": [
{
"name": "markForCheck",
"entryType": "function",
"description": "When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\nchange detection strategy, explicitly marks the view as changed so that\nit can be checked again.\n\nComponents are normally marked as dirty (in need of rerendering) when inputs\nhave changed or events have fired in the view. Call this method to ensure that\na component is checked even if these triggers have not occurred.\n\n<!-- TODO: Add a link to a chapter on OnPush components -->",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\n * change detection strategy, explicitly marks the view as changed so that\n * it can be checked again.\n *\n * Components are normally marked as dirty (in need of rerendering) when inputs\n * have changed or events have fired in the view. Call this method to ensure that\n * a component is checked even if these triggers have not occurred.\n *\n * <!-- TODO: Add a link to a chapter on OnPush components -->\n *\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "markForCheck",
"description": "When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\nchange detection strategy, explicitly marks the view as changed so that\nit can be checked again.\n\nComponents are normally marked as dirty (in need of rerendering) when inputs\nhave changed or events have fired in the view. Call this method to ensure that\na component is checked even if these triggers have not occurred.\n\n<!-- TODO: Add a link to a chapter on OnPush components -->",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\n * change detection strategy, explicitly marks the view as changed so that\n * it can be checked again.\n *\n * Components are normally marked as dirty (in need of rerendering) when inputs\n * have changed or events have fired in the view. Call this method to ensure that\n * a component is checked even if these triggers have not occurred.\n *\n * <!-- TODO: Add a link to a chapter on OnPush components -->\n *\n */"
},
"entryType": "function",
"description": "When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\nchange detection strategy, explicitly marks the view as changed so that\nit can be checked again.\n\nComponents are normally marked as dirty (in need of rerendering) when inputs\nhave changed or events have fired in the view. Call this method to ensure that\na component is checked even if these triggers have not occurred.\n\n<!-- TODO: Add a link to a chapter on OnPush components -->",
"jsdocTags": [],
"rawComment": "/**\n * When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\n * change detection strategy, explicitly marks the view as changed so that\n * it can be checked again.\n *\n * Components are normally marked as dirty (in need of rerendering) when inputs\n * have changed or events have fired in the view. Call this method to ensure that\n * a component is checked even if these triggers have not occurred.\n *\n * <!-- TODO: Add a link to a chapter on OnPush components -->\n *\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
},
{
"name": "detach",
"signatures": [
{
"name": "detach",
"entryType": "function",
"description": "Detaches this view from the change-detection tree.\nA detached view is not checked until it is reattached.\nUse in combination with `detectChanges()` to implement local change detection checks.\n\nDetached views are not checked during change detection runs until they are\nre-attached, even if they are marked as dirty.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * Detaches this view from the change-detection tree.\n * A detached view is not checked until it is reattached.\n * Use in combination with `detectChanges()` to implement local change detection checks.\n *\n * Detached views are not checked during change detection runs until they are\n * re-attached, even if they are marked as dirty.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "detach",
"description": "Detaches this view from the change-detection tree.\nA detached view is not checked until it is reattached.\nUse in combination with `detectChanges()` to implement local change detection checks.\n\nDetached views are not checked during change detection runs until they are\nre-attached, even if they are marked as dirty.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * Detaches this view from the change-detection tree.\n * A detached view is not checked until it is reattached.\n * Use in combination with `detectChanges()` to implement local change detection checks.\n *\n * Detached views are not checked during change detection runs until they are\n * re-attached, even if they are marked as dirty.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */"
},
"entryType": "function",
"description": "Detaches this view from the change-detection tree.\nA detached view is not checked until it is reattached.\nUse in combination with `detectChanges()` to implement local change detection checks.\n\nDetached views are not checked during change detection runs until they are\nre-attached, even if they are marked as dirty.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"jsdocTags": [],
"rawComment": "/**\n * Detaches this view from the change-detection tree.\n * A detached view is not checked until it is reattached.\n * Use in combination with `detectChanges()` to implement local change detection checks.\n *\n * Detached views are not checked during change detection runs until they are\n * re-attached, even if they are marked as dirty.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
},
{
"name": "detectChanges",
"signatures": [
{
"name": "detectChanges",
"entryType": "function",
"description": "Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\nto implement local change detection checks.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\n * to implement local change detection checks.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "detectChanges",
"description": "Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\nto implement local change detection checks.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\n * to implement local change detection checks.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */"
},
"entryType": "function",
"description": "Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\nto implement local change detection checks.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"jsdocTags": [],
"rawComment": "/**\n * Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\n * to implement local change detection checks.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
},
{
"name": "checkNoChanges",
"signatures": [
{
"name": "checkNoChanges",
"entryType": "function",
"description": "Checks the change detector and its children, and throws if any changes are detected.\n\nUse in development mode to verify that running change detection doesn't introduce\nother changes. Calling it in production mode is a noop.",
"generics": [],
"isNewType": false,
"jsdocTags": [
{
"name": "deprecated",
"comment": "This is a test-only API that does not have a place in production interface.\n`checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\nmode. For more granular `checkNoChanges` validation, use `ComponentFixture`."
}
],
"params": [],
"rawComment": "/**\n * Checks the change detector and its children, and throws if any changes are detected.\n *\n * Use in development mode to verify that running change detection doesn't introduce\n * other changes. Calling it in production mode is a noop.\n *\n * @deprecated This is a test-only API that does not have a place in production interface.\n * `checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\n * mode. For more granular `checkNoChanges` validation, use `ComponentFixture`.\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "checkNoChanges",
"description": "Checks the change detector and its children, and throws if any changes are detected.\n\nUse in development mode to verify that running change detection doesn't introduce\nother changes. Calling it in production mode is a noop.",
"entryType": "function",
"jsdocTags": [
{
"name": "deprecated",
"comment": "This is a test-only API that does not have a place in production interface.\n`checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\nmode. For more granular `checkNoChanges` validation, use `ComponentFixture`."
}
],
"rawComment": "/**\n * Checks the change detector and its children, and throws if any changes are detected.\n *\n * Use in development mode to verify that running change detection doesn't introduce\n * other changes. Calling it in production mode is a noop.\n *\n * @deprecated This is a test-only API that does not have a place in production interface.\n * `checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\n * mode. For more granular `checkNoChanges` validation, use `ComponentFixture`.\n */"
},
"entryType": "function",
"description": "Checks the change detector and its children, and throws if any changes are detected.\n\nUse in development mode to verify that running change detection doesn't introduce\nother changes. Calling it in production mode is a noop.",
"jsdocTags": [
{
"name": "deprecated",
"comment": "This is a test-only API that does not have a place in production interface.\n`checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\nmode. For more granular `checkNoChanges` validation, use `ComponentFixture`."
}
],
"rawComment": "/**\n * Checks the change detector and its children, and throws if any changes are detected.\n *\n * Use in development mode to verify that running change detection doesn't introduce\n * other changes. Calling it in production mode is a noop.\n *\n * @deprecated This is a test-only API that does not have a place in production interface.\n * `checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\n * mode. For more granular `checkNoChanges` validation, use `ComponentFixture`.\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
},
{
"name": "reattach",
"signatures": [
{
"name": "reattach",
"entryType": "function",
"description": "Re-attaches the previously detached view to the change detection tree.\nViews are attached to the tree by default.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * Re-attaches the previously detached view to the change detection tree.\n * Views are attached to the tree by default.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n *\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "reattach",
"description": "Re-attaches the previously detached view to the change detection tree.\nViews are attached to the tree by default.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * Re-attaches the previously detached view to the change detection tree.\n * Views are attached to the tree by default.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n *\n */"
},
"entryType": "function",
"description": "Re-attaches the previously detached view to the change detection tree.\nViews are attached to the tree by default.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->",
"jsdocTags": [],
"rawComment": "/**\n * Re-attaches the previously detached view to the change detection tree.\n * Views are attached to the tree by default.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n *\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
}
],
"generics": [],
"description": "Represents an Angular view.",
"jsdocTags": [
{
"name": "see",
"comment": "[Change detection usage](/api/core/ChangeDetectorRef?tab=usage-notes)"
},
{"name": "publicApi", "comment": ""}
],
"rawComment": "/**\n * Represents an Angular view.\n *\n * @see [Change detection usage](/api/core/ChangeDetectorRef?tab=usage-notes)\n *\n * @publicApi\n */",
"extends": "ChangeDetectorRef",
"implements": [],
"source": {
"filePath": "/packages/core/src/linker/view_ref.ts",
"startLine": 18,
"endLine": 37
}
},
{
"name": "afterNextRender",
"signatures": [
{
"name": "afterNextRender",
"entryType": "function",
"description": "Register callbacks to be invoked the next time the application finishes rendering, during the\nspecified phases. The available phases are:\n- `earlyRead`\n Use this phase to **read** from the DOM before a subsequent `write` callback, for example to\n perform custom layout that the browser doesn't natively support. Prefer the `read` phase if\n reading can wait until after the write phase. **Never** write to the DOM in this phase.\n- `write`\n Use this phase to **write** to the DOM. **Never** read from the DOM in this phase.\n- `mixedReadWrite`\n Use this phase to read from and write to the DOM simultaneously. **Never** use this phase if\n it is possible to divide the work among the other phases instead.\n- `read`\n Use this phase to **read** from the DOM. **Never** write to the DOM in this phase.\n\n<div class=\"alert is-critical\">\n\nYou should prefer using the `read` and `write` phases over the `earlyRead` and `mixedReadWrite`\nphases when possible, to avoid performance degradation.\n\n</div>\n\nNote that:\n- Callbacks run in the following phase order *once, after the next render*:\n 1. `earlyRead`\n 2. `write`\n 3. `mixedReadWrite`\n 4. `read`\n- Callbacks in the same phase run in the order they are registered.\n- Callbacks run on browser platforms only, they will not run on the server.\n\nThe first phase callback to run as part of this spec will receive no parameters. Each\nsubsequent phase callback in this spec will receive the return value of the previously run\nphase callback as a parameter. This can be used to coordinate work across multiple phases.\n\nAngular is unable to verify or enforce that phases are used correctly, and instead\nrelies on each developer to follow the guidelines documented for each value and\ncarefully choose the appropriate one, refactoring their code if necessary. By doing\nso, Angular is better able to minimize the performance degradation associated with\nmanual DOM access, ensuring the best experience for the end users of your application\nor library.\n\n<div class=\"alert is-important\">\n\nComponents are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\nYou must use caution when directly reading or writing the DOM and layout.\n\n</div>",
"generics": [
{"name": "E", "default": "never"},
{"name": "W", "default": "never"},
{"name": "M", "default": "never"}
],
"isNewType": false,
"jsdocTags": [
{"name": "param", "comment": "The callback functions to register"},
{"name": "param", "comment": "Options to control the behavior of the callback"},
{
"name": "usageNotes",
"comment": "Use `afterNextRender` to read or write the DOM once,\nfor example to initialize a non-Angular library.\n\n### Example\n```ts\n@Component({\n selector: 'my-chart-cmp',\n template: `<div #chart>{{ ... }}</div>`,\n})\nexport class MyChartCmp {\n @ViewChild('chart') chartRef: ElementRef;\n chart: MyChart|null;\n\n constructor() {\n afterNextRender({\n write: () => {\n this.chart = new MyChart(this.chartRef.nativeElement);\n }\n });\n }\n}\n```"
},
{"name": "developerPreview", "comment": ""}
],
"params": [
{
"name": "spec",
"description": "The callback functions to register",
"type": "{ earlyRead?: () => E; write?: (...args: [E] extends [never] ? [] : [E]) => W; mixedReadWrite?: (...args: [W] extends [never] ? [E] extends [never] ? [] : [E] : [W]) => M; read?: (...args: [...] extends [...] ? [...] extends [...] ? [...] extends [...] ? [] : [...] : [...] : [...]) => void; }",
"isOptional": false,
"isRestParam": false
},
{
"name": "options",
"description": "Options to control the behavior of the callback",
"type": "Omit<AfterRenderOptions, \"phase\">",
"isOptional": true,
"isRestParam": false
}
],
"rawComment": "/**\n * Register callbacks to be invoked the next time the application finishes rendering, during the\n * specified phases. The available phases are:\n * - `earlyRead`\n * Use this phase to **read** from the DOM before a subsequent `write` callback, for example to\n * perform custom layout that the browser doesn't natively support. Prefer the `read` phase if\n * reading can wait until after the write phase. **Never** write to the DOM in this phase.\n * - `write`\n * Use this phase to **write** to the DOM. **Never** read from the DOM in this phase.\n * - `mixedReadWrite`\n * Use this phase to read from and write to the DOM simultaneously. **Never** use this phase if\n * it is possible to divide the work among the other phases instead.\n * - `read`\n * Use this phase to **read** from the DOM. **Never** write to the DOM in this phase.\n *\n * <div class=\"alert is-critical\">\n *\n * You should prefer using the `read` and `write` phases over the `earlyRead` and `mixedReadWrite`\n * phases when possible, to avoid performance degradation.\n *\n * </div>\n *\n * Note that:\n * - Callbacks run in the following phase order *once, after the next render*:\n * 1. `earlyRead`\n * 2. `write`\n * 3. `mixedReadWrite`\n * 4. `read`\n * - Callbacks in the same phase run in the order they are registered.\n * - Callbacks run on browser platforms only, they will not run on the server.\n *\n * The first phase callback to run as part of this spec will receive no parameters. Each\n * subsequent phase callback in this spec will receive the return value of the previously run\n * phase callback as a parameter. This can be used to coordinate work across multiple phases.\n *\n * Angular is unable to verify or enforce that phases are used correctly, and instead\n * relies on each developer to follow the guidelines documented for each value and\n * carefully choose the appropriate one, refactoring their code if necessary. By doing\n * so, Angular is better able to minimize the performance degradation associated with\n * manual DOM access, ensuring the best experience for the end users of your application\n * or library.\n *\n * <div class=\"alert is-important\">\n *\n * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\n * You must use caution when directly reading or writing the DOM and layout.\n *\n * </div>\n *\n * @param spec The callback functions to register\n * @param options Options to control the behavior of the callback\n *\n * @usageNotes\n *\n * Use `afterNextRender` to read or write the DOM once,\n * for example to initialize a non-Angular library.\n *\n * ### Example\n * ```ts\n * @Component({\n * selector: 'my-chart-cmp',\n * template: `<div #chart>{{ ... }}</div>`,\n * })\n * export class MyChartCmp {\n * @ViewChild('chart') chartRef: ElementRef;\n * chart: MyChart|null;\n *\n * constructor() {\n * afterNextRender({\n * write: () => {\n * this.chart = new MyChart(this.chartRef.nativeElement);\n * }\n * });\n * }\n * }\n * ```\n *\n * @developerPreview\n */",
"returnType": "AfterRenderRef"
},
{
"name": "afterNextRender",
"entryType": "function",
"description": "Register a callback to be invoked the next time the application finishes rendering, during the\n`mixedReadWrite` phase.\n\n<div class=\"alert is-critical\">\n\nYou should prefer specifying an explicit phase for the callback instead, or you risk significant\nperformance degradation.\n\n</div>\n\nNote that the callback will run\n- in the order it was registered\n- on browser platforms only\n- during the `mixedReadWrite` phase\n\n<div class=\"alert is-important\">\n\nComponents are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\nYou must use caution when directly reading or writing the DOM and layout.\n\n</div>",
"generics": [],
"isNewType": false,
"jsdocTags": [
{"name": "param", "comment": "A callback function to register"},
{"name": "param", "comment": "Options to control the behavior of the callback"},
{
"name": "usageNotes",
"comment": "Use `afterNextRender` to read or write the DOM once,\nfor example to initialize a non-Angular library.\n\n### Example\n```ts\n@Component({\n selector: 'my-chart-cmp',\n template: `<div #chart>{{ ... }}</div>`,\n})\nexport class MyChartCmp {\n @ViewChild('chart') chartRef: ElementRef;\n chart: MyChart|null;\n\n constructor() {\n afterNextRender({\n write: () => {\n this.chart = new MyChart(this.chartRef.nativeElement);\n }\n });\n }\n}\n```"
},
{"name": "developerPreview", "comment": ""}
],
"params": [
{
"name": "callback",
"description": "A callback function to register",
"type": "VoidFunction",
"isOptional": false,
"isRestParam": false
},
{
"name": "options",
"description": "Options to control the behavior of the callback",
"type": "AfterRenderOptions",
"isOptional": true,
"isRestParam": false
}
],
"rawComment": "/**\n * Register a callback to be invoked the next time the application finishes rendering, during the\n * `mixedReadWrite` phase.\n *\n * <div class=\"alert is-critical\">\n *\n * You should prefer specifying an explicit phase for the callback instead, or you risk significant\n * performance degradation.\n *\n * </div>\n *\n * Note that the callback will run\n * - in the order it was registered\n * - on browser platforms only\n * - during the `mixedReadWrite` phase\n *\n * <div class=\"alert is-important\">\n *\n * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\n * You must use caution when directly reading or writing the DOM and layout.\n *\n * </div>\n *\n * @param callback A callback function to register\n * @param options Options to control the behavior of the callback\n *\n * @usageNotes\n *\n * Use `afterNextRender` to read or write the DOM once,\n * for example to initialize a non-Angular library.\n *\n * ### Example\n * ```ts\n * @Component({\n * selector: 'my-chart-cmp',\n * template: `<div #chart>{{ ... }}</div>`,\n * })\n * export class MyChartCmp {\n * @ViewChild('chart') chartRef: ElementRef;\n * chart: MyChart|null;\n *\n * constructor() {\n * afterNextRender({\n * write: () => {\n * this.chart = new MyChart(this.chartRef.nativeElement);\n * }\n * });\n * }\n * }\n * ```\n *\n * @developerPreview\n */",
"returnType": "AfterRenderRef"
}
],
"implementation": {
"params": [
{
"name": "callbackOrSpec",
"description": "",
"type": "VoidFunction | { earlyRead?: () => unknown; write?: (r?: unknown) => unknown; mixedReadWrite?: (r?: unknown) => unknown; read?: (r?: unknown) => void; }",
"isOptional": false,
"isRestParam": false
},
{
"name": "options",
"description": "",
"type": "AfterRenderOptions",
"isOptional": true,
"isRestParam": false
}
],
"isNewType": false,
"returnType": "AfterRenderRef",
"generics": [],
"name": "afterNextRender",
"description": "",
"entryType": "function",
"jsdocTags": [],
"rawComment": ""
},
"entryType": "function",
"description": "",
"jsdocTags": [],
"rawComment": "",
"source": {
"fileName": "/packages/core/src/user-profile.ts",
"line": 3,
"character": 7
"filePath": "/packages/core/src/render3/after_render_hooks.ts",
"startLine": 442,
"endLine": 450
}
},
{
"name": "provideClientHydration",
"signatures": [
{
"name": "provideClientHydration",
"entryType": "function",
"description": "Sets up providers necessary to enable hydration functionality for the application.\n\nBy default, the function enables the recommended set of features for the optimal\nperformance for most of the applications. It includes the following features:\n\n* Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\ntransferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n[here](guide/ssr#caching-data-when-using-httpclient).\n\nThese functions allow you to disable some of the default features or enable new ones:\n\n* {@link withNoHttpTransferCache} to disable HTTP transfer cache\n* {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n* {@link withI18nSupport} to enable hydration support for i18n blocks\n* {@link withEventReplay} to enable support for replaying user events",
"generics": [],
"isNewType": false,
"jsdocTags": [
{
"name": "usageNotes",
"comment": "Basic example of how you can enable hydration in your application when\n`bootstrapApplication` function is used:\n```\nbootstrapApplication(AppComponent, {\n providers: [provideClientHydration()]\n});\n```\n\nAlternatively if you are using NgModules, you would add `provideClientHydration`\nto your root app module's provider list.\n```\n@NgModule({\n declarations: [RootCmp],\n bootstrap: [RootCmp],\n providers: [provideClientHydration()],\n})\nexport class AppModule {}\n```"
},
{"name": "see", "comment": "{@link withNoHttpTransferCache}"},
{"name": "see", "comment": "{@link withHttpTransferCacheOptions}"},
{"name": "see", "comment": "{@link withI18nSupport}"},
{"name": "see", "comment": "{@link withEventReplay}"},
{
"name": "param",
"comment": "Optional features to configure additional router behaviors."
},
{"name": "returns", "comment": "A set of providers to enable hydration."},
{"name": "publicApi", "comment": ""}
],
"params": [
{
"name": "features",
"description": "Optional features to configure additional router behaviors.",
"type": "HydrationFeature<HydrationFeatureKind>[]",
"isOptional": false,
"isRestParam": true
}
],
"rawComment": "/**\n * Sets up providers necessary to enable hydration functionality for the application.\n *\n * By default, the function enables the recommended set of features for the optimal\n * performance for most of the applications. It includes the following features:\n *\n * * Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n * * [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\n * transferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n * [here](guide/ssr#caching-data-when-using-httpclient).\n *\n * These functions allow you to disable some of the default features or enable new ones:\n *\n * * {@link withNoHttpTransferCache} to disable HTTP transfer cache\n * * {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n * * {@link withI18nSupport} to enable hydration support for i18n blocks\n * * {@link withEventReplay} to enable support for replaying user events\n *\n * @usageNotes\n *\n * Basic example of how you can enable hydration in your application when\n * `bootstrapApplication` function is used:\n * ```\n * bootstrapApplication(AppComponent, {\n * providers: [provideClientHydration()]\n * });\n * ```\n *\n * Alternatively if you are using NgModules, you would add `provideClientHydration`\n * to your root app module's provider list.\n * ```\n * @NgModule({\n * declarations: [RootCmp],\n * bootstrap: [RootCmp],\n * providers: [provideClientHydration()],\n * })\n * export class AppModule {}\n * ```\n *\n * @see {@link withNoHttpTransferCache}\n * @see {@link withHttpTransferCacheOptions}\n * @see {@link withI18nSupport}\n * @see {@link withEventReplay}\n *\n * @param features Optional features to configure additional router behaviors.\n * @returns A set of providers to enable hydration.\n *\n * @publicApi\n */",
"returnType": "EnvironmentProviders"
}
],
"implementation": {
"params": [
{
"name": "features",
"description": "Optional features to configure additional router behaviors.",
"type": "HydrationFeature<HydrationFeatureKind>[]",
"isOptional": false,
"isRestParam": true
}
],
"isNewType": false,
"returnType": "EnvironmentProviders",
"returnDescription": "A set of providers to enable hydration.",
"generics": [],
"name": "provideClientHydration",
"description": "Sets up providers necessary to enable hydration functionality for the application.\n\nBy default, the function enables the recommended set of features for the optimal\nperformance for most of the applications. It includes the following features:\n\n* Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\ntransferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n[here](guide/ssr#caching-data-when-using-httpclient).\n\nThese functions allow you to disable some of the default features or enable new ones:\n\n* {@link withNoHttpTransferCache} to disable HTTP transfer cache\n* {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n* {@link withI18nSupport} to enable hydration support for i18n blocks\n* {@link withEventReplay} to enable support for replaying user events",
"entryType": "function",
"jsdocTags": [
{
"name": "usageNotes",
"comment": "Basic example of how you can enable hydration in your application when\n`bootstrapApplication` function is used:\n```\nbootstrapApplication(AppComponent, {\n providers: [provideClientHydration()]\n});\n```\n\nAlternatively if you are using NgModules, you would add `provideClientHydration`\nto your root app module's provider list.\n```\n@NgModule({\n declarations: [RootCmp],\n bootstrap: [RootCmp],\n providers: [provideClientHydration()],\n})\nexport class AppModule {}\n```"
},
{"name": "see", "comment": "{@link withNoHttpTransferCache}"},
{"name": "see", "comment": "{@link withHttpTransferCacheOptions}"},
{"name": "see", "comment": "{@link withI18nSupport}"},
{"name": "see", "comment": "{@link withEventReplay}"},
{
"name": "param",
"comment": "Optional features to configure additional router behaviors."
},
{"name": "returns", "comment": "A set of providers to enable hydration."},
{"name": "publicApi", "comment": ""}
],
"rawComment": "/**\n * Sets up providers necessary to enable hydration functionality for the application.\n *\n * By default, the function enables the recommended set of features for the optimal\n * performance for most of the applications. It includes the following features:\n *\n * * Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n * * [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\n * transferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n * [here](guide/ssr#caching-data-when-using-httpclient).\n *\n * These functions allow you to disable some of the default features or enable new ones:\n *\n * * {@link withNoHttpTransferCache} to disable HTTP transfer cache\n * * {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n * * {@link withI18nSupport} to enable hydration support for i18n blocks\n * * {@link withEventReplay} to enable support for replaying user events\n *\n * @usageNotes\n *\n * Basic example of how you can enable hydration in your application when\n * `bootstrapApplication` function is used:\n * ```\n * bootstrapApplication(AppComponent, {\n * providers: [provideClientHydration()]\n * });\n * ```\n *\n * Alternatively if you are using NgModules, you would add `provideClientHydration`\n * to your root app module's provider list.\n * ```\n * @NgModule({\n * declarations: [RootCmp],\n * bootstrap: [RootCmp],\n * providers: [provideClientHydration()],\n * })\n * export class AppModule {}\n * ```\n *\n * @see {@link withNoHttpTransferCache}\n * @see {@link withHttpTransferCacheOptions}\n * @see {@link withI18nSupport}\n * @see {@link withEventReplay}\n *\n * @param features Optional features to configure additional router behaviors.\n * @returns A set of providers to enable hydration.\n *\n * @publicApi\n */"
},
"entryType": "function",
"description": "Sets up providers necessary to enable hydration functionality for the application.\n\nBy default, the function enables the recommended set of features for the optimal\nperformance for most of the applications. It includes the following features:\n\n* Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\ntransferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n[here](guide/ssr#caching-data-when-using-httpclient).\n\nThese functions allow you to disable some of the default features or enable new ones:\n\n* {@link withNoHttpTransferCache} to disable HTTP transfer cache\n* {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n* {@link withI18nSupport} to enable hydration support for i18n blocks\n* {@link withEventReplay} to enable support for replaying user events",
"jsdocTags": [
{
"name": "usageNotes",
"comment": "Basic example of how you can enable hydration in your application when\n`bootstrapApplication` function is used:\n```\nbootstrapApplication(AppComponent, {\n providers: [provideClientHydration()]\n});\n```\n\nAlternatively if you are using NgModules, you would add `provideClientHydration`\nto your root app module's provider list.\n```\n@NgModule({\n declarations: [RootCmp],\n bootstrap: [RootCmp],\n providers: [provideClientHydration()],\n})\nexport class AppModule {}\n```"
},
{"name": "see", "comment": "{@link withNoHttpTransferCache}"},
{"name": "see", "comment": "{@link withHttpTransferCacheOptions}"},
{"name": "see", "comment": "{@link withI18nSupport}"},
{"name": "see", "comment": "{@link withEventReplay}"},
{"name": "param", "comment": "Optional features to configure additional router behaviors."},
{"name": "returns", "comment": "A set of providers to enable hydration."},
{"name": "publicApi", "comment": ""}
],
"rawComment": "/**\n * Sets up providers necessary to enable hydration functionality for the application.\n *\n * By default, the function enables the recommended set of features for the optimal\n * performance for most of the applications. It includes the following features:\n *\n * * Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n * * [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\n * transferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n * [here](guide/ssr#caching-data-when-using-httpclient).\n *\n * These functions allow you to disable some of the default features or enable new ones:\n *\n * * {@link withNoHttpTransferCache} to disable HTTP transfer cache\n * * {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n * * {@link withI18nSupport} to enable hydration support for i18n blocks\n * * {@link withEventReplay} to enable support for replaying user events\n *\n * @usageNotes\n *\n * Basic example of how you can enable hydration in your application when\n * `bootstrapApplication` function is used:\n * ```\n * bootstrapApplication(AppComponent, {\n * providers: [provideClientHydration()]\n * });\n * ```\n *\n * Alternatively if you are using NgModules, you would add `provideClientHydration`\n * to your root app module's provider list.\n * ```\n * @NgModule({\n * declarations: [RootCmp],\n * bootstrap: [RootCmp],\n * providers: [provideClientHydration()],\n * })\n * export class AppModule {}\n * ```\n *\n * @see {@link withNoHttpTransferCache}\n * @see {@link withHttpTransferCacheOptions}\n * @see {@link withI18nSupport}\n * @see {@link withEventReplay}\n *\n * @param features Optional features to configure additional router behaviors.\n * @returns A set of providers to enable hydration.\n *\n * @publicApi\n */",
"source": {
"filePath": "/packages/platform-browser/src/hydration.ts",
"startLine": 201,
"endLine": 238
}
},
{
"name": "AfterRenderOptions",
"isAbstract": false,
"entryType": "interface",
"members": [
{
"name": "injector",
"type": "Injector",
"memberType": "property",
"memberTags": ["optional"],
"description": "The `Injector` to use during creation.\n\nIf this is not provided, the current injection context will be used instead (via `inject`).",
"jsdocTags": []
},
{
"name": "phase",
"type": "AfterRenderPhase",
"memberType": "property",
"memberTags": ["optional"],
"description": "The phase the callback should be invoked in.\n\n<div class=\"alert is-critical\">\n\nDefaults to `AfterRenderPhase.MixedReadWrite`. You should choose a more specific\nphase instead. See `AfterRenderPhase` for more information.\n\n</div>",
"jsdocTags": [
{
"name": "deprecated",
"comment": "Specify the phase for your callback to run in by passing a spec-object as the first\nparameter to `afterRender` or `afterNextRender` instead of a function."
}
]
}
],
"generics": [],
"description": "Options passed to `afterRender` and `afterNextRender`.",
"jsdocTags": [{"name": "developerPreview", "comment": ""}],
"rawComment": "/**\n * Options passed to `afterRender` and `afterNextRender`.\n *\n * @developerPreview\n */",
"implements": [],
"source": {
"filePath": "/packages/core/src/render3/after_render_hooks.ts",
"startLine": 103,
"endLine": 125
}
}
]
@@ -0,0 +1,77 @@
import {runfiles} from '@bazel/runfiles';
import {readFile} from 'fs/promises';
import {JSDOM} from 'jsdom';
import {renderEntry} from '../rendering';
import {getRenderable} from '../processing';
import {initHighlighter} from '../shiki/shiki';
import {configureMarkedGlobally} from '../marked/configuration';
import {setSymbols} from '../symbol-context';
// Note: The tests will probably break if the schema of the api extraction changes.
// All entries in the fake-entries are extracted from Angular's api.
// You can just generate them an copy/replace the items in the fake-entries file.
describe('markdown to html', () => {
const entries = new Map<string, DocumentFragment>();
const entries2 = new Map<string, string>();
beforeAll(async () => {
await initHighlighter();
await configureMarkedGlobally();
const entryContent = await readFile(runfiles.resolvePackageRelative('fake-entries.json'), {
encoding: 'utf-8',
});
const entryJson = JSON.parse(entryContent) as any;
const symbols = new Map<string, string>([
['AfterRenderPhase', 'core'],
['afterRender', 'core'],
]);
setSymbols(symbols);
for (const entry of entryJson.entries) {
const renderableJson = getRenderable(entry, '@angular/fakeentry');
const fragment = JSDOM.fragment(await renderEntry(renderableJson));
entries.set(entry['name'], fragment);
entries2.set(entry['name'], await renderEntry(renderableJson));
}
});
it('should render description correctly', () => {
const afterNextRenderEntry = entries.get('afterNextRender')!;
const header = afterNextRenderEntry.querySelector('.docs-reference-header')!;
expect(header).toBeDefined();
expect(header.outerHTML).not.toContain('```');
console.log(entries2.get('afterNextRender'));
const list = afterNextRenderEntry.querySelector('ul')!;
expect(list).toBeDefined();
// List are rendered
expect(list.outerHTML).toContain('<li>');
// Code blocks are rendered
expect(list.outerHTML).toContain('<code>mixedReadWrite</code>');
});
it('should render multiple {@link} blocks', () => {
const provideClientHydrationEntry = entries.get('provideClientHydration')!;
expect(provideClientHydrationEntry).toBeDefined();
const cardItem = provideClientHydrationEntry.querySelector('.docs-reference-card-item')!;
expect(cardItem.innerHTML).not.toContain('@link');
});
it('should create cross-links', () => {
const entry = entries.get('AfterRenderOptions')!;
expect(entry).toBeDefined();
// In the description
const descriptionItem = entry.querySelector('.docs-reference-description')!;
expect(descriptionItem.innerHTML).toContain('<a href="/api/core/afterRender">afterRender</a>');
// In the card
const cardItem = entry.querySelectorAll('.docs-reference-card-item')[1];
expect(cardItem.innerHTML).toContain(
'<a href="/api/core/AfterRenderPhase#MixedReadWrite">AfterRenderPhase.MixedReadWrite</a>',
);
});
});
@@ -16,7 +16,7 @@ import {
addHtmlUsageNotes,
setEntryFlags,
} from './jsdoc-transforms';
import {addRenderableGroupMembers} from './member-transforms';
import {addRenderableMembers} from './member-transforms';
import {addModuleName} from './module-name';
/** Given an unprocessed class entry, get the fully renderable class entry. */
@@ -26,7 +26,7 @@ export function getClassRenderable(
): ClassEntryRenderable {
return setEntryFlags(
addRenderableCodeToc(
addRenderableGroupMembers(
addRenderableMembers(
addHtmlAdditionalLinks(
addHtmlUsageNotes(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(classEntry, moduleName))),
@@ -8,12 +8,11 @@
import {
DocEntry,
FunctionEntry,
FunctionSignatureMetadata,
MemberEntry,
MemberTags,
ParameterEntry,
PropertyEntry,
isFunctionEntryWithOverloads,
} from '../entities';
import {
@@ -32,8 +31,10 @@ import {
import {CodeLineRenderable} from '../entities/renderables';
import {HasModuleName, HasRenderableToc} from '../entities/traits';
import {codeToHtml} from '../shiki/shiki';
import {getModuleName} from '../symbol-context';
import {filterLifecycleMethods, mergeGettersAndSetters} from './member-transforms';
import {getLinkToModule} from './url-transforms';
// Allows to generate links for code lines.
interface CodeTableOfContentsData {
@@ -64,7 +65,9 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
const metadata = mapDocEntryToCode(entry);
appendPrefixAndSuffix(entry, metadata);
const codeWithSyntaxHighlighting = codeToHtml(metadata.contents, 'typescript');
const codeWithSyntaxHighlighting = codeToHtml(metadata.contents, 'typescript', {
removeFunctionKeyword: true,
});
// shiki returns the lines wrapped by 2 node : 1 pre node, 1 code node.
// As leveraging jsdom isn't trivial here, we rely on a regex to extract the line nodes
@@ -77,8 +80,13 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
const insideCode = match[2];
const afterCode = match[3];
const lines = splitLines(insideCode);
const groups = groupCodeLines(lines, metadata);
// Note: Don't expect enum value in signatures to be linked correctly
// as skihi already splits them into separate span blocks.
// Only the enum itself will recieve a link
const codeWithLinks = addApiLinksToHtml(insideCode);
const lines = splitLines(codeWithLinks);
const groups = groupCodeLines(lines, metadata, entry);
return {
...entry,
@@ -89,11 +97,12 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
}
/** Group overloaded methods */
function groupCodeLines(lines: string[], metadata: CodeTableOfContentsData) {
function groupCodeLines(lines: string[], metadata: CodeTableOfContentsData, entry: DocEntry) {
const hasSingleSignature = isFunctionEntry(entry) && entry.signatures.length === 1;
return lines.reduce((groups, line, index) => {
const tocItem = {
const tocItem: CodeLineRenderable = {
contents: line,
id: metadata.codeLineNumbersWithIdentifiers.get(index),
id: hasSingleSignature ? undefined : metadata.codeLineNumbersWithIdentifiers.get(index),
isDeprecated: metadata.deprecatedLineNumbers.some((lineNumber) => lineNumber === index),
};
@@ -109,46 +118,52 @@ function groupCodeLines(lines: string[], metadata: CodeTableOfContentsData) {
}
export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
const isDeprecated = isDeprecatedEntry(entry);
const deprecatedLineNumbers = isDeprecated ? [0] : [];
if (isClassEntry(entry)) {
const members = filterLifecycleMethods(mergeGettersAndSetters(entry.members));
return getCodeTocData(members, true);
return getCodeTocData(members, true, isDeprecated);
}
if (isConstantEntry(entry)) {
const isDeprecated = isDeprecatedEntry(entry);
return {
contents: `const ${entry.name}: ${entry.type};`,
codeLineNumbersWithIdentifiers: new Map(),
deprecatedLineNumbers: isDeprecated ? [0] : [],
deprecatedLineNumbers,
};
}
if (isEnumEntry(entry)) {
return getCodeTocData(entry.members, true);
return getCodeTocData(entry.members, true, isDeprecated);
}
if (isInterfaceEntry(entry)) {
return getCodeTocData(mergeGettersAndSetters(entry.members), true);
return getCodeTocData(mergeGettersAndSetters(entry.members), true, isDeprecated);
}
if (isFunctionEntry(entry)) {
const isDeprecated = isDeprecatedEntry(entry);
const codeLineNumbersWithIdentifiers = new Map<number, string>();
const hasSingleSignature = entry.signatures.length === 1;
if (isFunctionEntryWithOverloads(entry) && entry.overloads) {
if (entry.signatures.length > 0) {
const initialMetadata: CodeTableOfContentsData = {
contents: '',
codeLineNumbersWithIdentifiers: new Map<number, string>(),
deprecatedLineNumbers: [],
deprecatedLineNumbers,
};
return entry.overloads.reduce(
(acc: CodeTableOfContentsData, curr: FunctionEntry, index: number) => {
return entry.signatures.reduce(
(acc: CodeTableOfContentsData, curr: FunctionSignatureMetadata, index: number) => {
const lineNumber = index;
acc.codeLineNumbersWithIdentifiers.set(lineNumber, curr.name);
acc.contents += `${curr.name}(${curr.params
.map((param) => mapParamEntry(param))
.join(`, `)}): ${curr.returnType}\n`;
acc.codeLineNumbersWithIdentifiers.set(lineNumber, `${curr.name}_${index}`);
acc.contents += getMethodCodeLine(curr, [], hasSingleSignature, true);
// We don't want to add line break after the last item
if (!hasSingleSignature && index < entry.signatures.length - 1) {
acc.contents += '\n';
}
if (isDeprecatedEntry(curr)) {
acc.deprecatedLineNumbers.push(lineNumber);
}
@@ -157,11 +172,12 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
initialMetadata,
);
}
return {
// It is important to add the function keyword as shiki will only highlight valid ts
contents: `function ${getMethodCodeLine(entry, [], true)}`,
contents: `function ${getMethodCodeLine(entry.implementation, [], true)}`,
codeLineNumbersWithIdentifiers,
deprecatedLineNumbers: isDeprecated ? [0] : [],
deprecatedLineNumbers,
};
}
@@ -206,16 +222,13 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
return {
contents: lines.join('\n'),
codeLineNumbersWithIdentifiers,
deprecatedLineNumbers: [],
deprecatedLineNumbers,
};
}
if (isTypeAliasEntry(entry)) {
const isDeprecated = isDeprecatedEntry(entry);
const contents = `type ${entry.name} = ${entry.type}`;
let deprecatedLineNumbers = [];
if (isDeprecated) {
const numberOfLinesOfCode = getNumberOfLinesOfCode(contents);
@@ -234,36 +247,54 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
return {
contents: '',
codeLineNumbersWithIdentifiers: new Map(),
deprecatedLineNumbers: [],
deprecatedLineNumbers,
};
}
/** Generate code ToC data for list of members. */
function getCodeTocData(members: MemberEntry[], hasPrefixLine: boolean): CodeTableOfContentsData {
function getCodeTocData(
members: MemberEntry[],
hasPrefixLine: boolean,
isDeprecated: boolean,
): CodeTableOfContentsData {
const initialMetadata: CodeTableOfContentsData = {
contents: '',
codeLineNumbersWithIdentifiers: new Map<number, string>(),
deprecatedLineNumbers: [],
deprecatedLineNumbers: isDeprecated ? [0] : [],
};
// In case when hasPrefixLine is true we should take it into account when we're generating
// `codeLineNumbersWithIdentifiers` below.
const skip = !!hasPrefixLine ? 1 : 0;
let lineNumber = skip;
return members.reduce((acc: CodeTableOfContentsData, curr: MemberEntry, index: number) => {
const lineNumber = index + skip;
acc.codeLineNumbersWithIdentifiers.set(lineNumber, curr.name);
acc.contents += ` ${getCodeLine(curr).trim()}\n`;
if (isDeprecatedEntry(curr)) {
acc.deprecatedLineNumbers.push(lineNumber);
const setTocData = (entry: DocEntry | MemberEntry, content: string) => {
acc.contents += ` ${content.trim()}\n`;
acc.codeLineNumbersWithIdentifiers.set(lineNumber, entry.name);
if (isDeprecatedEntry(entry)) {
acc.deprecatedLineNumbers.push(lineNumber);
}
lineNumber++;
};
if (isClassMethodEntry(curr)) {
if (curr.signatures.length > 0) {
curr.signatures.forEach((signature) => {
setTocData(signature, getMethodCodeLine(signature, curr.memberTags));
});
} else {
setTocData(curr, getMethodCodeLine(curr.implementation, curr.memberTags));
}
} else {
setTocData(curr, getCodeLine(curr));
}
return acc;
}, initialMetadata);
}
function getCodeLine(member: MemberEntry) {
if (isClassMethodEntry(member)) {
return getMethodCodeLine(member, member.memberTags);
} else if (isGetterEntry(member)) {
function getCodeLine(member: MemberEntry): string {
if (isGetterEntry(member)) {
return getGetterCodeLine(member);
} else if (isSetterEntry(member)) {
return getSetterCodeLine(member);
@@ -281,11 +312,13 @@ function getPropertyCodeLine(member: PropertyEntry): string {
/** Map method entry to text */
function getMethodCodeLine(
member: FunctionEntry,
member: FunctionSignatureMetadata,
memberTags: MemberTags[] = [],
displayParamsInNewLines: boolean = false,
isFunction: boolean = false,
): string {
return `${memberTags.join(' ')} ${member.name}(${displayParamsInNewLines ? '\n ' : ''}${member.params
displayParamsInNewLines &&= member.params.length > 0;
return `${isFunction ? 'function' : ''}${memberTags.join(' ')} ${member.name}(${displayParamsInNewLines ? '\n ' : ''}${member.params
.map((param) => mapParamEntry(param))
.join(`,${displayParamsInNewLines ? '\n ' : ' '}`)}${
displayParamsInNewLines ? '\n' : ''
@@ -344,7 +377,7 @@ function getNumberOfLinesOfCode(contents: string): number {
/** Prints an initializer function signature into a single line. */
export function printInitializerFunctionSignatureLine(
name: string,
signature: FunctionEntry,
signature: FunctionSignatureMetadata,
showTypesInSignaturePreview: boolean,
): string {
let res = name;
@@ -381,7 +414,7 @@ export function printInitializerFunctionSignatureLine(
res += `: ${signature.returnType}`;
}
res += ';';
return res;
return `function ${res}`;
}
function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContentsData): void {
@@ -393,9 +426,28 @@ function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContents
data.contents = `${firstLine}\n${data.contents}${lastLine}`;
};
if (isClassEntry(entry)) {
const abstractPrefix = entry.isAbstract ? 'abstract ' : '';
appendFirstAndLastLines(codeTocData, `${abstractPrefix}class ${entry.name} {`, `}`);
if (isClassEntry(entry) || isInterfaceEntry(entry)) {
const generics =
entry.generics?.length > 0
? `<${entry.generics
.map((g) => (g.constraint ? `${g.name} extends ${g.constraint}` : g.name))
.join(', ')}>`
: '';
const extendsStr = entry.extends ? ` extends ${entry.extends}` : '';
// TODO: remove the ? when we distinguish Class & Decorator entries
const implementsStr =
entry.implements?.length > 0 ? ` implements ${entry.implements.join(' ,')}` : '';
const signature = `${entry.name}${generics}${extendsStr}${implementsStr}`;
if (isClassEntry(entry)) {
const abstractPrefix = entry.isAbstract ? 'abstract ' : '';
appendFirstAndLastLines(codeTocData, `${abstractPrefix}class ${signature} {`, `}`);
}
if (isInterfaceEntry(entry)) {
appendFirstAndLastLines(codeTocData, `interface ${signature} {`, `}`);
}
}
if (isEnumEntry(entry)) {
@@ -406,3 +458,31 @@ function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContents
appendFirstAndLastLines(codeTocData, `interface ${entry.name} {`, `}`);
}
}
/**
* Replaces any code block that isn't already wrapped by an anchor element
* by a link if the symbol is known
*/
export function addApiLinksToHtml(htmlString: string): string {
const result = htmlString.replace(
// This regex looks for span/code blocks not wrapped by an anchor block.
// Their content are then replaced with a link if the symbol is known
// The captured content ==> vvvvvvvv
/(?<!<a[^>]*>)(<(?:(?:span)|(?:code))[^>]*>\s*)([^<]*?)(\s*<\/(?:span|code)>)/g,
(type: string, span1: string, potentialSymbolName: string, span2: string) => {
let [symbol, subSymbol] = potentialSymbolName.split(/(?:#|\.)/) as [string, string?];
// mySymbol() => mySymbol
const symbolWithoutInvocation = symbol.replace(/\([^)]*\);?/g, '');
const moduleName = getModuleName(symbolWithoutInvocation)!;
if (moduleName) {
return `${span1}<a href="${getLinkToModule(moduleName, symbol, subSymbol)}">${potentialSymbolName}</a>${span2}`;
}
return type;
},
);
return result;
}
@@ -6,9 +6,11 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {FunctionEntry, isFunctionEntryWithOverloads} from '../entities';
import {FunctionEntryRenderable} from '../entities/renderables';
import {HasRenderableOverloads} from '../entities/traits';
import {FunctionEntry, FunctionSignatureMetadata} from '../entities';
import {
FunctionEntryRenderable,
FunctionSignatureMetadataRenderable,
} from '../entities/renderables';
import {addRenderableCodeToc} from './code-transforms';
import {
addHtmlAdditionalLinks,
@@ -27,15 +29,10 @@ export function getFunctionRenderable(
): FunctionEntryRenderable {
return setEntryFlags(
addRenderableCodeToc(
addRenderableFunctionParams(
addOverloads(
moduleName,
addHtmlAdditionalLinks(
addHtmlUsageNotes(
setEntryFlags(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
),
),
addHtmlAdditionalLinks(
addHtmlUsageNotes(
setEntryFlags(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
),
),
),
@@ -43,15 +40,17 @@ export function getFunctionRenderable(
);
}
function addOverloads<T extends FunctionEntry>(
moduleName: string,
entry: T,
): T & HasRenderableOverloads {
return {
...entry,
overloads:
isFunctionEntryWithOverloads(entry) && entry.overloads
? entry.overloads.map((overload) => getFunctionRenderable(overload, moduleName))
: null,
};
export function getFunctionMetadataRenderable(
entry: FunctionSignatureMetadata,
moduleName: string = '',
): FunctionSignatureMetadataRenderable {
return addHtmlAdditionalLinks(
addRenderableFunctionParams(
addHtmlUsageNotes(
setEntryFlags(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
),
),
),
);
}
@@ -16,7 +16,7 @@ import {
addHtmlUsageNotes,
setEntryFlags,
} from './jsdoc-transforms';
import {addRenderableGroupMembers} from './member-transforms';
import {addRenderableMembers} from './member-transforms';
import {addModuleName} from './module-name';
/** Given an unprocessed interface entry, get the fully renderable interface entry. */
@@ -26,7 +26,7 @@ export function getInterfaceRenderable(
): InterfaceEntryRenderable {
return setEntryFlags(
addRenderableCodeToc(
addRenderableGroupMembers(
addRenderableMembers(
addHtmlAdditionalLinks(
addHtmlUsageNotes(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
@@ -30,6 +30,8 @@ import {
} from '../entities/traits';
import {getLinkToModule} from './url-transforms';
import {addApiLinksToHtml} from './code-transforms';
import {getModuleName} from '../symbol-context';
export const JS_DOC_USAGE_NOTES_TAG = 'usageNotes';
export const JS_DOC_SEE_TAG = 'see';
@@ -37,6 +39,7 @@ export const JS_DOC_DESCRIPTION_TAG = 'description';
// Some links are written in the following format: {@link Route}
const jsDoclinkRegex = /\{\s*@link\s+([^}]+)\s*\}/;
const jsDoclinkRegexGlobal = new RegExp(jsDoclinkRegex.source, 'g');
/** Given an entity with a description, gets the entity augmented with an `htmlDescription`. */
export function addHtmlDescription<T extends HasDescription & HasModuleName>(
@@ -100,15 +103,18 @@ export function addHtmlUsageNotes<T extends HasJsDocTags>(entry: T): T & HasHtml
) as string)
: '';
const transformedHtml = addApiLinksToHtml(htmlUsageNotes);
return {
...entry,
htmlUsageNotes,
htmlUsageNotes: transformedHtml,
};
}
/** Given a markdown JsDoc text, gets the rendered HTML. */
function getHtmlForJsDocText<T extends HasModuleName>(text: string, entry: T): string {
return marked.parse(convertLinks(wrapExampleHtmlElementsWithCode(text), entry)) as string;
const parsed = marked.parse(convertLinks(wrapExampleHtmlElementsWithCode(text))) as string;
return addApiLinksToHtml(parsed);
}
export function setEntryFlags<T extends HasJsDocTags & HasModuleName>(
@@ -126,9 +132,7 @@ export function setEntryFlags<T extends HasJsDocTags & HasModuleName>(
};
}
function getHtmlAdditionalLinks<T extends HasJsDocTags & HasModuleName>(
entry: T,
): LinkEntryRenderable[] {
function getHtmlAdditionalLinks<T extends HasJsDocTags>(entry: T): LinkEntryRenderable[] {
const markdownLinkRule = /\[(.*?)\]\((.*?)(?: "(.*?)")?\)/;
const seeAlsoLinks = entry.jsdocTags
@@ -149,21 +153,8 @@ function getHtmlAdditionalLinks<T extends HasJsDocTags & HasModuleName>(
if (linkMatch) {
const link = linkMatch[1];
// handling links like {@link Route Some route with description}
const [symbol, description] = link.split(/\s(.+)/);
if (entry && description) {
return {
label: description.trim(),
url: `${getLinkToModule(entry.moduleName)}/${symbol}`,
};
}
// handling links like {@link Route}
return {
label: linkMatch[1].trim(),
url: `${getLinkToModule(entry.moduleName)}/${linkMatch[1].trim()}`,
};
const {url, label} = parseAtLink(link);
return {label, url};
}
return undefined;
@@ -195,15 +186,38 @@ function convertJsDocExampleToHtmlExample(text: string): string {
);
}
function convertLinks(text: string, entry: HasModuleName) {
return text.replace(jsDoclinkRegex, (_, link) => {
const [symbol, description] = link.split(/\s(.+)/);
if (symbol && description) {
// {@link Route Some route with description}
return `<a href="${getLinkToModule(entry.moduleName)}/${symbol}"><code>${description}</code></a>`;
} else {
// {@link Route}
return `<a href="${getLinkToModule(entry.moduleName)}/${symbol}"><code>${symbol}</code></a>`;
}
/**
* Converts {@link } tags into html anchor elements
*/
function convertLinks(text: string) {
return text.replace(jsDoclinkRegexGlobal, (_, link) => {
const {label, url} = parseAtLink(link);
return `<a href="${url}"><code>${label}</code></a>`;
});
}
function parseAtLink(link: string) {
// Because of microsoft/TypeScript/issues/59679
// getTextOfJSDocComment introduces an extra space between the symbol and a trailing ()
link = link.replace(/ \(\)$/, '');
let [rawSymbol, description] = link.split(/\s(.+)/);
let [symbol, subSymbol] = rawSymbol.split(/(?:#|\.)/);
const moduleName = getModuleName(symbol)!;
if (!moduleName) {
logWarning(link, symbol);
}
return {
label: description ?? rawSymbol,
url: getLinkToModule(moduleName, symbol, subSymbol),
};
}
function logWarning(link: string, symbol: string) {
// TODO: remove the links that generate this error
// TODO: throw an error when there are no more warning generated
console.warn(`WARNING: {@link ${link}} is invalid, ${symbol} is unknown in this context`);
}
@@ -8,14 +8,7 @@
import {MemberEntry, MemberTags, MemberType} from '../entities';
import {isClassMethodEntry} from '../entities/categorization';
import {MemberEntryRenderable} from '../entities/renderables';
import {
HasMembers,
HasModuleName,
HasRenderableMembers,
HasRenderableMembersGroups,
} from '../entities/traits';
import {HasMembers, HasModuleName, HasRenderableMembers} from '../entities/traits';
import {
addHtmlDescription,
@@ -69,44 +62,13 @@ export function mergeGettersAndSetters(members: MemberEntry[]): MemberEntry[] {
);
}
/** Given an entity with members, gets the entity augmented with renderable members. */
export function addRenderableGroupMembers<T extends HasMembers & HasModuleName>(
entry: T,
): T & HasRenderableMembersGroups {
const members = filterLifecycleMethods(entry.members);
const membersGroups = members.reduce((groups, item) => {
const member = setEntryFlags(
addMethodParamsDescription(
addHtmlDescription(
addHtmlUsageNotes(addHtmlJsDocTagComments(addModuleName(item, entry.moduleName))),
),
),
);
if (groups.has(member.name)) {
const group = groups.get(member.name);
group?.push(member);
} else {
groups.set(member.name, [member]);
}
return groups;
}, new Map<string, MemberEntryRenderable[]>());
return {
...entry,
membersGroups,
};
}
export function addRenderableMembers<T extends HasMembers & HasModuleName>(
entry: T,
): T & HasRenderableMembers {
const members = entry.members.map((member) =>
setEntryFlags(
addMethodParamsDescription(
addHtmlDescription(
addHtmlUsageNotes(addHtmlJsDocTagComments(addModuleName(member, entry.moduleName))),
),
addHtmlDescription(
addHtmlUsageNotes(addHtmlJsDocTagComments(addModuleName(member, entry.moduleName))),
),
),
);
@@ -116,15 +78,3 @@ export function addRenderableMembers<T extends HasMembers & HasModuleName>(
members,
};
}
function addMethodParamsDescription<T extends MemberEntry & HasModuleName>(entry: T): T {
if (isClassMethodEntry(entry)) {
return {
...entry,
params: entry.params.map((param) =>
addHtmlDescription(addModuleName(param, entry.moduleName)),
),
};
}
return entry;
}
@@ -9,12 +9,8 @@
export const API_PREFIX = 'api';
export const MODULE_NAME_PREFIX = '@angular/';
export function removeAngularPrefixFromModule(moduleName: string): string {
return moduleName.replace(MODULE_NAME_PREFIX, '');
}
export function getLinkToModule(moduleName: string) {
return `${API_PREFIX}/${removeAngularPrefixFromModule(moduleName)}`;
export function getLinkToModule(moduleName: string, symbol: string, subSymbol?: string) {
return `${API_PREFIX}/${moduleName}/${symbol}${subSymbol ? `#${subSymbol}` : ''}`;
}
export const normalizePath = (path: string): string => {
@@ -18,8 +18,8 @@ export interface DocsCodeToken extends CodeToken {
// Capture group 1: all attributes on the opening tag
// Capture group 2: all content between the open and close tags
const singleFileSelfClosingCodeRule = /^\s*<docs-code\s([^>]*)((?:.(?!\/>))*)\/>/s;
const singleFileCodeRule = /^\s*<docs-code\s([^>]*)>((?:.(?!\/docs-code))*)<\/docs-code>/s;
const singleFileCodeRule =
/^\s*<docs-code((?:\s+[\w-]+(?:="[^"]*"|='[^']*'|=[^\s>]*)?)*)\s*(?:\/>|>(.*?)<\/docs-code>)/s;
const pathRule = /path="([^"]*)"/;
const headerRule = /header="([^"]*)"/;
@@ -38,10 +38,7 @@ export const docsCodeExtension = {
return src.match(/^<docs-code\s/)?.index;
},
tokenizer(this: TokenizerThis, src: string): DocsCodeToken | undefined {
const code = singleFileCodeRule.exec(src);
const selfClosingCode = singleFileSelfClosingCodeRule.exec(src);
const match = selfClosingCode ?? code;
const match = singleFileCodeRule.exec(src);
if (match) {
const attr = match[1].trim();
@@ -55,7 +52,7 @@ export const docsCodeExtension = {
const visibleRegion = visibleRegionRule.exec(attr);
const preview = previewRule.exec(attr) ? true : false;
let code = match[2].trim();
let code = match[2]?.trim() ?? '';
if (path && path[1]) {
code = loadWorkspaceRelativeFile(path[1]);
// Remove ESLint Comments
@@ -7,7 +7,7 @@
*/
import {Token, Tokens, RendererThis, TokenizerThis} from 'marked';
import {headingRender} from '../../tranformations/heading';
import {formatHeading, headingRender} from '../../tranformations/heading';
interface DocsStepToken extends Tokens.Generic {
type: 'docs-step';
@@ -51,7 +51,7 @@ export const docsStepExtension = {
return `
<li>
<span class="docs-step-number" aria-hidden="true"></span>
${headingRender(token.title, 3, token.title)}
${formatHeading({text: token.title, depth: 3})}
${this.parser.parse(token.tokens)}
</li>
`;
+2 -3
View File
@@ -8,7 +8,7 @@
import {marked} from 'marked';
import {hooks} from './hooks';
import {renderer} from './renderer';
import {Renderer} from './renderer';
import {docsAlertExtension} from './extensions/docs-alert';
import {docsCalloutExtension} from './extensions/docs-callout';
import {docsPillExtension} from './extensions/docs-pill/docs-pill';
@@ -33,7 +33,6 @@ export async function parseMarkdown(
marked.use({
hooks,
renderer,
extensions: [
docsAlertExtension,
docsCalloutExtension,
@@ -55,5 +54,5 @@ export async function parseMarkdown(
async: true,
});
return marked.parse(markdownContent);
return marked.parse(markdownContent, {renderer: new Renderer()});
}
+9 -10
View File
@@ -1,20 +1,19 @@
import {RendererObject} from 'marked';
import {Renderer as _Renderer} from 'marked';
import {linkRender} from './tranformations/link';
import {tableRender} from './tranformations/table';
import {listRender} from './tranformations/list';
import {imageRender} from './tranformations/image';
import {textRender} from './tranformations/text';
import {headingRender} from './tranformations/heading';
/**
* Custom renderer for marked that will be used to transform markdown files to HTML
* files that can be used in the Angular docs.
*/
export const renderer: RendererObject = {
link: linkRender,
table: tableRender,
list: listRender,
image: imageRender,
text: textRender,
heading: headingRender,
};
export class Renderer extends _Renderer {
override link = linkRender;
override table = tableRender;
override list = listRender;
override image = imageRender;
override text = textRender;
override heading = headingRender;
}
@@ -8,3 +8,5 @@ this is code
<docs-code path="adev/shared-docs/pipeline/guides/testing/docs-code/new-code.ts"
diff="adev/shared-docs/pipeline/guides/testing/docs-code/old-code.ts" />
<docs-code header="src/locale/messages.fr.xlf (<trans-unit>)" path="docs/pipeline/guides/testing/docs-code/messages.fr.xlf.html" />
@@ -52,4 +52,9 @@ describe('markdown to html', () => {
expect(codeLines[2].classList.contains('add')).toBeFalse();
expect(codeLines[2].classList.contains('remove')).toBeFalse();
});
it('should load header and html code', () => {
const codeBlock = markdownDocument.querySelectorAll('code')[4];
expect(codeBlock).toBeTruthy();
});
});
@@ -0,0 +1,78 @@
<!-- The `messages.fr.xlf` after translation for documentation purposes -->
<!-- #docregion -->
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<!-- #docregion translated-hello-before -->
<trans-unit id="introductionHeader" datatype="html">
<source>Hello i18n!</source>
<note priority="1" from="description">An introduction header for this sample</note>
<note priority="1" from="meaning">User welcome</note>
</trans-unit>
<!-- #enddocregion translated-hello-before -->
<!-- #docregion translated-hello -->
<!-- #docregion custom-id -->
<trans-unit id="introductionHeader" datatype="html">
<!-- #enddocregion custom-id -->
<source>Hello i18n!</source>
<target>Bonjour i18n !</target>
<note priority="1" from="description">An introduction header for this sample</note>
<note priority="1" from="meaning">User welcome</note>
</trans-unit>
<!-- #enddocregion translated-hello -->
<!-- #docregion translated-other-nodes -->
<!-- #docregion generated-id -->
<trans-unit id="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
<!-- #enddocregion generated-id -->
<source>I don&apos;t output any element</source>
<target>Je n'affiche aucun élément</target>
</trans-unit>
<trans-unit id="701174153757adf13e7c24a248c8a873ac9f5193" datatype="html">
<source>Angular logo</source>
<target>Logo d'Angular</target>
</trans-unit>
<!-- #enddocregion translated-other-nodes -->
<!-- #docregion translated-plural -->
<trans-unit id="5a134dee893586d02bffc9611056b9cadf9abfad" datatype="html">
<source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago} }</source>
<target>{VAR_PLURAL, plural, =0 {à l'instant} =1 {il y a une minute} other {il y a <x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes} }</target>
</trans-unit>
<!-- #enddocregion translated-plural -->
<!-- #docregion translated-select -->
<!-- #docregion translate-select-1 -->
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
<source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
<target>L'auteur est <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>
</trans-unit>
<!-- #enddocregion translate-select-1 -->
<!-- #docregion translate-select-2 -->
<trans-unit id="eff74b75ab7364b6fa888f1cbfae901aaaf02295" datatype="html">
<source>{VAR_SELECT, select, male {male} female {female} other {other} }</source>
<target>{VAR_SELECT, select, male {un homme} female {une femme} other {autre} }</target>
</trans-unit>
<!-- #enddocregion translate-select-2 -->
<!-- #enddocregion translated-select -->
<!-- #docregion translate-nested -->
<!-- #docregion translate-nested-1 -->
<trans-unit id="972cb0cf3e442f7b1c00d7dab168ac08d6bdf20c" datatype="html">
<source>Updated: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></source>
<target>Mis à jour: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></target>
</trans-unit>
<!-- #enddocregion translate-nested-1 -->
<!-- #docregion translate-nested-2 -->
<trans-unit id="7151c2e67748b726f0864fc443861d45df21d706" datatype="html">
<source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago by {VAR_SELECT, select, male {male} female {female} other {other} }} }</source>
<target>{VAR_PLURAL, plural, =0 {à l'instant} =1 {il y a une minute} other {il y a <x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes par {VAR_SELECT, select, male {un homme} female {une femme} other {autre} }} }</target>
</trans-unit>
<!-- #enddocregion translate-nested-2 -->
<!-- #enddocregion translate-nested -->
<!-- #docregion i18n-duplicate-custom-id -->
<trans-unit id="myId" datatype="html">
<source>Hello</source>
<target state="new">Bonjour</target>
</trans-unit>
<!-- #enddocregion i18n-duplicate-custom-id -->
</body>
</file>
</xliff>
@@ -9,4 +9,5 @@
## Duplicate Anchor
## `myClass.myMethod` is the best
## ステップ 2 - アプリケーションのレイアウトに新しいコンポーネントを追加
## My heading {# my-custom-id }
## My heading {# my-custom-id }
## Query for the `<h1>`
@@ -83,4 +83,15 @@ describe('markdown to html', () => {
expect(h2HeaderId).toBe('my-custom-id');
expect(h2AnchorHref).toBe(`#${h2HeaderId}`);
});
it('should be able to parse heading with a valid tag in a code block', () => {
const h2List = markdownDocument.querySelectorAll('h2');
const h2 = h2List[6];
// The anchor element should be to only child
expect(h2.children.length).toBe(1);
expect(h2.firstElementChild?.tagName).toBe('A');
expect(h2.firstElementChild!.innerHTML).toBe('Query for the <code>&lt;h1&gt;</code>');
});
});
@@ -7,4 +7,6 @@
- Order
- here
- matter
- doesn't
- doesn't
- [some link](https://angular.dev)
- Code block `SomeClass`
@@ -21,7 +21,16 @@ describe('markdown to html', () => {
const unorderedList = markdownDocument.querySelector('ul');
expect(unorderedList?.className).toBe('docs-list');
expect(unorderedList?.childElementCount).toBe(4);
expect(unorderedList?.childElementCount).toBe(6);
expect(unorderedList?.textContent).toContain('matter');
});
it('should render list items', () => {
const unorderedList = markdownDocument.querySelector('ul');
const linkItem = unorderedList!.children[4];
expect(linkItem.outerHTML).toContain('href="https://angular.dev"');
const codeItem = unorderedList!.children[5];
expect(codeItem.outerHTML).toContain('<code>SomeClass</code>');
});
});
@@ -6,13 +6,18 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
import {getHeaderId} from '../state';
import {getPageTitle} from '../utils';
export const headingRender: RendererApi['heading'] = (text, level, raw) => {
if (level === 1) {
export function headingRender(this: Renderer, {depth, tokens}: Tokens.Heading): string {
const text = this?.parser.parseInline(tokens);
return formatHeading({text, depth});
}
export function formatHeading({text, depth}: {text: string; depth: number}): string {
if (depth === 1) {
return `
<header class="docs-header">
<docs-breadcrumb></docs-breadcrumb>
@@ -36,8 +41,8 @@ export const headingRender: RendererApi['heading'] = (text, level, raw) => {
const label = anchorLessText.replace(/`(.*?)`/g, '<code>$1</code>').replace(customIdRegex, '');
return `
<h${level} id="${link}">
<h${depth} id="${link}">
<a href="#${link}" class="docs-anchor" tabindex="-1" aria-label="Link to ${label}">${label}</a>
</h${level}>
</h${depth}>
`;
};
}
@@ -7,15 +7,16 @@
*/
import {normalize} from 'path';
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
// TODO(josephperrott): Determine how we can define/know the image content base path.
const imageContentBasePath = 'unknown';
export const imageRender: RendererApi['image'] = (href, title, text) => {
export function imageRender(this: Renderer, {href, title, text}: Tokens.Image) {
const isRelativeSrc = href?.startsWith('./');
const src = isRelativeSrc ? `${imageContentBasePath}/${normalize(href)}` : href;
return `
<img src="${src}" alt="${text}" title="${title}" class="docs-image">
`;
};
}
@@ -7,9 +7,9 @@
*/
import {anchorTarget} from '../helpers';
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
export const linkRender: RendererApi['link'] = (href, title, text) => {
export function linkRender(this: Renderer, {href, title, tokens}: Tokens.Link) {
const titleAttribute = title ? ` title=${title}` : '';
return `<a href="${href}"${titleAttribute}${anchorTarget(href)}>${text}</a>`;
};
return `<a href="${href}"${titleAttribute}${anchorTarget(href)}>${this.parser.parseInline(tokens)}</a>`;
}
@@ -6,19 +6,19 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
export const listRender: RendererApi['list'] = (body, ordered, start) => {
export function listRender(this: Renderer, {items, ordered}: Tokens.List) {
if (ordered) {
return `
<ol class="docs-ordered-list">
${body}
${items.map((item) => this.listitem(item)).join('')}
</ol>
`;
}
return `
<ul class="docs-list">
${body}
${items.map((item) => this.listitem(item)).join('')}
</ul>
`;
};
}
@@ -6,19 +6,27 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
export const tableRender: RendererApi['table'] = (header, body) => {
export function tableRender(this: Renderer, {header, rows}: Tokens.Table) {
return `
<div class="docs-table docs-scroll-track-transparent">
<table>
<thead>
${header}
${this.tablerow({
text: header.map((cell) => this.tablecell(cell)).join(''),
})}
</thead>
<tbody>
${body}
${rows
.map((row) =>
this.tablerow({
text: row.map((cell) => this.tablecell(cell)).join(''),
}),
)
.join('')}
</tbody>
</table>
</div>
`;
};
}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
import emojiRegex from 'emoji-regex';
/** Regex to find unicode emojis. */
@@ -15,8 +15,9 @@ const UNICODE_EMOJI_REGEX = /&#x[\dA-Fa-f]+;/g;
/** Regex to find emojis. */
const regex = emojiRegex();
export const textRender: RendererApi['text'] = (text) => {
return regex.test(text) || UNICODE_EMOJI_REGEX.test(text)
export function textRender(this: Renderer, token: Tokens.Text) {
const text = token.tokens ? this.parser.parseInline(token.tokens) : token.text;
return regex.test(token.text) || UNICODE_EMOJI_REGEX.test(token.text)
? `<span class="docs-emoji">${text}</span>`
: text;
};
}
@@ -43,6 +43,32 @@ export class Search {
? from(
this.index.search(query, {
maxValuesPerFacet: MAX_VALUE_PER_FACET,
attributesToRetrieve: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
'type',
'url',
],
hitsPerPage: 20,
snippetEllipsisText: '…',
highlightPreTag: '<ɵ>',
highlightPostTag: '</ɵ>',
attributesToHighlight: [],
attributesToSnippet: [
'hierarchy.lvl1:10',
'hierarchy.lvl2:10',
'hierarchy.lvl3:10',
'hierarchy.lvl4:10',
'hierarchy.lvl5:10',
'hierarchy.lvl6:10',
'content:10',
],
}),
)
: of(undefined);
@@ -72,6 +98,20 @@ export class Search {
const uniqueUrls = new Set<string>();
return items.filter((item) => {
if (item.type === 'content' && !item._snippetResult.content) {
return false;
}
// Ensure that this result actually matched on the type.
// If not, this is going to be a duplicate. There should be another result in
// the list that already matched on its type.
// A lvl2 match will also return all its lvl3 results as well, even if those
// values don't also match the query.
if (
item.type.indexOf('lvl') === 0 &&
item._snippetResult.hierarchy?.[item.type as 'lvl1']?.matchLevel === 'none'
) {
return false;
}
if (item.url && !uniqueUrls.has(item.url)) {
uniqueUrls.add(item.url);
return true;
@@ -14,7 +14,6 @@ import {
afterNextRender,
inject,
signal,
NgZone,
} from '@angular/core';
import {RESIZE_EVENT_DELAY} from '../constants/index';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
@@ -34,7 +33,6 @@ export class TableOfContentsScrollSpy {
private readonly tableOfContentsLoader = inject(TableOfContentsLoader);
private readonly document = inject(DOCUMENT);
private readonly window = inject(WINDOW);
private readonly ngZone = inject(NgZone);
private readonly viewportScroller = inject(ViewportScroller);
private readonly injector = inject(EnvironmentInjector);
private contentSourceElement: HTMLElement | null = null;
@@ -112,9 +110,7 @@ export class TableOfContentsScrollSpy {
takeUntilDestroyed(this.destroyRef),
);
this.ngZone.runOutsideAngular(() => {
scroll$.subscribe(() => this.setActiveItemId());
});
scroll$.subscribe(() => this.setActiveItemId());
}
private setActiveItemId(): void {
+1
View File
@@ -81,6 +81,7 @@
p > a,
td > a,
div > a:not(.docs-card),
code > a,
li:not(.docs-faceted-list *) a {
color: var(--bright-blue);
&:hover {
+28 -17
View File
@@ -90,21 +90,20 @@ $theme: mat.m2-define-light-theme(
}
}
.docs-dark-mode .shiki {
color: var(--shiki-dark) ;
background-color: var(--shiki-dark-bg) ;
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
span {
color: var(--shiki-dark) ;
background-color: var(--shiki-dark-bg) ;
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
/* Optional, if you also want font styles */
font-style: var(--shiki-dark-font-style) ;
font-weight: var(--shiki-dark-font-weight) ;
text-decoration: var(--shiki-dark-text-decoration) ;
font-style: var(--shiki-dark-font-style);
font-weight: var(--shiki-dark-font-weight);
}
.shiki-ln-line-highlighted, button:hover {
.shiki-ln-line-highlighted,
button:hover {
span {
background-color: inherit;
}
@@ -113,24 +112,36 @@ $theme: mat.m2-define-light-theme(
.shiki {
padding-block: 1rem;
&.cli {
padding-inline-start: 1rem;
}
a {
color: inherit;
&:hover {
text-decoration: underline;
}
}
}
.docs-light-mode .shiki {
color: var(--shiki-light);
background-color: var(--shiki-light-bg) ;
background-color: var(--shiki-light-bg);
span {
color: var(--shiki-light) ;
background-color: var(--shiki-light-bg) ;
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
/* Optional, if you also want font styles */
font-style: var(--shiki-light-font-style) ;
font-weight: var(--shiki-light-font-weight) ;
text-decoration: var(--shiki-light-text-decoration) ;
font-style: var(--shiki-light-font-style);
font-weight: var(--shiki-light-font-weight);
text-decoration: var(--shiki-light-text-decoration);
}
.shiki-ln-line-highlighted, button:hover {
.shiki-ln-line-highlighted,
button:hover {
span {
background-color: inherit;
}
}
}
}
+12 -2
View File
@@ -41,6 +41,13 @@ export class AppScroller {
this._lastScrollEvent = e;
}),
filter(() => !this.disableScrolling),
filter(() => {
const info = this.router.lastSuccessfulNavigation?.extras.info as Record<
'disableScrolling',
boolean
>;
return !info?.['disableScrolling'];
}),
switchMap((e) => {
return firstValueFrom(
this.appRef.isStable.pipe(
@@ -64,7 +71,7 @@ export class AppScroller {
const {anchor, position} = this._lastScrollEvent;
// Don't scroll during rendering
this.cancelScroll = afterNextRender(
const ref = afterNextRender(
{
write: () => {
if (position) {
@@ -77,6 +84,9 @@ export class AppScroller {
},
},
{injector: this.injector},
).destroy;
);
this.cancelScroll = () => {
ref.destroy();
};
}
}
+1
View File
@@ -1,5 +1,6 @@
@defer (when isBrowser) {
<adev-progress-bar />
<!-- <docs-top-level-banner id="ng-survey-2024" link="https://goo.gle/angular-v18" text="Take the Angular Developer Survey today!" /> -->
}
<button (click)="focusFirstHeading()" class="adev-skip">Skip to main content</button>
+32
View File
@@ -8,6 +8,38 @@
align-items: flex-start;
min-height: 100vh;
// Display top level banner below the navigation.
docs-top-level-banner {
@include mq.for-tablet {
top: 4.6875rem;
}
@include mq.for-phone-only {
top: 3.75rem;
transform: translateY(0);
transition: transform 0.3s ease-out 0.6s;
}
}
// Case: If secondary navigation exists, display banner below secondary navigation for tablets.
&:has(adev-secondary-navigation) {
docs-top-level-banner {
@include mq.for-tablet {
top: 8.125rem;
}
}
}
// Case: If primary navigation is opened, display banner at the top of the page.
&:has(.adev-nav-primary--open) {
docs-top-level-banner {
@include mq.for-phone-only {
transform: translateY(-3.75rem);
transition: transform 0.3s ease-in;
}
}
}
@include mq.for-tablet-landscape-down {
flex-direction: column;
}
+2
View File
@@ -25,6 +25,7 @@ import {
getActivatedRouteSnapshotFromRouter,
IS_SEARCH_DIALOG_OPEN,
SearchDialog,
TopLevelBannerComponent,
} from '@angular/docs';
import {Footer} from './core/layout/footer/footer.component';
import {Navigation} from './core/layout/navigation/navigation.component';
@@ -46,6 +47,7 @@ import {HeaderService} from './core/services/header.service';
RouterLink,
SearchDialog,
ProgressBarComponent,
TopLevelBannerComponent,
],
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
@@ -15,7 +15,7 @@ import {
ViewChild,
} from '@angular/core';
import {isPlatformBrowser} from '@angular/common';
import {NgProgressComponent} from 'ngx-progressbar';
import {NgProgressbar} from 'ngx-progressbar';
import {
NavigationCancel,
NavigationEnd,
@@ -32,7 +32,7 @@ export const PROGRESS_BAR_DELAY = 30;
@Component({
selector: 'adev-progress-bar',
standalone: true,
imports: [NgProgressComponent],
imports: [NgProgressbar],
template: `
<ng-progress aria-label="Page load progress" />
`,
@@ -41,7 +41,7 @@ export const PROGRESS_BAR_DELAY = 30;
export class ProgressBarComponent implements OnInit {
private readonly router = inject(Router);
@ViewChild(NgProgressComponent, {static: true}) progressBar!: NgProgressComponent;
@ViewChild(NgProgressbar, {static: true}) progressBar!: NgProgressbar;
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
@@ -10,8 +10,8 @@ import {HttpClient} from '@angular/common/http';
import {Injectable, inject} from '@angular/core';
import {DocContent, DocsContentLoader} from '@angular/docs';
import {Router} from '@angular/router';
import {firstValueFrom} from 'rxjs';
import {map} from 'rxjs/operators';
import {firstValueFrom, of} from 'rxjs';
import {catchError, map} from 'rxjs/operators';
@Injectable()
export class ContentLoader implements DocsContentLoader {
@@ -3,14 +3,26 @@
@if (group.isFeatured) {
<docs-icon aria-hidden>star</docs-icon>
}
<a routerLink="/api" [fragment]="group.id" class="adev-api-anchor" tabindex="-1">{{ group.title }}</a>
<!-- we use innerHtml because the title can be an html string-->
<a
routerLink="/api"
[fragment]="group.id"
queryParamsHandling="preserve"
class="adev-api-anchor"
tabindex="-1"
[innerHtml]="group.title"
></a>
</h3>
</header>
<ul class="adev-api-items-section-grid">
@for (apiItem of group.items; track apiItem.url) {
<li [class.adev-api-items-section-item-deprecated]="apiItem.isDeprecated">
<a [routerLink]="'/' + apiItem.url" class="adev-api-items-section-item" [attr.aria-describedby]="apiItem.isDeprecated ? 'deprecated-description' : null">
<a
[routerLink]="'/' + apiItem.url"
class="adev-api-items-section-item"
[attr.aria-describedby]="apiItem.isDeprecated ? 'deprecated-description' : null"
>
<docs-api-item-label
[type]="apiItem.itemType"
mode="short"
@@ -20,9 +32,7 @@
<span class="adev-item-title">{{ apiItem.title }}</span>
</a>
@if (apiItem.isDeprecated) {
<span class="docs-deprecated">
&lt;!&gt;
</span>
<span class="docs-deprecated"> &lt;!&gt; </span>
}
@if (apiItem.isFeatured) {
<docs-icon
@@ -232,6 +232,20 @@
letter-spacing: -0.00875rem;
}
.docs-reference-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.7rem 1rem;
code:not(pre *) {
padding: 0 0.3rem;
}
}
.docs-reference-member-card {
border: 1px solid var(--senary-contrast);
border-radius: 0.25rem;
@@ -255,17 +269,9 @@
}
}
&:has(.docs-reference-card-body) {
header {
border-radius: 0.25rem 0.25rem 0 0;
border-bottom: 1px solid var(--senary-contrast);
}
}
header {
display: flex;
flex-direction: column;
padding: 0.7rem 1rem;
border-radius: 0.25rem;
background-color: var(--octonary-contrast);
position: relative;
@@ -275,20 +281,23 @@
background-color 0.3s ease,
border 0.3s ease;
// h3 + code || # of overloads
.docs-reference-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
div {
display: flex;
align-items: center;
gap: 1rem;
}
& > code {
max-width: 100%;
}
code:has(pre) {
padding: 0;
}
pre {
margin: 0;
/* Do we have a better alternative ? */
overflow: auto;
}
}
.docs-reference-card-header {
h3 {
display: inline-block;
font-family: var(--code-font);
@@ -323,6 +332,10 @@
.docs-code {
margin-block-end: 1rem;
}
&:empty {
display: none;
}
}
// when it's not the only card...
@@ -386,7 +399,7 @@
margin-inline-end: 0.5rem;
}
.adev-param-name {
.docs-param-name {
color: var(--vivid-pink);
font-family: var(--code-font);
margin-inline-end: 0.25rem;
@@ -11,7 +11,7 @@ import {
ChangeDetectionStrategy,
Component,
DestroyRef,
EnvironmentInjector,
Injector,
OnInit,
ViewChild,
afterNextRender,
@@ -56,6 +56,7 @@ export default class ApiReferenceDetailsPage implements OnInit, AfterViewInit {
private readonly router = inject(Router);
private readonly scrollHandler = inject(ReferenceScrollHandler);
private readonly appScroller = inject(AppScroller);
private readonly injector = inject(Injector);
ApiItemType = ApiItemType;
@@ -96,7 +97,7 @@ export default class ApiReferenceDetailsPage implements OnInit, AfterViewInit {
)
.subscribe((doc: DocContent | undefined) => {
this.setContentForPageSections(doc);
this.setActiveTab();
afterNextRender(() => this.setActiveTab(), {injector: this.injector});
});
}
@@ -14,6 +14,7 @@ import {signal} from '@angular/core';
import {ApiItemType} from '../interfaces/api-item-type';
import {RouterTestingHarness} from '@angular/router/testing';
import {provideRouter} from '@angular/router';
import {Location} from '@angular/common';
describe('ApiReferenceList', () => {
let component: ApiReferenceList;
@@ -117,4 +118,30 @@ describe('ApiReferenceList', () => {
harness.navigateByUrl(`/api`);
expect(component.type()).toBe(ALL_STATUSES_KEY);
});
it('should set the value of the queryParam equal to the query value', async () => {
const location = TestBed.inject(Location);
component.query.set('item1');
await fixture.whenStable();
expect(location.path()).toBe(`?query=item1&type=All`);
});
it('should keep the values of existing queryParams and set new queryParam equal to the type', async () => {
const location = TestBed.inject(Location);
component.query.set('item1');
await fixture.whenStable();
expect(location.path()).toBe(`?query=item1&type=All`);
component.filterByItemType(ApiItemType.BLOCK);
await fixture.whenStable();
expect(location.path()).toBe(`?query=item1&type=${ApiItemType.BLOCK}`);
});
it('should display all items when query and type are undefined', async () => {
component.query.set(undefined);
component.type.set(undefined);
await fixture.whenStable();
expect(component.filteredGroups()![0].items).toEqual([fakeItem1, fakeItem2]);
});
});
@@ -23,6 +23,7 @@ import ApiItemsSection from '../api-items-section/api-items-section.component';
import {FormsModule} from '@angular/forms';
import {SlideToggle, TextField} from '@angular/docs';
import {NgFor, NgIf} from '@angular/common';
import {Params, Router} from '@angular/router';
import {ApiItemType} from '../interfaces/api-item-type';
import {ApiReferenceManager} from './api-reference-manager.service';
import ApiItemLabel from '../api-item-label/api-item-label.component';
@@ -50,6 +51,7 @@ export const ALL_STATUSES_KEY = 'All';
})
export default class ApiReferenceList {
private readonly apiReferenceManager = inject(ApiReferenceManager);
private readonly router = inject(Router);
filterInput = viewChild.required(TextField, {read: ElementRef});
private readonly injector = inject(EnvironmentInjector);
@@ -71,9 +73,28 @@ export default class ApiReferenceList {
{injector: this.injector},
);
});
effect(
() => {
const params: Params = {
'query': this.query() ? this.query() : null,
'type': this.type() ? this.type() : null,
};
this.router.navigate([], {
queryParams: params,
replaceUrl: true,
preserveFragment: true,
info: {
disableScrolling: true,
},
});
},
{allowSignalWrites: true},
);
}
query = signal('');
query = model<string | undefined>('');
includeDeprecated = signal(false);
type = model<string | undefined>(ALL_STATUSES_KEY);
@@ -87,8 +108,10 @@ export default class ApiReferenceList {
id: group.id,
items: group.items.filter((apiItem) => {
return (
(this.query()
? apiItem.title.toLocaleLowerCase().includes(this.query().toLocaleLowerCase())
(this.query() !== undefined
? apiItem.title
.toLocaleLowerCase()
.includes((this.query() as string).toLocaleLowerCase())
: true) &&
(this.includeDeprecated() ? true : apiItem.isDeprecated === this.includeDeprecated()) &&
(this.type() === undefined ||
@@ -9,7 +9,7 @@
import {Injectable, signal} from '@angular/core';
// This file is generated at build-time, error is expected here.
import API_MANIFEST_JSON from '../../../../../src/assets/api/manifest.json';
import {ANGULAR_PACKAGE_PREFIX, getApiUrl} from '../helpers/manifest.helper';
import {getApiUrl} from '../helpers/manifest.helper';
import {ApiItem} from '../interfaces/api-item';
import {ApiItemsGroup} from '../interfaces/api-items-group';
import {ApiManifest} from '../interfaces/api-manifest';
@@ -34,6 +34,8 @@ export const FEATURED_ITEMS_URLS = [
'api/router/CanActivate',
];
const manifest = API_MANIFEST_JSON as ApiManifest;
@Injectable({
providedIn: 'root',
})
@@ -50,20 +52,14 @@ export class ApiReferenceManager {
private mapManifestToApiGroups(): ApiItemsGroup[] {
const groups: ApiItemsGroup[] = [];
const manifest = API_MANIFEST_JSON as ApiManifest;
const packageNames = Object.keys(API_MANIFEST_JSON);
for (const packageName of packageNames) {
const packageNameWithoutPrefix = packageName.replace(ANGULAR_PACKAGE_PREFIX, '');
const packageApis = manifest[packageName];
for (const module of manifest) {
groups.push({
title: packageNameWithoutPrefix,
id: packageNameWithoutPrefix.replace(/\//g, '-'),
items: packageApis
title: module.moduleLabel.replace('@angular/', ''),
id: module.normalizedModuleName,
items: module.entries
.map((api) => {
const url = getApiUrl(packageNameWithoutPrefix, api.name);
const url = getApiUrl(module, api.name);
const isFeatured = FEATURED_ITEMS_URLS.some((featuredUrl) => featuredUrl === url);
const apiItem = {
itemType: api.type,
@@ -0,0 +1,44 @@
/*!
* @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 {ApiManifestPackage} from '../interfaces/api-manifest';
import {getApiUrl} from './manifest.helper';
describe('ManiferHelper', () => {
describe('getApiUrl', () => {
it('should return the correct URL for a given package and API name', () => {
const packageEntry: ApiManifestPackage = {
moduleName: '@angular/common',
moduleLabel: 'common',
normalizedModuleName: 'angular_common',
entries: [],
};
const apiName = 'DatePipe';
const result = getApiUrl(packageEntry, apiName);
expect(result).toBe('api/common/DatePipe');
const packageEntry2: ApiManifestPackage = {
moduleName: '@angular/animations/browser',
moduleLabel: 'animations/browser',
normalizedModuleName: 'angular_animations_browser',
entries: [],
};
const result2 = getApiUrl(packageEntry2, apiName);
expect(result2).toBe('api/animations/browser/DatePipe');
const packageEntry3: ApiManifestPackage = {
moduleName: '@angular/common/http/testing',
moduleLabel: 'common/http/testing',
normalizedModuleName: 'angular_common_http_testing',
entries: [],
};
const result3 = getApiUrl(packageEntry3, apiName);
expect(result3).toBe('api/common/http/testing/DatePipe');
});
});
});
@@ -8,31 +8,23 @@
import {Route} from '@angular/router';
import API_MANIFEST_JSON from '../../../../../src/assets/api/manifest.json';
import {ApiManifest, ApiManifestItem} from '../interfaces/api-manifest';
import {ApiManifest, ApiManifestEntry, ApiManifestPackage} from '../interfaces/api-manifest';
import {PagePrefix} from '../../../core/enums/pages';
import {NavigationItem, contentResolver} from '@angular/docs';
export const ANGULAR_PACKAGE_PREFIX = '@angular/';
const manifest = API_MANIFEST_JSON as ApiManifest;
export function mapApiManifestToRoutes(): Route[] {
const manifest = API_MANIFEST_JSON as ApiManifest;
const packageNames = Object.keys(API_MANIFEST_JSON);
const apiRoutes: Route[] = [];
for (const packageName of packageNames) {
const packageNameWithoutPrefix = packageName.replace(ANGULAR_PACKAGE_PREFIX, '');
const packageApis = manifest[packageName];
for (const api of packageApis) {
for (const packageEntry of manifest) {
for (const api of packageEntry.entries) {
apiRoutes.push({
path: getApiUrl(packageNameWithoutPrefix, api.name),
path: getApiUrl(packageEntry, api.name),
loadComponent: () =>
import('./../api-reference-details-page/api-reference-details-page.component'),
resolve: {
docContent: contentResolver(
`api/${getNormalizedFilename(packageNameWithoutPrefix, api)}`,
),
docContent: contentResolver(`api/${getNormalizedFilename(packageEntry, api)}`),
},
data: {
label: api.name,
@@ -46,20 +38,14 @@ export function mapApiManifestToRoutes(): Route[] {
}
export function getApiNavigationItems(): NavigationItem[] {
const manifest = API_MANIFEST_JSON as ApiManifest;
const packageNames = Object.keys(API_MANIFEST_JSON);
const apiNavigationItems: NavigationItem[] = [];
for (const packageName of packageNames) {
const packageNameWithoutPrefix = packageName.replace(ANGULAR_PACKAGE_PREFIX, '');
const packageApis = manifest[packageName];
for (const packageEntry of manifest) {
const packageNavigationItem: NavigationItem = {
label: packageNameWithoutPrefix,
children: packageApis
label: packageEntry.moduleLabel,
children: packageEntry.entries
.map((api) => ({
path: getApiUrl(packageNameWithoutPrefix, api.name),
path: getApiUrl(packageEntry, api.name),
label: api.name,
}))
.sort((a, b) => a.label.localeCompare(b.label)),
@@ -71,12 +57,18 @@ export function getApiNavigationItems(): NavigationItem[] {
return apiNavigationItems;
}
export function getApiUrl(packageNameWithoutPrefix: string, apiName: string): string {
return `${PagePrefix.API}/${packageNameWithoutPrefix}/${apiName}`;
export function getApiUrl(packageEntry: ApiManifestPackage, apiName: string): string {
const packageName = packageEntry.normalizedModuleName
// packages like `angular_core` should be `core`
// packages like `angular_animation_browser` should be `animation/browser`
.replace('angular_', '')
.replaceAll('_', '/');
return `${PagePrefix.API}/${packageName}/${apiName}`;
}
function getNormalizedFilename(moduleName: string, entry: ApiManifestItem): string {
// Angular entry points can contain `/`, we would like to swap `/` with an underscore
const normalizedModuleName = moduleName.replace(/\//g, '_');
return `angular_${normalizedModuleName}_${entry.name}_${entry.type}.html`;
function getNormalizedFilename(
manifestPackage: ApiManifestPackage,
entry: ApiManifestEntry,
): string {
return `${manifestPackage.normalizedModuleName}_${entry.name}_${entry.type}.html`;
}
@@ -8,12 +8,17 @@
import {ApiItemType} from './api-item-type';
export interface ApiManifestItem {
export interface ApiManifestEntry {
name: string;
type: ApiItemType;
isDeprecated?: boolean;
}
export interface ApiManifest {
[packageName: string]: ApiManifestItem[];
export interface ApiManifestPackage {
moduleName: string;
normalizedModuleName: string;
moduleLabel: string;
entries: ApiManifestEntry[];
}
export type ApiManifest = ApiManifestPackage[];
@@ -102,6 +102,11 @@ export class ReferenceScrollHandler implements OnDestroy {
fromEvent(tocContainer, 'click')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
if (event.target instanceof HTMLAnchorElement) {
event.stopPropagation();
return;
}
// Get the card member ID from the attributes
const target =
event.target instanceof HTMLButtonElement
@@ -125,7 +130,12 @@ export class ReferenceScrollHandler implements OnDestroy {
}
fromEvent(header, 'click')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
.subscribe((event) => {
const target = event.target as HTMLElement;
if (target instanceof HTMLAnchorElement) {
return;
}
this.router.navigate([], {fragment: card.id, replaceUrl: true});
});
});
@@ -2094,7 +2094,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v17 zone.js support',
action:
'Make sure that you are using a supported version of Zone.js before you upgrade your application. Angular v16 supports Zone.js version 0.14.x or later.',
'Make sure that you are using a supported version of Zone.js before you upgrade your application. Angular v17 supports Zone.js version 0.14.x or later.',
},
{
possibleIn: 1700,
+5 -10
View File
@@ -1462,16 +1462,6 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'reference/migrations/standalone',
contentPath: 'reference/migrations/standalone',
},
{
label: 'ModuleWithProviders',
path: 'reference/migrations/module-with-providers',
contentPath: 'reference/migrations/module-with-providers',
},
{
label: 'Typed Forms',
path: 'reference/migrations/typed-forms',
contentPath: 'reference/migrations/typed-forms',
},
{
label: 'Control Flow Syntax',
path: 'reference/migrations/control-flow',
@@ -1482,6 +1472,11 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'reference/migrations/inject-function',
contentPath: 'reference/migrations/inject-function',
},
{
label: 'Lazy-loaded routes',
path: 'reference/migrations/route-lazy-loading',
contentPath: 'reference/migrations/route-lazy-loading',
},
],
},
{

Some files were not shown because too many files have changed in this diff Show More