Compare commits

...

63 Commits

Author SHA1 Message Date
Ben Hong b36d53143a docs: fix missing security guide in navigation
The paths for the security guide were flipped in the original PR. As a result, it looked for a markdown file in the best-practices directory when it should have looked for it in the guide directory instead.
2024-03-12 12:18:54 -07:00
Gerald Monaco 456f18be2e refactor(core): add internal API to enable i18n hydration (#54784)
Add an internal API to enable and use i18n hydration for testing and development. This helps ensure that we don't accidentally break the current behavior until we are completely ready to roll out i18n support.

PR Close #54784
2024-03-12 11:38:59 -07:00
cexbrayat db2f9a9561 refactor(compiler-cli): cleanup unused code (#54775)
The initializer api no longer needs to take care of `ɵoutput`.

PR Close #54775
2024-03-12 11:31:39 -07:00
Ben Hong 4ff94aa46c docs: improve in-depth guides information architecture (#54365)
PR Close #54365
2024-03-12 10:25:31 -07:00
Joey Perrott 6a07c0c2b2 fix(docs-infra): remove the prerender directory from adev (#54820)
Remove the prerender directory as it is entirely unused and unneeded

PR Close #54820
2024-03-12 10:24:04 -07:00
Paul Gschwendtner 1294b9a02a refactor(core): report subscription errors for OutputEmitterRef to ErrorHandler (#54821)
Currently if an `(output)` listener fails, it will be handled gracefully
by Angular and reported to the `ErrorHandler`.

For programmatic subscriptions with `OutputEmitterRef`, this is not the case.
Instead, as soon as any subscription is failing, all other subsequent
subscription callbacks are not firing anymore.

This commit intends to make this more consistent by gracefully
reporting errors from `OutputEmitterRef#emit` to `ErrorHandler`,
allowing for listener execution to continue.

PR Close #54821
2024-03-12 10:21:48 -07:00
Andrew Scott 8735af08b9 feat(router): Add ability to return UrlTree with NavigationBehaviorOptions from guards (#45023)
Returning `UrlTree` from a guard was a convenient new feature added to
the `Router`. However, it does not have feature-parity with the old
`router.navigate(...); return false;` pattern. The most common use-case
for this feature is to redirect to a new page _without_ updating the URL
from the initially attempted navigation. For example, rendering a 404
page when the user does not have access privelages to a route.

Fixes #17004
Fixes #27148

BREAKING CHANGE: Guards can now return `RedirectCommand` for redirects
in addition to `UrlTree`. Code which expects only `boolean` or `UrlTree`
values in `Route` types will need to be adjusted.

PR Close #45023
2024-03-12 09:19:14 -07:00
cexbrayat 87cae55e55 docs: typo in output documentation (#54773)
PR Close #54773
2024-03-12 09:15:04 -07:00
Angular Robot bea7f33ad7 docs: update Angular CLI help [main] (#54815)
Updated Angular CLI help contents.

PR Close #54815
2024-03-12 09:13:55 -07:00
Kristiyan Kostadinov 018f8266b3 fix(core): ensure all initializer functions run in an injection context (#54761)
Ensures that all of the functions intended to be run in initializers are in an injection context. This is a stop-gap until we have a compiler diagnostic for it.

PR Close #54761
2024-03-12 09:08:06 -07:00
Pawel Kozlowski 47f79e78c0 refactor(core): assert presence of the track function (#54814)
This commits assert that the repeater instruction gets a reference
to a tracking function. This change will allow us to better track
occurences of https://github.com/angular/angular/issues/53628 -
in certain situations a reference to a tracking function might be
undefiened.

We are not fixing the underlying issue here, just getting better
visibility.

PR Close #54814
2024-03-11 16:33:01 -07:00
Paul Gschwendtner 142825d7b1 perf(core): speed up retrieval of DestroyRef in EventEmitter (#54748)
Speeds up the retrieval of `DestroyRef` in `EventEmitter` because
`try/catch` is expensive if there is no injection context.

We saw a script time regression in Cloud.

The goldens had to be updated because `getInjectImplementation` is now
referenced. `inject` also references the underlying field, but directly.
This is super minimal overhead of a function exposing the internal
field.

PR Close #54748
2024-03-11 16:31:03 -07:00
Matthieu Riegler f0a26001c5 docs: use the right path for first-app images (#54694)
PR Close #54694
2024-03-11 16:30:28 -07:00
Kristiyan Kostadinov 6ea208ee90 refactor(compiler-cli): move defer resolver compilation into compiler package (#54759)
Moves the logic that creates the defer resolver function into `@angular/compiler` for consistency with the rest of the compilation APIs. Also renames some of the symbols to make it clearer what they're used for.

PR Close #54759
2024-03-11 15:52:42 -07:00
Kristiyan Kostadinov 9b424d7224 fix(compiler-cli): preserve original reference to non-deferrable dependency (#54759)
Fixes an issue where we were outputting the reference to non-deferrable dependencies as strings, rather than going through the reference emitter. This caused some issues internally because the reference wasn't maintained in the generated JS.

PR Close #54759
2024-03-11 15:52:41 -07:00
Kristiyan Kostadinov 83932aa85e refactor(compiler): rework defer block analysis (#54759)
Currently we have the `deferrableDeclToImportDecl`, `deferBlocks`, `deferrableTypes` and `deferBlockDepsEmitMode` fields on the `R3ComponentMetadata` which is incorrect, because the interface is used both for JIT and AOT mode even though the information for those fields is AOT-specific. It will be problematic for partial compilation since the runtime will have a reference to the dependency loading function, but will not be able to provide any of the other information.

These changes make the following refactors:
1. It changes the defer-related information in `R3ComponentMetadata` to include only references to dependency functions which can be provided both in JIT and AOT.
2. Moves the AOT-specific defer analysis into the `ComponentResolutionData`.
3. Moves the construction the defer dependency function into the compilation phase of the `ComponentDecoratorHandler`.
4. Drops support for defer blocks from the `TemplateDefinitionBuilder`. This allows us to clean up some TDB-specific code and shouldn't have an effect on users since the TDB isn't used anymore.

PR Close #54759
2024-03-11 15:52:41 -07:00
Andrew Scott 64f870c12b fix(core): ApplicationRef.tick should respect OnPush for host bindings (#53718) (#53718)
This commit updates `ApplicationRef.tick` to use `detectChangesInternal` for root
views rather than go through the `ChangeDetectorRef.detectChanges` API
which refreshes the host view without first looking at whether the view
is `OnPush` and not dirty. The current behavior would hide errors in
`OnPush` components that do not correctly get marked for check and would
break when migrating to zoneless change detection because `markForCheck`
was never called so change detection was never scheduled.
The error would be surprising and blamed on switching to zoneless when in
reality the issue already exists and is a problem with the component not
calling `markForCheck`. However, this error is hidden today because
`ApplicationRef.tick` refresh host bindings unconditionally.

BREAKING CHANGE: `OnPush` views at the root of the application need to
be marked dirty for their host bindings to refresh. Previously, the host
bindings were refreshed for all root views without respecting the
`OnPush` change detection strategy.

PR Close #53718

PR Close #53718
2024-03-11 13:46:11 -07:00
Joey Perrott d714e998d6 docs: bump to 2024 for copyright text (#54822)
Bump copyright text to 2024

PR Close #54822
2024-03-11 13:39:38 -07:00
Andrew Kushnir eaff724b77 fix(core): prevent infinite loops in clobbered elements check (#54425)
This commit updates HTML sanitization logic to avoid infinite loops in case clobbered elements contain fields like `nextSibling` or `parentNode`. Those fields are used for DOM traversal and this update makes sure that those calls return valid results.

Also this commit fixes an issue when clobbering `nodeName` causes JS exceptions.

PR Close #54425
2024-03-11 12:46:16 -07:00
Gerald Monaco 280a3a2d62 refactor(core): add i18nNodes in preparation for i18n hydration (#54750)
An i18n message effectively acts as a dynamic template: two elements with contiguous instruction indices won't necessarily be contiguous in the DOM.

For that reason, we need to maintain a mapping from instruction index to a physical DOM node in order to hydrate views with i18n, pointing to where hydration for that view should begin.

PR Close #54750
2024-03-11 11:12:43 -07:00
Matthieu Riegler 837016ccca build: remove AIO related extensions (#54698)
With AIO being deprecated those extensions can be removed.
PR Close #54698
2024-03-11 11:04:38 -07:00
Kristiyan Kostadinov 5ae2bf4806 fix(compiler): handle two-way bindings to signal-based template variables in instruction generation (#54714)
Updates the instruction generation for two-way bindings to only emit the `twoWayBindingSet` call when writing to template variables. Since template variables are constants, it's only allowed to write to them when they're signals. Non-signal values are flagged during template type checking.

Fixes #54670.

PR Close #54714
2024-03-11 11:01:43 -07:00
Kristiyan Kostadinov ffb9b44333 fix(compiler-cli): flag two-way bindings to non-signal values in templates (#54714)
We have a diagnostic that reports writes to template variables which worked both for regular event bindings and two-way bindings, however the latter was broken by #54154 because two-way bindings no longer had a `PropertyWrite` AST.

These changes fix the diagnostic and expand it to allow two-way bindings to template variables that are signals.

PR Close #54714
2024-03-11 11:01:42 -07:00
Kristiyan Kostadinov fd17b4e155 refactor(compiler-cli): move illegal template assignment check into template semantics checker (#54714)
Moves the check which ensures that there are no writes to template variables into the `TemplateSemanticsChecker` to prepare for the upcoming changes.

PR Close #54714
2024-03-11 11:01:42 -07:00
Kristiyan Kostadinov 6235095011 refactor(compiler-cli): move signal identification function (#54714)
Moves the function that identifies signals into a separate file so that it can be reused outside of extended diagnostics.

PR Close #54714
2024-03-11 11:01:42 -07:00
Kristiyan Kostadinov f86088f164 refactor(compiler-cli): introduce template semantics checker (#54714)
Introduces a new `TemplateSemanticsChecker` that will be used to flag semantic errors in the user's template. Currently we do some of this in the type check block, but the problem is that it doesn't have access to the template type checker which prevents us from properly checking cases like #54670. This pass is also distinct from the extended template checks, because we don't want users to be able to turn the checks off and we want them to run even if `strictTemplates` are disabled.

PR Close #54714
2024-03-11 11:01:42 -07:00
Matthieu Riegler 09a2f3d592 docs: adding signal inputs and model to ADEV. (#54753)
Those docs are a copy from AIO.

PR Close #54753
2024-03-11 11:00:49 -07:00
theRealc0d3c 55c647bf90 docs: fix typo in tutorial (#54817)
PR Close #54817
2024-03-11 10:59:49 -07:00
Matthieu Riegler 1c9abb9ac8 build: Unplug AIO from CI (#54765)
The time has come to unplug AIO from the CI.

Farewell and thanks for your services 🫡

PR Close #54765
2024-03-11 10:59:07 -07:00
Kristiyan Kostadinov 81ccf5d102 fix(compiler): not catching for loop empty tracking expressions (#54772)
Fixes that the template parser wasn't catching empty expressions in the `track` parameter of for loops.

Fixes #54763.

PR Close #54772
2024-03-11 09:17:39 -07:00
Kristiyan Kostadinov 5b927c094d build: update to TypeScript 5.4 stable (#54743)
Updates the repo to the stable version of TypeScript 5.4.

PR Close #54743
2024-03-11 09:16:55 -07:00
JoostK d269c88ae4 refactor(core): avoid additional closure for queued microtask callback (#54801)
The `runCallbackOnce` closure is declared not to have any parameters itself, so it is
compatible as `queueMicrotask` callback without the extra closure. This reduces the call
stack by a frame and avoids the extra closure allocation.

PR Close #54801
2024-03-11 08:59:55 -07:00
JoostK fa315d4dad refactor(core): properly type withBody and withHead testing helpers (#54801)
This commit addresses a typing mismatch, where these functions were declared to return whichever
value their callback returned, but this was inaccurate: it's always a test callback function
with `done` argument.

PR Close #54801
2024-03-11 08:59:55 -07:00
JoostK 0bcaa0ebee test(core): ensure async tests are awaited properly (#54801)
The assertion in `packages/core/test/acceptance/after_render_hook_spec.ts:165` was prone to flakes,
where Jasmine could frequently report an error:

```
Error: 'expect' was used when there was no current spec, this could be because an asynchronous test timed out
    at Env.expect (node_modules/jasmine-core/lib/jasmine-core/jasmine.js:1945:15)
    at expect (node_modules/jasmine-core/lib/jasmine-core/jasmine.js:8267:18)
    at file:///packages/core/test/acceptance/after_render_hook_spec.ts:165:12
```

This happens because `wrapTestFn` checks for an exact type of `Promise`, which may have been patched by zone.js
such that the `instanceof` condition is dependent on whether zone.js has patched the `Promise` constructor.

PR Close #54801
2024-03-11 08:59:55 -07:00
Matthieu Riegler 484ae23bbf build: remove non-exisiting target from test:ci (#54791)
This target doesn't exist any more.

PR Close #54791
2024-03-11 14:06:50 +00:00
Dylan Hunn 54340a9fff refactor(compiler): Delete TemplateDefinitionBuilder acceptance test expectations (#54757)
Many acceptance tests define goldens for both `TemplateDefinitionBuilder` and Template Pipeline. All such tests have had the TDB golden files removed, and the corresponding expectations adjusted.

PR Close #54757
2024-03-08 16:51:01 -08:00
Dylan Hunn ef32b5322e refactor(compiler): Delete TemplateDefinitionBuilder and helpers (#54757)
`TemplateDefinitionBuilder` is the legacy template compiler, and was replaced by Template Pipeline as the default in v17.3.

This PR attempts to delete `TemplateDefinitionBuilder`, `ExpressionConverter`, and various helpers (i18n context, style builder, property visitors, etc).

Consider this a first pass: a lot of code has not yet been deleted (e.g. old TDB-specific test cases), and I'm sure I have missed additional helper code.

PR Close #54757
2024-03-08 16:51:01 -08:00
Andrew Scott 37d1f713a4 Revert "fix(core): ApplicationRef.tick should respect OnPush for host bindings (#53718)" (#54774)
This reverts commit d888da4606.
2024-03-08 05:57:15 -08:00
Andrew Scott fb189c6997 Revert "refactor(compiler): rework defer block analysis (#54700)" (#54758)
This reverts commit eee620aa00.
2024-03-07 16:20:09 -08:00
Kristiyan Kostadinov 8180c39479 build: fix size failure (#54756)
Fixes a broken size CI check.

PR Close #54756
2024-03-07 16:13:43 -08:00
Andrew Scott 115ee88ba9 fix(router): Navigations triggered by cancellation events should cancel previous navigation (#54710)
There is an edge case where synchronous navigations caused in
response to navigation events can result in a previous navigation not
being unsubscribed from. b/328219996

PR Close #54710
2024-03-07 13:07:08 -08:00
Andrew Scott 2787c50107 refactor(core): render hooks should always run on node attach or detach (#54083)
This commit ensures that render hooks are rerun when a node is attached
or detached. We do not necessarily need to run change detection but DOM
did change so render hooks should execute.

PR Close #54083
2024-03-07 12:59:10 -08:00
Andrew Scott d4247af8c5 refactor(core): Skip refresh views if render hooks are the only notification source (#54083)
Do not refresh views if the only thing that notified the scheduler was
registration of a new render hook.

PR Close #54083
2024-03-07 12:59:10 -08:00
Andrew Scott 53e743938f refactor(core): registering afterRender hooks notify scheduler (#54083)
This commit updates the `afterRender` and `afterNextRender` hooks to
notify the scheduler (which subsequently schedules change detection)
when created. This makes the hooks similar to `requestAnimationFrame`,
which requests that the browser schedule a rendering operation. This
reqeust is not conditional. Even if there was nothing to repaint, the
`requestAnimationFrame` callback will execute.

In Angular, this is useful because callers of `afterNextRender` don't
necessarily have any way of knowing whether a change detection is even
scheduled. For example, the anchor scrolling with the Angular Router
needs to wait for rendering to complete before attempting to scroll
because rendering can affect the size of the page. However, if the user
is already on the page that the navigation is targeting, such as
navigating to an anchor on the page, there is nothing new for the Router
to render so a render might not even be scheduled.

Related to https://github.com/angular/angular/issues/53985, which
could use `afterNextRender` instead of `setTimeout` to ensure the
scrolling happens in the same frame as the page rendering, but would not
necessarily work without this change (as described above). Note that the
scrolling _cannot_ use a microtask to ensure scrolling happens in the
same frame because `NgZone` will ensure microtasks flush before
change detection, so it would cause the scroll to happen before rendering.

PR Close #54083
2024-03-07 12:59:09 -08:00
Gerald Monaco 43ab781b70 refactor(core): refactor i18n node creation to prepare for hydration (#54722)
Adds a `locateOrCreateNode` helper and uses it for all DOM operations in preparation for supporting hydration.

PR Close #54722
2024-03-07 12:41:43 -08:00
Kristiyan Kostadinov eee620aa00 refactor(compiler): rework defer block analysis (#54700)
Currently we have the `deferrableDeclToImportDecl`, `deferBlocks`, `deferrableTypes` and `deferBlockDepsEmitMode` fields on the `R3ComponentMetadata` which is incorrect, because the interface is used both for JIT and AOT mode even though the information for those fields is AOT-specific. It will be problematic for partial compilation since the runtime will have a reference to the dependency loading function, but will not be able to provide any of the other information.

These changes make the following refactors:
1. It changes the defer-related information in `R3ComponentMetadata` to include only references to dependency functions which can be provided both in JIT and AOT.
2. Moves the AOT-specific defer analysis into the `ComponentResolutionData`.
3. Moves the construction the defer dependency function into the compilation phase of the `ComponentDecoratorHandler`.
4. Drops support for defer blocks from the `TemplateDefinitionBuilder`. This allows us to clean up some TDB-specific code and shouldn't have an effect on users since the TDB isn't used anymore.

PR Close #54700
2024-03-07 12:40:44 -08:00
Andrew Scott d888da4606 fix(core): ApplicationRef.tick should respect OnPush for host bindings (#53718)
This commit updates `ApplicationRef.tick` to use `detectChangesInternal` for root
views rather than go through the `ChangeDetectorRef.detectChanges` API
which refreshes the host view without first looking at whether the view
is `OnPush` and not dirty. The current behavior would hide errors in
`OnPush` components that do not correctly get marked for check and would
break when migrating to zoneless change detection because `markForCheck`
was never called so change detection was never scheduled.
The error would be surprising and blamed on switching to zoneless when in
reality the issue already exists and is a problem with the component not
calling `markForCheck`. However, this error is hidden today because
`ApplicationRef.tick` refresh host bindings unconditionally.

BREAKING CHANGE: `OnPush` views at the root of the application need to
be marked dirty for their host bindings to refresh. Previously, the host
bindings were refreshed for all root views without respecting the
`OnPush` change detection strategy.

PR Close #53718
2024-03-07 11:13:03 -08:00
Leonardo Ortiz 8bf842d595 docs: update content readme link (#53763)
PR Close #53763
2024-03-07 11:06:55 -08:00
Leonardo Ortiz 30dccd4b77 docs: readme from blossom (#53763)
PR Close #53763
2024-03-07 11:06:55 -08:00
Angular Robot 4f808ad353 docs: update events (#54721)
Generated `events.json` with the latest events retrieved from the Firebase DB.

PR Close #54721
2024-03-07 11:05:36 -08:00
Gerald Monaco e0c1f1e183 refactor(core): don't hydrate detached nodes (#54723)
In preparation for hydration support, make sure that we don't attempt to hydrate detached nodes and instead, always newly create them.

PR Close #54723
2024-03-07 11:01:29 -08:00
Gerald Monaco 9104c62ab3 refactor(core): add i18n AST to prepare for hydration (#54724)
In order to serialize and hydrate i18n blocks, we need to be able to walk an AST for the translated message. This AST is generated during normal parsing of the message.

PR Close #54724
2024-03-07 11:00:36 -08:00
Alan Agius 2258ac7a32 fix(http): exclude caching for authenticated HTTP requests (#54746)
This update modifies the transfer cache logic to prevent caching of HTTP requests that require authorization.

Closes: #54745

PR Close #54746
2024-03-07 11:00:00 -08:00
Dylan Hunn ef39107ce5 refactor(language-service): Replace tsserverlibrary -> typescript (#54726)
Typescript recently consolidated `tsserverlibrary` into `typescript`: [blog post](https://devblogs.microsoft.com/typescript/announcing-typescript-5-3/#consolidation-between-tsserverlibrary-js-and-typescript-js)

In this commit, we remove all references to `tsserverlibrary` accordingly. This should be safe, since v18 and later support TS 5.3+.

PR Close #54726
2024-03-07 10:49:01 -08:00
Dylan Hunn 126879e9d0 refactor(language-service): Make a variety of fixes to the language service to build in g3. (#54726)
The following changes help the language service code build in g3:
* `Omit<T>` produces an index signature, so we must access the resulting properties with square bracket (because `noPropertyAccessFromIndexSignature` is on in g3).
* Explicitly export `absoluteFrom` from `packages/compiler-cli/index.ts`, since the `*` re-export is patched out in g3.
* Remove const from a few const enums, since accessing const enums across modules is not compatible with `isolatedModules` (which is on in g3).

PR Close #54726
2024-03-07 10:49:01 -08:00
Andrew Scott cd242a11f5 refactor(core): Remove isInternal flag (#54740)
This is not needed any longer since the changes that used it have
landed.

PR Close #54740
2024-03-07 09:07:01 -08:00
Andrew Scott ad045efd4b fix(core): Ensure views marked for check are refreshed during change detection (#54735)
When a view has the `Dirty` flag and is reattached, we should ensure that it is
reached and refreshed during the next change detection run from above.

In addition, when a view is created and attached, we should ensure that it is reached
and refreshed during change detection. This can happen if the view is
created and attached outside a change run or when it is created and
attached after its insertion view was already checked. In both cases, we
should ensure that the view is reached and refreshed during either the
current change detection or the next one (if change detection is not
already running).

We can achieve this by creating all views with the `Dirty` flag set.

However, this does happen to be a breaking change in some scenarios.
The one identified internally was actually depending on change detection
_not_ running immediately because it relied on an input value that was
set using `ngModel`. Because `ngModel` sets its value in a `Promise`, it
is not available until the _next_ change detection cycle. Ensuring
created views run in the current change change detection will result in
different behavior in this case.

fixes #52928
fixes #15634

BREAKING CHANGE: Newly created and views marked for check and reattached
during change detection are now guaranteed to be refreshed in that same
change detection cycle. Previously, if they were attached at a location
in the view tree that was already checked, they would either throw
`ExpressionChangedAfterItHasBeenCheckedError` or not be refreshed until
some future round of change detection. In rare circumstances, this
correction can cause issues. We identified one instance that relied on
the previous behavior by reading a value on initialization which was
queued to be updated in a microtask instead of being available in the
current change detection round. The component only read this value during
initialization and did not read it again after the microtask updated it.

PR Close #54735
2024-03-06 15:44:09 -08:00
Andrew Scott ba8e465974 fix(core): Change Detection will continue to refresh views while marked for check (#54734)
When the `ApplicationRef` refreshes attached views, it will continue to
do so while there is still one marked for check after the refresh
completes.

BREAKING CHANGE: When Angular runs change detection, it will continue to
refresh any views attached to `ApplicationRef` that are still marked for
check after one round completes. In rare cases, this can result in infinite
loops when certain patterns continue to mark views for check using
`ChangeDetectorRef.detectChanges`. This will be surfaced as a runtime
error with the `NG0103` code.

PR Close #54734
2024-03-06 15:43:16 -08:00
Andrew Scott d9c0a16521 refactor(core): Fix internal test util types (#54736)
This fixes and error that has been seen recently where compilation fails
due to domino.impl type not being recognized

PR Close #54736
2024-03-06 13:34:53 -08:00
Andrew Scott 24bc0ed4f2 fix(core): ComponentFixture autodetect should detect changes within ApplicationRef.tick (#54733)
The current behavior of `autoDetect` in `ComponentFixture` does not
match production very well. It has several flaws that make it an
insufficient change detection mechanism:

* It runs change detection for the component under test _after_ views
  attached to the `ApplicationRef`. This can cause real behavior
  differences that break in production, because tests can observe view
  refreshes in the incorrect order (for example, a dialog refreshing
  before the component which opened it).
* Because of the above ordering, render hooks registered during change
  detection of the fixture views _will not execute at all_ because
  `ApplicationRef.tick` already happen.
* It does not rerun change detection on the view tree if there are more
  dirty views to refresh after the render hooks complete.
* It effectively hides/swallows errors from change detection inside the
  `onMicrotaskEmpty` subscription by not reporting them to the error
  handler. Instead, this error ends up being unhandled in the
  subscription and rxjs throws these in a `setTimeout`.

All of the above are problematic but this commit _does not_ fix the
final point. Ideally, we can land this in a future change but this
requires additional internal fixes. In the meantime, we have to juggle
special-case handling of the component fixture views within
`ApplicationRef.tick` using some special events to retain current
behavior and avoid errors from the fixture propagating to the `ErrorHandler`.

BREAKING CHANGE: The `ComponentFixture.autoDetect` feature now executes
change detection for the fixture within `ApplicationRef.tick`. This more
closely matches the behavior of how a component would refresh in
production. The order of component refresh in tests may be slightly
affected as a result, especially when dealing with additional components
attached to the application, such as dialogs. Tests sensitive to this
type of change (such as screenshot tests) may need to be updated.
Concretely, this change means that the component will refresh _before_
additional views attached to `ApplicationRef` (i.e. dialog components).
Prior to this change, the fixture component would refresh _after_ other
views attached to the application.

PR Close #54733
2024-03-06 13:33:54 -08:00
Andrew Scott fc0f47c53f release: bump the next branch to v18.0.0-next.0 (#54737) 2024-03-06 13:26:34 -08:00
Pawel Kozlowski 781dbe2cc2 docs: release notes for the v17.3.0-rc.0 release 2024-03-06 20:44:52 +01:00
Pawel Kozlowski 24a35366ca release: bump the next branch to v17.4.0-next.0 2024-03-06 20:44:52 +01:00
287 changed files with 3517 additions and 9557 deletions
-42
View File
@@ -1,42 +0,0 @@
# This workflow builds the AIO previews for pull requests when a certain label is applied.
# The actual deployment happens as part of a dedicated second workflow to avoid security
# issues where the building would otherwise occur in an authorized context where secrets
# could be leaked. More details can be found here:
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
name: Build AIO app for preview deployment
on:
pull_request:
types: [synchronize, labeled]
permissions: read-all
jobs:
aio-build:
runs-on: ubuntu-latest
# We only want to build and deploy the AIO app if the `aio: preview` label has been
# added, or if the label is already applied and new changes have been made in the PR.
if: |
(github.event.action == 'labeled' && github.event.label.name == 'aio: preview') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'aio: preview'))
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
with:
bazelrc: ./.bazelrc.user
# Build the web package. Note: We run Bazel from a low-resource Github action container,
# so we manually need to instruct Bazel to run more actions concurrently as by default
# the number of concurrent actions is determined based on the host resources.
- run: bazel build //aio:build --jobs=32 --announce_rc --verbose_failures
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@c83e99a12397014162531ca125c94549db55dd84
with:
workflow-artifact-name: 'aio'
pull-number: '${{github.event.pull_request.number}}'
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
deploy-directory: './dist/bin/aio/build'
-44
View File
@@ -1,44 +0,0 @@
# This workflow runs whenever the AIO build workflow has completed. Deployment happens
# as part of a dedicated second workflow to avoid security issues where the building would
# otherwise occur in an authorized context where secrets could be leaked.
#
# More details can be found here:
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
name: Deploying AIO preview to Firebase
on:
workflow_run:
workflows: ['Build AIO app for preview deployment']
types: [completed]
permissions:
# Needed in order to be able to comment on the pull request.
pull-requests: write
env:
PREVIEW_PROJECT: ng-dev-previews
PREVIEW_SITE: ng-dev-previews-fw
jobs:
aio-deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- name: Configure Firebase deploy target
working-directory: aio/
run: |
# We can use `npx` as the Firebase deploy actions uses it too.
npx -y firebase-tools@latest target:clear --project ${{env.PREVIEW_PROJECT}} hosting aio
npx -y firebase-tools@latest target:apply --project ${{env.PREVIEW_PROJECT}} hosting aio ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c83e99a12397014162531ca125c94549db55dd84
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'aio'
firebase-config-dir: './aio/'
firebase-public-dir: './aio/dist'
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
-88
View File
@@ -35,8 +35,6 @@ jobs:
run: yarn install --frozen-lockfile --cwd aio
- name: Check code lint
run: yarn -s tslint
- name: Check code lint in aio
run: yarn -s --cwd aio lint
- name: Check for circular dependencies
run: yarn -s ts-circular-deps:check
- name: Validate pull approve configuration
@@ -90,26 +88,6 @@ jobs:
- name: Run CI tests for framework
run: yarn test:ci
aio:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with upstream packages
run: yarn --cwd aio test:ci
- name: Check generated bundle sizes
run: yarn --cwd aio payload-size
adev:
runs-on:
labels: ubuntu-latest-4core
@@ -125,72 +103,6 @@ jobs:
- name: Build adev to ensure it continues to work
run: yarn bazel test --config=aio_local_deps //adev:test
aio-local:
runs-on:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with local packages
run: yarn --cwd aio test-local:ci
- name: Check generated bundle sizes
run: yarn --cwd aio payload-size aio-local
aio-deploy:
needs: [aio]
if: needs.aio.result == 'success' && github.event_name == 'push'
runs-on:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Set the stable branch environment variable
id: latest_version
run: |
echo "CI_STABLE_BRANCH=$(npm info @angular/core dist-tags.latest | sed -r 's/^\s*([0-9]+\.[0-9]+)\.[0-9]+.*$/\1.x/')" >> $GITHUB_OUTPUT
echo "CI_BRANCH=$(echo ${{ github.event.ref }} | cut -d '/' -f 3)" >> $GITHUB_OUTPUT
- name: Deploy aio to production
env:
CI_AIO_MIN_PWA_SCORE: '95'
CI_COMMIT: ${{ github.event.after }}
CI_REPO_NAME: 'angular'
CI_PULL_REQUEST: 'false'
CI_REPO_OWNER: 'angular'
CI_BUILD_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
CI_STABLE_BRANCH: ${{ steps.latest_version.outputs.CI_STABLE_BRANCH }}
CI_BRANCH: ${{ steps.latest_version.outputs.CI_BRANCH }}
CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN: ${{ secrets.AIO_DEPLOY_TOKEN }}
CI_SECRET_PAYLOAD_FIREBASE_TOKEN: ${{ secrets.AIO_PAYLOAD_TOKEN }}
run: yarn --cwd aio deploy-production
- name: Notify about failed deployment
if: ${{ failure() }}
uses: slackapi/slack-github-action@2a8087d4af6f83146a87539a70defe909fe6dbe6 # v=v1.24.0
with:
channel-id: 'C07DT5M6V,CKV1F72BG'
slack-message: 'Deploy to aio job failed for ${{ github.base_ref }} branch failed on build ${{ github.event.after }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
env:
SLACK_BOT_TOKEN: ${{ secrets.ANGULAR_ROBOT_SLACK_TOKEN }}
publish-snapshots:
if: github.event_name == 'push'
runs-on:
-48
View File
@@ -1,48 +0,0 @@
# Update the `events.json` file that powers the [AIO events page](https://angular.io/events) (if
# necessary) and create a pull request.
#
# For more details on the overall process, see
# [aio/scripts/generate-events/README.md](../../aio/scripts/generate-events/README.md).
name: Update AIO events
on:
workflow_dispatch:
inputs: {}
schedule:
# Run every day at 15:00.
- cron: '0 15 * * *'
# Declare default permissions as read only.
permissions:
contents: read
jobs:
update_events:
name: Update `events.json` (if necessary)
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
# Setting `persist-credentials: false` prevents the github-action account from being the
# account that is attempted to be used for authentication, instead the remote is set to
# an authenticated URL.
persist-credentials: false
# This is needed as otherwise the PR creation will fail with `shallow update not allowed` when the forked branch is not in sync.
fetch-depth: 0
- name: Generate `events.json`
run: node aio/scripts/generate-events/index.mjs --ignore-invalid-dates
- name: Create a PR (if necessary)
uses: angular/dev-infra/github-actions/create-pr-for-changes@c83e99a12397014162531ca125c94549db55dd84
with:
branch-prefix: docs-update-events
pr-title: 'docs: update events'
pr-description: |
Generated `events.json` with the latest events retrieved from the Firebase DB.
pr-labels: |
action: review
area: docs
target: patch
angular-robot-token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
-5
View File
@@ -5,12 +5,7 @@
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"BazelBuild.vscode-bazel",
"gkalpak.aio-docs-utils",
"ms-vscode.vscode-typescript-tslint-plugin",
"xaver.clang-format",
// The following extensions are useful when working on angular.io (i.e. inside the `aio/` directory).
//"angular.ng-template",
//"dbaeumer.vscode-eslint",
//"errata-ai.vale-server",
],
}
+25
View File
@@ -1,3 +1,28 @@
<a name="17.3.0-rc.0"></a>
# 17.3.0-rc.0 (2024-03-06)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [5afa4f0ec1](https://github.com/angular/angular/commit/5afa4f0ec1b64b88ef875d48bd143e0f36e0a955) | fix | support `ModuleWithProviders` literal detection with `typeof` ([#54650](https://github.com/angular/angular/pull/54650)) |
| [ae7dbe42de](https://github.com/angular/angular/commit/ae7dbe42de5779e578fb636938699b8ba85b33f6) | fix | unwrap expressions with type parameters in query read property ([#54647](https://github.com/angular/angular/pull/54647)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [fb540e169a](https://github.com/angular/angular/commit/fb540e169a78a61f38d611f538eea8fdb0971f1d) | feat | add migration for invalid two-way bindings ([#54630](https://github.com/angular/angular/pull/54630)) |
| [c687b8f453](https://github.com/angular/angular/commit/c687b8f4531252cd1c3dfbb9a7bd42bdbe666a36) | feat | expose new `output()` API ([#54650](https://github.com/angular/angular/pull/54650)) |
| [c809069f21](https://github.com/angular/angular/commit/c809069f213244afd0e2d803a6a43510b218e6f5) | feat | introduce `outputFromObservable()` interop function ([#54650](https://github.com/angular/angular/pull/54650)) |
| [aff65fd1f4](https://github.com/angular/angular/commit/aff65fd1f4a61ed76a6f9b623852f197eb3500e4) | feat | introduce `outputToObservable` interop helper ([#54650](https://github.com/angular/angular/pull/54650)) |
| [33a6fab094](https://github.com/angular/angular/commit/33a6fab094205cc74aceb916e45c8afa22293cf4) | fix | apply TestBed provider overrides to `@defer` dependencies ([#54667](https://github.com/angular/angular/pull/54667)) |
| [78e69117f0](https://github.com/angular/angular/commit/78e69117f07e8ae11aab3abe0343df815b3649be) | fix | generic inference for signal inputs may break with `--strictFunctionTypes` ([#54652](https://github.com/angular/angular/pull/54652)) |
| [7243c704cf](https://github.com/angular/angular/commit/7243c704cf8a4986fae419793027458e142658f0) | fix | return a readonly signal on `asReadonly`. ([#54706](https://github.com/angular/angular/pull/54706)) |
| [ffad7b8ea9](https://github.com/angular/angular/commit/ffad7b8ea9d1286ddb3ee7a2dac7dd33fd76b3aa) | fix | untrack various core operations ([#54614](https://github.com/angular/angular/pull/54614)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [c1c7384e02](https://github.com/angular/angular/commit/c1c7384e02becc623c6a42985f7178ca98137264) | feat | Add reusable types for router guards ([#54580](https://github.com/angular/angular/pull/54580)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.2.4"></a>
# 17.2.4 (2024-03-06)
### compiler-cli
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
-135
View File
@@ -1,135 +0,0 @@
/*!
* @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 {input, confirm} from '@inquirer/prompts';
import {existsSync} from 'fs';
import {copyFile, mkdir, writeFile} from 'fs/promises';
import {dirname, join} from 'path';
import {fileURLToPath} from 'url';
import {copyFolder} from './utils/fs.mjs';
const EXAMPLES_BASE = join(
dirname(fileURLToPath(import.meta.url)),
'../../src/content/examples',
);
const STACKBLITZ_TEMPLATE = join(
dirname(fileURLToPath(import.meta.url)),
'../../src/content/stackblitz-template',
);
const MAIN_TS_FILENAME = 'main.ts';
const STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
createExample();
async function createExample(): Promise<void> {
console.info('Create example');
const name = await input({message: 'What is the example name?'});
const description = await input({message: 'Could you provide the description of the example?'});
const tags: string[] = [];
let addMoreTags = true;
while (addMoreTags) {
const tag = await input({message: 'Could you provide the tag of the example?'});
if (!!tag) {
tags.push(tag);
}
console.info();
addMoreTags = await confirm({message: 'Would you like to add another tag?', default: true});
}
const shouldCopySrcAppFolderFromTemplate = await confirm({
message: 'Would you like to copy /src/app/**.* from template?',
default: true,
});
const shouldCopyMainTsFromTemplate = await confirm({
message: 'Would you like to copy /src/main.ts from template?',
default: false,
});
const examplePath = join(EXAMPLES_BASE, name);
await createEmptyExample(examplePath, name, description, tags);
await copyFilesFromTemplate(
examplePath,
shouldCopySrcAppFolderFromTemplate,
shouldCopyMainTsFromTemplate,
);
}
/**
* Create the directory and marker files for the new example.
*/
async function createEmptyExample(
examplePath: string,
exampleName: string,
description: string,
tags: string[],
): Promise<void> {
validateExampleName(exampleName);
await ensureExamplePath(examplePath);
await writeStackBlitzFile(examplePath, description, tags);
}
function validateExampleName(exampleName: string): void {
if (/\s/.test(exampleName)) {
throw new Error(`Unable to create example. The example name contains spaces: '${exampleName}'`);
}
}
/**
* Ensure that the new example directory exists.
*/
async function ensureExamplePath(examplePath: string): Promise<void> {
if (existsSync(examplePath)) {
throw new Error(
`Unable to create example. The path to the new example already exists: ${examplePath}`,
);
}
await mkdir(examplePath);
}
/**
* Write the `stackblitz.json` file into the new example.
*/
async function writeStackBlitzFile(
examplePath: string,
description: string,
tags: string[],
): Promise<void> {
const config = {
description,
ignore: ['**/*.d.ts', '**/*.js', '**/*.[1,2].*'],
tags,
};
await writeFile(
join(examplePath, STACKBLITZ_CONFIG_FILENAME),
JSON.stringify(config, null, 2) + '\n',
);
}
async function copyFilesFromTemplate(
examplePath: string,
shouldCopySrcAppFolderFromTemplate: boolean,
shouldCopyMainTsFromTemplate: boolean,
): Promise<void> {
if (shouldCopySrcAppFolderFromTemplate) {
const appTemplateFolderPath = join(STACKBLITZ_TEMPLATE, 'src/app');
const exampleAppFolderPath = join(examplePath, 'src/app');
await copyFolder(appTemplateFolderPath, exampleAppFolderPath);
}
if (shouldCopyMainTsFromTemplate) {
const mainTsTemplatePath = join(STACKBLITZ_TEMPLATE, 'src', MAIN_TS_FILENAME);
const exampleMainTsPath = join(examplePath, 'src', MAIN_TS_FILENAME);
await copyFile(mainTsTemplatePath, exampleMainTsPath);
}
}
@@ -1,158 +0,0 @@
/*!
* @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 ts from 'typescript';
import * as fs from 'fs';
import {dirname, join, relative} from 'path';
import {fileURLToPath} from 'url';
import {readFile, writeFile} from 'fs/promises';
const TYPESCRIPT_EXTENSION = '.ts';
const SKIP_FILES_WITH_EXTENSIONS = ['.e2e-spec.ts', '.spec.ts', '.po.ts'];
const EXAMPLES_PATH = `../../content/examples`;
interface File {
path: string;
content: string;
}
interface AnalyzedFiles {
path: string;
componentNames: string[];
}
/** Absolute disk path to the project directory. */
const examplesDir = join(
dirname(fileURLToPath(import.meta.url)),
'../../src/content/examples',
);
const projectDir = join(dirname(fileURLToPath(import.meta.url)), '../../src');
const previewsComponentMapFileName = 'previews.ts';
main();
async function main() {
const files = await retrieveAllTypescriptFiles(
examplesDir,
(path) => !SKIP_FILES_WITH_EXTENSIONS.some((extensionToSkip) => path.endsWith(extensionToSkip)),
);
const filesWithComponent = files
.map((file) => ({
componentNames: analyzeFile(file),
path: file.path,
}))
.filter((result) => result.componentNames.length > 0);
const previewsComponentMap = generatePreviewsComponentMap(filesWithComponent);
fs.mkdirSync(join(projectDir, 'assets/previews'), {recursive: true});
await writeFile(
join(projectDir, 'assets/previews', previewsComponentMapFileName),
previewsComponentMap,
);
}
/** Recursively search the provided directory for all typescript files and asynchronously load them. */
function retrieveAllTypescriptFiles(
baseDir: string,
predicateFn: (path: string) => boolean,
): Promise<File[]> {
const typescriptFiles: Promise<File>[] = [];
const checkFilesInDirectory = (dir: string) => {
const files = fs.readdirSync(dir, {withFileTypes: true});
for (const file of files) {
const fullPathToFile = join(dir, file.name);
const relativeFilePath = relative(baseDir, fullPathToFile);
if (
file.isFile() &&
file.name.endsWith(TYPESCRIPT_EXTENSION) &&
predicateFn(relativeFilePath)
) {
typescriptFiles.push(
readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => {
return {
path: relativeFilePath,
content: fileContent,
};
}),
);
} else if (file.isDirectory()) {
checkFilesInDirectory(fullPathToFile);
}
}
};
checkFilesInDirectory(baseDir);
return Promise.all(typescriptFiles);
}
/** Returns list of the `Standalone` @Component class names for given file */
function analyzeFile(file: File): string[] {
const componentClassNames: string[] = [];
const sourceFile = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, false);
const visitNode = (node: ts.Node): void => {
if (ts.isClassDeclaration(node)) {
const decorators = ts.getDecorators(node);
const componentName = node.name ? node.name.text : null;
if (decorators && decorators.length) {
for (const decorator of decorators) {
const call = decorator.expression;
if (
ts.isCallExpression(call) &&
ts.isIdentifier(call.expression) &&
call.expression.text === 'Component' &&
call.arguments.length > 0 &&
ts.isObjectLiteralExpression(call.arguments[0])
) {
const hasStandaloneProperty = call.arguments[0].properties.some(
(property) =>
property.name &&
ts.isIdentifier(property.name) &&
property.name.text === 'standalone',
);
if (hasStandaloneProperty && componentName) {
componentClassNames.push(componentName);
}
}
}
}
}
ts.forEachChild(node, visitNode);
};
visitNode(sourceFile);
return componentClassNames;
}
function generatePreviewsComponentMap(data: AnalyzedFiles[]): string {
let result = '';
for (const fileData of data) {
for (const componentName of fileData.componentNames) {
const key = `${fileData.path}${
fileData.componentNames.length > 1 ? '_' + componentName : ''
}`.replace(/\\/g, '/');
result += `['${key}']: () => import('${EXAMPLES_PATH}/${fileData.path
.replace(/\\/g, '/')
.replace('.ts', '')}').then(c => c.${componentName}),\n`;
}
}
return fs
.readFileSync('scripts/examples/previews-components.template', 'utf8')
.replace(/\${previewsComponents}/g, result);
}
@@ -1,23 +0,0 @@
/*!
* @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
*/
/**
******************************************************************************
* DO NOT MANUALLY EDIT THIS FILE. THIS FILE IS AUTOMATICALLY GENERATED.
******************************************************************************
*/
import {Type} from '@angular/core';
/**
* Map of the previews components, values are functions which returns the promise of the component type, which will be displayed as preview in the ExampleViewer component.
* Keys has to be equal to paths written down in the docs markdown files.
*/
export const PREVIEWS_COMPONENTS_MAP: Record<string, () => Promise<Type<unknown>>> = {
${previewsComponents}
};
@@ -1,221 +0,0 @@
/*!
* @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 {dirname, join} from 'path';
import {readFile, writeFile} from 'fs/promises';
import {
ASSETS_EXAMPLE_PATH,
CSS_TS_COPYRIGHT,
EXAMPLES_PATH,
HTML_COPYRIGHT,
STACKBLITZ_TEMPLATE_PATH,
STACKBLITZ_CONFIG_FILENAME,
TEMPORARY_EXAMPLES_PATH,
EXCLUDE_FILES_FOR_STACKBLITZ,
} from './utils/examples-constants.mjs';
import {copyFolder, createFolder, removeFolder} from './utils/fs.mjs';
import jsdom from 'jsdom';
import {glob} from 'glob';
import {regionParser} from './../../prerender/markdown-pipeline/regions/region-parser.mjs';
interface StackblitzConfig {
ignore: string[];
file: string;
tags: string[];
description: string;
}
export async function generateStackblitzExample(
exampleFolderName: string,
primaryFilePath: string,
title: string,
) {
const exampleDir = join(EXAMPLES_PATH, exampleFolderName);
const temporaryExampleDir = join(TEMPORARY_EXAMPLES_PATH, exampleFolderName);
const config = await readFile(join(exampleDir, STACKBLITZ_CONFIG_FILENAME), 'utf-8');
const stackblitzConfig: StackblitzConfig = JSON.parse(config);
primaryFilePath = join(...primaryFilePath.split('/'));
await createFolder(temporaryExampleDir);
await combineTemplateWithExample(exampleDir, temporaryExampleDir);
await generateStackblitzHtml(
temporaryExampleDir,
stackblitzConfig,
exampleFolderName,
primaryFilePath,
title,
);
await removeFolder(temporaryExampleDir);
}
async function combineTemplateWithExample(
exampleDir: string,
temporaryExampleDir: string,
): Promise<void> {
// Copy template files to TEMP folder
await copyFolder(STACKBLITZ_TEMPLATE_PATH, temporaryExampleDir);
// Copy example files to TEMP folder
await copyFolder(exampleDir, temporaryExampleDir);
}
async function generateStackblitzHtml(
temporaryExampleDir: string,
stackBlitzConfig: StackblitzConfig,
exampleFolderName: string,
primaryFilePath: string,
title: string,
): Promise<void> {
const defaultIncludes = [
'**/*.ts',
'**/*.js',
'**/*.css',
'**/*.html',
'**/*.md',
'**/*.json',
'**/*.svg',
];
const exampleFilePaths = await glob(defaultIncludes, {
cwd: temporaryExampleDir,
nodir: true,
dot: true,
ignore: stackBlitzConfig.ignore,
});
const postData = await createPostData(
temporaryExampleDir,
stackBlitzConfig,
exampleFilePaths,
title,
);
const primaryFile = getPrimaryFile(primaryFilePath ?? stackBlitzConfig.file, exampleFilePaths);
const html = createStackblitzHtml(postData, primaryFile);
const stackblitzHtmlPath = join(
join(ASSETS_EXAMPLE_PATH, exampleFolderName),
`${primaryFile}.html`,
);
await createFolder(dirname(stackblitzHtmlPath));
await writeFile(stackblitzHtmlPath, html, 'utf-8');
}
function getPrimaryFile(primaryFilePath: string, exampleFilePaths: string[]): string {
if (primaryFilePath) {
if (!exampleFilePaths.some((filePath) => filePath === primaryFilePath)) {
throw new Error(`The specified primary file (${primaryFilePath}) does not exist!`);
}
return primaryFilePath;
} else {
const defaultPrimaryFilePaths = [
'src/app/app.component.html',
'src/app/app.component.ts',
'src/app/main.ts',
];
const primaryFile = defaultPrimaryFilePaths.find((path) =>
exampleFilePaths.some((filePath) => filePath === path),
);
if (!primaryFile) {
throw new Error(
`None of the default primary files (${defaultPrimaryFilePaths.join(', ')}) exists.`,
);
}
return primaryFile;
}
}
async function createPostData(
exampleDir: string,
config: StackblitzConfig,
exampleFilePaths: string[],
title: string,
): Promise<Record<string, string>> {
const postData: Record<string, string> = {};
for (const filePath of exampleFilePaths) {
if (EXCLUDE_FILES_FOR_STACKBLITZ.some((excludedFile) => filePath.endsWith(excludedFile))) {
continue;
}
let content = await readFile(join(exampleDir, filePath), 'utf-8');
content = appendCopyright(filePath, content);
content = extractRegions(filePath, content);
postData[`project[files][${filePath}]`] = content;
}
const tags = ['angular', 'example', ...(config.tags || [])];
tags.forEach((tag, index) => (postData[`project[tags][${index}]`] = tag));
postData['project[description]'] = `Angular Example - ${config.description}`;
postData['project[template]'] = 'node';
postData['project[title]'] = title ?? 'Angular Example';
return postData;
}
function createStackblitzHtml(postData: Record<string, string>, primaryFile: string): string {
const baseHtml = createBaseStackblitzHtml(primaryFile);
const doc = new jsdom.JSDOM(baseHtml).window.document;
const form = doc.querySelector('form');
for (const [key, value] of Object.entries(postData)) {
const element = htmlToElement(doc, `<input type="hidden" name="${key}">`);
if (element && form) {
element.setAttribute('value', value as string);
form.appendChild(element);
}
}
return doc.documentElement.outerHTML;
}
function createBaseStackblitzHtml(primaryFile: string) {
const file = `?file=${primaryFile}`;
const action = `https://stackblitz.com/run${file}`;
return `
<!DOCTYPE html><html lang="en"><body>
<form id="mainForm" method="post" action="${action}" target="_self"></form>
<script>
var embedded = 'ctl=1';
var isEmbedded = window.location.search.indexOf(embedded) > -1;
if (isEmbedded) {
var form = document.getElementById('mainForm');
var action = form.action;
var actionHasParams = action.indexOf('?') > -1;
var symbol = actionHasParams ? '&' : '?'
form.action = form.action + symbol + embedded;
}
document.getElementById("mainForm").submit();
</script>
</body></html>
`.trim();
}
function appendCopyright(filename: string, content: string): string {
if (filename.endsWith('.html')) {
return `${HTML_COPYRIGHT}${content}`;
} else if (filename.endsWith('.ts') || filename.endsWith('.css')) {
return `${CSS_TS_COPYRIGHT}${content}`;
}
return content;
}
function htmlToElement(document: Document, html: string) {
const div = document.createElement('div');
div.innerHTML = html;
return div.firstElementChild;
}
function extractRegions(path: string, contents: string): string {
const regionParserResult = regionParser(contents, path);
return regionParserResult.contents;
}
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.json",
"ts-node": {
"compilerOptions": {
"module": "es2020"
}
}
}
@@ -1,55 +0,0 @@
/*!
* @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 {dirname, join} from 'path';
import {fileURLToPath} from 'url';
const BASE = dirname(fileURLToPath(import.meta.url));
// Directory paths
export const ASSETS_EXAMPLE_PATH = join(
BASE,
'../../../src/assets/content/examples',
);
export const EXAMPLES_PATH = join(BASE, '../../../src/content/examples');
export const STACKBLITZ_TEMPLATE_PATH = join(
BASE,
'../../../src/content/stackblitz-template',
);
export const TEMPORARY_EXAMPLES_PATH = join(fileURLToPath(import.meta.url), '../../../../TEMP');
// Extensions
export const TEST_FILES_EXTENSION_SUFFIX = '.spec.ts';
export const TEST_FILES_E2E_EXTENSION_SUFFIX = '.e2e-spec.ts';
// Filename
// TODO: BUILD.bazel and example-config will be deprecated when migration to standalone examples will be done.
export const BUILD_BAZEL_FILENAME = 'BUILD.bazel';
export const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
export const STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
// Copyright
const PAD = '\n\n';
const COPYRIGHT =
'@license\n' +
'Copyright Google LLC All Rights Reserved.\n' +
'\n' +
'Use of this source code is governed by an MIT-style license that can be\n' +
'found in the LICENSE file at https://angular.dev/license\n';
export const CSS_TS_COPYRIGHT = `/*\n${COPYRIGHT}\n*/${PAD}`;
export const HTML_COPYRIGHT = `<!-- \n${COPYRIGHT}\n-->${PAD}`;
// Exclude
export const EXCLUDE_FILES_FOR_STACKBLITZ = [
STACKBLITZ_CONFIG_FILENAME,
BUILD_BAZEL_FILENAME,
EXAMPLE_CONFIG_FILENAME,
TEST_FILES_EXTENSION_SUFFIX,
TEST_FILES_E2E_EXTENSION_SUFFIX,
];
-77
View File
@@ -1,77 +0,0 @@
/*!
* @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 {Dirent, existsSync, readdirSync} from 'fs';
import {copyFile, mkdir, readFile, readdir, rm, stat} from 'fs/promises';
import {join, relative} from 'path';
export async function copyFolder(source: string, destination: string) {
if (!existsSync(destination)) {
await mkdir(destination, {recursive: true});
}
const files = await readdir(source);
for (const file of files) {
const sourcePath = join(source, file);
const destPath = join(destination, file);
const stats = await stat(sourcePath);
const isDirectory = await stats.isDirectory();
if (isDirectory) {
await copyFolder(sourcePath, destPath);
} else {
await copyFile(sourcePath, destPath);
}
}
}
/** Recursively search the provided directory for all files which satisfy predicate and asynchronously load them. */
export function retrieveFiles(
baseDir: string,
predicate?: (file: Dirent) => boolean,
): Promise<{path: string; content: string}[]> {
const filesWithContent: Promise<{path: string; content: string}>[] = [];
const checkFilesInDirectory = (dir: string) => {
const files = readdirSync(dir, {withFileTypes: true});
for (const file of files) {
const fullPathToFile = join(dir, file.name);
if (file.isFile() && (predicate ? predicate(file) : true)) {
filesWithContent.push(
readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => {
return {
path: relative(baseDir, fullPathToFile),
content: fileContent,
};
}),
);
} else if (file.isDirectory()) {
checkFilesInDirectory(fullPathToFile);
}
}
};
checkFilesInDirectory(baseDir);
return Promise.all(filesWithContent);
}
export async function createFolder(path: string): Promise<void> {
if (!existsSync(path)) {
await mkdir(path, {recursive: true});
}
}
export async function removeFolder(path: string): Promise<void> {
if (existsSync(path)) {
await rm(path, {recursive: true});
}
}
@@ -116,7 +116,7 @@
</div>
</div>
<p class="docs-license">
Super-powered by Google ©2010-2023. Code licensed under an
Super-powered by Google ©2010-2024. Code licensed under an
<a routerLink="/license" title="License text">MIT-style License</a>
. Documentation licensed under
<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>
+207
View File
@@ -0,0 +1,207 @@
# EmbeddedEditor components, services and functionality
- [Scenarios](#scenarios)
- [Loading a project](#loading-a-project)
- [Updating the code](#updating-the-code)
- [Creating a new file](#creating-a-new-file)
- [Deleting a file](#deleting-a-file)
- [Switching a project](#switching-a-project)
- [Components and services](#components-and-services)
- [EmbeddedEditor](#EmbeddedEditor)
- [CodeEditor](#CodeEditor)
- [CodeMirrorEditor](#CodeMirrorEditor)
- [TypeScript Web Worker](#typescript-web-worker)
- [Preview](#Preview)
- [Terminal](#Terminal)
- [InteractiveTerminal](#InteractiveTerminal)
- [Console](#Console)
- [NodeRuntimeSandbox](#NodeRuntimeSandbox)
- [NodeRuntimeState](#NodeRuntimeState)
- [EmbeddedTutorialManager](#EmbeddedTutorialManager)
- [EditorUiState](#EditorUiState)
- [DownloadManager](#DownloadManager)
- [AlertManager](#AlertManager)
- [TypingsLoader](#TypingsLoader)
## External libraries
- [WebContainers API](https://webcontainers.io/)
- [CodeMirror](https://codemirror.net/)
- [@typescript/vfs](https://www.npmjs.com/package/@typescript/vfs)
- [Xterm.js](https://xtermjs.org/)
## Notes
- See [scripts/tutorials/README.md](/scripts/tutorials/README.md) for more information about the tutorials script.
- See [adev/src/content/tutorials/README.md](/adev/src/content/tutorials/README.md) for more information about the tutorials content.
---
## Scenarios
### Loading a project
1. The page responsible for the embedded editor lazy loads the [`EmbeddedEditor`](./embedded-editor.component.ts) component and the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts), then triggers the initialization of all components and services. The embedded editor is available in the following pages:
- homepage: https://angular.dev
- playground: https://angular.dev/playground
- tutorial pages: https://angular.dev/tutorials
2. The project assets are fetched by the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts). Meanwhile:
- The code editor is initialized
- The code editor initializes the TypeScript Web Worker, which initializes the "default file system map" using TypeScript's CDN.
- The WebContainer is initialized
- The terminal is initialized
3. The tutorial source code is mounted in the `WebContainer`'s filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts)
4. The tutorial project dependencies are installed by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
5. The development server is started by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts) and the types are loaded by the [`TypingsLoader`](./typings-loader.service.ts) service.
6. The preview is loaded with the URL provided by the WebContainer API after the development server is started.
7. The project is ready.
### Updating the code
1. The user update the code in the code editor.
2. The code editor state is updated on real time, without debouncing so that the user can see the changes in the code editor and CodeMirror can handle the changes accordingly.
3. At the same time, the changes are sent to the TypeScript web worker to provide diagnostics, autocomplete and type features as soon as possible.
4. The code changes are debounced to be written in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
5. After the debounce time is reached, the code changes are written in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts), then the user can see the changes in the preview.
### Creating a new file
1. The user clicks on the new file button.
2. The new file tab is opened.
3. The user types the new file name.
4. If the file name is valid, the file is created in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
- `..` is disallowed in the file name to prevent users to create files outside the `src` directory.
5. The file is added to the TypeScript virtual file system, allowing the TypeScript web worker to provide diagnostics, autocomplete and type features for the new file. Also, exports from the new file are available in other files.
6. The new file is added as the last tab in the code editor and the new file can be edited.
Note: If the new file name matches a file that already exists but is hidden in the code editor, the content for that file will show up in the created file. An example for a file that always exists is `index.html`.
### Deleting a file
1. The user clicks on the delete file button.
2. The file is deleted from the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
3. The file is removed from the TypeScript virtual file system.
4. The file is removed from the code editor tabs.
Note: Some files can't be deleted to prevent users to break the app, being `src/main.ts`and `src/index.html`
### Switching a project
The embedded editor considers a project change when the embedded editor was already initialized and the user changes the page in the following scenarios:
- Navigating through tutorial steps
- Going from the homepage after the embedded editor is initialized to the playground
- Going from a tutorial page to the playground
- Going from a tutorial page to the homepage
- Going from the playground to the homepage
When a project change is detected, the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts) emits the `tutorialChanged` observable, which is listened in multiple sub-components and services, then each component/service performs the necessary operations to switch the project.
The following steps are executed on project change:
1. The new project files are fetched by the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts).
2. The new project files are mounted in the WebContainer filesystem.
3. The TypeScript virtual filesystem is updated with the new files and contents.
4. The previous project and new project files are compared.
1. Files that are not available in the new project are deleted from the WebContainer filesystem.
2. Files that have the same path and name have their content replaced on the previous step when the files are mounted.
5. The previous project dependencies are compared with the new project dependencies.
1. If there are differences, a `npm install` is triggered, hiding the preview and going to the install loading step.
2. If there are no differences, the project is ready.
6. Some states are resetted, for example the "reveal answer" state if the previous project was in the "reveal answer" state.
## Components and services
### [`EmbeddedEditor`](./embedded-editor.component.ts)
The embedded editor is the parent component that holds all the components and services that compose the embedded editor.
#### [`CodeEditor`](./code-editor/code-editor.component.ts)
The component that holds the code editor view and the code editor state.
##### [`CodeMirrorEditor`](./code-editor/code-mirror-editor.service.ts)
[CodeMirror](https://codemirror.net/) is the library used to handle the code editor.
The `CodeMirrorEditor` service manages the CodeMirror instance and all the interactions with the library used to handle the code editor.
- handle the file edits and the CodeMirror view and state
- handle the current project files in the code editor
- handle the file creations and deletions
- handle the file changes
- handle all the CodeMirror specific events and extensions
###### [TypeScript Web Worker](./code-editor/workers/typescript-vfs.worker.ts)
The TypeScript features are provided by the TypeScript web worker, that is initialized by the `CodeMirrorEditor` service.
The TypeScript web worker uses `@typescript/vfs` and the TypeScript language service to provide diagnostics, autocomplete and type features.
#### [`Preview`](./preview/preview.component.ts)
The preview component manages the `iframe` responsible for displaying the tutorial project preview, with the URL provided by the WebContainer API after the development server is started.
While the project is being initialized, the preview displays the loading state.
#### [`Terminal`](./terminal/terminal.component.ts)
[Xterm.js](https://xtermjs.org/) is the library used to handle the terminals.
The terminal component handles the Xterm.js instance for the console and for the interactive terminal.
##### [`InteractiveTerminal`](./terminal/interactive-terminal.ts)
The interactive terminal is the terminal where the user can interact with the terminal and run commands, supporting only commands for the Angular CLI.
##### Console
The console displays the output for `npm install` and `ng serve`.
#### [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts)
Responsible for managing the WebContainer instance and all communication with its API. This service handles:
- the WebContainer instance
- all Node.js scripts
- the WebContainer filesystem, mounting the tutorial project files, writing new content, deleting and creating files.
- the terminal session, reading and processing user inputs.
- the tutorial project dependencies, installing the dependencies.
- the processes running inside the WebContainer, being the npm scripts to install the dependencies, run the development server and the user inputs for the `ng` CLI.
##### [`NodeRuntimeState`](./node-runtime-state.service.ts)
Manages the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts) loading and error state.
#### [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts)
Manages the tutorial assets, being responsible for fetching the tutorial source code and metadata.
The source code is mounted in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
The metadata is used to manage the project, handle the project changes and the user interactivity with the app.
This service also handles the reveal answer and reset reveal answer feature.
#### [`EditorUiState`](./editor-ui-state.service.ts)
Manages the editor UI state, being responsible for handling the user interactions with the editor tabs, switching between the preview, the terminal and the console.
#### [`DownloadManager`](./download-manager.service.ts)
Responsible for handling the download button in the embedded editor, fetching the tutorial project files and generating a zip file with the project content.
#### [`AlertManager`](./alert-manager.service.ts)
Manage the alerts displayed in the embedded editor, being the out of memory alert when multiple tabs are opened, and unsupported enviroments alerts.
#### [`TypingsLoader`](./typings-loader.service.ts)
Manages the types definitions for the code editor.
+187 -172
View File
@@ -157,6 +157,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'guide/components/advanced-configuration',
contentPath: 'guide/components/advanced-configuration',
},
{
label: 'Custom Elements',
path: 'guide/elements',
contentPath: 'guide/elements',
},
],
},
{
@@ -218,9 +223,39 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
contentPath: 'guide/templates/control-flow',
},
{
label: 'Deferrable views',
path: 'guide/defer',
contentPath: 'guide/defer',
label: 'Pipes',
children: [
{
label: 'Overview',
path: 'guide/pipes',
contentPath: 'guide/pipes/overview',
},
{
label: 'Using a pipe in a template',
path: 'guide/pipes/template',
contentPath: 'guide/pipes/template',
},
{
label: 'Custom pipes',
path: 'guide/pipes/transform-data',
contentPath: 'guide/pipes/transform-data',
},
{
label: 'Pipe precedence in expressions',
path: 'guide/pipes/precedence',
contentPath: 'guide/pipes/precedence',
},
{
label: 'Change detection with pipes',
path: 'guide/pipes/change-detection',
contentPath: 'guide/pipes/change-detection',
},
{
label: 'Unwrapping data from an observable',
path: 'guide/pipes/unwrapping-data-observables',
contentPath: 'guide/pipes/unwrapping-data-observables',
},
],
},
{
label: 'Template reference variables',
@@ -317,6 +352,16 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'guide/signals/rxjs-interop',
contentPath: 'guide/signals/rxjs-interop',
},
{
label: 'Inputs as signals',
path: 'guide/signals/inputs',
contentPath: 'guide/signals/inputs',
},
{
label: 'Model inputs',
path: 'guide/signals/model',
contentPath: 'guide/signals/model',
},
{
label: 'Queries as signals',
path: 'guide/signals/queries',
@@ -324,71 +369,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'NgModule',
children: [
{
label: 'Overview',
path: 'guide/ngmodules',
contentPath: 'guide/ngmodules/overview',
},
{
label: 'JS Modules vs NgModules',
path: 'guide/ngmodules/vs-jsmodule',
contentPath: 'guide/ngmodules/vs-jsmodule',
},
{
label: 'Launching your app with a root module',
path: 'guide/ngmodules/bootstrapping',
contentPath: 'guide/ngmodules/bootstrapping',
},
{
label: 'Sharing NgModules',
path: 'guide/ngmodules/sharing',
contentPath: 'guide/ngmodules/sharing',
},
{
label: 'Frequently used NgModules',
path: 'guide/ngmodules/frequent',
contentPath: 'guide/ngmodules/frequent',
},
{
label: 'Feature modules',
path: 'guide/ngmodules/feature-modules',
contentPath: 'guide/ngmodules/feature-modules',
},
{
label: 'Types of feature modules',
path: 'guide/ngmodules/module-types',
contentPath: 'guide/ngmodules/module-types',
},
{
label: 'Providing dependencies',
path: 'guide/ngmodules/providers',
contentPath: 'guide/ngmodules/providers',
},
{
label: 'Singleton services',
path: 'guide/ngmodules/singleton-services',
contentPath: 'guide/ngmodules/singleton-services',
},
{
label: 'Lazy-loading feature modules',
path: 'guide/ngmodules/lazy-loading',
contentPath: 'guide/ngmodules/lazy-loading',
},
{
label: 'NgModule API',
path: 'guide/ngmodules/api',
contentPath: 'guide/ngmodules/api',
},
{
label: 'NgModule FAQs',
path: 'guide/ngmodules/faq',
contentPath: 'guide/ngmodules/faq',
},
],
},
{
label: 'Routing',
children: [
@@ -455,29 +435,64 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
],
},
{
label: 'Server-side Rendering',
path: 'guide/ssr',
contentPath: 'guide/ssr',
label: 'HTTP Client',
children: [
{
label: 'Overview',
path: 'guide/http',
contentPath: 'guide/http/overview',
},
{
label: 'Setting up HttpClient',
path: 'guide/http/setup',
contentPath: 'guide/http/setup',
},
{
label: 'Making requests',
path: 'guide/http/making-requests',
contentPath: 'guide/http/making-requests',
},
{
label: 'Intercepting requests and responses',
path: 'guide/http/interceptors',
contentPath: 'guide/http/interceptors',
},
{
label: 'Testing',
path: 'guide/http/testing',
contentPath: 'guide/http/testing',
},
],
},
{
label: 'Build-time prerendering',
path: 'guide/prerendering',
contentPath: 'guide/prerendering',
},
{
label: 'Hydration',
path: 'guide/hydration',
contentPath: 'guide/hydration',
},
{
label: 'Deferrable views',
path: 'guide/defer',
contentPath: 'guide/defer',
},
{
label: 'Image Optimization',
path: 'guide/image-optimization',
contentPath: 'guide/image-optimization',
label: 'Performance',
children: [
{
label: 'Deferrable views',
path: 'guide/defer',
contentPath: 'guide/defer',
},
{
label: 'Image Optimization',
path: 'guide/image-optimization',
contentPath: 'guide/image-optimization',
},
{
label: 'Server-side Rendering',
path: 'guide/ssr',
contentPath: 'guide/ssr',
},
{
label: 'Build-time prerendering',
path: 'guide/prerendering',
contentPath: 'guide/prerendering',
},
{
label: 'Hydration',
path: 'guide/hydration',
contentPath: 'guide/hydration',
},
],
},
{
label: 'Testing',
@@ -589,81 +604,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'Security',
path: 'guide/security',
contentPath: 'guide/security',
},
{
label: 'Pipes',
children: [
{
label: 'Overview',
path: 'guide/pipes',
contentPath: 'guide/pipes/overview',
},
{
label: 'Using a pipe in a template',
path: 'guide/pipes/template',
contentPath: 'guide/pipes/template',
},
{
label: 'Custom pipes',
path: 'guide/pipes/transform-data',
contentPath: 'guide/pipes/transform-data',
},
{
label: 'Pipe precedence in expressions',
path: 'guide/pipes/precedence',
contentPath: 'guide/pipes/precedence',
},
{
label: 'Change detection with pipes',
path: 'guide/pipes/change-detection',
contentPath: 'guide/pipes/change-detection',
},
{
label: 'Unwrapping data from an observable',
path: 'guide/pipes/unwrapping-data-observables',
contentPath: 'guide/pipes/unwrapping-data-observables',
},
],
},
{
label: 'Http Client',
children: [
{
label: 'Overview',
path: 'guide/http',
contentPath: 'guide/http/overview',
},
{
label: 'Setting up HttpClient',
path: 'guide/http/setup',
contentPath: 'guide/http/setup',
},
{
label: 'Making requests',
path: 'guide/http/making-requests',
contentPath: 'guide/http/making-requests',
},
{
label: 'Intercepting requests and responses',
path: 'guide/http/interceptors',
contentPath: 'guide/http/interceptors',
},
{
label: 'Security',
path: 'guide/http/security',
contentPath: 'guide/http/security',
},
{
label: 'Testing',
path: 'guide/http/testing',
contentPath: 'guide/http/testing',
},
],
},
{
label: 'Animations',
children: [
@@ -694,11 +634,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'Custom Elements',
path: 'guide/elements',
contentPath: 'guide/elements',
},
],
},
{
@@ -830,9 +765,9 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
contentPath: 'best-practices/style-guide',
},
{
label: 'Keeping up-to-date',
path: 'update',
contentPath: 'best-practices/update',
label: 'Security',
path: 'best-practices/security',
contentPath: 'guide/security', // Have not refactored due to build issues
},
{
label: 'Accessibility',
@@ -864,6 +799,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'Keeping up-to-date',
path: 'update',
contentPath: 'best-practices/update',
},
],
},
{
@@ -1349,6 +1289,81 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'Concepts',
children: [
{
label: 'Overview',
path: 'reference/concepts',
contentPath: 'reference/concepts/overview',
},
{
label: 'NgModule',
children: [
{
label: 'Overview',
path: 'guide/ngmodules',
contentPath: 'guide/ngmodules/overview',
},
{
label: 'JS Modules vs NgModules',
path: 'guide/ngmodules/vs-jsmodule',
contentPath: 'guide/ngmodules/vs-jsmodule',
},
{
label: 'Launching your app with a root module',
path: 'guide/ngmodules/bootstrapping',
contentPath: 'guide/ngmodules/bootstrapping',
},
{
label: 'Sharing NgModules',
path: 'guide/ngmodules/sharing',
contentPath: 'guide/ngmodules/sharing',
},
{
label: 'Frequently used NgModules',
path: 'guide/ngmodules/frequent',
contentPath: 'guide/ngmodules/frequent',
},
{
label: 'Feature modules',
path: 'guide/ngmodules/feature-modules',
contentPath: 'guide/ngmodules/feature-modules',
},
{
label: 'Types of feature modules',
path: 'guide/ngmodules/module-types',
contentPath: 'guide/ngmodules/module-types',
},
{
label: 'Providing dependencies',
path: 'guide/ngmodules/providers',
contentPath: 'guide/ngmodules/providers',
},
{
label: 'Singleton services',
path: 'guide/ngmodules/singleton-services',
contentPath: 'guide/ngmodules/singleton-services',
},
{
label: 'Lazy-loading feature modules',
path: 'guide/ngmodules/lazy-loading',
contentPath: 'guide/ngmodules/lazy-loading',
},
{
label: 'NgModule API',
path: 'guide/ngmodules/api',
contentPath: 'guide/ngmodules/api',
},
{
label: 'NgModule FAQs',
path: 'guide/ngmodules/faq',
contentPath: 'guide/ngmodules/faq',
},
],
},
],
},
];
const FOOTER_NAVIGATION_DATA: NavigationItem[] = [
+1
View File
@@ -27,6 +27,7 @@ copy_to_directory(
"//adev/src/content/introduction",
"//adev/src/content/introduction/essentials",
"//adev/src/content/reference",
"//adev/src/content/reference/concepts",
"//adev/src/content/reference/configs",
"//adev/src/content/reference/errors",
"//adev/src/content/reference/extended-diagnostics",

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before

Width:  |  Height:  |  Size: 724 KiB

After

Width:  |  Height:  |  Size: 724 KiB

Before

Width:  |  Height:  |  Size: 745 KiB

After

Width:  |  Height:  |  Size: 745 KiB

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -170,7 +170,7 @@ These events are shown in the following table.
| [`ChildActivationEnd`](api/router/ChildActivationEnd) | Triggered when the Router finishes activating a route's children. |
| [`ActivationEnd`](api/router/ActivationEnd) | Triggered when the Router finishes activating a route. |
| [`NavigationEnd`](api/router/NavigationEnd) | Triggered when navigation ends successfully. |
| [`NavigationCancel`](api/router/NavigationCancel) | Triggered when navigation is canceled. This can happen when a Route Guard returns false during navigation, or redirects by returning a `UrlTree`. |
| [`NavigationCancel`](api/router/NavigationCancel) | Triggered when navigation is canceled. This can happen when a Route Guard returns false during navigation, or redirects by returning a `UrlTree` or `RedirectCommand`. |
| [`NavigationError`](api/router/NavigationError) | Triggered when navigation fails due to an unexpected error. |
| [`Scroll`](api/router/Scroll) | Represents a scrolling event. |
+148
View File
@@ -0,0 +1,148 @@
# Signal inputs
Signal inputs allow values to be bound from parent components.
Those values are exposed using a `Signal` and can change during the lifecycle of your component.
HELPFUL: Signal inputs are currently in [developer preview](/guide/releases#developer-preview).
Angular supports two variants of inputs:
**Optional inputs**
Inputs are optional by default, unless you use `input.required`.
You can specify an explicit initial value, or Angular will use `undefined` implicitly.
**Required inputs**
Required inputs always have a value of the given input type.
They are declared using the `input.required` function.
```typescript
import {Component, input} from '@angular/core';
@Component({...})
export class MyComp {
// optional
firstName = input<string>(); // InputSignal<string|undefined>
age = input(0); // InputSignal<number>
// required
lastName = input.required<string>(); // InputSignal<string>
}
```
An input is automatically recognized by Angular whenever you use the `input` or `input.required` functions as initializer of class members.
## Aliasing an input
Angular uses the class member name as the name of the input.
You can alias inputs to change their public name to be different.
```typescript
class StudentDirective {
age = input(0, {alias: 'studentAge'});
}
```
This allows users to bind to your input using `[studentAge]`, while inside your component you can access the input values using `this.age`.
## Using in templates
Signal inputs are read-only signals.
As with signals declared via `signal()`, you access the current value of the input by calling the input signal.
```html
<p>First name: {{firstName()}}</p>
<p>Last name: {{lastName()}}</p>
```
This access to the value is captured in reactive contexts and can notify active consumers, like Angular itself, whenever the input value changes.
An input signal in practice is a trivial extension of signals that you know from [the signals guide](/guide/signals).
```typescript
export class InputSignal<T> extends Signal<T> { ... }`.
```
## Deriving values
As with signals, you can derive values from inputs using `computed`.
```typescript
import {Component, input, computed} from '@angular/core';
@Component({...})
export class MyComp {
age = input(0);
// age multiplied by two.
ageMultiplied = computed(() => this.age() * 2);
}
```
Computed signals memoize values.
See more details in the [dedicated section for computed](/guide/signals#computed-signals).
## Monitoring changes
With signal inputs, users can leverage the `effect` function.
The function will execute whenever the input changes.
Consider the following example.
The new value is printed to the console whenever the `firstName` input changes.
```typescript
import {input, effect} from '@angular/core';
class MyComp {
firstName = input.required<string>();
constructor() {
effect(() => {
console.log(this.firstName());
});
}
}
```
The `console.log` function is invoked every time the `firstName` input changes.
This will happen as soon as `firstName` is available, and for subsequent changes during the lifetime of `MyComp`.
## Value transforms
You may want to coerce or parse input values without changing the meaning of the input.
Transforms convert the raw value from parent templates to the expected input type.
Transforms should be [pure functions](https://en.wikipedia.org/wiki/Pure_function).
```typescript
class MyComp {
disabled = input(false, {
transform: (value: boolean|string) => typeof value === 'string' ? value === '' : value,
});
}
```
In the example above, you are declaring an input named `disabled` that is accepting values of type `boolean` and `string`.
This is captured by the explicit parameter type of `value` in the `transform` option.
These values are then parsed to a `boolean` with the transform, resulting in booleans.
That way, you are only dealing with `boolean` inside your component when calling `this.disabled()`, while users of your component can pass an empty string as a shorthand to mark your component as disabled.
```html
<my-custom-comp disabled>
```
IMPORTANT:Do not use transforms if they change the meaning of the input, or if they are [impure](https://en.wikipedia.org/wiki/Pure_function#Impure_functions).
Instead, use `computed` for transformations with different meaning, or an `effect` for impure code that should run whenever the input changes.
## Why should we use signal inputs and not `@Input()`?
Signal inputs are a reactive alternative to decorator-based `@Input()`.
In comparison to decorator-based `@Input`, signal inputs provide numerous benefits:
1. Signal inputs are more **type safe**:
<br/>• Required inputs do not require initial values, or tricks to tell TypeScript that an input _always_ has a value.
<br/>• Transforms are automatically checked to match the accepted input values.
2. Signal inputs, when used in templates, will **automatically** mark `OnPush` components as dirty.
3. Values can be easily **derived** whenever an input changes using `computed`.
4. Easier and more local monitoring of inputs using `effect` instead of `ngOnChanges` or setters.
+138
View File
@@ -0,0 +1,138 @@
# Model inputs
**Model inputs** are a special type of input that enable a component to propagate new values
back to another component.
HELPFUL: Model inputs are currently in [developer preview](/guide/releases#developer-preview).
When creating a component, you can define a model input similarly to how you create a standard
input.
```typescript
import {Component, model, input} from '@angular/core';
@Component({...})
export class CustomCheckbox {
// This is a model input.
checked = model(false);
// This is a standard input.
disabled = input(false);
}
```
Both types of input allow someone to bind a value into the property. However, **model inputs allow
the component author to write values into the property**.
In other respects, you can use model inputs the same way you use standard inputs. You can read the
value by calling the signal function, including in reactive contexts like `computed` and `effect`.
```typescript
import {Component, model, input} from '@angular/core';
@Component({
selector: 'custom-checkbox',
template: '<div (click)="toggle()"> ... </div>',
})
export class CustomCheckbox {
checked = model(false);
disabled = input(false);
toggle() {
// While standard inputs are read-only, you can write directly to model inputs.
this.checked.set(!this.checked());
}
}
```
When a component writes a new value into a model input, Angular can propagate the new value back
to the component that is binding a value into that input. This is called **two-way binding** because
values can flow in both directions.
## Two-way binding with signals
You can bind a writable signal to a model input.
```typescript
@Component({
...,
// `checked` is a model input.
// The parenthesis-inside-square-brackets syntax (aka "banana-in-a-box") creates a two-way binding
template: '<custom-checkbox [(checked)]="isAdmin" />',
})
export class UserProfile {
protected isAdmin = signal(false);
}
```
In the above example, the `CustomCheckbox` can write values into its `checked` model input, which
then propagates those values back to the `isAdmin` signal in `UserProfile`. This binding keeps that
values of `checked` and `isAdmin` in sync. Notice that the binding passes the `isAdmin` signal
itself, not the _value_ of the signal.
## Two-way binding with plain properties
You can bind a plain JavaScript property to a model input.
```typescript
@Component({
...,
// `checked` is a model input.
// The parenthesis-inside-square-brackets syntax (aka "banana-in-a-box") creates a two-way binding
template: '<custom-checkbox [(checked)]="isAdmin" />',
})
export class UserProfile {
protected isAdmin = false;
}
```
In the example above, the `CustomCheckbox` can write values into its `checked` model input, which
then propagates those values back to the `isAdmin` property in `UserProfile`. This binding keeps
that values of `checked` and `isAdmin` in sync.
## Implicit `change` events
When you declare a model input in a component or directive, Angular automatically creates a
corresponding [output](/guide/inputs-outputs) for that model. The output's name is the model
input's name suffixed with "Change".
```typescript
@Directive({...})
export class CustomCheckbox {
// This automatically creates an output named "checkedChange".
// Can be subscribed to using `(checkedChange)="handler()"` in the template.
checked = model(false);
}
```
Angular emits this change event whenever you write a new value into the model input by calling
its `set` or `update` methods.
## Customizing model inputs
You can mark a model input as required or provide an alias in the same way as a
[standard input](/guide/signal-inputs).
Model inputs do not support input transforms.
## Differences between `model()` and `input()`
Both `input()` and `model()` functions are ways to define signal-based inputs in Angular, but they
differ in a few ways:
1. `model()` defines **both** an input and an output. The output's name is always the name of the
input suffixed with `Change` to support two-way bindings. It will be up to the consumer of your
directive to decide if they want to use just the input, just the output, or both.
2. `ModelSignal` is a `WritableSignal` which means that its value can be changed from anywhere
using the `set` and `update` methods. When a new value is assigned, the `ModelSignal` will emit
to its output. This is different from `InputSignal` which is read-only and can only be changed
through the template.
3. Model inputs do not support input transforms while signal inputs do.
## When to use model inputs
Use model inputs in components that exist to modify a value based on user interaction.
Custom form controls, such as a date picker or combobox, should use model inputs for their
primary value.
Avoid using model inputs as a convenience to avoid introducing an additional class property for
containing local state.
@@ -0,0 +1,9 @@
load("@npm//@angular/docs:index.bzl", "generate_guides")
generate_guides(
name = "concepts",
srcs = glob([
"*.md",
]),
visibility = ["//adev:__subpackages__"],
)
@@ -0,0 +1,7 @@
# Concepts
<docs-card-container>
<docs-card title="NgModules" link="Learn more" href="guide/ngmodules">
NgModules is a concept that commonly used in architecture v16 and earlier to help configure the injector and the compiler and help organize related things together.
</docs-card>
</docs-card-container>
+1 -1
View File
@@ -1,6 +1,6 @@
# The MIT License
Copyright (c) 2010-2023 Google LLC. https://angular.dev/license
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
@@ -32,7 +32,7 @@ Note: Look for alerts like this one, which call out steps that may only be for y
The lessons in this tutorial create an Angular app that lists houses for rent and shows the details of individual houses.
This app uses features that are common to many Angular apps.
<img alt="Output of heroes dashboard" src="assets/images/tutorials/first-app/homes-app-landing-page.png">
<img alt="Output of homes landing page" src="assets/images/tutorials/first-app/homes-app-landing-page.png">
## Local development environment
@@ -8,7 +8,7 @@ In this activity you will learn to use constructor-based dependency injection.
<hr>
To inject a service or some other injectable resource into your componenet use the following syntax:
To inject a service or some other injectable resource into your component use the following syntax:
<docs-code language="ts" highlight="[3]">
@Component({...})
+1 -1
View File
@@ -1,4 +1,4 @@
{
"branchName": "refs/heads/main",
"sha": "01f78129bcbea278a959d365dee7f34666d5e341"
"sha": "6452ed70f1464c97e1ed4551dcf9f1346c5452f1"
}
+3 -3
View File
@@ -10,7 +10,7 @@
{
"name": "allowed-hosts",
"type": "array",
"description": "List of hosts that are allowed to access the dev server."
"description": "List of hosts that are allowed to access the dev server. This option has no effect when using the 'application' or other esbuild-based builders."
},
{
"name": "browser-target",
@@ -35,7 +35,7 @@
"name": "disable-host-check",
"type": "boolean",
"default": false,
"description": "Don't verify connected clients are part of allowed hosts."
"description": "Don't verify connected clients are part of allowed hosts. This option has no effect when using the 'application' or other esbuild-based builders."
},
{
"name": "force-esbuild",
@@ -105,7 +105,7 @@
{
"name": "public-host",
"type": "string",
"description": "The URL that the browser client (or live-reload client, if enabled) should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies."
"description": "The URL that the browser client (or live-reload client, if enabled) should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies. This option has no effect when using the 'application' or other esbuild-based builders."
},
{
"name": "serve-path",
+1 -1
View File
@@ -2,7 +2,7 @@
@description
The MIT License
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files \(the "Software"\), to deal
+6
View File
@@ -1336,5 +1336,11 @@
"start": "2024-06-13"
},
"name": "JSNation"
},
{
"date": {
"start": "2024-06-27"
},
"name": "dotJS"
}
]
-4
View File
@@ -12,10 +12,6 @@
"packages/compiler/src/render3/view/i18n/meta.ts",
"packages/compiler/src/render3/view/i18n/util.ts"
],
[
"packages/compiler/src/render3/view/styling_builder.ts",
"packages/compiler/src/render3/view/template.ts"
],
[
"packages/core/src/change_detection/change_detector_ref.ts",
"packages/core/src/render3/view_ref.ts"
@@ -58,7 +58,6 @@ export interface MiscOptions {
compileNonExportedClasses?: boolean;
disableTypeScriptVersionCheck?: boolean;
forbidOrphanComponents?: boolean;
useTemplatePipeline?: boolean;
}
// @public
+10 -1
View File
@@ -306,7 +306,7 @@ export interface ExtraOptions extends InMemoryScrollingOptions, RouterConfigOpti
}
// @public
export type GuardResult = boolean | UrlTree;
export type GuardResult = boolean | UrlTree | RedirectCommand;
// @public
export class GuardsCheckEnd extends RouterEvent {
@@ -595,6 +595,15 @@ export function provideRoutes(routes: Routes): Provider[];
// @public
export type QueryParamsHandling = 'merge' | 'preserve' | '';
// @public
export class RedirectCommand {
constructor(redirectTo: UrlTree, navigationBehaviorOptions?: NavigationBehaviorOptions | undefined);
// (undocumented)
readonly navigationBehaviorOptions?: NavigationBehaviorOptions | undefined;
// (undocumented)
readonly redirectTo: UrlTree;
}
// @public @deprecated
export interface Resolve<T> {
// (undocumented)
@@ -17,7 +17,7 @@
"uncompressed": {
"runtime": 2734,
"main": 231349,
"polyfills": 33810,
"polyfills": 34169,
"src_app_lazy_lazy_routes_ts": 487
}
},
+1 -1
View File
@@ -20,7 +20,7 @@
"@angular/upgrade": "file:../../dist/packages-dist/upgrade",
"@types/jasmine": "file:../../node_modules/@types/jasmine",
"rxjs": "file:../../node_modules/rxjs",
"typescript": "5.4.1-rc",
"typescript": "5.4.2",
"zone.js": "file:../../dist/zone.js-dist/archive/zone.js.tgz"
},
"scripts": {
+4 -4
View File
@@ -796,10 +796,10 @@ tslib@^2.1.0, tslib@^2.3.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
typescript@5.4.1-rc:
version "5.4.1-rc"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.1-rc.tgz#1ecdd897df1d9ef5bd1f844bad64691ecc23314d"
integrity sha512-gInURzaO0bbfzfQAc3mfcHxh8qev+No4QOFUZHajo9vBgOLaljELJ3wuzyoGo/zHIzMSezdhtrsRdqL6E9SvNA==
typescript@5.4.2:
version "5.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372"
integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==
update-browserslist-db@^1.0.13:
version "1.0.13"
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "17.3.0-next.1",
"version": "18.0.0-next.0",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@@ -27,7 +27,7 @@
"ng-dev": "ts-node --esm --project .ng-dev/tsconfig.json --transpile-only node_modules/@angular/ng-dev/bundles/cli.mjs",
"build": "ts-node --esm --project scripts/tsconfig.json scripts/build/build-packages-dist.mts",
"test": "bazelisk test",
"test:ci": "bazelisk test -- //... -//adev/... -//devtools/... -//aio/... && bazelisk test --//packages/compiler:use_template_pipeline //packages/compiler-cli/test/compliance/full",
"test:ci": "bazelisk test -- //... -//adev/... -//devtools/... -//aio/...",
"test-tsec": "bazelisk test //... --build_tag_filters=tsec --test_tag_filters=tsec",
"lint": "yarn -s tslint && yarn -s ng-dev format changed --check",
"tslint": "tslint -c tslint.json --project tsconfig-tslint.json",
@@ -141,7 +141,7 @@
"todomvc-common": "^1.0.5",
"tslib": "^2.3.0",
"tslint": "6.1.3",
"typescript": "5.4.1-rc",
"typescript": "5.4.2",
"webtreemap": "^2.0.1",
"ws": "^8.15.0",
"xhr2": "0.2.1",
+4 -1
View File
@@ -100,7 +100,10 @@ export function transferCacheInterceptorFn(
// POST requests are allowed either globally or at request level
(requestMethod === 'POST' && !globalOptions.includePostRequests && !requestOptions) ||
(requestMethod !== 'POST' && !ALLOWED_METHODS.includes(requestMethod)) ||
requestOptions === false || //
// Do not cache request that require authorization
req.headers.has('authorization') ||
req.headers.has('proxy-authorization') ||
requestOptions === false ||
globalOptions.filter?.(req) === false
) {
return next(req);
@@ -248,6 +248,22 @@ describe('TransferCache', () => {
makeRequestAndExpectNone('/test-2?foo=1', 'POST', {transferCache: true});
});
it('should not cache request that requires authorization', async () => {
makeRequestAndExpectOne('/test-auth', 'foo', {
headers: {Authorization: 'Basic YWxhZGRpbjpvcGVuc2VzYW1l'},
});
makeRequestAndExpectOne('/test-auth', 'foo');
});
it('should not cache request that requires proxy authorization', async () => {
makeRequestAndExpectOne('/test-auth', 'foo', {
headers: {'Proxy-Authorization': 'Basic YWxhZGRpbjpvcGVuc2VzYW1l'},
});
makeRequestAndExpectOne('/test-auth', 'foo');
});
describe('caching with global setting', () => {
beforeEach(
withBody('<test-app-http></test-app-http>', () => {
+1 -1
View File
@@ -35,7 +35,7 @@ export {OptimizeFor} from './src/ngtsc/typecheck/api';
// needing to use a default import. NodeJS will expose named CJS exports as named ESM exports.
// TODO(devversion): Remove these duplicate exports once devmode&prodmode is combined/ESM.
export {ConsoleLogger, Logger, LogLevel} from './src/ngtsc/logging';
export {NodeJSFileSystem} from './src/ngtsc/file_system';
export {NodeJSFileSystem, absoluteFrom} from './src/ngtsc/file_system';
// Export documentation entities for Angular-internal API doc generation.
export * from './src/ngtsc/docs/src/entities';
@@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {BoundTarget, ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareDirectiveDependencyMetadata, R3DeclarePipeDependencyMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3PartialDeclaration, R3TargetBinder, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstDeferredTrigger, TmplAstElement, ViewEncapsulation} from '@angular/compiler';
import {BoundTarget, ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentDeferMetadata, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareDirectiveDependencyMetadata, R3DeclarePipeDependencyMetadata, R3DirectiveDependencyMetadata, R3PartialDeclaration, R3TargetBinder, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, TmplAstDeferredBlock, ViewEncapsulation} from '@angular/compiler';
import semver from 'semver';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
@@ -16,7 +16,7 @@ import {GetSourceFileFn} from '../get_source_file';
import {toR3DirectiveMeta} from './partial_directive_linker_1';
import {LinkedDefinition, PartialLinker} from './partial_linker';
import {extractForwardRef, PLACEHOLDER_VERSION, SHOULD_USE_TEMPLATE_PIPELINE_FOR_LINKER} from './util';
import {extractForwardRef, PLACEHOLDER_VERSION} from './util';
function makeDirectiveMetadata<TExpression>(
directiveExpr: AstObject<R3DeclareDirectiveDependencyMetadata, TExpression>,
@@ -178,13 +178,7 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
declarationListEmitMode,
styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) :
[],
deferBlocks: this.createR3DeferredMetadata(boundTarget),
// Defer blocks are not yet fully supported in partial compilation.
deferrableDeclToImportDecl: new Map(),
deferrableTypes: new Map(),
deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock,
defer: this.createR3ComponentDeferMetadata(boundTarget),
encapsulation: metaObj.has('encapsulation') ?
parseEncapsulation(metaObj.getValue('encapsulation')) :
ViewEncapsulation.Emulated,
@@ -196,7 +190,6 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
relativeContextFilePath: this.sourceUrl,
i18nUseExternalIds: false,
declarations,
useTemplatePipeline: SHOULD_USE_TEMPLATE_PIPELINE_FOR_LINKER,
};
}
@@ -264,32 +257,16 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
};
}
private createR3DeferredMetadata(boundTarget: BoundTarget<any>):
Map<TmplAstDeferredBlock, R3DeferBlockMetadata> {
private createR3ComponentDeferMetadata(boundTarget: BoundTarget<any>): R3ComponentDeferMetadata {
const deferredBlocks = boundTarget.getDeferBlocks();
const meta = new Map<TmplAstDeferredBlock, R3DeferBlockMetadata>();
const blocks = new Map<TmplAstDeferredBlock, o.ArrowFunctionExpr|null>();
for (const block of deferredBlocks) {
const triggerElements = new Map<TmplAstDeferredTrigger, TmplAstElement>();
this.resolveDeferTriggers(block, block.triggers, boundTarget, triggerElements);
this.resolveDeferTriggers(block, block.prefetchTriggers, boundTarget, triggerElements);
// TODO: leaving `deps` empty for now, to be implemented as one of the next steps.
meta.set(block, {deps: [], triggerElements});
blocks.set(block, null);
}
return meta;
}
private resolveDeferTriggers(
block: TmplAstDeferredBlock, triggers: TmplAstDeferredBlockTriggers,
boundTarget: BoundTarget<any>,
triggerElements: Map<TmplAstDeferredTrigger, TmplAstElement|null>): void {
Object.keys(triggers).forEach(key => {
const trigger = triggers[key as keyof TmplAstDeferredBlockTriggers]!;
triggerElements.set(trigger, boundTarget.getDeferredTriggerTarget(block, trigger));
});
return {mode: DeferBlockDepsEmitMode.PerBlock, blocks};
}
}
@@ -13,7 +13,7 @@ import {AstObject, AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
import {LinkedDefinition, PartialLinker} from './partial_linker';
import {extractForwardRef, SHOULD_USE_TEMPLATE_PIPELINE_FOR_LINKER, wrapReference} from './util';
import {extractForwardRef, wrapReference} from './util';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareDirective()` call expressions.
@@ -145,7 +145,6 @@ function toHostMetadata<TExpression>(metaObj: AstObject<R3DeclareDirectiveMetada
listeners: {},
properties: {},
specialAttributes: {},
useTemplatePipeline: SHOULD_USE_TEMPLATE_PIPELINE_FOR_LINKER,
};
}
@@ -170,7 +169,6 @@ function toHostMetadata<TExpression>(metaObj: AstObject<R3DeclareDirectiveMetada
host.getObject('properties').toLiteral(value => value.getString()) :
{},
specialAttributes,
useTemplatePipeline: SHOULD_USE_TEMPLATE_PIPELINE_FOR_LINKER,
};
}
@@ -12,8 +12,6 @@ import {FatalLinkerError} from '../../fatal_linker_error';
export const PLACEHOLDER_VERSION = '0.0.0-PLACEHOLDER';
export const SHOULD_USE_TEMPLATE_PIPELINE_FOR_LINKER = true;
export function wrapReference<TExpression>(wrapped: o.WrappedNodeExpr<TExpression>): R3Reference {
return {value: wrapped, type: wrapped};
}
@@ -24,11 +24,10 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope",
#"//packages/compiler-cli/src/ngtsc/shims:api",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
#"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics/api",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/src/ngtsc/xi18n",
"@npm//@types/node",
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, DomElementSchemaRegistry, Expression, ExternalExpr, FactoryTarget, makeBindingParser, R3ComponentMetadata, R3DeferBlockMetadata, R3DeferBlockTemplateDependency, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstDeferredTrigger, TmplAstElement, ViewEncapsulation, WrappedNodeExpr} from '@angular/compiler';
import {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, compileDeferResolverFunction, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, DomElementSchemaRegistry, ExternalExpr, FactoryTarget, makeBindingParser, outputAst as o, R3ComponentDeferMetadata, R3ComponentMetadata, R3DeferPerComponentDependency, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, TmplAstDeferredBlock, ViewEncapsulation} from '@angular/compiler';
import ts from 'typescript';
import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles';
@@ -25,6 +25,7 @@ import {getDiagnosticNode, makeUnknownComponentDeferredImportDiagnostic} from '.
import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform';
import {TypeCheckableDirectiveMeta, TypeCheckContext} from '../../../typecheck/api';
import {ExtendedTemplateChecker} from '../../../typecheck/extended/api';
import {TemplateSemanticsChecker} from '../../../typecheck/template_semantics/api/api';
import {getSourceFile} from '../../../util/src/typescript';
import {Xi18nContext} from '../../../xi18n';
import {combineResolvers, compileDeclareFactory, compileInputTransformFields, compileNgFactoryDefField, compileResults, extractClassDebugInfo, extractClassMetadata, extractSchemas, findAngularDecorator, forwardRefResolver, getDirectiveDiagnostics, getProviderDiagnostics, InjectableClassRegistry, isExpressionForwardReference, readBaseClass, ReferencesRegistry, removeIdentifierReferences, resolveEncapsulationEnumValueLocally, resolveEnumValue, resolveImportedFile, resolveLiteral, resolveProvidersRequiringFactory, ResourceLoader, toFactoryMetadata, tryUnwrapForwardRef, validateHostDirectives, wrapFunctionExpressionsInParens} from '../../common';
@@ -32,7 +33,7 @@ import {extractDirectiveMetadata, parseDirectiveStyles} from '../../directive';
import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module';
import {checkCustomElementSelectorForErrors, makeCyclicImportInfo} from './diagnostics';
import {ComponentAnalysisData, ComponentResolutionData} from './metadata';
import {ComponentAnalysisData, ComponentResolutionData, DeferredComponentDependency} from './metadata';
import {_extractTemplateStyleUrls, extractComponentStyleUrls, extractStyleResources, extractTemplate, makeResourceNotFoundError, ParsedTemplateWithSource, parseTemplateDeclaration, preloadAndParseTemplate, ResourceTypeForDiagnostics, StyleUrlMeta, transformDecoratorResources} from './resources';
import {ComponentSymbol} from './symbol';
import {animationTriggerResolver, collectAnimationNames, validateAndFlattenComponentImports} from './util';
@@ -88,7 +89,6 @@ export class ComponentDecoratorHandler implements
private readonly compilationMode: CompilationMode,
private readonly deferredSymbolTracker: DeferredSymbolTracker,
private readonly forbidOrphanRendering: boolean, private readonly enableBlockSyntax: boolean,
private readonly useTemplatePipeline: boolean,
private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker|
null) {
this.extractTemplateOptions = {
@@ -228,7 +228,7 @@ export class ComponentDecoratorHandler implements
const directiveResult = extractDirectiveMetadata(
node, decorator, this.reflector, this.importTracker, this.evaluator, this.refEmitter,
this.referencesRegistry, this.isCore, this.annotateForClosureCompiler, this.compilationMode,
this.elementSchemaRegistry.getDefaultComponentElementName(), this.useTemplatePipeline);
this.elementSchemaRegistry.getDefaultComponentElementName());
if (directiveResult === undefined) {
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
// case, compilation of the decorator is skipped. Returning an empty object signifies
@@ -245,19 +245,19 @@ export class ComponentDecoratorHandler implements
resolveEncapsulationEnumValueLocally(component.get('encapsulation'))) ??
ViewEncapsulation.Emulated;
let changeDetection: number|Expression|null = null;
let changeDetection: number|o.Expression|null = null;
if (this.compilationMode !== CompilationMode.LOCAL) {
changeDetection =
resolveEnumValue(this.evaluator, component, 'changeDetection', 'ChangeDetectionStrategy');
} else if (component.has('changeDetection')) {
changeDetection = new WrappedNodeExpr(component.get('changeDetection')!);
changeDetection = new o.WrappedNodeExpr(component.get('changeDetection')!);
}
let animations: Expression|null = null;
let animations: o.Expression|null = null;
let animationTriggerNames: AnimationTriggerNames|null = null;
if (component.has('animations')) {
const animationExpression = component.get('animations')!;
animations = new WrappedNodeExpr(animationExpression);
animations = new o.WrappedNodeExpr(animationExpression);
const animationsValue =
this.evaluator.evaluate(animationExpression, animationTriggerResolver);
animationTriggerNames = {includesDynamicAnimations: false, staticTriggerNames: []};
@@ -281,13 +281,13 @@ export class ComponentDecoratorHandler implements
// we can distinguish where an error is coming from when logging the diagnostics in `resolve`.
let viewProvidersRequiringFactory: Set<Reference<ClassDeclaration>>|null = null;
let providersRequiringFactory: Set<Reference<ClassDeclaration>>|null = null;
let wrappedViewProviders: Expression|null = null;
let wrappedViewProviders: o.Expression|null = null;
if (component.has('viewProviders')) {
const viewProviders = component.get('viewProviders')!;
viewProvidersRequiringFactory =
resolveProvidersRequiringFactory(viewProviders, this.reflector, this.evaluator);
wrappedViewProviders = new WrappedNodeExpr(
wrappedViewProviders = new o.WrappedNodeExpr(
this.annotateForClosureCompiler ? wrapFunctionExpressionsInParens(viewProviders) :
viewProviders);
}
@@ -484,13 +484,13 @@ export class ComponentDecoratorHandler implements
// (if it exists) and populate the `DeferredSymbolTracker` state. These operations are safe
// for the local compilation mode, since they don't require accessing/resolving symbols
// outside of the current source file.
let explicitlyDeferredTypes: Map<string, {importPath: string, isDefaultImport: boolean}>|null =
null;
let explicitlyDeferredTypes: R3DeferPerComponentDependency[]|null = null;
if (metadata.isStandalone && rawDeferredImports !== null) {
const deferredTypes = this.collectExplicitlyDeferredSymbols(rawDeferredImports);
for (const [deferredType, importDetails] of deferredTypes) {
explicitlyDeferredTypes ??= new Map();
explicitlyDeferredTypes.set(importDetails.name, {
explicitlyDeferredTypes ??= [];
explicitlyDeferredTypes.push({
symbolName: importDetails.name,
importPath: importDetails.from,
isDefaultImport: isDefaultImport(importDetails.node),
});
@@ -520,8 +520,7 @@ export class ComponentDecoratorHandler implements
viewProviders: wrappedViewProviders,
i18nUseExternalIds: this.i18nUseExternalIds,
relativeContextFilePath,
rawImports: rawImports !== null ? new WrappedNodeExpr(rawImports) : undefined,
useTemplatePipeline: this.useTemplatePipeline,
rawImports: rawImports !== null ? new o.WrappedNodeExpr(rawImports) : undefined,
},
typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector),
classMetadata: this.includeClassMetadata ?
@@ -673,6 +672,12 @@ export class ComponentDecoratorHandler implements
return extendedTemplateChecker.getDiagnosticsForComponent(component);
}
templateSemanticsCheck(
component: ts.ClassDeclaration,
templateSemanticsChecker: TemplateSemanticsChecker): ts.Diagnostic[] {
return templateSemanticsChecker.getDiagnosticsForComponent(component);
}
resolve(
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
symbol: ComponentSymbol): ResolveResult<ComponentResolutionData> {
@@ -710,10 +715,10 @@ export class ComponentDecoratorHandler implements
declarationListEmitMode: (!analysis.meta.isStandalone || analysis.rawImports !== null) ?
DeclarationListEmitMode.RuntimeResolved :
DeclarationListEmitMode.Direct,
deferBlocks: this.locateDeferBlocksWithoutScope(analysis.template),
deferPerBlockDependencies: this.locateDeferBlocksWithoutScope(analysis.template),
deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerComponent,
deferrableDeclToImportDecl: new Map(),
deferrableTypes: new Map(),
deferPerComponentDependencies: analysis.explicitlyDeferredTypes ?? [],
};
if (this.localCompilationExtraImportsTracker === null) {
@@ -726,10 +731,10 @@ export class ComponentDecoratorHandler implements
data = {
declarations: EMPTY_ARRAY,
declarationListEmitMode: DeclarationListEmitMode.Direct,
deferBlocks: new Map(),
deferPerBlockDependencies: new Map(),
deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock,
deferrableDeclToImportDecl: new Map(),
deferrableTypes: new Map(),
deferPerComponentDependencies: [],
};
}
@@ -910,8 +915,7 @@ export class ComponentDecoratorHandler implements
// Process information related to defer blocks
if (this.compilationMode !== CompilationMode.LOCAL) {
this.resolveDeferBlocks(
node, deferBlocks, declarations, data, analysis, eagerlyUsed, bound);
this.resolveDeferBlocks(node, deferBlocks, declarations, data, analysis, eagerlyUsed);
}
const cyclesFromDirectives = new Map<UsedDirective, Cycle>();
@@ -1028,7 +1032,7 @@ export class ComponentDecoratorHandler implements
} else {
// If there is no scope, we can still use the binder to retrieve *some* information about the
// deferred blocks.
data.deferBlocks = this.locateDeferBlocksWithoutScope(metadata.template);
data.deferPerBlockDependencies = this.locateDeferBlocksWithoutScope(metadata.template);
}
// Run diagnostics only in global mode.
@@ -1048,7 +1052,7 @@ export class ComponentDecoratorHandler implements
}
if (analysis.providersRequiringFactory !== null &&
analysis.meta.providers instanceof WrappedNodeExpr) {
analysis.meta.providers instanceof o.WrappedNodeExpr) {
const providerDiagnostics = getProviderDiagnostics(
analysis.providersRequiringFactory, analysis.meta.providers!.node,
this.injectableRegistry);
@@ -1056,7 +1060,7 @@ export class ComponentDecoratorHandler implements
}
if (analysis.viewProvidersRequiringFactory !== null &&
analysis.meta.viewProviders instanceof WrappedNodeExpr) {
analysis.meta.viewProviders instanceof o.WrappedNodeExpr) {
const viewProviderDiagnostics = getProviderDiagnostics(
analysis.viewProvidersRequiringFactory, analysis.meta.viewProviders!.node,
this.injectableRegistry);
@@ -1141,12 +1145,10 @@ export class ComponentDecoratorHandler implements
const deferrableTypes = this.collectDeferredSymbols(resolution);
const useTemplatePipeline = this.useTemplatePipeline;
const meta: R3ComponentMetadata<R3TemplateDependency> = {
...analysis.meta,
...resolution,
deferrableTypes,
useTemplatePipeline,
defer: this.compileDeferBlocks(resolution),
};
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
@@ -1176,15 +1178,14 @@ export class ComponentDecoratorHandler implements
sourceUrl: analysis.template.declaration.resolvedTemplateUrl,
isInline: analysis.template.declaration.isInline,
inlineTemplateLiteralExpression: analysis.template.sourceMapping.type === 'direct' ?
new WrappedNodeExpr(analysis.template.sourceMapping.node) :
new o.WrappedNodeExpr(analysis.template.sourceMapping.node) :
null,
};
const useTemplatePipeline = this.useTemplatePipeline;
const meta: R3ComponentMetadata<R3TemplateDependencyMetadata> = {
...analysis.meta,
...resolution,
useTemplatePipeline
defer: this.compileDeferBlocks(resolution),
};
const fac = compileDeclareFactory(toFactoryMetadata(meta, FactoryTarget.Component));
const inputTransformFields = compileInputTransformFields(analysis.inputs);
@@ -1209,12 +1210,10 @@ export class ComponentDecoratorHandler implements
// doesn't have information on which dependencies belong to which defer blocks.
const deferrableTypes = analysis.explicitlyDeferredTypes;
const useTemplatePipeline = this.useTemplatePipeline;
const meta = {
...analysis.meta,
...resolution,
deferrableTypes: deferrableTypes ?? new Map(),
useTemplatePipeline,
defer: this.compileDeferBlocks(resolution),
} as R3ComponentMetadata<R3TemplateDependency>;
if (analysis.explicitlyDeferredTypes !== null) {
@@ -1240,18 +1239,15 @@ export class ComponentDecoratorHandler implements
* For example, this happens in the local compilation mode.
*/
private locateDeferBlocksWithoutScope(template: ComponentTemplate):
Map<TmplAstDeferredBlock, R3DeferBlockMetadata> {
const deferBlocks = new Map<TmplAstDeferredBlock, R3DeferBlockMetadata>();
Map<TmplAstDeferredBlock, DeferredComponentDependency[]> {
const deferBlocks = new Map<TmplAstDeferredBlock, DeferredComponentDependency[]>();
const directivelessBinder = new R3TargetBinder<DirectiveMeta>(new SelectorMatcher());
const bound = directivelessBinder.bind({template: template.nodes});
const deferredBlocks = bound.getDeferBlocks();
const triggerElements = new Map<TmplAstDeferredTrigger, TmplAstElement|null>();
for (const block of deferredBlocks) {
this.resolveDeferTriggers(block, block.triggers, bound, triggerElements);
this.resolveDeferTriggers(block, block.prefetchTriggers, bound, triggerElements);
// We can't determine the dependencies without a scope so we leave them empty.
deferBlocks.set(block, {deps: [], triggerElements});
deferBlocks.set(block, []);
}
return deferBlocks;
}
@@ -1261,26 +1257,19 @@ export class ComponentDecoratorHandler implements
* the `@Component.imports` field and their usage in `@defer` blocks.
*/
private collectDeferredSymbols(resolution: Readonly<ComponentResolutionData>) {
const deferrableTypes = new Map<string, {importPath: string, isDefaultImport: boolean}>();
const deferrableTypes: R3DeferPerComponentDependency[] = [];
// Go over all dependencies of all defer blocks and update the value of
// the `isDeferrable` flag and the `importPath` to reflect the current
// state after visiting all components during the `resolve` phase.
for (const [_, metadata] of resolution.deferBlocks) {
for (const deferBlockDep of metadata.deps) {
const dep = deferBlockDep as unknown as {
classDeclaration: ts.ClassDeclaration;
};
const classDecl = dep.classDeclaration as unknown as Expression;
const importDecl = (resolution.deferrableDeclToImportDecl.get(classDecl) ??
null) as (ts.ImportDeclaration | null);
for (const [_, deps] of resolution.deferPerBlockDependencies) {
for (const deferBlockDep of deps) {
const importDecl =
resolution.deferrableDeclToImportDecl.get(deferBlockDep.declaration.node) ?? null;
if (importDecl !== null && this.deferredSymbolTracker.canDefer(importDecl)) {
deferBlockDep.isDeferrable = true;
deferBlockDep.importPath = (importDecl.moduleSpecifier as ts.StringLiteral).text;
deferBlockDep.isDefaultImport = isDefaultImport(importDecl);
deferrableTypes.set(deferBlockDep.symbolName, {
importPath: deferBlockDep.importPath,
isDefaultImport: deferBlockDep.isDefaultImport,
});
deferrableTypes.push(deferBlockDep as R3DeferPerComponentDependency);
}
}
}
@@ -1320,7 +1309,7 @@ export class ComponentDecoratorHandler implements
* @returns a `Cycle` object if a cycle would be created, otherwise `null`.
*/
private _checkForCyclicImport(
importedFile: ImportedFile, expr: Expression, origin: ts.SourceFile): Cycle|null {
importedFile: ImportedFile, expr: o.Expression, origin: ts.SourceFile): Cycle|null {
const imported = resolveImportedFile(this.moduleResolver, importedFile, expr, origin);
if (imported === null) {
return null;
@@ -1330,7 +1319,7 @@ export class ComponentDecoratorHandler implements
}
private maybeRecordSyntheticImport(
importedFile: ImportedFile, expr: Expression, origin: ts.SourceFile): void {
importedFile: ImportedFile, expr: o.Expression, origin: ts.SourceFile): void {
const imported = resolveImportedFile(this.moduleResolver, importedFile, expr, origin);
if (imported === null) {
return;
@@ -1350,7 +1339,6 @@ export class ComponentDecoratorHandler implements
resolutionData: ComponentResolutionData,
analysisData: Readonly<ComponentAnalysisData>,
eagerlyUsedDecls: Set<ClassDeclaration>,
componentBoundTarget: BoundTarget<DirectiveMeta>,
) {
// Collect all deferred decls from all defer blocks from the entire template
// to intersect with the information from the `imports` field of a particular
@@ -1360,9 +1348,14 @@ export class ComponentDecoratorHandler implements
for (const [deferBlock, bound] of deferBlocks) {
const usedDirectives = new Set(bound.getEagerlyUsedDirectives().map(d => d.ref.node));
const usedPipes = new Set(bound.getEagerlyUsedPipes());
const deps: Array<R3DeferBlockTemplateDependency&{classDeclaration: ts.ClassDeclaration}> =
[];
const triggerElements = new Map<TmplAstDeferredTrigger, TmplAstElement|null>();
let deps: DeferredComponentDependency[];
if (resolutionData.deferPerBlockDependencies.has(deferBlock)) {
deps = resolutionData.deferPerBlockDependencies.get(deferBlock)!;
} else {
deps = [];
resolutionData.deferPerBlockDependencies.set(deferBlock, deps);
}
for (const decl of Array.from(deferrableDecls.values())) {
if (decl.kind === R3TemplateDependencyKind.NgModule) {
@@ -1379,23 +1372,15 @@ export class ComponentDecoratorHandler implements
// `isDeferrable`, `importPath` and `isDefaultImport` will be
// added later during the `compile` step.
deps.push({
type: decl.type as WrappedNodeExpr<ts.Identifier>,
symbolName: decl.ref.node.name.escapedText as string,
typeReference: decl.type,
symbolName: decl.ref.node.name.text,
isDeferrable: false,
importPath: null,
isDefaultImport: false,
// Extra info to match corresponding import during the `compile` phase.
classDeclaration: decl.ref.node as ts.ClassDeclaration,
declaration: decl.ref,
});
allDeferredDecls.add(decl.ref.node);
}
this.resolveDeferTriggers(
deferBlock, deferBlock.triggers, componentBoundTarget, triggerElements);
this.resolveDeferTriggers(
deferBlock, deferBlock.prefetchTriggers, componentBoundTarget, triggerElements);
resolutionData.deferBlocks.set(deferBlock, {deps, triggerElements});
}
// For standalone components with the `imports` and `deferredImports` fields -
@@ -1483,23 +1468,51 @@ export class ComponentDecoratorHandler implements
// Keep track of how this class made it into the current source file
// (which ts.ImportDeclaration was used for this symbol).
resolutionData.deferrableDeclToImportDecl.set(
decl.node as unknown as Expression, imp.node as unknown as Expression);
resolutionData.deferrableDeclToImportDecl.set(decl.node, imp.node);
this.deferredSymbolTracker.markAsDeferrableCandidate(
node, imp.node, componentClassDecl, isDeferredImport);
}
}
/** Resolves the triggers of the defer block to the elements that they're pointing to. */
private resolveDeferTriggers(
block: TmplAstDeferredBlock, triggers: TmplAstDeferredBlockTriggers,
componentBoundTarget: BoundTarget<DirectiveMeta>,
triggerElements: Map<TmplAstDeferredTrigger, TmplAstElement|null>): void {
Object.keys(triggers).forEach(key => {
const trigger = triggers[key as keyof TmplAstDeferredBlockTriggers]!;
triggerElements.set(trigger, componentBoundTarget.getDeferredTriggerTarget(block, trigger));
});
private compileDeferBlocks(resolution: Readonly<Partial<ComponentResolutionData>>):
R3ComponentDeferMetadata {
const {
deferBlockDepsEmitMode: mode,
deferPerBlockDependencies: perBlockDeps,
deferPerComponentDependencies: perComponentDeps
} = resolution;
if (mode === DeferBlockDepsEmitMode.PerBlock) {
if (!perBlockDeps) {
throw new Error(
'Internal error: deferPerBlockDependencies must be present when compiling in PerBlock mode');
}
const blocks = new Map<TmplAstDeferredBlock, o.ArrowFunctionExpr|null>();
for (const [block, dependencies] of perBlockDeps) {
blocks.set(
block,
dependencies.length === 0 ? null : compileDeferResolverFunction({mode, dependencies}));
}
return {mode, blocks};
}
if (mode === DeferBlockDepsEmitMode.PerComponent) {
if (!perComponentDeps) {
throw new Error(
'Internal error: deferPerComponentDependencies must be present in PerComponent mode');
}
return {
mode: DeferBlockDepsEmitMode.PerComponent,
dependenciesFn: perComponentDeps.length === 0 ?
null :
compileDeferResolverFunction({mode, dependencies: perComponentDeps})
};
}
throw new Error(`Invalid deferBlockDepsEmitMode. Cannot compile deferred block metadata.`);
}
}
@@ -1541,13 +1554,12 @@ function extractPipes(dependencies: Array<PipeMeta|DirectiveMeta|NgModuleMeta>):
* in the `setClassMetadataAsync` call. Otherwise, an import declaration gets retained.
*/
function removeDeferrableTypesFromComponentDecorator(
analysis: Readonly<ComponentAnalysisData>,
deferrableTypes: Map<string, {importPath: string, isDefaultImport: boolean}>) {
analysis: Readonly<ComponentAnalysisData>, deferrableTypes: R3DeferPerComponentDependency[]) {
if (analysis.classMetadata) {
const deferrableSymbols = new Set(deferrableTypes.keys());
const deferrableSymbols = new Set(deferrableTypes.map(t => t.symbolName));
const rewrittenDecoratorsNode = removeIdentifierReferences(
(analysis.classMetadata.decorators as WrappedNodeExpr<ts.Node>).node, deferrableSymbols);
analysis.classMetadata.decorators = new WrappedNodeExpr(rewrittenDecoratorsNode);
(analysis.classMetadata.decorators as o.WrappedNodeExpr<ts.Node>).node, deferrableSymbols);
analysis.classMetadata.decorators = new o.WrappedNodeExpr(rewrittenDecoratorsNode);
}
}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationTriggerNames, R3ClassDebugInfo, R3ClassMetadata, R3ComponentMetadata, R3TemplateDependencyMetadata, SchemaMetadata} from '@angular/compiler';
import {AnimationTriggerNames, DeclarationListEmitMode, DeferBlockDepsEmitMode, R3ClassDebugInfo, R3ClassMetadata, R3ComponentMetadata, R3DeferPerBlockDependency, R3DeferPerComponentDependency, R3TemplateDependencyMetadata, SchemaMetadata, TmplAstDeferredBlock} from '@angular/compiler';
import ts from 'typescript';
import {Reference} from '../../../imports';
@@ -24,8 +24,7 @@ import {ParsedTemplateWithSource, StyleUrlMeta} from './resources';
*/
export type ComponentMetadataResolvedFields = SubsetOfKeys<
R3ComponentMetadata<R3TemplateDependencyMetadata>,
'declarations'|'declarationListEmitMode'|'deferBlocks'|'deferrableDeclToImportDecl'|
'deferrableTypes'|'deferBlockDepsEmitMode'>;
'declarations'|'declarationListEmitMode'|'defer'>;
export interface ComponentAnalysisData {
/**
@@ -77,7 +76,7 @@ export interface ComponentAnalysisData {
/**
* Map of symbol name -> import path for types from `@Component.deferredImports` field.
*/
explicitlyDeferredTypes: Map<string, {importPath: string, isDefaultImport: boolean}>|null;
explicitlyDeferredTypes: R3DeferPerComponentDependency[]|null;
schemas: SchemaMetadata[]|null;
@@ -90,5 +89,41 @@ export interface ComponentAnalysisData {
rawHostDirectives: ts.Expression|null;
}
export type ComponentResolutionData =
Pick<R3ComponentMetadata<R3TemplateDependencyMetadata>, ComponentMetadataResolvedFields>;
export interface ComponentResolutionData {
declarations: R3TemplateDependencyMetadata[];
declarationListEmitMode: DeclarationListEmitMode;
/**
* Map of all types that can be defer loaded (ts.ClassDeclaration) ->
* corresponding import declaration (ts.ImportDeclaration) within
* the current source file.
*/
deferrableDeclToImportDecl: Map<ClassDeclaration, ts.ImportDeclaration>;
/**
* Map of `@defer` blocks -> their corresponding dependencies.
* Required to compile the defer resolver function in `PerBlock` mode.
*/
deferPerBlockDependencies: Map<TmplAstDeferredBlock, DeferredComponentDependency[]>;
/**
* Defines how dynamic imports for deferred dependencies should be grouped:
* - either in a function on per-component basis (in case of local compilation)
* - or in a function on per-block basis (in full compilation mode)
*/
deferBlockDepsEmitMode: DeferBlockDepsEmitMode;
/**
* List of deferrable dependencies in the entire component. Used to compile the
* defer resolver function in `PerComponent` mode.
*/
deferPerComponentDependencies: R3DeferPerComponentDependency[];
}
/**
* Describes a dependency used within a `@defer` block.
*/
export type DeferredComponentDependency = R3DeferPerBlockDependency&{
/** Reference to the declaration that defines the dependency. */
declaration: Reference<ClassDeclaration>;
};
@@ -106,7 +106,6 @@ function setup(
new DeferredSymbolTracker(checker, /* onlyExplicitDeferDependencyImports */ false),
/* forbidOrphanRenderering */ false,
/* enableBlockSyntax */ true,
/* useTemplatePipeline */ true,
/* localCompilationExtraImportsTracker */ null,
);
return {reflectionHost, handler, resourceLoader, metaRegistry};
@@ -65,7 +65,6 @@ export class DirectiveDecoratorHandler implements
private importTracker: ImportedSymbolsTracker,
private includeClassMetadata: boolean,
private readonly compilationMode: CompilationMode,
private readonly useTemplatePipeline: boolean,
private readonly generateExtraImportsInLocalMode: boolean,
) {}
@@ -107,7 +106,7 @@ export class DirectiveDecoratorHandler implements
const directiveResult = extractDirectiveMetadata(
node, decorator, this.reflector, this.importTracker, this.evaluator, this.refEmitter,
this.referencesRegistry, this.isCore, this.annotateForClosureCompiler, this.compilationMode,
/* defaultSelector */ null, this.useTemplatePipeline);
/* defaultSelector */ null);
if (directiveResult === undefined) {
return {};
}
@@ -25,8 +25,8 @@ import {ClassMember, ReflectionHost} from '../../../reflection';
export interface InitializerApiFunction {
owningModule: '@angular/core'|'@angular/core/rxjs-interop';
functionName: ('input'|'model'|'ɵoutput'|'output'|'outputFromObservable'|'viewChild'|
'viewChildren'|'contentChild'|'contentChildren');
functionName: ('input'|'model'|'output'|'outputFromObservable'|'viewChild'|'viewChildren'|
'contentChild'|'contentChildren');
}
/**
@@ -41,7 +41,7 @@ export function extractDirectiveMetadata(
importTracker: ImportedSymbolsTracker, evaluator: PartialEvaluator,
refEmitter: ReferenceEmitter, referencesRegistry: ReferencesRegistry, isCore: boolean,
annotateForClosureCompiler: boolean, compilationMode: CompilationMode,
defaultSelector: string|null, useTemplatePipeline: boolean): {
defaultSelector: string|null): {
decorator: Map<string, ts.Expression>,
metadata: R3DirectiveMetadata,
inputs: ClassPropertyMapping<InputMapping>,
@@ -243,7 +243,6 @@ export function extractDirectiveMetadata(
deps: ctorDeps,
host: {
...host,
useTemplatePipeline,
},
lifecycle: {
usesOnChanges,
@@ -183,7 +183,7 @@ runInEachFileSystem(() => {
/*semanticDepGraphUpdater*/ null,
/*annotateForClosureCompiler*/ false, NOOP_PERF_RECORDER, importTracker,
/*includeClassMetadata*/ true,
/*compilationMode */ CompilationMode.FULL, /* useTemplatePipeline */ true,
/*compilationMode */ CompilationMode.FULL,
/*generateExtraImportsInLocalMode*/ false);
const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration);
@@ -39,6 +39,8 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck/extended",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics",
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics/api",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/src/ngtsc/xi18n",
"@npm//@types/semver",
@@ -438,9 +438,4 @@ export interface MiscOptions {
* another library without option set will not issue error if rendered in orphan way.
*/
forbidOrphanComponents?: boolean;
/**
* Whether to use TemplateDefinitionBuilder as the code generator, or Template Pipeline.
*/
useTemplatePipeline?: boolean;
}
@@ -33,14 +33,14 @@ import {TemplateTypeCheckerImpl} from '../../typecheck';
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig} from '../../typecheck/api';
import {ALL_DIAGNOSTIC_FACTORIES, ExtendedTemplateCheckerImpl, SUPPORTED_DIAGNOSTIC_NAMES} from '../../typecheck/extended';
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
import {TemplateSemanticsChecker} from '../../typecheck/template_semantics/api/api';
import {TemplateSemanticsCheckerImpl} from '../../typecheck/template_semantics/src/template_semantics_checker';
import {getSourceFileOrNull, isDtsPath, toUnredirectedSourceFile} from '../../util/src/typescript';
import {Xi18nContext} from '../../xi18n';
import {DiagnosticCategoryLabel, NgCompilerAdapter, NgCompilerOptions} from '../api';
import {coreVersionSupportsFeature} from './feature_detection';
const SHOULD_USE_TEMPLATE_PIPELINE = true;
/**
* State information about a compilation which is only generated once some data is requested from
* the `NgCompiler` (for example, by calling `getDiagnostics`).
@@ -59,6 +59,7 @@ interface LazyCompilationState {
templateTypeChecker: TemplateTypeChecker;
resourceRegistry: ResourceRegistry;
extendedTemplateChecker: ExtendedTemplateChecker|null;
templateSemanticsChecker: TemplateSemanticsChecker|null;
/**
* Only available in local compilation mode when option `generateExtraImportsInLocalMode` is set.
@@ -442,11 +443,7 @@ export class NgCompiler {
// by running the extended template checking code, which will attempt to
// generate the same TCB.
try {
diagnostics.push(...this.getTemplateDiagnostics());
if (this.options.strictTemplates) {
diagnostics.push(...this.getExtendedTemplateDiagnostics());
}
diagnostics.push(...this.getTemplateDiagnostics(), ...this.runAdditionalChecks());
} catch (err: unknown) {
if (!isFatalDiagnosticError(err)) {
throw err;
@@ -472,11 +469,9 @@ export class NgCompiler {
// by running the extended template checking code, which will attempt to
// generate the same TCB.
try {
diagnostics.push(...this.getTemplateDiagnosticsForFile(file, optimizeFor));
if (this.options.strictTemplates) {
diagnostics.push(...this.getExtendedTemplateDiagnostics(file));
}
diagnostics.push(
...this.getTemplateDiagnosticsForFile(file, optimizeFor),
...this.runAdditionalChecks(file));
} catch (err: unknown) {
if (!isFatalDiagnosticError(err)) {
throw err;
@@ -503,8 +498,12 @@ export class NgCompiler {
try {
diagnostics.push(...ttc.getDiagnosticsForComponent(component));
const extendedTemplateChecker = compilation.extendedTemplateChecker;
if (this.options.strictTemplates && extendedTemplateChecker) {
const {extendedTemplateChecker, templateSemanticsChecker} = compilation;
if (templateSemanticsChecker !== null) {
diagnostics.push(...templateSemanticsChecker.getDiagnosticsForComponent(component));
}
if (this.options.strictTemplates && extendedTemplateChecker !== null) {
diagnostics.push(...extendedTemplateChecker.getDiagnosticsForComponent(component));
}
} catch (err: unknown) {
@@ -968,26 +967,23 @@ export class NgCompiler {
return this.nonTemplateDiagnostics;
}
/**
* Calls the `extendedTemplateCheck` phase of the trait compiler
* @param sf optional parameter to get diagnostics for a certain file
* or all files in the program if `sf` is undefined
* @returns generated extended template diagnostics
*/
private getExtendedTemplateDiagnostics(sf?: ts.SourceFile): ts.Diagnostic[] {
private runAdditionalChecks(sf?: ts.SourceFile): ts.Diagnostic[] {
const diagnostics: ts.Diagnostic[] = [];
const compilation = this.ensureAnalyzed();
const extendedTemplateChecker = compilation.extendedTemplateChecker;
if (!extendedTemplateChecker) {
return [];
}
const {extendedTemplateChecker, templateSemanticsChecker} = compilation;
const files = sf ? [sf] : this.inputProgram.getSourceFiles();
if (sf !== undefined) {
return compilation.traitCompiler.extendedTemplateCheck(sf, extendedTemplateChecker);
}
for (const sf of this.inputProgram.getSourceFiles()) {
diagnostics.push(
...compilation.traitCompiler.extendedTemplateCheck(sf, extendedTemplateChecker));
for (const sf of files) {
if (templateSemanticsChecker !== null) {
diagnostics.push(...compilation.traitCompiler.runAdditionalChecks(sf, (clazz, handler) => {
return handler.templateSemanticsCheck?.(clazz, templateSemanticsChecker) || null;
}));
}
if (this.options.strictTemplates && extendedTemplateChecker !== null) {
diagnostics.push(...compilation.traitCompiler.runAdditionalChecks(sf, (clazz, handler) => {
return handler.extendedTemplateCheck?.(clazz, extendedTemplateChecker) || null;
}));
}
}
return diagnostics;
@@ -1175,7 +1171,6 @@ export class NgCompiler {
this.closureCompilerEnabled, this.delegatingPerfRecorder, hostDirectivesResolver,
importTracker, supportTestBed, compilationMode, deferredSymbolsTracker,
!!this.options.forbidOrphanComponents, this.enableBlockSyntax,
this.options.useTemplatePipeline ?? SHOULD_USE_TEMPLATE_PIPELINE,
localCompilationExtraImportsTracker),
// TODO(alxhub): understand why the cast here is necessary (something to do with `null`
@@ -1188,7 +1183,6 @@ export class NgCompiler {
this.delegatingPerfRecorder,
importTracker,
supportTestBed, compilationMode,
this.options.useTemplatePipeline ?? SHOULD_USE_TEMPLATE_PIPELINE,
!!this.options.generateExtraImportsInLocalMode,
) as Readonly<DecoratorHandler<unknown, unknown, SemanticSymbol | null,unknown>>,
// clang-format on
@@ -1233,6 +1227,10 @@ export class NgCompiler {
templateTypeChecker, checker, ALL_DIAGNOSTIC_FACTORIES, this.options) :
null;
const templateSemanticsChecker = this.constructionDiagnostics.length === 0 ?
new TemplateSemanticsCheckerImpl(templateTypeChecker) :
null;
return {
isCore,
traitCompiler,
@@ -1248,6 +1246,7 @@ export class NgCompiler {
resourceRegistry,
extendedTemplateChecker,
localCompilationExtraImportsTracker,
templateSemanticsChecker,
};
}
}
@@ -21,6 +21,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics/api",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/src/ngtsc/xi18n",
"@npm//typescript",
@@ -16,6 +16,7 @@ import {ClassDeclaration, Decorator, ReflectionHost} from '../../reflection';
import {ImportManager} from '../../translator';
import {TypeCheckContext} from '../../typecheck/api';
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
import {TemplateSemanticsChecker} from '../../typecheck/template_semantics/api/api';
import {Xi18nContext} from '../../xi18n';
/**
@@ -175,6 +176,10 @@ export interface DecoratorHandler<D, A, S extends SemanticSymbol|null, R> {
(component: ts.ClassDeclaration, extendedTemplateChecker: ExtendedTemplateChecker):
ts.Diagnostic[];
templateSemanticsCheck?
(component: ts.ClassDeclaration, templateSemanticsChecker: TemplateSemanticsChecker):
ts.Diagnostic[];
/**
* Generate a description of the field which should be added to the class, including any
* initialization code to be generated.
@@ -17,7 +17,6 @@ import {IndexingContext} from '../../indexer';
import {PerfEvent, PerfRecorder} from '../../perf';
import {ClassDeclaration, DeclarationNode, Decorator, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
import {ProgramTypeCheckAdapter, TypeCheckContext} from '../../typecheck/api';
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
import {getSourceFile} from '../../util/src/typescript';
import {Xi18nContext} from '../../xi18n';
@@ -522,8 +521,12 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
}
}
extendedTemplateCheck(sf: ts.SourceFile, extendedTemplateChecker: ExtendedTemplateChecker):
ts.Diagnostic[] {
runAdditionalChecks(
sf: ts.SourceFile,
check:
(clazz: ts.ClassDeclaration,
handler: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>) =>
ts.Diagnostic[] | null): ts.Diagnostic[] {
if (this.compilationMode === CompilationMode.LOCAL) {
return [];
}
@@ -539,10 +542,10 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
}
const record = this.classes.get(clazz)!;
for (const trait of record.traits) {
if (trait.handler.extendedTemplateCheck === undefined) {
continue;
const result = check(clazz, trait.handler);
if (result !== null) {
diagnostics.push(...result);
}
diagnostics.push(...trait.handler.extendedTemplateCheck(clazz, extendedTemplateChecker));
}
}
return diagnostics;
@@ -15,7 +15,6 @@ import {NOOP_PERF_RECORDER} from '../../perf';
import {ClassDeclaration, Decorator, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing';
import {CompilationMode, DetectResult, DtsTransformRegistry, TraitCompiler} from '../../transform';
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
import {AnalysisOutput, CompileResult, DecoratorHandler, HandlerPrecedence, ResolveResult} from '../src/api';
const fakeSfTypeIdentifier = {
@@ -309,10 +308,6 @@ runInEachFileSystem(() => {
register(): void {}
extendedTemplateCheck() {
return [];
}
updateResources() {}
symbol(): null {
@@ -368,21 +363,6 @@ runInEachFileSystem(() => {
expect(handler.register).toHaveBeenCalled();
});
it('should not call extendedTemplateCheck', () => {
const contents = `
export class Test {}
`;
const handler = new TestDecoratorHandler();
spyOn(handler, 'extendedTemplateCheck');
const {compiler, sourceFile} = setup(contents, [handler], CompilationMode.LOCAL);
compiler.analyzeSync(sourceFile);
compiler.resolve();
compiler.extendedTemplateCheck(sourceFile, {} as ExtendedTemplateChecker);
expect(handler.extendedTemplateCheck).not.toHaveBeenCalled();
});
it('should not call updateResources', () => {
const contents = `
export class Test {}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AST, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler';
import {AST, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import ts from 'typescript';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
@@ -213,6 +213,13 @@ export interface TemplateTypeChecker {
*/
invalidateClass(clazz: ts.ClassDeclaration): void;
/**
* Gets the target of a template expression, if possible.
* See `BoundTarget.getExpressionTarget` for more information.
*/
getExpressionTarget(expression: AST, clazz: ts.ClassDeclaration): TmplAstReference|TmplAstVariable
|null;
/**
* Constructs a `ts.Diagnostic` for a given `ParseSourceSpan` within a template.
*/
@@ -10,6 +10,7 @@ ts_library(
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"@npm//typescript",
@@ -11,16 +11,9 @@ import ts from 'typescript';
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic, SymbolKind} from '../../../api';
import {isSignalReference} from '../../../src/symbol_util';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
/** Names of known signal functions. */
const SIGNAL_FNS = new Set([
'WritableSignal',
'Signal',
'InputSignal',
'InputSignalWithTransform',
'ModelSignal',
]);
/** Names of known signal instance properties. */
const SIGNAL_INSTANCE_PROPERTIES = new Set(['set', 'update', 'asReadonly']);
@@ -50,18 +43,6 @@ class InterpolatedSignalCheck extends
}
}
function isSignal(symbol: ts.Symbol|undefined): boolean {
const declarations = symbol?.getDeclarations();
return declarations !== undefined && declarations.some(decl => {
const fileName = decl.getSourceFile().fileName;
return (ts.isInterfaceDeclaration(decl) || ts.isTypeAliasDeclaration(decl)) &&
SIGNAL_FNS.has(decl.name.text) &&
(fileName.includes('@angular/core') || fileName.includes('angular2/rc/packages/core'));
});
}
function isFunctionInstanceProperty(name: string): boolean {
return FUNCTION_INSTANCE_PROPERTIES.has(name);
}
@@ -76,8 +57,7 @@ function buildDiagnosticForSignal(
Array<NgTemplateDiagnostic<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>> {
// check for `{{ mySignal }}`
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
if (symbol?.kind === SymbolKind.Expression &&
(isSignal(symbol.tsType.symbol) || isSignal(symbol.tsType.aliasSymbol))) {
if (symbol !== null && symbol.kind === SymbolKind.Expression && isSignalReference(symbol)) {
const templateMapping =
ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation)!;
const errorString = `${node.name} is a function and should be invoked: ${node.name}()`;
@@ -92,8 +72,8 @@ function buildDiagnosticForSignal(
// `{{ mySignal.asReadonly }}` as these are the names of instance properties of Signal
const symbolOfReceiver = ctx.templateTypeChecker.getSymbolOfNode(node.receiver, component);
if ((isFunctionInstanceProperty(node.name) || isSignalInstanceProperty(node.name)) &&
symbolOfReceiver?.kind === SymbolKind.Expression &&
(isSignal(symbolOfReceiver.tsType.symbol) || isSignal(symbolOfReceiver.tsType.aliasSymbol))) {
symbolOfReceiver !== null && symbolOfReceiver.kind === SymbolKind.Expression &&
isSignalReference(symbolOfReceiver)) {
const templateMapping =
ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbolOfReceiver.tcbLocation)!;
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AST, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstTextAttribute, WrappedNodeExpr} from '@angular/compiler';
import {AST, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, WrappedNodeExpr} from '@angular/compiler';
import ts from 'typescript';
import {ErrorCode, ngErrorCode} from '../../diagnostics';
@@ -345,6 +345,12 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
this.isComplete = false;
}
getExpressionTarget(expression: AST, clazz: ts.ClassDeclaration): TmplAstReference|TmplAstVariable
|null {
return this.getLatestComponentState(clazz).data?.boundTarget.getExpressionTarget(expression) ||
null;
}
makeTemplateDiagnostic<T extends ErrorCode>(
clazz: ts.ClassDeclaration, sourceSpan: ParseSourceSpan, category: ts.DiagnosticCategory,
errorCode: T, message: string, relatedInformation?: {
@@ -70,9 +70,6 @@ export interface OutOfBandDiagnosticRecorder {
*/
deferredComponentUsedEagerly(templateId: TemplateId, element: TmplAstElement): void;
illegalAssignmentToTemplateVar(
templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void;
/**
* Reports a duplicate declaration of a template variable.
*
@@ -217,27 +214,6 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY), errorMsg));
}
illegalAssignmentToTemplateVar(
templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void {
const mapping = this.resolver.getSourceMapping(templateId);
const errorMsg = `Cannot use variable '${
assignment
.name}' as the left-hand side of an assignment expression. Template variables are read-only.`;
const sourceSpan = this.resolver.toParseSourceSpan(templateId, assignment.sourceSpan);
if (sourceSpan === null) {
throw new Error(`Assertion failure: no SourceLocation found for property binding.`);
}
this._diagnostics.push(makeTemplateDiagnostic(
templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.WRITE_TO_READ_ONLY_VARIABLE), errorMsg, [{
text: `The variable ${assignment.name} is declared here.`,
start: target.valueSpan?.start.offset || target.sourceSpan.start.offset,
end: target.valueSpan?.end.offset || target.sourceSpan.end.offset,
sourceFile: mapping.node.getSourceFile(),
}]));
}
duplicateTemplateVar(
templateId: TemplateId, variable: TmplAstVariable, firstDecl: TmplAstVariable): void {
const mapping = this.resolver.getSourceMapping(templateId);
@@ -0,0 +1,42 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import ts from 'typescript';
import {Symbol, SymbolKind} from '../api';
/** Names of known signal functions. */
const SIGNAL_FNS = new Set([
'WritableSignal',
'Signal',
'InputSignal',
'InputSignalWithTransform',
'ModelSignal',
]);
/** Returns whether a symbol is a reference to a signal. */
export function isSignalReference(symbol: Symbol): boolean {
return (symbol.kind === SymbolKind.Expression || symbol.kind === SymbolKind.Variable) &&
// Note that `tsType.symbol` isn't optional in the typings,
// but it appears that it can be undefined at runtime.
(symbol.tsType.symbol !== undefined && isSignalSymbol(symbol.tsType.symbol) ||
(symbol.tsType.aliasSymbol !== undefined && isSignalSymbol(symbol.tsType.aliasSymbol)));
}
/** Checks whether a symbol points to a signal. */
function isSignalSymbol(symbol: ts.Symbol): boolean {
const declarations = symbol.getDeclarations();
return declarations !== undefined && declarations.some(decl => {
const fileName = decl.getSourceFile().fileName;
return (ts.isInterfaceDeclaration(decl) || ts.isTypeAliasDeclaration(decl)) &&
SIGNAL_FNS.has(decl.name.text) &&
(fileName.includes('@angular/core') || fileName.includes('angular2/rc/packages/core'));
});
}
@@ -1,44 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AST, BoundTarget, ImplicitReceiver, PropertyWrite, RecursiveAstVisitor, TmplAstVariable} from '@angular/compiler';
import {TemplateId} from '../api';
import {OutOfBandDiagnosticRecorder} from './oob';
/**
* Visits a template and records any semantic errors within its expressions.
*/
export class ExpressionSemanticVisitor extends RecursiveAstVisitor {
constructor(
private templateId: TemplateId, private boundTarget: BoundTarget<any>,
private oob: OutOfBandDiagnosticRecorder) {
super();
}
override visitPropertyWrite(ast: PropertyWrite, context: any): void {
super.visitPropertyWrite(ast, context);
if (!(ast.receiver instanceof ImplicitReceiver)) {
return;
}
const target = this.boundTarget.getExpressionTarget(ast);
if (target instanceof TmplAstVariable) {
// Template variables are read-only.
this.oob.illegalAssignmentToTemplateVar(this.templateId, ast, target);
}
}
static visit(
ast: AST, id: TemplateId, boundTarget: BoundTarget<any>,
oob: OutOfBandDiagnosticRecorder): void {
ast.visit(new ExpressionSemanticVisitor(id, boundTarget, oob));
}
}
@@ -20,7 +20,6 @@ import {DomSchemaChecker} from './dom';
import {Environment} from './environment';
import {astToTypescript, NULL_AS_ANY} from './expression';
import {OutOfBandDiagnosticRecorder} from './oob';
import {ExpressionSemanticVisitor} from './template_semantics';
import {tsCallMethod, tsCastToAny, tsCreateElement, tsCreateTypeQueryForCoercedInput, tsCreateVariable, tsDeclareVariable} from './ts_util';
import {requiresInlineTypeCtor} from './type_constructor';
import {TypeParameterEmitter} from './type_parameter_emitter';
@@ -1214,9 +1213,6 @@ export class TcbDirectiveOutputsOp extends TcbOp {
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
}
ExpressionSemanticVisitor.visit(
output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
}
return null;
@@ -1292,9 +1288,6 @@ class TcbUnclaimedOutputsOp extends TcbOp {
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
}
ExpressionSemanticVisitor.visit(
output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
}
return null;
@@ -0,0 +1,18 @@
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "template_semantics",
srcs = glob(
["**/*.ts"],
),
visibility = ["//packages/compiler-cli/src/ngtsc:__subpackages__"],
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics/api",
"@npm//typescript",
],
)
@@ -0,0 +1,16 @@
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "api",
srcs = glob(
["**/*.ts"],
),
visibility = ["//packages/compiler-cli/src/ngtsc:__subpackages__"],
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"@npm//typescript",
],
)
@@ -0,0 +1,21 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import ts from 'typescript';
import {TemplateDiagnostic} from '../../api';
/**
* Interface to generate diagnostics related to the semantics of a component's template.
*/
export interface TemplateSemanticsChecker {
/**
* Run `TemplateSemanticsChecker`s for a component and return the generated `ts.Diagnostic`s.
*/
getDiagnosticsForComponent(component: ts.ClassDeclaration): TemplateDiagnostic[];
}
@@ -0,0 +1,121 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AST, ASTWithSource, ImplicitReceiver, ParsedEventType, PropertyRead, PropertyWrite, RecursiveAstVisitor, TmplAstBoundEvent, TmplAstNode, TmplAstRecursiveVisitor, TmplAstVariable} from '@angular/compiler';
import ts from 'typescript';
import {ErrorCode, ngErrorCode} from '../../../diagnostics';
import {TemplateDiagnostic, TemplateTypeChecker} from '../../api';
import {isSignalReference} from '../../src/symbol_util';
import {TemplateSemanticsChecker} from '../api/api';
export class TemplateSemanticsCheckerImpl implements TemplateSemanticsChecker {
constructor(private templateTypeChecker: TemplateTypeChecker) {}
getDiagnosticsForComponent(component: ts.ClassDeclaration): TemplateDiagnostic[] {
const template = this.templateTypeChecker.getTemplate(component);
return template !== null ?
TemplateSemanticsVisitor.visit(template, component, this.templateTypeChecker) :
[];
}
}
/** Visitor that verifies the semantics of a template. */
class TemplateSemanticsVisitor extends TmplAstRecursiveVisitor {
private constructor(private expressionVisitor: ExpressionsSemanticsVisitor) {
super();
}
static visit(
nodes: TmplAstNode[], component: ts.ClassDeclaration,
templateTypeChecker: TemplateTypeChecker) {
const diagnostics: TemplateDiagnostic[] = [];
const expressionVisitor =
new ExpressionsSemanticsVisitor(templateTypeChecker, component, diagnostics);
const templateVisitor = new TemplateSemanticsVisitor(expressionVisitor);
nodes.forEach(node => node.visit(templateVisitor));
return diagnostics;
}
override visitBoundEvent(event: TmplAstBoundEvent): void {
super.visitBoundEvent(event);
event.handler.visit(this.expressionVisitor, event);
}
}
/** Visitor that verifies the semantics of the expressions within a template. */
class ExpressionsSemanticsVisitor extends RecursiveAstVisitor {
constructor(
private templateTypeChecker: TemplateTypeChecker, private component: ts.ClassDeclaration,
private diagnostics: TemplateDiagnostic[]) {
super();
}
override visitPropertyWrite(ast: PropertyWrite, context: TmplAstNode): void {
super.visitPropertyWrite(ast, context);
this.checkForIllegalWriteInEventBinding(ast, context);
}
override visitPropertyRead(ast: PropertyRead, context: TmplAstNode) {
super.visitPropertyRead(ast, context);
this.checkForIllegalWriteInTwoWayBinding(ast, context);
}
private checkForIllegalWriteInEventBinding(ast: PropertyWrite, context: TmplAstNode) {
if (!(context instanceof TmplAstBoundEvent) || !(ast.receiver instanceof ImplicitReceiver)) {
return;
}
const target = this.templateTypeChecker.getExpressionTarget(ast, this.component);
if (target instanceof TmplAstVariable) {
const errorMessage = `Cannot use variable '${
target
.name}' as the left-hand side of an assignment expression. Template variables are read-only.`;
this.diagnostics.push(this.makeIllegalTemplateVarDiagnostic(target, context, errorMessage));
}
}
private checkForIllegalWriteInTwoWayBinding(ast: PropertyRead, context: TmplAstNode) {
// Only check top-level property reads inside two-way bindings for illegal assignments.
if (!(context instanceof TmplAstBoundEvent) || context.type !== ParsedEventType.TwoWay ||
!(ast.receiver instanceof ImplicitReceiver) ||
ast !== unwrapAstWithSource(context.handler)) {
return;
}
const target = this.templateTypeChecker.getExpressionTarget(ast, this.component);
if (!(target instanceof TmplAstVariable)) {
return;
}
// Two-way bindings to template variables are only allowed if the variables are signals.
const symbol = this.templateTypeChecker.getSymbolOfNode(target, this.component);
if (symbol !== null && !isSignalReference(symbol)) {
const errorMessage = `Cannot use a non-signal variable '${
target.name}' in a two-way binding expression. Template variables are read-only.`;
this.diagnostics.push(this.makeIllegalTemplateVarDiagnostic(target, context, errorMessage));
}
}
private makeIllegalTemplateVarDiagnostic(
target: TmplAstVariable, expressionNode: TmplAstBoundEvent,
errorMessage: string): TemplateDiagnostic {
return this.templateTypeChecker.makeTemplateDiagnostic(
this.component, expressionNode.handlerSpan, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.WRITE_TO_READ_ONLY_VARIABLE), errorMessage, [{
text: `The variable ${target.name} is declared here.`,
start: target.valueSpan?.start.offset || target.sourceSpan.start.offset,
end: target.valueSpan?.end.offset || target.sourceSpan.end.offset,
sourceFile: this.component.getSourceFile(),
}]);
}
}
function unwrapAstWithSource(ast: AST): AST {
return ast instanceof ASTWithSource ? ast.ast : ast;
}
@@ -859,7 +859,6 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder {
missingPipe(): void {}
deferredPipeUsedEagerly(templateId: TemplateId, ast: BindingPipe): void {}
deferredComponentUsedEagerly(templateId: TemplateId, element: TmplAstElement): void {}
illegalAssignmentToTemplateVar(): void {}
duplicateTemplateVar(): void {}
requiresInlineTcb(): void {}
requiresInlineTypeConstructors(): void {}
@@ -26,8 +26,7 @@
"files": [
{
"generated": "local_reference_nested.js",
"expected": "local_reference_nested_template.js",
"templatePipelineExpected": "local_reference_nested.pipeline.js"
"expected": "local_reference_nested.pipeline.js"
}
]
}
@@ -43,8 +42,7 @@
"failureMessage": "Incorrect template",
"files": [
{
"expected": "local_reference_and_context_variables_template.js",
"templatePipelineExpected": "local_reference_and_context_variables_template.pipeline.js",
"expected": "local_reference_and_context_variables_template.pipeline.js",
"generated": "local_reference_and_context_variables.js"
}
]
@@ -1,38 +0,0 @@
function MyComponent_div_0_span_3_Template(rf, ctx) {
if (rf & 1) {
$i0$.ɵɵelementStart(0, "span");
$i0$.ɵɵtext(1);
$i0$.ɵɵelementEnd();
}
if (rf & 2) {
const $item$ = $i0$.ɵɵnextContext().$implicit;
const $foo$ = $i0$.ɵɵreference(2);
$r3$.ɵɵadvance();
$i0$.ɵɵtextInterpolate2(" ", $foo$, " - ", $item$, " ");
}
}
function MyComponent_div_0_Template(rf, ctx) {
if (rf & 1) {
$i0$.ɵɵelementStart(0, "div");
$i0$.ɵɵelement(1, "div", null, 1);
$i0$.ɵɵtemplate(3, MyComponent_div_0_span_3_Template, 2, 2, "span", 2);
$i0$.ɵɵelementEnd();
}
if (rf & 2) {
const $app$ = $i0$.ɵɵnextContext();
$r3$.ɵɵadvance(3);
$i0$.ɵɵproperty("ngIf", $app$.showing);
}
}
// ...
consts: [[__AttributeMarker.Template__, "ngFor", "ngForOf"], ["foo", ""], [__AttributeMarker.Template__, "ngIf"]],
template:function MyComponent_Template(rf, ctx){
if (rf & 1) {
$i0$.ɵɵtemplate(0, MyComponent_div_0_Template, 4, 1, "div", 0);
}
if (rf & 2) {
$i0$.ɵɵproperty("ngForOf", ctx.items);
}
}
@@ -1,55 +0,0 @@
function MyComponent_div_3_span_2_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "span");
$r3$.ɵɵtext(1);
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵnextContext();
const $bar$ = $r3$.ɵɵreference(4);
$r3$.ɵɵnextContext();
const $foo$ = $r3$.ɵɵreference(1);
const $baz$ = $r3$.ɵɵreference(5);
$r3$.ɵɵadvance();
$r3$.ɵɵtextInterpolate3("", $foo$, "-", $bar$, "-", $baz$, "");
}
}
function MyComponent_div_3_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵtext(1);
$r3$.ɵɵtemplate(2, MyComponent_div_3_span_2_Template, 2, 3, "span", 1);
$r3$.ɵɵelement(3, "span", null, 3);
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
const $bar$ = $r3$.ɵɵreference(4);
$r3$.ɵɵnextContext();
const $foo$ = $r3$.ɵɵreference(1);
$r3$.ɵɵadvance();
$r3$.ɵɵtextInterpolate2(" ", $foo$, "-", $bar$, " ");
}
}
// ...
MyComponent.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({
type: MyComponent,
selectors: [["my-component"]],
decls: 6,
vars: 1,
consts: [["foo", ""], [__AttributeMarker.Template__, "if"], ["baz", ""], ["bar", ""]],
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelement(0, "div", null, 0);
$r3$.ɵɵtext(2);
$r3$.ɵɵtemplate(3, MyComponent_div_3_Template, 5, 2, "div", 1);
$r3$.ɵɵelement(4, "div", null, 2);
}
if (rf & 2) {
const $foo$ = $r3$.ɵɵreference(1);
$r3$.ɵɵadvance(2);
$r3$.ɵɵtextInterpolate1(" ", $foo$, " ");
}
},
dependencies:[IfDirective],
encapsulation: 2
});
@@ -47,8 +47,7 @@
"failureMessage": "Invalid MyApp definition",
"files": [
{
"expected": "pipes_my_app_def.js",
"templatePipelineExpected": "pipes_my_app_def.pipeline.js",
"expected": "pipes_my_app_def.pipeline.js",
"generated": "pipes.js"
}
]
@@ -1,27 +0,0 @@
const $c0$ = $a0$ => [$a0$, 1, 2, 3, 4, 5];
// ...
MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({
type: MyApp,
selectors: [["my-app"]],
decls: 7,
vars: 20,
template: function MyApp_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0);
$r3$.ɵɵpipe(1, "myPurePipe");
$r3$.ɵɵpipe(2, "myPipe");
$r3$.ɵɵelementStart(3, "p");
$r3$.ɵɵtext(4);
$r3$.ɵɵpipe(5, "myPipe");
$r3$.ɵɵpipe(6, "myPipe");
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵtextInterpolate($r3$.ɵɵpipeBind2(1, 3, $r3$.ɵɵpipeBind2(2, 6, ctx.name, ctx.size), ctx.size));
$r3$.ɵɵadvance(4);
$r3$.ɵɵtextInterpolate2("", $r3$.ɵɵpipeBindV(5, 9, $r3$.ɵɵpureFunction1(18, $c0$, ctx.name)), " ", ctx.name ? 1 : $r3$.ɵɵpipeBind1(6, 16, 2), "");
}
},
dependencies: [MyPipe, MyPurePipe],
encapsulation: 2
});
@@ -43,8 +43,7 @@
{
"files": [
{
"expected": "safe_access_temporaries_template.js",
"templatePipelineExpected": "safe_access_temporaries_template.pipeline.js",
"expected": "safe_access_temporaries_template.pipeline.js",
"generated": "safe_access_temporaries.js"
}
],
@@ -78,8 +77,7 @@
{
"files": [
{
"expected": "safe_call_template.js",
"templatePipelineExpected": "safe_call_template.pipeline.js",
"expected": "safe_call_template.pipeline.js",
"generated": "safe_call.js"
}
],
@@ -96,8 +94,7 @@
{
"files": [
{
"expected": "safe_access_non_null_template.js",
"templatePipelineExpected": "safe_access_non_null_template.pipeline.js",
"expected": "safe_access_non_null_template.pipeline.js",
"generated": "safe_access_non_null.js"
}
],
@@ -1,9 +0,0 @@
template: function MyApp_Template(rf, $ctx$) {
if (rf & 1) {
i0.ɵɵtext(0);
}
if (rf & 2) {
let $tmp_0_0$;
i0.ɵɵtextInterpolate4(" ", $ctx$.val == null ? null : $ctx$.val.foo.bar, " ", $ctx$.val == null ? null : $ctx$.val[0].foo.bar, " ", ($tmp_0_0$ = $ctx$.foo($ctx$.val)) == null ? null : $tmp_0_0$.foo.bar, " ", ($tmp_0_0$ = $ctx$.val) == null ? null : $tmp_0_0$.foo.bar, " ");
}
}
@@ -1,25 +0,0 @@
} if (rf & 2) {
let $tmp_0_0$;
let $tmp_0_1$;
let $tmp_0_2$;
let $tmp_0_3$;
let $tmp_1_0$;
let $tmp_1_1$;
let $tmp_1_2$;
let $tmp_1_3$;
let $tmp_1_4$;
let $tmp_2_0$;
let $tmp_2_1$;
let $tmp_3_0$;
let $tmp_3_1$;
let $tmp_3_2$;
let $tmp_3_3$;
i0.ɵɵadvance();
i0.ɵɵtextInterpolate1("Safe Property with Calls: ", ($tmp_0_0$ = ctx.p()) == null ? null : ($tmp_0_1$ = $tmp_0_0$.a()) == null ? null : ($tmp_0_2$ = $tmp_0_1$.b()) == null ? null : ($tmp_0_3$ = $tmp_0_2$.c()) == null ? null : $tmp_0_3$.d(), "");
i0.ɵɵadvance(2);
i0.ɵɵtextInterpolate1("Safe and Unsafe Property with Calls: ", ctx.p == null ? null : ($tmp_1_0$ = ctx.p.a()) == null ? null : ($tmp_1_1$ = $tmp_1_0$.b().c().d()) == null ? null : ($tmp_1_2$ = $tmp_1_1$.e()) == null ? null : $tmp_1_2$.f == null ? null : $tmp_1_2$.f.g.h == null ? null : ($tmp_1_3$ = $tmp_1_2$.f.g.h.i()) == null ? null : ($tmp_1_4$ = $tmp_1_3$.j()) == null ? null : $tmp_1_4$.k().l, "");
i0.ɵɵadvance(2);
i0.ɵɵtextInterpolate1("Nested Safe with Calls: ", ($tmp_2_0$ = ctx.f1()) == null ? null : $tmp_2_0$[($tmp_2_1$ = ctx.f2()) == null ? null : $tmp_2_1$.a] == null ? null : $tmp_2_0$[($tmp_2_1$ = $tmp_2_1$) == null ? null : $tmp_2_1$.a].b, "");
i0.ɵɵadvance(2);
i0.ɵɵtextInterpolate1("Deep Nested Safe with Calls: ", ($tmp_3_0$ = ctx.f1()) == null ? null : $tmp_3_0$[($tmp_3_1$ = ctx.f2()) == null ? null : ($tmp_3_2$ = $tmp_3_1$.f3()) == null ? null : $tmp_3_2$[($tmp_3_3$ = ctx.f4()) == null ? null : $tmp_3_3$.f5()]] == null ? null : $tmp_3_0$[($tmp_3_1$ = $tmp_3_1$) == null ? null : ($tmp_3_2$ = $tmp_3_2$) == null ? null : $tmp_3_2$[($tmp_3_3$ = $tmp_3_3$) == null ? null : $tmp_3_3$.f5()]].f6(), "");
}
@@ -1,17 +0,0 @@
template: function MyApp_Template(rf, $ctx$) {
if (rf & 1) {
$i0$.ɵɵelementStart(0, "span", 0);
$i0$.ɵɵtext(1);
$i0$.ɵɵelementEnd();
}
if (rf & 2) {
let $tmp_0_0$;
let $tmp_1_0$;
let $tmp_1_1$;
let $tmp_1_2$;
let $tmp_1_3$;
$i0$.ɵɵproperty("title", "Your last name is " + (($tmp_0_0$ = $ctx$.person.getLastName == null ? null : $ctx$.person.getLastName()) !== null && $tmp_0_0$ !== undefined ? $tmp_0_0$ : "unknown"));
$i0$.ɵɵadvance();
$i0$.ɵɵtextInterpolate2(" Hello, ", $ctx$.person.getName == null ? null : $ctx$.person.getName(), "! You are a Balrog: ", ($ctx$.person.getSpecies == null ? null : ($tmp_1_0$ = $ctx$.person.getSpecies()) == null ? null : ($tmp_1_1$ = $tmp_1_0$()) == null ? null : ($tmp_1_2$ = $tmp_1_1$()) == null ? null : ($tmp_1_3$ = $tmp_1_2$()) == null ? null : $tmp_1_3$()) || "unknown", " ");
}
}
@@ -89,8 +89,7 @@
"failureMessage": "Incorrect attribute array",
"files": [
{
"expected": "exclude_bindings_from_consts_template.js",
"templatePipelineExpected": "exclude_bindings_from_consts_template.pipeline.js",
"expected": "exclude_bindings_from_consts_template.pipeline.js",
"generated": "exclude_bindings_from_consts.js"
}
]
@@ -1,3 +0,0 @@
consts: [["target", "_blank", "aria-label", "link", __AttributeMarker.Bindings__, "title", "id", "customEvent"]],
…
@@ -379,8 +379,7 @@
"files": [
{
"generated": "deceptive_attrs.js",
"expected": "deceptive_attrs.template.js",
"templatePipelineExpected": "deceptive_attrs.pipeline.js"
"expected": "deceptive_attrs.pipeline.js"
}
]
}
@@ -1,9 +0,0 @@
hostAttrs: ["class.is-compact", "false", "style.width", "0", "attr.tabindex", "5"],
…
hostBindings: function MyComponent2_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("tabindex", 5);
i0.ɵɵstyleProp("width", 0);
i0.ɵɵclassProp("is-compact", false);
}
}
@@ -12,8 +12,7 @@
"files": [
{
"generated": "local_ref_on_host.js",
"expected": "local_ref_on_host.template.js",
"templatePipelineExpected": "local_ref_on_host.pipeline.js"
"expected": "local_ref_on_host.pipeline.js"
}
]
}
@@ -1,18 +0,0 @@
consts: [["id", "my-id"], ["myRef", ""]],
template:function MyComponent_Template(rf, $ctx$){
if (rf & 1) {
$i0$.ɵɵelementStart(0, "b", 0, 1);
$i0$.ɵɵdisableBindings();
$i0$.ɵɵelementStart(2, "i");
$i0$.ɵɵtext(3, "Hello {{ name }}!");
$i0$.ɵɵelementEnd();
$i0$.ɵɵenableBindings();
$i0$.ɵɵelementEnd();
$i0$.ɵɵtext(4);
}
if (rf & 2) {
const $_r0$ = $i0$.ɵɵreference(1);
$r3$.ɵɵadvance(4);
$i0$.ɵɵtextInterpolate1(" ", $_r0$.id, " ");
}
}
@@ -53,9 +53,8 @@
{
"files": [
{
"expected": "switch_with_pipe_template.js",
"generated": "switch_with_pipe.js",
"templatePipelineExpected": "switch_with_pipe_template.pipeline.js"
"expected": "switch_with_pipe_template.pipeline.js"
}
],
"failureMessage": "Incorrect template"
@@ -144,9 +143,8 @@
{
"files": [
{
"expected": "if_with_pipe_template.js",
"generated": "if_with_pipe.js",
"templatePipelineExpected": "if_with_pipe_template.pipeline.js"
"expected": "if_with_pipe_template.pipeline.js"
}
],
"failureMessage": "Incorrect template"
@@ -190,8 +188,7 @@
{
"files": [
{
"expected": "if_nested_alias_listeners_template.js",
"templatePipelineExpected": "if_nested_alias_listeners_template.pipeline.js",
"expected": "if_nested_alias_listeners_template.pipeline.js",
"generated": "if_nested_alias_listeners.js"
}
],
@@ -281,8 +278,7 @@
{
"files": [
{
"expected": "for_template_variables_template.js",
"templatePipelineExpected": "for_template_variables_template.pipeline.js",
"expected": "for_template_variables_template.pipeline.js",
"generated": "for_template_variables.js"
}
],
@@ -297,8 +293,7 @@
{
"files": [
{
"expected": "for_aliased_template_variables_template.js",
"templatePipelineExpected": "for_aliased_template_variables_template.pipeline.js",
"expected": "for_aliased_template_variables_template.pipeline.js",
"generated": "for_aliased_template_variables.js"
}
],
@@ -328,8 +323,7 @@
{
"files": [
{
"expected": "for_template_variables_listener_template.js",
"templatePipelineExpected": "for_template_variables_listener_template.pipeline.js",
"expected": "for_template_variables_listener_template.pipeline.js",
"generated": "for_template_variables_listener.js"
}
],
@@ -374,8 +368,7 @@
{
"files": [
{
"expected": "for_template_variables_scope_template.js",
"templatePipelineExpected": "for_template_variables_scope_template.pipeline.js",
"expected": "for_template_variables_scope_template.pipeline.js",
"generated": "for_template_variables_scope.js"
}
],
@@ -1,25 +0,0 @@
function MyApp_For_3_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0);
}
if (rf & 2) {
const $idx_r2$ = ctx.$index;
const $co_r3$ = ctx.$count;
$r3$.ɵɵtextInterpolate6(" Index: ", $idx_r2$, " First: ", $idx_r2$ === 0, " Last: ", $idx_r2$ === $co_r3$ - 1, " Even: ", $idx_r2$ % 2 === 0, " Odd: ", $idx_r2$ % 2 !== 0, " Count: ", $co_r3$, " ");
}
}
…
function MyApp_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵtext(1);
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 6, null, null, $r3$.ɵɵrepeaterTrackByIdentity);
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵadvance();
$r3$.ɵɵtextInterpolate1(" ", ctx.message, " ");
$r3$.ɵɵadvance();
$r3$.ɵɵrepeater(ctx.items);
}
}
@@ -1,29 +0,0 @@
function MyApp_For_3_Template(rf, ctx) {
if (rf & 1) {
const $_r5$ = $r3$.ɵɵgetCurrentView();
$r3$.ɵɵelementStart(0, "div", 0);
$r3$.ɵɵlistener("click", function MyApp_For_3_Template_div_click_0_listener() {
const $restoredCtx$ = $r3$.ɵɵrestoreView($_r5$);
const $index_r2$ = $restoredCtx$.$index;
const $count_r3$ = $restoredCtx$.$count;
const $ctx_r4$ = $r3$.ɵɵnextContext();
return $r3$.ɵɵresetView($ctx_r4$.log($index_r2$, $index_r2$ % 2 === 0, $index_r2$ === 0, $count_r3$));
});
$r3$.ɵɵelementEnd();
}
}
…
function MyApp_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵtext(1);
$r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 0, "div", null, $r3$.ɵɵrepeaterTrackByIdentity);
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵadvance();
$r3$.ɵɵtextInterpolate1(" ", ctx.message, " ");
$r3$.ɵɵadvance();
$r3$.ɵɵrepeater(ctx.items);
}
}

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