Compare commits

...

288 Commits

Author SHA1 Message Date
Jessica Janiuk 4626691198 release: cut the v17.0.1 release 2023-11-08 11:24:30 -08:00
Matthieu Riegler 5c6f3f8ec0 fix(http): Don't override the backend when using the InMemoryWebAPI (#52425)
When using `withFetch`,  the `PRIMARY_HTTP_BACKEND` token is set.

The InMemory Backend services will also set that token.

This means that providers order will matter and the latest on the list will be the one instantiated

PR Close #52425
2023-11-08 18:42:21 +00:00
Jessica Janiuk 70d30c28e0 fix(migrations): Add support for ng-templates with i18n attributes (#52597)
This makes sure that i18n attributes are preserved on ng-templates being removed during the migration.

fixes: #52517

PR Close #52597
2023-11-08 10:26:20 -08:00
Andrew Scott 6240613477 refactor(language-service): Add flag to allow disabling block syntax parsing (#52691)
This commit adds a flag to the language service config options to disable block
parsing in the compiler.

PR Close #52691
2023-11-08 10:25:22 -08:00
Kristiyan Kostadinov 227de98201 refactor(compiler): add flag to disable block syntax in language service (#52683)
Adds the private `_enableBlockSyntax` flag that can be used by the language service to disable blocks on apps that aren't on Angular v17.

PR Close #52683
2023-11-08 09:34:10 -08:00
Kristiyan Kostadinov a46c8cbf4e fix(docs-infra): fix menu icon size (#52689)
Fixes that the hamburger icon was too small, because the top nav is using the wrong kind of button. Switching it to the right one would require more refactoring so I went with the simpler approach for now.

PR Close #52689
2023-11-08 09:23:43 -08:00
cexbrayat 1d31a0c129 refactor(core): sanitize reactivity tests (#52632)
It was intriguing to see a double `fixture.detectChanges()` introduced by 38c9f08c8d
It turns out this is not needed.

PR Close #52632
2023-11-08 08:51:50 -08:00
Swami bd0f811405 docs: typo fix (#52511)
PR Close #52511
2023-11-08 08:51:28 -08:00
Andrew Scott f729215f0a refactor(core): Remove RootViewRef<T> because it is the same as ViewRef<T> (#52430)
`RootViewRef<T>` extends `ViewRef<T>` and overrides 3 methods with behavior
that is identical to `ViewRef<T>`. This commit removes `RootViewRef<T>`
because it is not needed.

PR Close #52430
2023-11-08 08:50:50 -08:00
Matthieu Riegler bd275b1b26 docs: add v17 versions support (#51443)
* Node 16 support is dropped per angular/angular-cli#25675
* TS 5.2 is supported per #51334
* TS 5.1 and lower support is dropped by #51792

PR Close #51443
2023-11-08 08:50:20 -08:00
Lucas 181d5755ee docs: updating information about polyfills (#52519)
PR Close #52519
2023-11-08 08:49:28 -08:00
Andrew Scott e1c0493135 test(core): Add test to ensure writing to signals in afterRender hooks throws error (#52475)
We do not yet handle running change detection again if `afterRender`
hooks write to signals.

PR Close #52475
2023-11-08 08:48:44 -08:00
Jessica Janiuk 4f125c5f9a fix(migrations): Switches to multiple passes to fix several reported bugs (#52592)
Rather than migrate all in one pass, this now migrates in a separate pass per control flow item plus one for templates at the end. This resolves issues with multiple control flow items on a single element as well as making sure ng-templates are fully migrated before being moved to new locations.

fixes: #52518
fixes: #52516
fixes: #52513

PR Close #52592
2023-11-08 08:47:10 -08:00
Alex Rickabaugh c30dde257b build: bump in-memory-web-api to v17 (#52676)
This commit updates the in-memory-web-api package versions from v16 -> v17.

PR Close #52676
2023-11-08 17:05:45 +01:00
Pawel Kozlowski 1cc87b26cb build: update aio to 17.0.0 (#52635) (#52674)
This commit updates the Angular version for AIO and its examples to 17.0.0

PR Close #52674
2023-11-08 17:04:51 +01:00
Alex Rickabaugh da429c59b6 Revert "fix(http): Don't override the backend when using the InMemoryWebAPI (#52425)" (#52630)
This reverts commit 49b037f811. Reason: it
breaks tests in aio-local.

PR Close #52630
2023-11-08 16:28:00 +01:00
Pawel Kozlowski fc6b2f2669 docs: add the v17 blog post link to the changelog (#52675)
This commit adds the short link 'http://goo.gle/angular-v17' to the project changelog.

PR Close #52675
2023-11-08 16:23:01 +01:00
Pawel Kozlowski fecd141dfa release: cut the v17.0.0 release 2023-11-08 13:23:45 +01:00
Andrew Scott 6b83328577 release: cut the v17.0.0-rc.3 release 2023-11-06 16:07:18 -08:00
Andrew Scott d4c20aca71 refactor(core): Add warning when signal equality is false for object.is (#52532)
This commit adds a warning when a signal equality function returns
`false` but `Object.is` returns `true`.

PR Close #52532
2023-11-06 15:53:10 -08:00
AleksanderBodurri a86661ff24 refactor(devtools): update Angular logo for extension and loading animation (#52546)
Previously these were using the pre v17 Angular logo.

Now these have been updated to use the new Angular logo for V17+.

PR Close #52546
2023-11-06 12:34:21 -08:00
Dylan Hunn 590c61889f refactor(compiler): Fix two-way binding source maps (#52479)
Now that two-way bindings work correctly with implicit receivers, we can fix the corresponing source map tests. The main issue was that we were not properly mapping `elementEnd` for elements with no closing tag (self-closing elements).

PR Close #52479
2023-11-06 11:43:00 -08:00
Dylan Hunn b44f2f9e47 refactor(compiler): Fix two-way bindings in template pipeline (#52479)
Some two-way bindings tests were not working properly, because we could not ingest the implicit receiver required to write to the `ngModelChanges` property. Now, we properly resolve that implicit receiver to the root component context.

Also, add some tests, both for the simple case, and the case where the listener is inside a nested view.

PR Close #52479
2023-11-06 11:43:00 -08:00
Dylan Hunn a48d65cdf4 refactor(compiler): Support extracting deps functions for defer in template pipeline (#52479)
Some `defer` blocks have external dependencies on other components or directives. These dependencies need to be extracted into deps functions, which either return local deps, or use a dynamic import for non-local deps. Template Pipeline can now generate these functions.

PR Close #52479
2023-11-06 11:43:00 -08:00
Dylan Hunn 42899c5aa0 refactor(compiler): Emit a template ref extractor on ng-templates with local refs (#52479)
When an `ng-template` has local refs, such as `<ng-template #foo>`, we must emit a `ɵɵtemplateRefExtractor` argument to the template creation functino. The template pipeline now supports this.

PR Close #52479
2023-11-06 11:42:59 -08:00
Dylan Hunn 2367b038fb refactor(compiler): Add template pipeline golden for differing consts (#52479)
The consts for this test are emitted in a different order, but the generated code is valid.

PR Close #52479
2023-11-06 11:42:59 -08:00
Dylan Hunn d8eab6f807 refactor(compiler): Add template pipeline goldens for some tests with const attributes (#52479)
`TemplateDefinitionBuilder` is somewhat unreliable about extracting constant attributes (e.g. `[attr.foo]="'one'"`). It never extracts const non-string expressions, and usually, but not always, extracts const string expressions. Template pipeline consistently extracts const strings, and we add new goldens for a couple such cases.

PR Close #52479
2023-11-06 11:42:59 -08:00
Dylan Hunn dab522f011 refactor(compiler): Support defer triggers with no arguments in template pipeline (#52479)
Some defer triggers, such as `hover`, expect a local reference as an argument. For example, `@defer (on hover(target))` waits until the user hovers over the target.

However, these defer conditions also have a nullary form, in which the trigger is implicitly the first element in the placeholder block. We now support that case in template pipeline.

PR Close #52479
2023-11-06 11:42:59 -08:00
Dylan Hunn 8400533555 refactor(compiler): Support deferWhen instructions in template pipeline (#52479)
We already supported `defer on` conditions, which become instructions in the create mode block.

Now, we also support `defer when` conditions, where are very similar, with the notable difference that they go in the update block (because a user-supplied condition must be re-evaluated on each update.)

PR Close #52479
2023-11-06 11:42:59 -08:00
Andrew Scott 43acd44cf8 refactor(core): Remove change detection flag used in g3 (#52530)
This has now been cleaned up internally and is no longer needed

PR Close #52530
2023-11-06 11:42:16 -08:00
twerske cab7e003b3 docs: add angular.dev (#52534)
PR Close #52534
2023-11-06 11:09:41 -08:00
Emma Twersky 2c3daebc5d docs: add angular.dev banner (#52535)
PR Close #52535
2023-11-06 11:04:13 -08:00
Miles Malerba 91dbcb5eab refactor(compiler): Add more comments for i18n template pipeline phases (#52503)
Adds some additional comments about the i18n termplate pipeline phases
to more clearly document how they work.

PR Close #52503
2023-11-06 09:49:48 -08:00
Miles Malerba fa9bca23f7 refactor(compiler): Add support for ICUs as part of another i18n message (#52503)
Previously we supported ICUs where the ICU itself represetned the entire
translated message. This change allows ICUs to act as a sub-message
inside other translated messages.

PR Close #52503
2023-11-06 09:49:47 -08:00
Miles Malerba 51d73ed957 refactor(compiler): Introduce i18n context objects (#52503)
Previously we assumed that all i18n messages that are eventually
extracted into the consts array would be generated based on an i18n
block. However, it is also possible to have messages generated directly
from ICUs. This change introduces an i18n context op, so that we can
consistently extract i18n messages from the context op in all cases.

PR Close #52503
2023-11-06 09:49:47 -08:00
Jessica Janiuk 9c2be715a3 fix(migrations): Fixes a bug in the ngFor pre-v5 alias translation (#52531)
The logic that transformed the value to get the alias name was incorrect.

fixes: #52522

PR Close #52531
2023-11-06 09:47:37 -08:00
Jessica Janiuk c267f54bc3 fix(migrations): Update regex to better match ng-templates (#52529)
This addresses an edge case where an ng-template name matches an opening element name, preventing the template from being removed.

fixes #52523

PR Close #52529
2023-11-06 09:05:55 -08:00
Kristiyan Kostadinov 83067b3ef2 fix(compiler): ng-template directive invoke twice at the root of control flow (#52515)
Discovered this while validating #52414 against Angular Material. We were projecting `<ng-template>` nodes at the root of `@if` and `@for` with the `ng-template` tag name which enables directive matching and applies the directive to the control flow node.

These changes fix the issue by never passing along the `ng-template` tag name.

PR Close #52515
2023-11-06 09:03:45 -08:00
Kristiyan Kostadinov 09e905ad67 fix(migrations): account for separator characters inside strings (#52525)
Fixes that the control flow migrations wasn't accounting for separator characters used inside string literals.

PR Close #52525
2023-11-06 09:00:23 -08:00
Kristiyan Kostadinov 57404d4723 fix(migrations): handle comma-separated syntax in ngFor (#52525)
This is something that came up when running the script against the Components repo. The `ngFor` syntax can be delimited either by semicolons or by commas, but the migration only accounted for commas.

PR Close #52525
2023-11-06 09:00:23 -08:00
AleksanderBodurri 1c92609719 test(devtools): fix transformInjectorResolutionPathsIntoTree tests (#52489)
Previously transformInjectorResolutionPathsIntoTree returned an array, now it returns a tree node so we update the test cases to reflect that.

PR Close #52489
2023-11-03 17:37:56 -07:00
AleksanderBodurri e5bd6e9ccc feat(devtools): implement zoom and pan in injector graph visualizer (#52489)
Previously only the trackpad could be used to navigate this view.

Now we can zoom and pan around using our mouse wheel and scroll.

Additionally, this commit fixes many issues related to the visualization of the injector graph visualization, allowing it to be more compact without impacting legibility and minimizing edge collisions in larger graphs.

PR Close #52489
2023-11-03 17:37:55 -07:00
AleksanderBodurri 5a816b8739 perf(devtools): optimize injector graph discovery (#52489)
Our algorithm for discovering the injector graph of an application involves calculating resolution paths for each angular node on a page, and then using those paths to construct the underlying tree.

Along the we way we perform many expensive computations that are candidates for optimization through caching. This commit implements this caching, resulting in a substantial increase in performance for large applications.

PR Close #52489
2023-11-03 17:37:55 -07:00
AleksanderBodurri c0d7bac111 feat(devtools): squash multi providers into 1 and allow them to be logged to the console (#52489)
Implements a feature allowing users to visualize multiproviders as one row in the providers table.

Also enables the user to log to console any provider in the table. This log includes information about the provider selected, the injector it was configured in, and also calls `Injector.get(Token)` to determine the value the provider evaluates to in it's injector.

PR Close #52489
2023-11-03 17:37:55 -07:00
AleksanderBodurri 3afc9d0cdb refactor(devtools): stop inspector from changing tab to components (#52489)
Previously this was the only page that used the inspector, so we automatically changed the tab out of convenience.

Now, the injector tree tab also does uses the inspector for some functionality, so we disable this behaviour.

PR Close #52489
2023-11-03 17:37:55 -07:00
Alan Agius de136105aa release: cut the zone.js-0.14.2 release (#52508)
PR Close #52508
2023-11-03 10:54:12 -07:00
Alan Agius 95accb07b4 docs: update ssr docs to add information about CommonEngine (#52506)
This commit add contructor information about `CommonEngine`

PR Close #52506
2023-11-03 09:50:13 -07:00
AleksanderBodurri a2ba5482c3 fix(core): use TNode instead of LView for mapping injector providers (#52436)
Previously, LViews were used here to be consistent with other debug APIs. Using LViews for tracking injector providers does not work because providers only get configured once per TNode type.

Now we use the TNode as the key to track element injector providers, allowing the injector for each item rendered in a list (`ngFor` or `@for`) to be targeted with debug APIs for inspecting providers

PR Close #52436
2023-11-03 09:36:00 -07:00
JiaLiPassion 842b2cceb6 fix(zone.js): disable wrapping unhandled promise error by default (#52492)
Before this commit, zone.js wraps the uncaught promise rejection error
to a new Error object includes more information such as Zone stack
traces. This feature is provided from the very beginning of Zone.js,
but this feature becomes very annoying and make the user difficult to
debug.

So this commit disable this wrapping behavior by default, and user can
enable this feature back by setting
`DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION` to `false`.

PR Close #52492
2023-11-03 09:33:20 -07:00
Kristiyan Kostadinov 6988a0070e fix(migrations): handle ngIf else condition with no whitespaces (#52504)
Fixes that the control flow migration wasn't migrating an `*ngIf` with an `else` condition that doesn't have spaces before the `else`.

Fixes #52502.

PR Close #52504
2023-11-03 09:32:47 -07:00
Aanchal Agarwal 236b448364 docs: omit repetitions (#52413)
PR Close #52413
2023-11-03 07:46:55 -07:00
Angular Robot 1f9ff84e28 build: update dependency multimatch to v7 (#52426)
See associated pull request for more information.

PR Close #52426
2023-11-03 07:45:31 -07:00
Alan Agius 7ff5152547 build: configure tslint to ban performance.mark usages (#52505)
While `performance.mark` is available on all supported browsers and node.js version this API is not available in JSDOM which is used by Jest and Cloudflare worker.

PR Close #52505
2023-11-03 07:43:37 -07:00
Alan Agius 80e7a0f8fa fix(core): guard usages of performance.mark (#52505)
While `performance.mark` is available on all supported browsers and node.js version this API is not available in JSDOM which is used by Jest and Cloudflare worker.

This commit, updates the usage to a safer variant.

PR Close #52505
2023-11-03 07:43:36 -07:00
Alex Rickabaugh 739c0d27ec refactor(core): mark effect as developer preview again (#52490)
Re-add the `@developerPreview` flags to `effect()`. While we don't expect
the `effect` API itself to change, we may change how other FW APIs interact
with `effect`s. Also, add a missing export for one of those effect APIs.

PR Close #52490
2023-11-02 15:29:45 -07:00
Alex Rickabaugh a8e57a1fab release: cut the v17.0.0-rc.2 release 2023-11-02 15:03:38 -07:00
Andrew Scott 32f417f77f refactor(core): Do not refresh view if producers did not actually change (#52476)
Producers represent values which can deliver change notifications.
When a producer value is changed, a change notification is propagated through the graph,
notifying live consumers which depend on the producer of the potential update.
Note here that this is a _potential_ update.

A producer may not have actually "changed" based on its equality function. With
this commit, before refreshing a view that is only marked for refresh
because its consumer is dirty, we poll producers for change to see if
they really have. If not, we can skip the refresh. The example test in this commit
shows that a `computed` which depends on a `signal` that is updated but
produces a value that is the same as before will _not_ cause the
component's template to refresh.

fixes #51797

PR Close #52476
2023-11-02 13:23:49 -07:00
Andrew Scott c99e5cf168 refactor(core): Update LView consumer to use only 1 consumer for a component (#52476)
This commit updates the reactive consumer used for `LView`s to be shared
between a component and its embedded views. This allows us to use the
consumer flag directly for a dirty indicator rather than needing to
find a component view for updating its flags.

In the future, this will also allow us to effectively poll producers to see if
they really changed before refreshing a view.

PR Close #52476
2023-11-02 13:23:49 -07:00
Andrew Scott 2ce2ad1c42 refactor(core): Remove warning about signal set during template execution (#52476)
The significance of the combination of #51854 and #52302 went mostly
unnoticed. The first removed a unidirectional data flow constraint for
transplanted views and the second updated the signal implementation to
share transplanted view logic. The result is that we automatically get behavior
that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are
used to drive application state to DOM synchronization.

fixes #50320

PR Close #52476
2023-11-02 13:23:49 -07:00
Charles Lyding 0d88e25311 docs: additional application builder SSR conversion information (#52485)
This change updates the new build system migration guide with additional
information regarding the steps needed to convert an existing SSR project
to use the new integrated SSR capabilities.

PR Close #52485
2023-11-02 11:06:21 -07:00
arturovt 79b21662fd refactor(core): do not run CDs when setting up image perf warnings (#52482)
This commit wraps the `load` event listener with `runOutsideAngular` to avoid
running unnecessary change detections on asynchronous tasks spawned within the
scope of the `ImagePerformanceWarning` functionality. This was previously causing
extra change detection cycles in development mode and interfering with debugging.

PR Close #52482
2023-11-02 11:03:42 -07:00
Jeremy Elbourn f9b226f9dc refactor(compiler): escape decorators in API JsDoc extraction (#52481)
TypeScript JsDoc parsing, by default, treats occurences of Angular decorators (e.g. `@Component`) in JsDoc comments as JsDoc tags. This commit escapes these decorator strings by copying the raw JS doc onto a dummy symbol in a new SourceFile to make TypeScript re-parse the comment.

PR Close #52481
2023-11-02 11:03:09 -07:00
Jeremy Elbourn 6d39551f53 build: add rules for generating block/element API data (#52480)
Adds build rules for "artificially" generating `DocEntry` collections for block and element APIs. The two rules are very similar, but _just_ different enough that it's worth having two separate implementations.

PR Close #52480
2023-11-02 11:02:08 -07:00
Jeremy Elbourn 5dd4bd6cf5 build: add target to generate api manifest (#52472)
This adds a target to generate a manifest of all public api symbols. The majority of inputs are generated from the extraction rules, but API entries that don't have a TypeScript source symbol (elements and blocks) are defined in hand-written json collections.

PR Close #52472
2023-11-02 11:00:59 -07:00
Pawel Kozlowski 6d4271c340 refactor(core): short-circuits invocations of signals equality (#52465)
This change skips signal equality calls on set / update when the
two values (current and the new one) are referentially identical.
The assumption is that equality function implementation should
never return false for 2 values that are the same (according to
the Object.is logic).

PR Close #52465
2023-11-02 10:59:59 -07:00
Dylan Hunn 0717c08b7d refactor(compiler): Delete some unused imports in template pipeline (#52464)
Remove a variety of unused imports.

PR Close #52464
2023-11-02 10:58:48 -07:00
Dylan Hunn 2837342852 refactor(compiler): Rename slot -> handle in slot consumers (#52464)
This name is a bit more precise, and avoids the `foo.slot.slot` "stuttering access" on consumers.

PR Close #52464
2023-11-02 10:58:48 -07:00
Dylan Hunn 1841d7903b refactor(compiler): Move an enum to enums.ts for Template Pipeline (#52464)
This enum belongs in the main enums file.

PR Close #52464
2023-11-02 10:58:48 -07:00
Dylan Hunn 90af00a07c refactor(compiler): Delete an unused OpKind in template pipeline. (#52464)
`DeferSecondaryBlock` is no longer used.

PR Close #52464
2023-11-02 10:58:48 -07:00
Dylan Hunn e06a25b6c3 refactor(compiler): Replace cpl with job in template pipeline (#52464)
Eliminate all the remaining `cpl` names, and use `job` instead, which is the predominant convention.

Also, replace `.views.values()` with `.unit` in a few places, and perform the corresponding rename.

PR Close #52464
2023-11-02 10:58:48 -07:00
Dylan Hunn 8358dcd7e3 refactor(compiler): Rename and comment template pipeline phases (#52464)
In this cleanup commit:
1. Add explanatory comments to all phases that were previously missing them.
2. Rename all phases, to eliminate the "phase" prefix, and directly describe their functions.

PR Close #52464
2023-11-02 10:58:48 -07:00
Angular Robot cc13d6f1cd build: update peter-evans/create-or-update-comment digest to 23ff157 (#52279)
See associated pull request for more information.

PR Close #52279
2023-11-02 10:43:24 -07:00
Angular Robot 0754122232 build: update actions/checkout digest to b4ffde6 (#52252)
See associated pull request for more information.

PR Close #52252
2023-11-02 10:32:11 -07:00
arturovt bf3281a6a5 fix(animations): remove finish listener once player is destroyed (#51136)
This commit removes the `finish` listener from the Animation object once
the animation is finished, effectively resolving a memory leak. Previously,
the `finish` listener captured `this`, which prevented `this` from being garbage collected.

PR Close #51136
2023-11-02 10:29:51 -07:00
Alex Rickabaugh 356da9c9f6 refactor(core): add a flag for whether we skip logic in checkNoChanges (#52488)
This can be used to patch different behavior in g3.

PR Close #52488
2023-11-01 21:43:21 -07:00
Doug Parker 4e4c678180 docs: update examples to use buildTarget instead of browserTarget (#52361)
`browserTarget` is deprecated and `buildTarget` is the newly preferred name.

PR Close #52361
2023-11-01 16:24:43 -07:00
Doug Parker 0f37b12a17 docs: update examples to use application builder (#52361)
`application` builder is preferred over `browser` builder. This updates all the examples I could find to use `application` builder. A couple notes of fixes that needed to happen:
1.  The testing example was broken because it relied on `~` imports in CSS files, which is a Webpack-specific features. However I don't think we need those custom styles at all, so I just removed the import altogether.
2.  A few tests seemed to be affected and needed to be cleaned up.

PR Close #52361
2023-11-01 16:24:43 -07:00
Alex Rickabaugh 7c950794ea Revert "refactor(core): Remove RootViewRef<T> because it is the same as ViewRef<T> (#52430)" (#52484)
This reverts commit a3028e2340 as it breaks
tests in g3.

PR Close #52484
2023-11-01 10:13:13 -07:00
Alex Rickabaugh c9a3708e55 Revert "refactor(core): rename ViewRef<T> to InternalViewRef<T> and remove existing InternalViewRef (#52430)" (#52484)
This reverts commit a568bc5d97 as it breaks
tests in g3.

PR Close #52484
2023-11-01 10:13:12 -07:00
Alex Rickabaugh ddef3ac9a4 fix(core): effects wait for ngOnInit for their first run (#52473)
When an effect is created in a component constructor, it might read signals
which are derived from component inputs. These signals may be unreliable or
(in the case of the proposed input signals) may throw if accessed before the
component is first change detected (which is what makes required inputs
available).

Depending on the scenario involved, the effect may or may not run before
this initialization takes place, which isn't a great developer experience.
In particular, effects created during CD (e.g. via control flow) work fine,
as do effects created in bootstrap thanks to the sync CD it performs. When
an effect is created through dynamic component creation outside of CD though
(such as on router navigations), it runs before the component is first CD'd,
causing the issue.

In fact, in the signal components RFC we described how effects would wait
until ngOnInit for their first execution for exactly this reason, but this
behavior was never implemented as it was thought our effect scheduling
design made it unnecessary. This is true of the regular execution of effects
but the above scenario shows that *creation* of the effect is still
vulnerable. Thus, this logic is needed.

This commit makes effects sensitive to their creation context, by injecting
`ChangeDetectorRef` optionally. An effect created with an injector that's
tied to a component will wait until that component is initialized before
initially being scheduled. TestBed effect flushing is also adjusted to
account for the additional interaction with change detection.

PR Close #52473
2023-11-01 08:07:35 -07:00
Alex Rickabaugh 935c1816fd fix(core): add rejectErrors option to toSignal (#52474)
By default, `toSignal` transforms an `Observable` into a `Signal`, including
the error channel of the Observable. When an error is received, the signal
begins throwing the error.

`toSignal` is intended to serve the same purpose as the `async` pipe, but
the async pipe has a different behavior with errors: it rejects them
outright, throwing them back into RxJS. Rx then propagates the error into
the browser's uncaught error handling logic. In the case of Angular, the
error is then caught by zone.js and reported via the application's
`ErrorHandler`.

This commit introduces a new option for `toSignal` called `rejectErrors`.
With that flag set, `toSignal` copies the async pipe's behavior, allowing
for easier migrations.

Fixes #51949

PR Close #52474
2023-10-31 14:59:26 -07:00
Jeremy Elbourn 2e12e277c5 refactor(compiler): rename decorator extracted "options" to "members" (#52462)
The property name `members` makes it easier to use the same rendering
code as other constructs.

PR Close #52462
2023-10-31 14:58:03 -07:00
Alex Castle 3bd85fb7b0 fix(common): apply fixed_srcset_width value only to fixed srcsets (#52459)
add logic to NgOptimizedImage to keep fixed_srcset_width from applying to large responsive images, which is incorrect behavior

PR Close #52459
2023-10-31 14:57:19 -07:00
Jessica Janiuk af46256043 docs: update defer guide to use Deferrable View language (#52457)
This updates the defer guide to use the recommended language of
Deferrable Views.

PR Close #52457
2023-10-31 14:55:48 -07:00
AleksanderBodurri 34a59967d7 refactor(core): ignore ENVIRONMENT_INTIIALIZER and INJECTOR_DEF_TYPES provides in getInjectorProviders (#52458)
These special providers are configured when `walkProviderTree` is called. Because of this, they do not maintain any equality between subsequent runs of `walkProviderTree`. This prevents us from being able to compare the provider objects for equality between runs.

This commit changes the behaviour of getInjectorProviders to ignore these providers. In the future we will consider another approach for differentiating these providers from ones provided by users rather than the framework.

PR Close #52458
2023-10-31 14:55:04 -07:00
Matthieu Riegler 792026bc7d build: add missing entries for the api extraction (#52452)
Were missing :

* `@angular/common/http/testing`
* `@angular/platform-browser/animations/async`

PR Close #52452
2023-10-31 14:53:34 -07:00
Kristiyan Kostadinov d2fca84bc4 refactor(compiler): implement control flow content projection fix in template pipeline (#52414)
Recreates the fix for content projection in control flow in the new template pipeline. I also had to make the following adjustments to the pipeline:
1. The `TemplateOp.tag` property was being used to generate the name of the template function, rather than the actual tag name being passed into `ɵɵtemplate`. Since the content projection fix requires the tag name to be passed in, I've introduced a new `functionNameSuffix` property instead.
2. `TemplateOp.block` was being used to determine whether to pass `TemplateOp.tag` into the `ɵɵtemplate` instruction. Now that we're always passing in the tag name after the refactor in point 1, we no longer need this flag.

In addition to the refactors above, I also made some minor cleanups where I saw the opportunity to do so.

PR Close #52414
2023-10-31 14:52:31 -07:00
Kristiyan Kostadinov 1f5039bbd6 fix(compiler): project control flow root elements into correct slot (#52414)
With the directive-based control flow users were able to conditionally project content using the `*` syntax. E.g. `<div *ngIf="expr" projectMe></div>` will be projected into `<ng-content select="[projectMe]"/>`, because the attributes and tag name from the `div` are copied to the template via the template creation instruction. With `@if` and `@for` that is not the case, because the conditional is placed *around* elements, rather than *on* them. The result is that content projection won't work in the same way if a user converts from `*ngIf` to `@if`.

These changes aim to cover the most common case by doing the same copying when a control flow node has *one and only one* root element or template node.

This approach comes with some caveats:
1. As soon as any other node is added to the root, the copying behavior won't work anymore. A diagnostic will be added to flag cases like this and to explain how to work around it.
2. If `preserveWhitespaces` is enabled, it's very likely that indentation will break this workaround, because it'll include an additional text node as the first child. We can work around it here, but in a discussion it was decided not to, because the user explicitly opted into preserving the whitespace and we would have to drop it from the generated code. The diagnostic mentioned point #1 will flag such cases to users.

Fixes #52277.

PR Close #52414
2023-10-31 14:52:31 -07:00
AleksanderBodurri 696f003553 fix(core): mutation bug in getDependenciesFromInjectable (#52450)
Previously, we would modified `dep.flags` directly to convert injection flags to booleans. This caused a mutation bug where subsequent calls to `getDependenciesFromInjectable` would result in the flags object containing false for every injection flag.

Now, we stop modifying `dep.flags` directly and instead assign the converted flags to a new object.

PR Close #52450
2023-10-31 14:21:36 -07:00
Alan Agius e5c3b25496 build: update node.js engines version to be more explicate about v20 support (#52448)
This commit adds Node.js 20 as explicitly supported version to match the Angular CLI engines.

See: https://github.com/angular/angular-cli/pull/26173

PR Close #52448
2023-10-31 14:18:36 -07:00
cexbrayat b9ea2d6900 fix(core): handle aliased index with no space in control flow migration (#52444)
The current regexp supposes that there is at least one space between the `=` and the aliased variable.
As it is possible to write `let myIndex=index`, this commit updates the regexp to handle such a case.

PR Close #52444
2023-10-31 13:45:00 -07:00
Mayuresh Dharwadkar abb4a9bad3 docs: Fix typos (#52442)
PR Close #52442
2023-10-31 13:44:14 -07:00
JoostK d517a75c9b fix(compiler-cli): use originally used module specifier for transform functions (#52437)
Prior to this change, the transform function would be referenced with a potentially
relative import into an external declaration file. Such imports are not portable
and should not be created in this context. This commit addresses the issue by threading
though the originally used module specifier by means of the `Reference` type.

Fixes #52324

PR Close #52437
2023-10-31 13:42:53 -07:00
JoostK 96fd73dc5d fix(compiler-cli): properly emit literal types in input coercion function arguments (#52437)
This commit fixes an issue where using literal types in the arguments of an input coercion
function could result in emitting invalid output, due to an assumption that TypeScript makes
when emitting literal types. Specifically, it takes the literal's text from its containing
source file, but this breaks when the literal type node has been transplanted into a
different source file. This issue has surfaced in the type-check code generator and is
already being addressed there, so this commit moves the relevant `TypeEmitter` class
from the `typecheck` module to the `translator` module, such that it can be reused for
emitting types in the type translator.

Fixes #51672

PR Close #52437
2023-10-31 13:42:53 -07:00
Swami fdd82857b1 docs(core): remove documentation of mutate function (#52435)
PR Close #52435
2023-10-31 13:42:15 -07:00
Kristiyan Kostadinov ddd9df68bb fix(compiler): allow decimals in defer block time values (#52433)
Fixes that our regex for parsing time values in defer blocks didn't allow for decimals. This isn't relevant for times in milliseconds, but it can be convenient to write something like `on timer(1.5s)`.

PR Close #52433
2023-10-31 13:39:27 -07:00
Andrew Scott 59b6ec6be8 docs(core): Deprecate ChangeDetectorRef.checkNoChanges (#52431)
The `checkNoChanges` method does not belong in the API of production interface. `checkNoChanges` is
limited to testing and should not be used in any application code. Test
code should use `ComponentFixture` instead of `ChangeDetectorRef`.
Additionally, it is not desirable to have the `checkNoChanges` API
available in a context where `detectChanges` is not run first.

DEPRECATED: `ChangeDetectorRef.checkNoChanges` is deprecated.

Test code should use `ComponentFixture` instead of `ChangeDetectorRef`.
Application code should not call `ChangeDetectorRef.checkNoChanges` directly.

PR Close #52431
2023-10-31 13:38:44 -07:00
Kristiyan Kostadinov 3f456821b5 refactor(compiler): account for comments when resolving implicit deferred triggers (#52449)
Adds some logic to skip over comments when resolving implicit `@defer` block triggers. This currently isn't a problem since we don't capture comments by default, but it may come up if we start capturing comments.

PR Close #52449
2023-10-31 13:35:00 -07:00
Kristiyan Kostadinov 2aaddd3f64 fix(compiler): allow comments between switch cases (#52449)
Fixes that the template parser was throwing an error if a comment is used directly inside an `@switch` block.

Fixes #52421.

PR Close #52449
2023-10-31 13:35:00 -07:00
Kristiyan Kostadinov 6c58252521 fix(compiler): compilation error when for loop block expression contains new line (#52447)
Fixes that the regex which captures the expression of a `@for` loop block wasn't accounting for line breaks.

Fixes #52446.

PR Close #52447
2023-10-31 13:30:42 -07:00
Andrew Scott d8280ace7d refactor(core): Remove RootViewRef<T> because it is the same as ViewRef<T> (#52430)
`RootViewRef<T>` extends `ViewRef<T>` and overrides 3 methods with behavior
that is identical to `ViewRef<T>`. This commit removes `RootViewRef<T>`
because it is not needed.

PR Close #52430
2023-10-31 13:22:56 -07:00
Andrew Scott b2c38399f9 refactor(core): rename ViewRef<T> to InternalViewRef<T> and remove existing InternalViewRef (#52430)
The `ViewRef<T>` interface extends `InternalViewRef` and is already not
part of the public API. There is no need for the extra `InternalViewRef`
interface. This confusing setup is likely leftover from the types
necessary to support both Ivy and ViewEngine.

PR Close #52430
2023-10-31 13:22:56 -07:00
Jeremy Elbourn 65ccaa4169 docs: minor revisions to control flow guide (#52411)
This commit makes some minor writing changes to the control flow guide for brevity, consistency, and active voice.

PR Close #52411
2023-10-31 13:20:35 -07:00
Matthieu Riegler 8156b3d4ec fix(http): Don't override the backend when using the InMemoryWebAPI (#52425)
When using `withFetch`,  the `PRIMARY_HTTP_BACKEND` token is set.

The InMemory Backend services will also set that token.

This means that providers order will matter and the latest on the list will be the one instantiated

PR Close #52425
2023-10-31 13:17:19 -07:00
Alex Rickabaugh 59a3475d77 refactor(core): global epoch to optimize non-live signal reads (#52420)
This commit adds a global epoch to the reactive graph, which can optimize
non-live reads.

When a non-live read occurs, a computed must poll its dependencies to check
if they've changed, and this operation is transitive and not cacheable.
Since non-live computeds don't receive dirty notifications, they're forced
to assume potential dirtiness on each and every read.

Using a global epoch, we can add an important optimization: if *no* signals
have been set globally since the last time it polled its dependencies, then
we *can* assume a clean state. This significantly improves performance of
large unwatched graphs when repeatedly reading values.

PR Close #52420
2023-10-31 13:12:18 -07:00
Charles Lyding 6e82ae5871 docs: update esbuild guide for v17 (#52407)
Update new build system guide to reflect stable status and the use of the `application` builder in existing projects.

PR Close #52407
2023-10-31 13:10:43 -07:00
Andrew Scott de3454a507 refactor(language-service): Improve autocomplete snippet of for block (#52405)
The `for` block has several parts which we know are required. This
commit improves the autocomplete snippet of the `for` block by adding
those required parts and providing placeholders.

PR Close #52405
2023-10-31 13:08:11 -07:00
Francesco Donzello 0451e7eabc docs: added missing imports for toSignal and toObservable (#52393)
PR Close #52393
2023-10-31 13:07:31 -07:00
Andrew Scott e355f05b9a refactor(language-service): Add quick info for built in control flow/blocks (#52386)
Adds hover info for:

* Defer blocks
* Triggers and trigger behavior keywords
* For loop empty block
* Track keyword in for loop block

resolves https://github.com/angular/vscode-ng-language-service/issues/1946

PR Close #52386
2023-10-31 12:56:45 -07:00
Dylan Hunn 595009ecf4 refactor(compiler): Implement the remaining defer on triggers (#52387)
The previous commits provided the scaffolding for `defer on`. In this commit, we build on that work, adding triggers for `immediate`, `timer`, `hover`, and `viewport`.

PR Close #52387
2023-10-31 12:45:18 -07:00
Dylan Hunn 1d7d53caa3 refactor(compiler): Introduce ConstCollectedExpr, and use it in defer on expressions (#52387)
Previously, we supported a `HasConst` trait, allowing an op to be const collected automatically. However, that approach had the shortcoming that each op could only collect a single constant.

Instead, we now provide a `ConstCollectedExpr`, which collects constants at the expression level, allowing ops to have multiple collectible consts.

Then, we use this new abstraction to support the `defer on` conditions.

PR Close #52387
2023-10-31 12:45:18 -07:00
Dylan Hunn 6a11e9e8b4 refactor(compiler): Implement defer conditions, and change the way slots are linked (#52387)
Previously, we had an "empty shell" implementation of defer conditions, and we used separate ops to represent secondary defer blocks.

Now, we have a real scaffolding for supporting the various defer conditions, and the secondary defer block information has been refactored onto the main defer op.

Additionally, to enable this, we refactor the way that using slot indices works. Instead of having a trait that causes users of slot indices to be linked to the allocated slot, we share a single `SlotHandle` object by reference. This allows an op to use slot information for more than one Xref at a time, and eliminates a layer of indirection.

Co-authored-by: Alex Rickabaugh <alxhub@users.noreply.github.com>

PR Close #52387
2023-10-31 12:45:18 -07:00
Pawel Kozlowski 877ba3e3c1 refactor(core): use performance API for control flow and standalone (#52370)
This commit adds a standard performance marker that can be viewed in Chrome dev tools and other tooling.
See more info at https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark

PR Close #52370
2023-10-31 12:43:51 -07:00
Andrew Scott 3861a73135 perf(core): Update LView consumer to only mark component for check (#52302)
This commit updates the reactive template and host binding consumers to
only mark their declaration components for refresh, but not parents/ancestors.

This also updates the `AfterViewChecked` hook to run when a component is
refreshed during change detection but its host is not. It is reasonable
to expect that the `ngAfterViewChecked` lifecycle hook will run when a
signal updates and the component is refreshed. The hooks are typically
run when the host is refreshed so without this change, the update to
not mark ancestors dirty would have caused `ngAfterViewChecked` to not
run.

resolves #14628
resolves #22646

resolves #34347 - this is not the direct request of the issue but
generally forcing change detection to run is necessary only because a
value was updated that needs to be synced to the DOM. Values that use
signals will mark the component for check automatically so accessing the
`ChangeDetectorRef` of a child is not necessary. The other part of this
request was to avoid the need to "mark all views for checking since
it wouldn't affect anything but itself". This is directly addressed by
this commit - updating a signal that's read in the view's template
will not cause ancestors/"all views" to be refreshed.

PR Close #52302
2023-10-31 12:37:56 -07:00
marktechson e74b578bd9 docs: update home page for live stream event (#52456)
PR Close #52456
2023-10-31 11:58:18 -07:00
Emma Twersky b14f0dc5d6 docs: make banner persist until 11/8 (#52432)
PR Close #52432
2023-10-31 11:57:08 -07:00
cexbrayat 4461cefa4f fix(core): handle trackBy and aliased index in control flow migration (#52423)
Currently, the migration always use `$index` in the migrated trackBy function, whereas this variable might be aliased.
The compiler then errors with:

```
error TS2339: Property '$index' does not exist on type 'UsersComponent'.

110 @for (user of users; track byId($index, user); let i = $index) {
```

This commit updates the migration to use the aliased index if there is one.

PR Close #52423
2023-10-27 16:16:46 -07:00
Miles Malerba ce54ead2b5 refactor(compiler): Split the i18n placeholder mega-phase (#52390)
The i18n placeholder resolution phase has accumulated too much logic,
making it difficult to understand. This commit refactors it into several
smaller phases to make it easier to manage.

I suspect this will undergo further refactoring in the near future as I
work through the ICU logic. In particular `ExtractedMessageOp` feels
like a bit of a grab bag of properties, and the i18n const collection
phase is also starting to get quite heavy. This refactor at least feels
like a good start.

PR Close #52390
2023-10-27 16:16:04 -07:00
Miles Malerba f6c2b39812 test(compiler): Enable passing i18n tests (#52390)
Enables a handful of i18n tests that are currently skipped, but pass if
enabled. Some of them require alternate golden files because of
inconsequential differences in the cost array order.

PR Close #52390
2023-10-27 16:16:04 -07:00
Matthieu Riegler 8e17dd719d refactor(localize): mark TargetMessage and MessageId as public. (#52418)
Both entries are exported in the `index.ts`

PR Close #52418
2023-10-27 16:06:11 -07:00
Andrew Kushnir 57b9798cf6 docs: update SSR docs (#52408)
This commit updates the `ssr.md` file to restructure (and simplify) SSR docs.

PR Close #52408
2023-10-27 15:16:14 -07:00
Jessica Janiuk 2579c7121e refactor(migrations): Add optional path param for control flow migration (#52403)
This adds the option to pass in a path to the control flow migration in order to run the migration against one single file.

PR Close #52403
2023-10-27 15:15:22 -07:00
Jessica Janiuk d7397fb29b fix(migrations): Ensure control flow migration ignores new block syntax (#52402)
This fix ensures that the control flow migration does not encounter any problems when new block sytax already exists in a template.

PR Close #52402
2023-10-27 13:02:03 -07:00
Jessica Janiuk e40e55d902 fix(migrations): Remove unhelpful parsing errors from the log (#52401)
When running the control flow migration, unhelpful ICU parsing errors were being logged and creating a bunch of noise for users.

PR Close #52401
2023-10-27 13:00:24 -07:00
Pawel Kozlowski ffd0b4c30b docs: control flow API reference (#52397)
Adds API reference for the new built-in control flow.

PR Close #52397
2023-10-27 12:56:51 -07:00
Jeremy Elbourn 5dd2822523 refactor(compiler): extract api docs for inherited members (#52389)
This commit expands docs extraction for classes and interfaces to include inherited members. This relies on the type checker to get the _resolved_ members of the type so that the extractor doesn't need to reason about inheritance rules, which can get tricky (especially with regards to method overloads).

PR Close #52389
2023-10-27 12:54:14 -07:00
Jeremy Elbourn 7e8a214474 refactor(compiler): extract decorator API docs (#52389)
This commit adds decorators to the extracted API docs. It makes some
very hard-coded assumptions about the pattern used to declare decorators
that's extremely specific to what the framework does today.

PR Close #52389
2023-10-27 12:54:14 -07:00
Jessica Janiuk e92eda9a31 docs: Add defer guide (#52380)
This adds a user guide for how to use defer.

PR Close #52380
2023-10-27 12:50:24 -07:00
Ivad 499c22251d docs(docs-infra): fix typo in tap into the observable tutorial example (#52359)
PR Close #52359
2023-10-27 12:49:39 -07:00
Evan Kleiner becbcdcff0 docs: add warning for localhost proxy (#51948)
PR Close #51948
2023-10-27 12:32:22 -07:00
Alex Rickabaugh 384d7aacd0 fix(core): replace assertion with more intentional error (#52427)
Issue #50320 shows that in some cases, updating a signal that's a dependency
of a template during change detection of that template can have several
adverse effects. This can happen, for example, if the signal is set during
the lifecycle hook of a directive within the same template that reads the
signal.

This can cause a few things to happen:

* Straightforwardly, it can cause `ExpressionChanged` errors.
* Surprisingly, it can cause an assertion within the `ReactiveLViewConsumer`
  to fail.
* Very surprisingly, it can cause change detection for an `OnPush` component
  to stop working.

The root cause of these later behaviors is subtle, and is ultimately a
desync between the reactive graph and the view tree's notion of "dirty" for
a given view. This will be fixed with further work planned for change
detection to handle such updates directly. Until then, this commit improves
the DX through two changes:

1. The mechanism of "committing" `ReactiveLViewConsumer`s to a view is
   changed to use the `consumerOnSignalRead` hook from the reactive graph.
   This prevents the situation which required the assertion in the first
   place.

2. A `console.warn` warning is added when a view is marked dirty via a
   signal while it's still executing.

The warning informs users that they're pushing data against the direction of
change detection, risking `ExpressionChanged` or other issues. It's a
warning and not an error because the check is overly broad and captures
situations where the application would not actually break as a result, such
as if a `computed` marked the template dirty but still returned the same
value.

PR Close #52427
2023-10-27 12:30:17 -07:00
Angular Robot 612986bcd3 build: update cross-repo angular dependencies (#52191)
See associated pull request for more information.

PR Close #52191
2023-10-26 14:12:34 -07:00
Alan Agius da1b71e0a5 docs: update SSR doc to reflect version 17 (#52398)
Changes to SSR doc

PR Close #52398
2023-10-26 12:30:59 -07:00
Jessica Janiuk 6a01d62b9d fix(migrations): fix broken migration when no control flow is present (#52399)
This addresses a bug that caused the control flow migration to crash when no control flow was present in the template.

PR Close #52399
2023-10-26 09:40:11 -07:00
Pawel Kozlowski befd197933 docs: guide for the built-in control flow (#52369)
This change adds a dedicated developer guide for the
built-in control flow.

PR Close #52369
2023-10-26 09:38:56 -07:00
Alan Agius 9376e7f05e refactor(common): avoid repetitive globalThis in global locales (#52394)
This changes reduced slightly the bundle size as `global` can be minified unlike direct usage of `globalThis`.

PR Close #52394
2023-10-26 09:37:41 -07:00
Andrew Kushnir 576338a9f5 build: update zone.js version to 0.14.1 (#52385)
This commit updates the `package.json` of the zone.js package.

PR Close #52385
2023-10-26 09:28:32 -07:00
Andrea Canciani 8580542153 refactor: fix a number of typos throughout the codebase (#52249)
Fix some typos such as `boostrap`, `propery` and more, both in
documentation and in code (comments, identifiers).

PR Close #52249
2023-10-25 16:51:24 -07:00
Susheel Thapa 57630c3769 docs: fixed typos (#52297)
PR Close #52297
2023-10-25 16:39:09 -07:00
Alan Agius f1d42cef87 docs: update to standalone and routing by default (#52293)
This commit covers several changes:
 - routing enabled by default
 - standalone by default

PR Close #52293
2023-10-25 16:37:52 -07:00
Pawel Kozlowski 48816e8c22 docs: remove the dev preview warning from the signals guide (#52375)
Signals are out of dev preview now.

PR Close #52375
2023-10-25 16:37:23 -07:00
Jessica Janiuk d3cc0e998f ci: Add members to fw-migrations pullapprove group (#52378)
This adds jessicajaniuk and AndrewKushnir to the fw-migrations reviewers list.

PR Close #52378
2023-10-25 16:36:56 -07:00
Jessica Janiuk 13b16b905f docs: Adjust defer api summary for prefetch clarity (#52379)
This updates language to clarify exactly how prefetch works in a defer block configuration.

PR Close #52379
2023-10-25 16:36:29 -07:00
Alan Agius 40f75364d4 release: cut the zone.js-0.14.1 release (#52374)
PR Close #52374
2023-10-25 14:39:44 -07:00
Dylan Hunn 2b0698def0 release: cut the v17.0.0-rc.1 release 2023-10-25 14:35:56 -07:00
Andrew Scott 6858149eb1 fix(router): RouterTestingHarness should throw if a component is expected but navigation fails (#52357)
The `RouterTestingHarness` should throw an error if the call to `navigateByUrl`
expects a component to be activated but the navigation failed.

fixes #52344

PR Close #52357
2023-10-25 11:04:26 -07:00
AleksanderBodurri a803c89a21 fix(core): load global utils before creating platform injector in the standalone case (#52365)
This is needed so that the providers configured on platform creation are picked up by the injector profiler.

PR Close #52365
2023-10-25 10:51:24 -07:00
AleksanderBodurri 957effa404 fix(core): get root and platform injector providers in special cases (#52365)
Previously, because the platform injector does not have a provider container, this API would fail. Now, we account for this case specifically by returning the found providers immediately, without trying to calculate their importpaths.

Also previously, in the case where a boostrapped standalone component did not import any feature modules, the environment injector connected to that bootstrapped component would be the root injector configured by `bootstrapApplication`. This injector is configured through a `providers` array instead of an `imports` array, and also does not have a provider container. Similarly to the platform case, we account this for this by returning the found providers immediately if there is no provider container for our standalone component.

PR Close #52365
2023-10-25 10:51:23 -07:00
AleksanderBodurri ea6d75291f fix(core): emit provider configured event when a service is configured with providedIn (#52365)
Previously this case was missed by the default framework injector profiler. Now in ngDevMode this event emits correctly when a service is configured with `providedIn`. This includes the case where injection tokens are configured with a `providedIn`.

This commit also includes unit tests for this new case in the injector profiler.

PR Close #52365
2023-10-25 10:51:23 -07:00
Osama Heykal 403d217a59 docs: added missing 'next steps' sections (#52229)
PR Close #52229
2023-10-25 10:50:46 -07:00
Jessica Janiuk 1acff00468 docs: Add @defer API reference documentation (#52336)
This adds the initial API reference documentation for the new defer blocks.

PR Close #52336
2023-10-25 10:47:49 -07:00
Jessica Janiuk 4c878f90d2 fix(migrations): Add support for nested structures inside a switch statement (#52358)
This updates the code to handle switches more elegantly in line with how the other blocks are handled. This allows nesting to be handled just like other blocks.

PR Close #52358
2023-10-25 09:39:47 -07:00
Alvaro Junqueira 938007df9d docs: the Component import is not being used (#52323)
PR Close #52323
2023-10-25 09:35:24 -07:00
Naaajii cd903cc9f4 docs(docs-infra): fix codeblock snippet for APP_INITIALIZER (#52354)
fixes broken code snippet for standalone application using APP_INITIALIZER

PR Close #52354
2023-10-25 09:34:48 -07:00
Matthieu Riegler 9b32d9148e docs(core): fix the language code (#52352)
PR Close #52352
2023-10-25 09:32:19 -07:00
mgechev 0536b61bb8 docs: update the roadmap to reflect the progress as of v17 (#52356)
PR Close #52356
2023-10-25 09:31:46 -07:00
Alan Agius df68ad4731 fix(zone.js): use globalThis instead of global and window (#52367)
`globalThis` global property contains the global `this` value, which is usually akin to the global object. This is needed for better compatibility with CloudFlare workers were global nor window are defined as globals.

PR Close #52367
2023-10-25 09:31:18 -07:00
Alan Agius 6d0362cb44 refactor(core): modernize $localize checks (#52368)
Use nullish coalescing.

PR Close #52368
2023-10-25 09:30:51 -07:00
Alan Agius 1660f667e0 refactor(common): modernize code in locale file (#52368)
Use `globalThis` global property contains the global this value, which is usually akin to the global object.

PR Close #52368
2023-10-25 09:30:51 -07:00
Andrew Scott 1dd8558f82 fix(core): Ensure backwards-referenced transplanted views are refreshed (#51854)
This commit runs change detection in a loop while there are still dirty
views to be refreshed in the tree. At the moment, this only applies to
transplanted views but will also apply to views with changed signals.

fixes angular#49801

PR Close #51854
2023-10-24 14:50:19 -07:00
Kristiyan Kostadinov c9b1ddff4d fix(migrations): handle nested classes in control flow migration (#52309)
Fixes that the control flow migration was only processing top-level classes. Nested classes could come up during unit tests.

PR Close #52309
2023-10-24 14:34:16 -07:00
Kristiyan Kostadinov 54bc384661 fix(migrations): handle nested classes in block entities migration (#52309)
Fixes that the block entities migration was only processing top-level classes. Nested classes could come up during unit tests.

PR Close #52309
2023-10-24 14:34:16 -07:00
Alan Agius bd9e91ecf7 perf(http): reduce data transfer when using HTTP caching (#52347)
This commit reduces the property size in the http transfer cache to reduce the page payload.

Before
```html
<script id="ng-state" type="application/json">
{
  "4155228514": {
    "body": "....",
    "headers": {},
    "status": 200,
    "statusText": "OK",
    "url": "http://foo.com/assets/media.json",
    "responseType": "json"
  },
}
</script>
```

Now
```html
<script id="ng-state" type="application/json">
{
  "4155228514": {
    "b": "....",
    "h": {},
    "s": 200,
    "st": "OK",
    "u": "http://foo.com/assets/media.json",
    "rt": "json"
  },
}
</script>
```

PR Close #52347
2023-10-24 14:33:26 -07:00
Joey Perrott 4e00851c04 build: lock file maintenance (#52353)
See associated pull request for more information.

PR Close #52353
2023-10-24 14:29:17 -07:00
Jessica Janiuk 54fed68bbf fix(migrations): Fixes the root level template offset in control flow migration (#52355)
When migrating an ng-template later on in a file, the migrationResult was not being reset to zero and causing offsets to be double applied due to ng-template nodes being included in the migration loop.

PR Close #52355
2023-10-24 14:22:32 -07:00
Dylan Hunn 114a2f8153 refactor(compiler): Don't double-create pipes in switch cases (#52289)
Previously, we would emit *two* pipe creation instructions for each pipe in a switch case. This is because we were visiting both the transformed and raw versions of the pipe bindings.

Now, we clear the raw case expressions array after generating the transformed test expression.

Also, we introduce some new goldens, because our pipe creation order is harmlessly different.

PR Close #52289
2023-10-24 11:07:52 -07:00
Dylan Hunn 7002cfc811 refactor(compiler): Support content projection source maps (#52289)
The `projection` op should map onto the entire corresponding `ng-content`.

PR Close #52289
2023-10-24 11:07:52 -07:00
Dylan Hunn 0d23baa740 refactor(compiler): Update pipe test golden for alternative create order (#52289)
We roughly attempt to match TemplateDefinitionBuilder's pipe creation order, by placing pipe creation instructions after their target elements. However, we cannot fully emulate the "inside-out" ordering TemplateDefinitionBuilder uses when multiple pipes apply to one element, because TemplateDefinitionBuilder creates the pipes as expressions are visited, from the leaves up. Our order is perfectly adequate though.

We also add a non-compatibility-mode ordering, which just appends them to the end of the create block. This is better because it allows for more chaining opportunities.

PR Close #52289
2023-10-24 11:07:52 -07:00
Dylan Hunn 3c08f91a01 refactor(compiler): Fix a special case involving var counting for singleton propertyInterpolate (#52289)
Singleton property interpolation instructions consume only one variable, but are still emitted as an interpolation instruction (they cannot be collapsed because `propertyInterpolate` implicitly stringifies its argument.)

PR Close #52289
2023-10-24 11:07:52 -07:00
Dylan Hunn 20993f7a87 refactor(compiler): The projection instruction takes an array literal (#52289)
We were incorrectly emiting a extracted constant pool index for the final argument of the projection instruction. It actually takes an array literal.

(N.B.: This means we re-create the array every time! We should probably modify the runtime to use a const index for this.)

Additionally, we alter the projection op to not extend the element op base type.

PR Close #52289
2023-10-24 11:07:52 -07:00
Dylan Hunn 6ade8c9c8c refactor(compiler): Improve the ordering of update ops (#52289)
The correct order of attributes and properties is:

1. Interpolated properties
2. Interpolated attributes
3. Non-interpolated properties
4. Non-interpolated attributes

This includes an additional nuance: singleton attribute interpolations, such as `[attr.foo]="{{bar}}"`, will be "collaped" into a simple `attribute` instruction. However, this is *not* the case for singleton property interpolations! The ordering phase must take this nuance into account to match the TemplateDefinitionBuilder order.

After the project lands, it might be nice to also collapse singleton property interpolations.

PR Close #52289
2023-10-24 11:07:51 -07:00
Dylan Hunn e4df686ec6 refactor(compiler): Order elements before other phases (#52289)
Previously, we ran the ordering phase near the end of the compilation. However, this meant that phases like slot assignment and variable offset assignment would happen first, and then the nice, monotonically-increasing orders would be scrambled by the reordering.

It's much more intelligible to order first, and then perform these assignments. However, to make this happen, some modifications to the ordering phase are required. In particular, we can no longer rely on `advance` instructions to break up orderable groups.

PR Close #52289
2023-10-24 11:07:51 -07:00
Dylan Hunn 1fa5020739 refactor(compiler): Imitate TemplateDefinitionBuilder's variable offset assignment order (#52289)
Many instructions consume variable slots, which are used to persist data between update runs. For top-level instructions, the offset into the variable data array is implicitly advanced, because those instructions always run.

However, instructions in non-top-level expressions cannot be assumed to run every time, because they might be conditionally executed. Therefore, they cannot implicitly advance the offset into the variable data, and must be given an explicitly assigned variable offset.

TemplateDefinitionBuilder assigned offsets top-to-bottom for all instructions *except* pure functions. Pure functions would be assigned offsets lazily, on a second pass.

Template Pipeline can now imitate this behavior, when in compatibility mode: pure functions are assigned offsets on a second pass.

This also makes the "variadic var offsets" phase unnecessary -- the new approach is more general and correct.

PR Close #52289
2023-10-24 11:07:51 -07:00
Dylan Hunn d2df9fb59b refactor(compiler): Use already available context in closures, instead of saving it (#52289)
Previously, inside an event listener, template pipeline would always save the context from restoring a view, e.g.

```
const restored_ctx = r0.ɵɵrestoreView(s);
```

This is usually correct! However, consider the case of a listener in the template's root view. The appropriate context will already be available via closure capture, and we can just use it (as `ctx`).

Now, the context resolution phase understands that we don't need to use the restored view's saved context if we would have access to it by closure.

Note: we also create a new golden, because the const array is in a harmlessly different order.

PR Close #52289
2023-10-24 11:07:51 -07:00
Dylan Hunn fe82292583 refactor(compiler): Empty context variable reads are $implicit (#52289)
Previously, the template pipeline did not handle "empty" reads gracefully: it would emit syntactically invalid reads of empty properties. Now we read `$implicit`.

This allows us to enable a test that relies on `$implicit`. However, we also have to create another golden, because our variable inlining is more aggressive.

PR Close #52289
2023-10-24 11:07:51 -07:00
Dylan Hunn 439e83a974 refactor(compiler): Update golden for pipe binding collapsing (#52289)
We currently allow elements to be collapsed around pipe creation instructions. TemplateDefinitionBuilder disallows this, but only sometimes. Collapsing in this case is actually less generated code, and it's OK to allow it.

PR Close #52289
2023-10-24 11:07:51 -07:00
Dylan Hunn 4ade911e66 refactor(compiler): Enable already-passing tests (#52289)
Some tests are already passing, but were not enabled at the time they were previously fixed.

PR Close #52289
2023-10-24 11:07:50 -07:00
Alan Agius d2f8412322 refactor(platform-server): replace global usages with globalThis (#52342)
This commit updates a number of `global` usages to `globalThis` to better support other platforms like CF workers

PR Close #52342
2023-10-24 09:26:50 -07:00
Alan Agius 00dee8384c refactor(platform-server): remove dependency between @angular/platform-server and @angular/platform-server/init (#52342)
Previously, the cross entry-point dependency was created to share the `setDomTypes` function. This however, causes extra transformations "linking" during the application build since other Angular code in `@angular/platform-server` is pulled in.

With this commit we remove the cross dependency and thus remove the need for extra transformations in the server polyfill bundle with the result of having a slightly faster build.

See: https://github.com/angular/angular-cli/pull/26113

PR Close #52342
2023-10-24 09:26:50 -07:00
Alex Castle ceb7f1eaf0 docs(common): add preconnect generation to NgOptimizedImage docs (#52248)
add a note on automated preconnect generation to the NgOptimizedImage docs' preconnect section, and add a related FAQ entry.

PR Close #52248
2023-10-24 09:26:06 -07:00
Andrew Kushnir f069b3a71a refactor(core): ignore after and minimum when transition between states in tests (#52314)
This commit updates the logic to ignore `after` and `minimum` conditions when `DeferBlockFixture.render` method is used in tests.

Resolves #52313.

PR Close #52314
2023-10-24 09:25:14 -07:00
Andrew Kushnir ec1e652f8a refactor(core): report @defer errors using ErrorHandler (#52320)
This commit updates the code to report errors via `ErrorHandler` instance.
For dependency loading problems, errors are reported only when `@error` block is not provided.

PR Close #52320
2023-10-24 09:24:43 -07:00
cexbrayat a87586538a docs: remove test.ts file in aio (#52327)
Same as 7ba5dc87ed, but for aio
This file is not necessary since CLI v15+

PR Close #52327
2023-10-24 09:23:02 -07:00
Jessica Janiuk 6a3a7b0d13 refactor(migrations): Fixes offset calculations for nesting (#52332)
This updates offset to handle pre and post offset properly for nested situations, rather than relying on solely nestCount. This should properly apply offset calculations at the right time to handle any nested situation.

PR Close #52332
2023-10-24 09:22:26 -07:00
Andrew Scott 0c1cf7ac6b test(core): fix test observing broken OnPush checkNoChanges (#52337)
The test is actually observing that OnPush results in a view not being checked
when checkNoChanges runs.

PR Close #52337
2023-10-24 09:21:55 -07:00
Doug Parker 394c0fb719 docs: fix typo (#52339)
PR Close #52339
2023-10-24 09:21:15 -07:00
Kristiyan Kostadinov 6795cccbbb fix(compiler): account for type-only imports in defer blocks (#52343)
Fixes that `@defer` blocks didn't account for type-only imports which could cause the import to be considered as not deferrable.

PR Close #52343
2023-10-24 09:20:49 -07:00
Angular Robot 96ad3bf065 docs: update Angular CLI help [17.0.x] (#52331)
Updated Angular CLI help contents.

Closes #52280

PR Close #52331
2023-10-23 13:45:59 -07:00
Dylan Hunn 3f7d081112 refactor(compiler): Support track functions and context variables in @for loops for template pipeline (#52001)
The template pipeline can now generate track functions, and extract them into the constant pool (or optimize them if needed). Additionally, context variables such as `$index` can be used inside track functions and for loop bodies.

PR Close #52001
2023-10-23 13:44:30 -07:00
Dylan Hunn 2560115793 refactor(compiler): Support @for blocks in template pipeline (#52001)
Add support for `repeaterCreate` and `repeater` instructions. Correctly count decls and vars, and support primary and empty blocks.

`track` functions are not yet extracted.

PR Close #52001
2023-10-23 13:44:30 -07:00
Dylan Hunn 9060cebc02 refactor(compiler): Create an alias concept on views (#52001)
View compilations previously had context variables, which were variables available in the view that would result in a property read on the context object.

We now also support the notion of aliases. An alias is a variable available in the view compilation, which might be derived from a context variable, which it may reference by name. It is always inlined at all usage sites, and therefore is not allowed to depend on the current context.

Under the hood, aliases rely on the new `AlwaysInline` mode.

Co-authored-by: Alex Rickabaugh <alxhub@users.noreply.github.com>

PR Close #52001
2023-10-23 13:44:30 -07:00
Dylan Hunn db81193ce6 refactor(compiler): Create an AlwaysInline variable mode (#52001)
The template pipeline now supports lexical variables that are always inlined into their call sites, even if multiple call sites exist.

An `AlwaysInline` variable may not rely on the current context, because it will potentially be inlined at several different locations.

Co-authored-by: Alex Rickabaugh <alxhub@users.noreply.github.com>

PR Close #52001
2023-10-23 13:44:29 -07:00
Emma Twersky af6a75a837 docs: add event banner (#52334)
PR Close #52334
2023-10-23 13:40:02 -07:00
Gerald Monaco f206d1041f refactor(core): use performance API for afterRender feature logging (#52235)
Add a standard performance marker that can be viewed in Chrome Dev Tools and other tooling. See more info at https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark

PR Close #52235
2023-10-23 12:03:42 -07:00
Alan Agius f5aed23924 docs: update app-shell docs to reflect v17 (#52294)
Update app-shell docs to reflect v17 changes

PR Close #52294
2023-10-23 12:03:15 -07:00
Andrew Scott 8473675db2 refactor(core): Remove duplicate null check in helper function (#52312)
This commit is a small refactor to remove the top-level null check on the
variable that's already checked for null in the main loop

PR Close #52312
2023-10-23 12:02:48 -07:00
Alan Agius 0c66e2424c fix(platform-server): resolve relative requests URL (#52326)
Prior to this commit relative HTTP requests were not being resolved to absolute even thought the behaviour is documented in https://angular.io/guide/universal#using-absolute-urls-for-http-data-requests-on-the-server.

This caused relative HTTP requests to fail when done on the server because of missing request context. This change is also required to eventually support HTTP requests handled during prerendering (SSG).

Closes #51626

PR Close #52326
2023-10-23 12:02:21 -07:00
Dylan Hunn 9d565cd6d6 fix(language-service): Autocomplete block keywords in more cases (#52198)
Previously, autocompletions were not available in two main cases. We correct them.

1. Autocompletions immediately after `@` were usually not working, for example `foo @|`. We fix this by causing the lexer to not consider the `@` part of the text node.
2. Autocompletions such as `@\nfoo`, where a newline follows a bare `@`, were not working because the language service visitor considered us inside the subsequent text node. We fix this by adding a block name span for the block keyword, and special-case whether we are completing inside the name span. If we are, we don't continue to the following text node.

PR Close #52198
2023-10-23 12:01:52 -07:00
Andrew Kushnir 182d574fa3 refactor(core): defer triggers cleanup (#52291)
This commit adds the logic to cleanup all triggers once defer block is triggered.

When a trigger is created, its cleanup function is stored alongside other defer block info. Prefetch and regular triggers are store in different slots, since we need to invoke them at different time.

PR Close #52291
2023-10-23 12:00:19 -07:00
Joey Perrott 569f57daa9 build: update lockfile (#52270)
Update lockfiles throughout

PR Close #52270
2023-10-23 09:30:39 -07:00
Andrew Kushnir dfc77e5f58 refactor(core): use performance API for defer feature (#52288)
This commit adds a standard performance marker that can be viewed in Chrome dev tools and other tooling. See more info at https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark

PR Close #52288
2023-10-23 09:29:37 -07:00
Andrew Kushnir 6005fe2d70 refactor(core): use performance API for hydration-related features (#52288)
This commit adds a standard performance marker that can be viewed in Chrome dev tools and other tooling. See more info at https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark

PR Close #52288
2023-10-23 09:29:37 -07:00
Kristiyan Kostadinov 1dc14d9853 perf(core): avoid unnecessary callbacks in after render hooks (#52292)
A few performance improvements and code cleanups in the after render hooks:
1. We were wrapping each `destroy` callback in another callback, because it was typed as `|undefined`. This is unnecessary, because the callback is guaranteed to exist. These changes pass the `destroy` function around directly and avoid the additional callback.
2. In server platforms we were recreating a noop `AfterRenderRef` on each invocation. We can save some memory by returning the same one.
3. Reworks the `AfterRenderCallback` so that it injects `NgZone` and `ErrorHandler` itself, instead of expecting them to be passed in. This reduces the amount of repetition in the code.

PR Close #52292
2023-10-23 09:29:10 -07:00
Alan Agius 129b5d8bde docs: update prerendering guide (#52300)
This commits update the prerendering docs with the version 17 changes.

PR Close #52300
2023-10-23 09:28:42 -07:00
Andrew Kushnir 7a064e372a refactor(core): avoid invoking IntersectionObserver in defer triggers on the server (#52306)
This commit updates the logic to make sure that DOM-specific triggers do not produce errors on the server.

Resolves #52304.

PR Close #52306
2023-10-23 09:28:15 -07:00
Kristiyan Kostadinov 81c315ec6e fix(compiler): template type checking not reporting diagnostics for incompatible type comparisons (#52322)
In #52110 the compiler was changed to produce `if` statements when type checking `@switch` in order to avoid a bug in the TypeScript compiler. In order to avoid duplicate diagnostics, the main `@switch` expression was ignored in each of the `@case` comparisons. This appears to have caused a regression where comparing incompatible types wasn't being reported anymore.

These changes resolve the issue by wrapping the expression in parentheses which allows the compiler to report comparison diagnostics while ignoring diagnostics in the expression itself.

Fixes #52315.

PR Close #52322
2023-10-23 09:27:24 -07:00
Andrew Scott 4dce8766f8 fix(router): Ensure newly resolved data is inherited by child routes (#52167)
The current way of computing a route's params and data recomputes
inherited data from the inheritance root every time. When the
inheritance strategy is "emptyOnly", this isn't necessarily the root of
the tree, but some point along the way (it stops once it reaches an
ancestor route with a component).

Instead, this commit updates parameter inheritance to only inherit data
directly from the parent route (again, instead of recomputing all
inherited data back to the inheritance root). The only requirement for
making this work is that the parent route data has already calculated
and updated its own inherited data. This was really already a
requirement -- parents need to be processed before children.

In addition, the update to the inheritance algorithm in this commit
requires more of an understanding that a resolver running higher up in
the tree has to propagate inherited data downwards. The previous
algorithm hid this knowledge because resolvers would recompute inherited
data from the root when run. However, routes that did not have resolvers
rerun or never had resolvers at all would not get the updated resolved data.

fixes #51934

PR Close #52167
2023-10-19 10:26:28 -07:00
Alex Castle 0b4c013aac refactor(common): use performance API for NgOptimizedImage feature logging (#52205)
add a standard performance marker that can be viewed in Chrome dev tools and other tooling. See more info
at https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark

PR Close #52205
2023-10-19 09:39:06 -07:00
Sylvain DEDIEU 958346107f refactor(core): no need for signals property in component interface since inherited from directive (#52039)
Removed the signals property definition from the Component interface since it already exists in the Directive interface and Component inherits from Directive

PR Close #52039
2023-10-19 09:37:57 -07:00
Andrew Kushnir a207421ada refactor(core): check "skip hydration" condition only when hydration info is present (#52221)
This commit refactors a couple places to improve performance:

* avoid checking parent tree if a current node has "skip hydration" flag
* avoid calling `isInSkipHydrationBlock` if there is no hydration info present

PR Close #52221
2023-10-19 09:27:55 -07:00
Alan Agius 7cc9f193fe refactor(platform-server): replace Node.js URL API usage (#52231)
This commit replace the usage of URL node API to WHATWG URL API.

See: https://nodejs.org/api/url.html#the-whatwg-url-api

PR Close #52231
2023-10-19 09:26:45 -07:00
Kristiyan Kostadinov 30f993a356 test(core): clean up unnecessary nesting in old tests (#52239)
A lot of our tests are wrapped in `{}` which serves no purpose, aside from increasing the nesting level and, in some cases, causing confusion. The braces appear to be a leftover from a time when all tests were wrapped in a `function main() {}`. The function declaration was removed in #21053, but the braces remained, presumably because it was easier to search&replace for `function main()`, but not to remove the braces at the same time.

PR Close #52239
2023-10-19 09:26:15 -07:00
Etienne FRANK aaf00092c8 docs(forms): replace fb with formBuilder (#52261)
Shorten variable names isn't a good practice. To avoid spreading it, we removed it from Angular's documentation.

PR Close #52261
2023-10-19 09:24:40 -07:00
Andrew Scott 44f08baf63 docs: Update control flow diagnostic to direct devs to built-ins (#52268)
The commit adds messaging to the control flow template diagnostic to direct developers to the new
built-in control flow syntax in Angular.

PR Close #52268
2023-10-19 09:24:08 -07:00
Matthieu Riegler b4c1ffbfa1 refactor(forms): Log a warning when FormGroup keys include a dot. (#50642)
Due to the dotted synthax to resolve controls, keys in FormGroups cannot include dots.

fixes #50608

PR Close #50642
2023-10-18 12:11:25 -07:00
Andrew Kushnir 4567a090c9 test(core): add benchmark for hydration runtime logic (#52206)
This commit adds a benchmark for hydration runtime logic, which contains 2 parts: a baseline (create DOM nodes from scratch) and a main scenario (DOM matching instead of re-creating nodes).

PR Close #52206
2023-10-18 12:07:13 -07:00
Andrew Kushnir fc3cde6cff test(core): add benchmark for defer runtime logic (#52222)
This commit adds a benchmark for `@defer` runtime logic and uses `@if` as a baseline.

PR Close #52222
2023-10-18 12:06:45 -07:00
Kristiyan Kostadinov 1a8a5cf68f refactor(core): avoid duplicate work when checking if hydration is disabled (#52253)
Minor refactor to avoid lowercasing the `ngSkipHydration` attribute on each call. Since it's static, we can do it once ahead of time.

PR Close #52253
2023-10-18 12:06:03 -07:00
cexbrayat 866d0f422d refactor(core): remove test.ts files in integration tests (#52109)
These files are not necessary since CLI v15+

PR Close #52109
2023-10-18 11:52:55 -07:00
Quentin b00dd23846 docs: fix typo (#52233)
Fix word: standalone

PR Close #52233
2023-10-18 11:51:49 -07:00
Jeevan Mahesha 6fbe371b2c docs: updated the export default router to satisfies instead of typecast (#52269)
PR Close #52269
2023-10-18 11:51:20 -07:00
Pawel Kozlowski edd060efbf release: cut the v17.0.0-rc.0 release 2023-10-18 18:59:22 +02:00
Andrew Kushnir 1091239d2d refactor(core): remove developer preview note from hydration stats note (#52197)
This commit updates the message that we output in the console (once hydration is completed) to drop the reference to the developer preview.

PR Close #52197
2023-10-18 18:21:49 +02:00
Andrew Kushnir dc6ca34d4c docs: remove developer preview note from hydration feature in docs (#52197)
PR Close #52197
2023-10-18 18:21:49 +02:00
Andrew Kushnir 33bd523be3 docs: promote provideClientHydration and related symbols to stable (#52197)
This commit removes the `@developerPreview` annotation from the `provideClientHydration` function and related symbols, promoting them to stable.

PR Close #52197
2023-10-18 18:21:49 +02:00
Andrew Kushnir 08553046bf docs: promote renderApplication to stable (#52197)
This commit removes the `@developerPreview` annotation from the `renderApplication`, promoting it to stable.

PR Close #52197
2023-10-18 18:21:49 +02:00
Andrew Kushnir 603f30cab0 docs: promote withFetch and FetchBackend to stable (#52197)
This commit removes the `@developerPreview` annotation from the `withFetch` and `FetchBackend` APIs, promoting them to stable.

PR Close #52197
2023-10-18 18:21:49 +02:00
vivekkoya eccbf36dbd docs: Update BAZEL.md with correct bazelrc documentation link (#52090)
PR Close #52090
2023-10-18 18:15:04 +02:00
Quentin 419855629d docs: fix typo (#52258)
Fix typo in aio/content/errors/NG05104.md file

PR Close #52258
2023-10-18 18:13:44 +02:00
Angular Robot 37768da011 build: update all non-major dependencies (#51897)
See associated pull request for more information.

PR Close #51897
2023-10-18 17:49:46 +02:00
Angular Robot 1e941f6f84 build: update scorecard action dependencies (#52126)
See associated pull request for more information.

PR Close #52126
2023-10-18 17:45:31 +02:00
Angular Robot 920f072824 build: update io_bazel_rules_sass digest to 425ecaf (#52173)
See associated pull request for more information.

PR Close #52173
2023-10-18 17:37:09 +02:00
Pawel Kozlowski 37d627dbd4 perf(core): minimze trackBy calculations (#52227)
Re-organize code to minimize number of calls to the
trackBy function.

PR Close #52227
2023-10-18 14:04:24 +02:00
Pawel Kozlowski 1032c1e1a5 perf(core): cache LiveCollectionLContainerImpl (#52227)
This change avoid re-creation of the LiveCollectionLContainerImpl instance
every time the repeater runs (on every change detection).

PR Close #52227
2023-10-18 14:04:24 +02:00
Pawel Kozlowski 8e4a7ab52b perf(core): avoid repeated access to LContainer and trackBy calculation (#52227)
Assuming that the trackBy function is a pure derivation from the collection
object and its index, we can skip trackBy calculation if items in the live
and new colelction have the same identity and index. Additionally this change
minimizes access to the LContainer array.

PR Close #52227
2023-10-18 14:04:24 +02:00
Pawel Kozlowski 3fec271fa3 refactor(core): move key calculation in list reconciler (#52227)
We can speedup items comparision by having access to raw values
and delay key calculation in certain conditions.

PR Close #52227
2023-10-18 14:04:24 +02:00
Pawel Kozlowski f9ef762bc7 refactor(core): remove the at method from the LiveCollection type (#52227)
The at operation is private and doesn't have to be part of the public interface.

PR Close #52227
2023-10-18 14:04:24 +02:00
Miles Malerba e87ac245e0 docs(compiler): Clarify the purpose of i18n param resolution time (#52250)
Adds some additional doc comments clarifying when the `Creation`
resolution time is used vs the `Postprocessing` resolution time.

PR Close #52250
2023-10-18 14:00:04 +02:00
Miles Malerba 39a6f62551 refactor(compiler): Wrap bare ICUs in an i18n block (#52250)
ICUs can be used outside of an i18n block. In this case the ICU should
be automatically wrapped in a new i18n block. This commit adds a new
phase to handle wrapping these bare ICUs.

PR Close #52250
2023-10-18 14:00:04 +02:00
Miles Malerba 96267922fe refactor(compiler): Resolve ICU params during i18n post-processing (#52250)
ICU params in i18n messages are now resolved in the post-processing call
rather than in the initial message creation. This matches the output
generated by TemplateDefinitionBuilder.

PR Close #52250
2023-10-18 14:00:04 +02:00
Miles Malerba ba37440cbf refactor(compiler): Add support for ingesting ICUs (#52250)
ICUs are now ingested by adding ops to both the creation and update IR.
Both of these ops are ultimately removed before reification, but they
are needed to coordinate and link data between the creation and update
ops. This is done in a new ICU extraction phase that removes both ICU
ops and adds an i18nExpr op to the update IR.

PR Close #52250
2023-10-18 14:00:04 +02:00
Alan Agius 5d3f66d1a6 Revert "perf(platform-browser): disable styles of removed components instead of removing (#51808)" (#52238)
This reverts commit 65786b2b96 due to an oberved perf regression in Pantheon.

See: http://b/303667704

PR Close #52238
2023-10-18 11:34:21 +02:00
Quentin 36993f3459 docs: fix typo (#52247)
Fix word: corresponding

PR Close #52247
2023-10-18 11:29:41 +02:00
Angular Robot d811d82a12 build: update babel dependencies to v7.23.2 (#52236)
See associated pull request for more information.

PR Close #52236
2023-10-17 18:11:09 +02:00
Quentin c6e4cf12f0 docs: fix typo (#52232)
Fix word: privileges

PR Close #52232
2023-10-17 18:10:05 +02:00
Jeremy Elbourn a318e6ac7c refactor(compiler): extract generic info for api reference (#52204)
This commit extracts the API reference info for generic parameters for
classes, methods, interfaces, and functions. It includes any constraints
and the default type if present.

PR Close #52204
2023-10-17 12:29:24 +02:00
Alan Agius c9cde3ab10 perf(platform-browser): only append style element on creation (#52237)
Prior to this change the style element was appended to host element multiple times. Whilst the actual element was not added multiple to the DOM multiple times. This causes a performance regression and it caused repainting.

This can be observed in the below benchmark.

```js
(() => {
  const time = (name, fn) => {
    const t = performance.now();
    fn();
    console.log(name, performance.now() - t);
  }

  const s = document.createElement("style");
  s.textContent = "@layer x{} @font-face { font-family: foo; }";

  time("append and enable", () => {
    document.head.append(s);
    s.disabled = false;
  });
  time("compute body color", () => {
    getComputedStyle(document.body).color;
  });
  time("compute body layout", () => {
    document.body.offsetTop;
  });
  time("append and disable", () => {
    document.head.append(s);
    s.disabled = false;
  });
  time("compute body color", () => {
    getComputedStyle(document.body).color;
  });
  time("compute body layout", () => {
    document.body.offsetTop;
  });
})();
```

Output
```
append and enable 0.20000000298023224
compute body color 0.7999999970197678
compute body layout 2.899999998509884
append and disable 0.10000000149011612
compute body color 0.7000000029802322
compute body layout 2.2999999970197678
```

When commenting the 2nd `document.head.append(s);`, the results are slightly different and we can see that calling `getComputedStyle` does not incur any performance impact this is a result of no repainting.

```
append and enable 0.10000000149011612
compute body color 0.7999999970197678
compute body layout 3.1999999955296516
append and disable 0.10000000149011612
compute body color 0
compute body layout 0
```

Pantheon benchmarks: http://docs/spreadsheets/d/1iLRLGCmVYZHuVRdI7dO_WM7wnQ1DvkS-tJzi-0-u1KY?resourcekey=0-kwtrf0nbAhcPqAGdqbdz4g#gid=0

PR Close #52237
2023-10-17 11:18:02 +02:00
Miles Malerba 35fd10a9fd test(compiler): Update golden partial file (#52202)
Updates the golden partial file to account for the newly added test

PR Close #52202
2023-10-17 10:13:23 +02:00
Miles Malerba e760fd7865 refactor(compiler): Fix handling of structural directive on i18n element (#52202)
Placing a structural directive on an element with an `i18n` attribute
was generating too many i18n blocks. This was due to both the element
and the template generating their own i18n block. To fix the issue, we
no longer generate top-level i18n blocks for structural directive
templates.

PR Close #52202
2023-10-17 10:13:23 +02:00
Miles Malerba 4e135947e6 refactor(compiler): Fix handling of sturctural directive on ng-template (#52202)
Structural directives on an ng-template (e.g. <ng-template *ngIf>) were
being assigned the wrong tag name ('ng-template' instead of null).

PR Close #52202
2023-10-17 10:13:23 +02:00
Miles Malerba 6fe4f44b1d refactor(compiler): Fix i18n placeholders for slef-closing elements (#52195)
Fixes handling of placeholders for self-closing tags. Self-closing tags
set a combined value for the start tag placeholder, rather than separate
values for the start and close placeholders.

This commit also enables a number of now passing tests. For some of
these tests I had create a separate golden file due to the different
ordering of the const array. In the template pipeline, i18n and
attribute const collection happen in different pahses and we therefore
get a different order than TemplateDefinitionBuilder, which collected
everything in one pass. The order should not affect the overall behavior.

PR Close #52195
2023-10-16 19:25:05 +02:00
Miles Malerba 920820a1a8 refactor(compiler): Add support for i18n post-processing (#52195)
Runs post-processing on any i18n messages that have multiple values
assigned to a single placeholder.

PR Close #52195
2023-10-16 19:25:05 +02:00
Miles Malerba 6735f5f634 refactor(compiler): Fix propagation of child i18n params (#52195)
The way we were propagating params up to parent i18n ops didn't account
for the fact that a parent and child could both have a value for the
same placeholder. In order to properly merge the value for these cases,
we need to propagate the params up *before* serialization. Therefore I
removed the standalone param propagation phase and folded the logic into
the placeholder resolution phase.

PR Close #52195
2023-10-16 19:25:05 +02:00
Miles Malerba 2d197f0c65 refactor(compiler): Remove unnecessary checks for empty container types (#52195)
I added these in an earlier PR when we were considering moving the empty
elements phase earlier. Since we decided not to do that, this commit
cleans up unnecessary references to the empty versions of the element to
simplify the code and types.

PR Close #52195
2023-10-16 19:25:05 +02:00
cexbrayat e5720edb46 fix(core): handle if alias in control flow migration (#52181)
This adds the support of `if ` conditions with `as` clause when migrating to the control flow syntax.
It now adds the required semicolon before the `as` when migrating the template.

Before: `@if (user$ | async as user) {`
After: `@if (user$ | async; as user) {`

PR Close #52181
2023-10-16 18:35:19 +02:00
Quentin eaf735da58 docs: fix typo (#52218)
Fix word: adjustments

PR Close #52218
2023-10-16 18:33:22 +02:00
Quentin bd1e78d746 docs: fix typo (#52219)
Fix word: incompatibilities

PR Close #52219
2023-10-16 18:30:46 +02:00
Kristiyan Kostadinov 81a287a79a fix(compiler): avoid error in template parser for tag names that can occur in object prototype (#52225)
Fixes that the compiler was throwing an error if an element tag name is the same as a built-in prototype property (e.g. `constructor` or `toString`). The problem was that we were storing the tag names in an object literal with the `Object` prototype. These changes resolve the issue by creating an object without a prototype.

Fixes #52224.

PR Close #52225
2023-10-16 18:22:15 +02:00
Andrew Miller 2fa5e702de docs(language-service): add neovim setup to angular language service docs (#52212)
PR Close #52212
2023-10-16 18:19:12 +02:00
Matthieu Riegler 65c2f02ccb refactor(core): remove hack on render3 (#51238)
Once useful, the hack to prevent circular dependency serves no purpose
We can safely remove it.

PR Close #51238
2023-10-16 17:43:36 +02:00
Osama Heykal a6be2e221f docs: add missing next steps section (#52220)
Added missing 'next steps' section in the tutorial.

PR Close #52220
2023-10-16 16:27:15 +02:00
Gerald Monaco 6ae68f39b9 refactor(core): run internal work outside of public afterRender phases (#52145)
Public afterRender phases have specific API guarantees which can be invalidated if the internal framework is implemented using them. Instead, the framework should use dedicated internal functions.

PR Close #52145
2023-10-16 12:24:00 +02:00
Andrew Kushnir 450f360bd7 refactor(core): simplify hydration annotation key (#52207)
This commit drops the `ɵ` symbol from hydration annotation key: `__ɵnghData__` -> `__nghData__`. This helps ensure that there are no UTF8 symbols that might be damaged in case a web server is misconfigured.

Noticed while working on https://github.com/angular/angular/pull/52206.

PR Close #52207
2023-10-16 11:25:57 +02:00
Kristiyan Kostadinov c1dc86d186 refactor(core): tree shake depepdency interceptor token (#52199)
We have the `DEFER_BLOCK_DEPENDENCY_INTERCEPTOR` DI token that we use in tests to intercept the dependency loading function from deferred blocks, however we were referencing it in a way that caused it to be retained in production bundles as well.

These changes guard the call site with `ngDevMode` since the token is only used for testing.

PR Close #52199
2023-10-16 11:17:24 +02:00
Kristiyan Kostadinov a58301f980 refactor(core): minor fixes for the block entities migration (#52209)
Contains the following minor improvements to the block entities migration:
* The migration won't be stopped anymore if it can't read a template file.
* The migration will exit early if a template doesn't contain the two characters we need to migrate.
* Reduced the amount of code that is wrapped by a try/catch to avoid suppressing errors.

PR Close #52209
2023-10-16 11:15:31 +02:00
thomas b2ba678e73 docs: add thomas laforge to GDE resources and add angularchallenges to resources (#52075)
PR Close #52075
2023-10-16 11:13:01 +02:00
Paul Gschwendtner 2d98726ec9 refactor(core): remove unused helper for setting LView for reactive consumer (#52192)
This code path is never hit because the assignment of the lview happens
in `commitLViewConsumerIfHasProducers`.

PR Close #52192
2023-10-13 14:08:35 +02:00
Thomas Wilkinson 030a68b3df refactor(router): Introduce StateManager interface (#52171)
Move existing logic into `HistoryStateManager`

PR Close #52171
2023-10-13 13:48:17 +02:00
Matthieu Riegler 6fd863d6e1 refactor(core): cleanup DebugElement (#50896)
* `style` is never `null`.
* adding some typing
* Add 2 todos per #49777

PR Close #50896
2023-10-13 12:24:17 +02:00
Matthieu Riegler ba9fc2419e refactor(core): deprecate the NgProbeToken (#51396)
DEPRECATED: NgProbeToken

The `NgProbeToken` is not used internally since the transition from View Engine to Ivy. The token has no utility and can be removed from applications and libraries.

PR Close #51396
2023-10-13 12:19:05 +02:00
Ward Bell e90f5e5f22 docs: Phase 2 Sample Code Conversions to Standalone (#51918)
Phase 2 sample code conversions per instructions in doc: https://docs.google.com/document/d/1QqnVK8Mc0O1gU9ctPrs-JJ42V9_dHCuEttD_01pXh34/edit

**Included**

1. ajs-quick-reference

Minor changes to remove references to NgModule. Converted sample to standalone.

2. architecture

Migrated the code sample. Made minor changes to remove references to NgModule. I explicitly deprecated the `architecture-module.md` page and removed it from the navigation.

4. deprecations / deprecations-guide

Migrated the code sample which required pulling NgModule and Reactive Forms material into a new `deprecated` folder. Made minor changes to clarify when advice was specific to NgModule (see `loadChildren` in particular).

5. displaying-data

Deleted this `displaying-data` sample entirely as there is no need for a sample app just to display a single, vanilla data binding in the deprecated `zone.md` page. Replaced reference in `zone.md` with an inline code snippet.

10. ngcontainer

Converted the code sample. There is no guide page for this sample. It can be displayed in Stackblitz

**Excluded**

3. cli-builder

Code sample is not an app; nothing to convert.

6-9 - related to getting-started and setup.

Waiting until Angular team is ready to proceed.

11. providers

This is in its own commit and PR 51918 because it may not make sense to convert. See that PR for details.

12. routing-with-urlmatcher

No work needed. Already Standalone.

13 schematics-for-libraries

Non-trivial (see doc notes) and Angular may decide to leave as is. Should have own PR.

14 setup

Non-trivial (see doc notes) and Angular may decide to leave as is. Should have own PR.

PR Close #51918
2023-10-13 11:20:09 +02:00
Kristiyan Kostadinov 8e5d54a3d5 refactor(core): clean up viewport trigger event tracking (#52156)
When the `viewport` triggers were first introduced, we ended up having to use a service to keep track of them, because using the same global event handling as the other events led to some inconsistent test failures. It looks like the failures were caused by the same bug fixed #52115 so now we can switch back to the previous approach which is a bit more compact.

PR Close #52156
2023-10-13 11:16:26 +02:00
Miroslav Foltýn 54f7578d55 docs(router): add clarification for behavior of paramsInheritanceStrategy: 'emptyOnly' (#52170)
Follows up to the https://github.com/angular/angular/issues/52108 which was opened due to the confusion surrounding paramsInheritanceStrategy: 'emptyOnly'.

PR Close #52170
2023-10-13 11:14:23 +02:00
Andrew Kushnir a6fcf75423 test(core): verify directive order inside and outside of @defer blocks (#52194)
This test verifies that the order does not change when a particular part of code is wrapped by the `@defer` block.

PR Close #52194
2023-10-13 11:10:20 +02:00
cexbrayat 99e7629159 fix(core): do not remove used ng-template nodes in control flow migration (#52186)
This fixes an issue where `ng-template` nodes were removed even when used in other places than control flow directives.

Template to migrate:
```html
<ng-template #blockUsedElsewhere><div>Block</div></ng-template>
<ng-container *ngTemplateOutlet="blockUsedElsewhere"></ng-container>
```

Before:
```html
<ng-container *ngTemplateOutlet="blockUsedElsewhere"></ng-container>
```

After:
```html
<ng-template #blockUsedElsewhere><div>Block</div></ng-template>
<ng-container *ngTemplateOutlet="blockUsedElsewhere"></ng-container>
```

PR Close #52186
2023-10-12 19:28:23 +02:00
Jessica Janiuk 6f19117a6a refactor(core): Add better error handling to control flow migration (#52189)
This adds some clear error handling and messaging for when errors occur during the control flow migration.

PR Close #52189
2023-10-12 19:27:00 +02:00
cexbrayat ffe9b1fcc2 fix(core): handle for alias with as in control flow migration (#52183)
This adds the support of `let index as myIndex` in `*ngFor` for the control flow migration.

Before: `@for (itm of items; track itm)`
After: `@for (itm of items; track itm; let myIndex = $index)`

PR Close #52183
2023-10-12 19:15:20 +02:00
Matthieu Riegler 0792424df2 refactor(animations): make AnimationBuilder tree-shakable (#52097)
This commit allows also to use the `AnimationBuilder` when using `provideAnimationsAsync()`

fixes #52096

PR Close #52097
2023-10-12 18:13:22 +02:00
Nicolas Frizzarin 5c70148d20 docs: add Nicolas Frizzarin to GDE resources (#52003)
This PR add Nicolas Frizzarin to the GDE resources

docs: fix typo on description

PR Close #52003
2023-10-12 17:44:49 +02:00
cexbrayat 65b460448e fix(common): missing space in ngSwitch equality warning (#52180)
This adds a missing space between the sentences of the `===` vs `==` ngSwitch warning.

PR Close #52180
2023-10-12 15:44:47 +02:00
Andrew Kushnir 4cd1b3d38b refactor(core): better organization of @defer runtime code (#52152)
This commit splits the `render3/instructions/defer.ts` file (that contained most of the runtime code) into smalle
r files that are easier to maintain.

There are no functional changes in this PR, just organizing code.

PR Close #52152
2023-10-12 12:43:54 +02:00
Angular Robot 4e9d6878a5 build: update cross-repo angular dependencies (#52159)
See associated pull request for more information.

PR Close #52159
2023-10-12 12:42:24 +02:00
Pawel Kozlowski 2bf088e7bb Revert "refactor(compiler-cli): remove MethodIdentifier type (#49611)" (#52174)
This reverts commit c2b1a242e8.

PR Close #52174
2023-10-12 12:35:50 +02:00
Matthieu Riegler 2e5ee1c198 refactor(compiler-cli): remove MethodIdentifier type (#49611)
`MethodIdentifier` is unused as is `IdentifierKind.Method`. They both can be removed.

PR Close #49611
2023-10-11 12:34:51 -07:00
Angular Robot 371e26f6b3 docs: update Angular CLI help [main] (#52168)
Updated Angular CLI help contents.

PR Close #52168
2023-10-11 12:19:22 -07:00
Angular Robot d50d3b85bd docs: update events (#52053)
Generated `events.json` with the latest events retrieved from the Firebase DB.

PR Close #52053
2023-10-11 12:18:06 -07:00
Matthieu Riegler 073ebfe09e fix(compiler): apply style on :host attributes in prod builds. (#49118)
In prod builds, selectors are optimized and spaces a removed. #48558 introduced a regression on selectors without spaces. This commit fixes tihs.

Fixes #49100

PR Close #49118
2023-10-11 11:32:19 -07:00
Andrew Scott 926db6dd36 release: cut the v17.0.0-next.8 release 2023-10-11 10:12:33 -07:00
4129 changed files with 257969 additions and 52708 deletions
+4
View File
@@ -38,3 +38,7 @@ integration/typings_test_ts49/node_modules
integration/typings_test_ts50/node_modules
integration/typings_test_ts51/node_modules
integration/typings_test_ts52/node_modules
# Temporarily ignore all of adev
adev/
+3 -3
View File
@@ -22,10 +22,10 @@ jobs:
(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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
bazelrc: ./.bazelrc.user
@@ -34,7 +34,7 @@ jobs:
# 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@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
workflow-artifact-name: 'aio'
pull-number: '${{github.event.pull_request.number}}'
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- name: Configure Firebase deploy target
working-directory: aio/
@@ -34,7 +34,7 @@ jobs:
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@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'aio'
@@ -13,9 +13,9 @@ jobs:
assistant_to_the_branch_manager:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
- uses: angular/dev-infra/github-actions/branch-manager@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: angular/dev-infra/github-actions/branch-manager@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+4 -4
View File
@@ -19,7 +19,7 @@ jobs:
exit: true
# Indicate that the benchmark command was received.
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
with:
comment-id: ${{github.event.comment.id}}
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
@@ -28,7 +28,7 @@ jobs:
- uses: alessbell/pull-request-comment-branch@aad01d65d6982b8eacabed5e9a684cd8ceb98da6 # v1.1
id: comment-branch
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
# Specify repository as the PR branch might be from a fork.
repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}}
@@ -38,7 +38,7 @@ jobs:
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
bazelrc: ./.bazelrc.user
@@ -53,7 +53,7 @@ jobs:
id: benchmark
name: Running benchmark
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
with:
issue-number: ${{github.event.issue.number}}
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
# Checking out the pull request commit is intended here as we need to run the changed code tests.
@@ -31,7 +31,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Starting Saucelabs tunnel service
run: ./tools/saucelabs/sauce-service.sh run &
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
+25 -25
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
node-module-directories: |
@@ -60,13 +60,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Run unit tests
@@ -78,13 +78,13 @@ jobs:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Install node modules
run: yarn install --frozen-lockfile --network-timeout 100000
- name: Run CI tests for framework
@@ -94,15 +94,15 @@ jobs:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with upstream packages
@@ -115,15 +115,15 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Install node modules
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with local packages
@@ -138,15 +138,15 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Set the stable branch environment variable
@@ -182,13 +182,13 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Install node modules
run: yarn install --frozen-lockfile
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
@@ -200,7 +200,7 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
node-module-directories: |
@@ -208,9 +208,9 @@ jobs:
./packages/zone.js/node_modules
./packages/zone.js/test/typings/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Install node modules
run: yarn install --frozen-lockfile
- run: |
@@ -251,7 +251,7 @@ jobs:
JOBS: 2
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
# Checking out the pull request commit is intended here as we need to run the changed code tests.
@@ -259,9 +259,9 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Set up Sauce Tunnel Daemon
run: yarn bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
env:
+4 -4
View File
@@ -12,14 +12,14 @@ jobs:
labels:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- uses: angular/dev-infra/github-actions/commit-message-based-labels@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/commit-message-based-labels@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
post_approval_changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- uses: angular/dev-infra/github-actions/post-approval-changes@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/post-approval-changes@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -14,6 +14,6 @@ jobs:
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/feature-request@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: angular/dev-infra/github-actions/feature-request@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+2 -2
View File
@@ -13,8 +13,8 @@ jobs:
statuses: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- uses: angular/dev-infra/github-actions/google-internal-tests@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/google-internal-tests@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
run-tests-guide-url: http://go/angular-g3sync-start
github-token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -9,6 +9,6 @@ jobs:
status:
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/unified-status-check@031962443584a0ac5cbd9d1c1b78b241453e4702
- uses: angular/dev-infra/github-actions/unified-status-check@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+4 -4
View File
@@ -22,15 +22,15 @@ jobs:
version: ['https://next.angular.io/', 'https://angular.io/']
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
- name: Install node modules in aio
run: yarn install --frozen-lockfile --cwd aio
- name: Run basic e2e and deployment config tests.
@@ -44,7 +44,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
cache-node-modules: true
node-module-directories: |
+3 -3
View File
@@ -25,12 +25,12 @@ jobs:
steps:
- name: 'Checkout code'
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
- name: 'Run analysis'
uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0
uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0
with:
results_file: results.sarif
results_format: sarif
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9
uses: github/codeql-action/upload-sarif@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
with:
sarif_file: results.sarif
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
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
@@ -32,7 +32,7 @@ jobs:
env:
ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN: ${{ secrets.ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN }}
- name: Create a PR (if necessary)
uses: angular/dev-infra/github-actions/create-pr-for-changes@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/create-pr-for-changes@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
branch-prefix: update-cli-help
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
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
@@ -35,7 +35,7 @@ jobs:
- 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@031962443584a0ac5cbd9d1c1b78b241453e4702
uses: angular/dev-infra/github-actions/create-pr-for-changes@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
with:
branch-prefix: docs-update-events
pr-title: 'docs: update events'
+2
View File
@@ -29,6 +29,8 @@ export const format: FormatConfig = {
'!packages/core/src/i18n/locale_en.ts',
'!packages/common/locales/closure-locale.ts',
'!packages/common/src/i18n/currencies.ts',
// Temporarily disable formatting for adev
'!adev/**',
],
},
'buildifier': true,
+25 -5
View File
@@ -182,6 +182,8 @@ groups:
- crisbeto
- devversion
- dylhunn
- jessicajaniuk
- AndrewKushnir
# =========================================================
# Framework: Core
@@ -339,7 +341,9 @@ groups:
'aio/content/special-elements/**/{*,.*}',
'aio/content/blocks/**/{*,.*}',
'aio/content/guide/hydration.md',
'aio/content/guide/defer.md',
'aio/content/guide/signals.md',
'aio/content/guide/control_flow.md',
'aio/content/examples/injection-token/**/{*,.*}',
])
reviewers:
@@ -517,10 +521,8 @@ groups:
contains_any_globs(files, [
'packages/platform-server/**/{*,.*}',
'aio/content/guide/prerendering.md',
'aio/content/guide/universal.md',
'aio/content/examples/universal/**/{*,.*}',
'aio/content/guide/universal-ngmodule.md',
'aio/content/examples/universal-ngmodule/**/{*,.*}'
'aio/content/guide/ssr.md',
'aio/content/examples/ssr/**/{*,.*}',
])
reviewers:
users:
@@ -938,6 +940,7 @@ groups:
- alxhub
- AndrewKushnir
- atscott
- jessicajaniuk
- jelbourn
# =========================================================
@@ -1054,6 +1057,22 @@ groups:
- dylhunn
- mgechev
# =========================================================
# Docs: Angular Dev Site
# =========================================================
angular-dev:
<<: *defaults
conditions:
- >
contains_any_globs(files, [
'adev/**/{.*,*}'
])
reviewers:
users:
- twerske
- josephperrott
- jelbourn
# =========================================================
# Docs-infra
# =========================================================
@@ -1165,10 +1184,11 @@ groups:
'tools/esm-interop/**/{*,.*}',
'tools/gulp-tasks/**/{*,.*}',
'tools/legacy-saucelabs/**/{*,.*}',
'tools/manual_api_docs/**/{*,.*}',
'tools/npm-patches/**/{*,.*}',
'tools/rxjs/**/{*,.*}',
'tools/saucelabs/**/{*,.*}',
'tools/saucelabs-daemon/**/{*,.*}',
'tools/saucelabs/**/{*,.*}',
'tools/symbol-extractor/**/{*,.*}',
'tools/testing/**/{*,.*}',
'tools/tslint/**/{*,.*}',
+330 -270
View File
@@ -1,16 +1,158 @@
<a name="16.2.9"></a>
# 16.2.9 (2023-10-11)
### forms
<a name="17.0.1"></a>
# 17.0.1 (2023-11-08)
### http
| Commit | Type | Description |
| -- | -- | -- |
| [51a5baace3](https://github.com/angular/angular/commit/51a5baace3b1eee130fa911327773066139cc68e) | fix | reset() call with null values on nested group ([#48830](https://github.com/angular/angular/pull/48830)) |
| [5c6f3f8ec0](https://github.com/angular/angular/commit/5c6f3f8ec0f1dd9b5505f3c94e654a675e75f147) | fix | Don't override the backend when using the InMemoryWebAPI ([#52425](https://github.com/angular/angular/pull/52425)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [70d30c28e0](https://github.com/angular/angular/commit/70d30c28e04f4ead51145e4e47df342492bfb336) | fix | Add support for ng-templates with i18n attributes ([#52597](https://github.com/angular/angular/pull/52597)) |
| [4f125c5f9a](https://github.com/angular/angular/commit/4f125c5f9ae572a8216ec1fbb88f52e47b875e1e) | fix | Switches to multiple passes to fix several reported bugs ([#52592](https://github.com/angular/angular/pull/52592)) |
Web Frameworks: the internet frontier.<br/>
These are the voyages of the framework Angular.<br/>
Its continuing mission:<br/>
To explore strange, new technologies.<br/>
To seek out new users and new applications.<br/>
To boldly go where no web framework has gone before.<br/>
In honor of v17.0.1
```
______
___.--------'------`---------.____
_.---'----------------------------------`---.__
.'___=]===========================================
,-----------------------..__/.' >--.______ _______.---'
]====================<==||(__) .' `------'
`-----------------------`' ----.___--/
/ /---' `/
/_______(______________________/
`-------------.--------------.'
\________|_.-'
```
Live long and prosper 🖖🏻
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.0.0-next.7"></a>
# 17.0.0-next.7 (2023-10-04)
## Deprecations
<a name="17.0.0"></a>
# 17.0.0 (2023-11-08)
[Blog post "Angular v17 is now available"](http://goo.gle/angular-v17).
## Breaking Changes
###
- Node.js v16 support has been removed and the minimum support version has been bumped to 18.13.0.
Node.js v16 is planned to be End-of-Life on 2023-09-11. Angular will stop supporting Node.js v16 in Angular v17. For Node.js release schedule details, please see: https://github.com/nodejs/release#release-schedule
### common
-
- the NgSwitch directive now defaults to the === equality operator,
migrating from the previously used ==. NgSwitch expressions and / or
individual condition values need adjusting to this stricter equality
check. The added warning message should help pinpointing NgSwitch
usages where adjustements are needed.
### core
- Angular now required `zone.js` version `~0.14.0`
- Versions of TypeScript older than 5.2 are no longer supported.
- The `mutate` method was removed from the `WritableSignal` interface and completely
dropped from the public API surface. As an alternative please use the update method and
make immutable changes to the object.
Example before:
```typescript
items.mutate(itemsArray => itemsArray.push(newItem));
```
Example after:
```typescript
items.update(itemsArray => [itemsArray, …newItem]);
```
- The `mutate` method was removed from the `WritableSignal` interface and completely
dropped from the public API surface. As an alternative please use the update method and
make immutable changes to the object.
Example before:
```typescript
items.mutate(itemsArray => itemsArray.push(newItem));
```
Example after:
```typescript
items.update(itemsArray => [itemsArray, …newItem]);
```
- `OnPush` components that are created dynamically now
only have their host bindings refreshed and `ngDoCheck run` during change
detection if they are dirty.
Previously, a bug in the change detection would result in the `OnPush`
configuration of dynamically created components to be ignored when
executing host bindings and the `ngDoCheck` function. This is
rarely encountered but can happen if code has a handle on the
`ComponentRef` instance and updates values read in the `OnPush`
component template without then calling either `markForCheck` or
`detectChanges` on that component's `ChangeDetectorRef`.
### platform-browser
- `REMOVE_STYLES_ON_COMPONENT_DESTROY` default value is now `true`. This causes CSS of components to be removed from the DOM when destroyed. You retain the previous behaviour by providing the `REMOVE_STYLES_ON_COMPONENT_DESTROY` injection token.
```ts
import {REMOVE_STYLES_ON_COMPONENT_DESTROY} from '@angular/platform-browser';
...
providers: [{
provide: REMOVE_STYLES_ON_COMPONENT_DESTROY,
useValue: false,
}]
```
- The `withNoDomReuse()` function was removed from the public API. If you need to disable hydration, you can exclude the `provideClientHydration()` call from provider list in your application (which would disable hydration features for the entire application) or use `ngSkipHydration` attribute to disable hydration for particular components. See this guide for additional information: https://angular.io/guide/hydration#how-to-skip-hydration-for-particular-components.
### router
- Absolute redirects no longer prevent further redirects.
Route configurations may need to be adjusted to prevent infinite
redirects where additional redirects were previously ignored after an
absolute redirect occurred.
- Routes with `loadComponent` would incorrectly cause
child routes to inherit their data by default. The default
`paramsInheritanceStrategy` is `emptyOnly`. If parent data should be
inherited in child routes, this should be manually set to `always`.
- `urlHandlingStrategy` has been removed from the Router public API.
This should instead be configured through the provideRouter or RouterModule.forRoot APIs.
- The following Router properties have been removed from
the public API:
- canceledNavigationResolution
- paramsInheritanceStrategy
- titleStrategy
- urlUpdateStrategy
- malformedUriErrorHandler
These should instead be configured through the `provideRouter` or
`RouterModule.forRoot` APIs.
- The `setupTestingRouter` function has been removed. Use
`RouterModule.forRoot` or `provideRouter` to setup the `Router` for
tests instead.
- `malformedUriErrorHandler` is no longer available in
the `RouterModule.forRoot` options. URL parsing errors should instead be
handled in the `UrlSerializer.parse` method.
### zone.js
- Deep and legacy `dist/` imports like `zone.js/bundles/zone-testing.js` and `zone.js/dist/zone` are no longer allowed. `zone-testing-bundle` and `zone-testing-node-bundle` are also no longer part of the package.
The proper way to import `zone.js` and `zone.js/testing` is:
```js
import 'zone.js';
import 'zone.js/testing';
```
## Deprecations
### animations
- The `AnimationDriver.NOOP` symbol is deprecated, use `NoopAnimationDriver` instead.
### core
- `ChangeDetectorRef.checkNoChanges` is deprecated.
Test code should use `ComponentFixture` instead of `ChangeDetectorRef`.
Application code should not call `ChangeDetectorRef.checkNoChanges` directly.
- Swapping out the context object for `EmbeddedViewRef`
is no longer supported. Support for this was introduced with v12.0.0, but
this pattern is rarely used. There is no replacement, but you can use
@@ -19,41 +161,219 @@
as an example).
Also adds a warning if the deprecated
- NgProbeToken
The `NgProbeToken` is not used internally since the transition from View Engine to Ivy. The token has no utility and can be removed from applications and libraries.
###
| Commit | Type | Description |
| -- | -- | -- |
| [59aa0634f4](https://github.com/angular/angular/commit/59aa0634f4d4694203f2a69c40017fe5a3962514) | build | remove support for Node.js v16 ([#51755](https://github.com/angular/angular/pull/51755)) |
### animations
| Commit | Type | Description |
| -- | -- | -- |
| [e753278faa](https://github.com/angular/angular/commit/e753278faae79a53e235e0d8e03f89555a712d80) | feat | Add the possibility of lazy loading animations code. ([#50738](https://github.com/angular/angular/pull/50738)) |
| [698c058e1c](https://github.com/angular/angular/commit/698c058e1c975c573722407f4843a4a774ceb92a) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
| [0598613950](https://github.com/angular/angular/commit/0598613950c76f4a13601c6942e30ab4ce1e3b67) | refactor | deprecation of `AnimationDriver.NOOP` ([#51843](https://github.com/angular/angular/pull/51843)) |
### benchpress
| Commit | Type | Description |
| -- | -- | -- |
| [2da3551a70](https://github.com/angular/angular/commit/2da3551a703ebef401d76a8e88e388437e851d85) | feat | report gc and render time spent in script ([#50771](https://github.com/angular/angular/pull/50771)) |
### common
| Commit | Type | Description |
| -- | -- | -- |
| [fe2fd7e1a8](https://github.com/angular/angular/commit/fe2fd7e1a898a4525c219065a6d0908988dfd7e2) | feat | make the warning for lazy-loaded lcp image an error ([#51748](https://github.com/angular/angular/pull/51748)) |
| [dde3fdabbd](https://github.com/angular/angular/commit/dde3fdabbd24b48dd6afd120d23e92a3605eb04d) | feat | upgrade warning to logged error for lazy-loaded LCP images using NgOptimizedImage ([#52004](https://github.com/angular/angular/pull/52004)) |
| [da056a1fe2](https://github.com/angular/angular/commit/da056a1fe2816299319fb3f87416316be2029479) | fix | add missing types field for @angular/common/locales of exports in package.json ([#52080](https://github.com/angular/angular/pull/52080)) |
| [85843e8212](https://github.com/angular/angular/commit/85843e8212e99deb8b70f3d3f8dfe002b978cbb1) | fix | allow to specify only some properties of `DatePipeConfig` ([#51287](https://github.com/angular/angular/pull/51287)) |
| [3bd85fb7b0](https://github.com/angular/angular/commit/3bd85fb7b0723ed807bca771e9fa95af60a3cfaf) | fix | apply fixed_srcset_width value only to fixed srcsets ([#52459](https://github.com/angular/angular/pull/52459)) |
| [65b460448e](https://github.com/angular/angular/commit/65b460448ec5fdcee5aecca0cdc3cf498b0832cb) | fix | missing space in ngSwitch equality warning ([#52180](https://github.com/angular/angular/pull/52180)) |
| [86c5e34601](https://github.com/angular/angular/commit/86c5e34601d7901a11688124aa902646524177eb) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
| [28a5925f53](https://github.com/angular/angular/commit/28a5925f53790067d45f1f68d204a36088dbf5e3) | fix | use === operator to match NgSwitch cases ([#51504](https://github.com/angular/angular/pull/51504)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [1934524a0c](https://github.com/angular/angular/commit/1934524a0c673fb65cd927c55c712f59446f9c93) | feat | add docs extraction for type aliases ([#52118](https://github.com/angular/angular/pull/52118)) |
| [7f6d9a73ab](https://github.com/angular/angular/commit/7f6d9a73ab8b658d0d8148080dfefb2550bee6b4) | feat | expand class api doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
| [a7fa25306f](https://github.com/angular/angular/commit/a7fa25306f8ce47d8aa330531382106efec55a55) | feat | extract api docs for interfaces ([#52006](https://github.com/angular/angular/pull/52006)) |
| [7bfe20707f](https://github.com/angular/angular/commit/7bfe20707fedff7290e12356a1545644b436d41c) | feat | extract api for fn overloads and abtract classes ([#52040](https://github.com/angular/angular/pull/52040)) |
| [c7daf7ea16](https://github.com/angular/angular/commit/c7daf7ea1692391f7cac8f794ed777887a2764af) | feat | extract directive docs info ([#51733](https://github.com/angular/angular/pull/51733)) |
| [e0b1bb33d7](https://github.com/angular/angular/commit/e0b1bb33d77babe881f77f52cb1b71e345f5696b) | feat | extract doc info for JsDoc ([#51733](https://github.com/angular/angular/pull/51733)) |
| [b9c70158ab](https://github.com/angular/angular/commit/b9c70158abecd81a5af512c8b4da685851cf159f) | feat | extract docs for accessors, rest params, and types ([#51733](https://github.com/angular/angular/pull/51733)) |
| [a24ae994a0](https://github.com/angular/angular/commit/a24ae994a0470fdac09a69937fd0580cff6c6d68) | feat | extract docs for top level functions and consts ([#51733](https://github.com/angular/angular/pull/51733)) |
| [2e41488296](https://github.com/angular/angular/commit/2e41488296879685b19dfba8d78037690347bda3) | feat | extract docs info for enums, pipes, and NgModules ([#51733](https://github.com/angular/angular/pull/51733)) |
| [34495b3533](https://github.com/angular/angular/commit/34495b35337892ab209d9955ff7fe2897a0c5d41) | feat | extract docs via exports ([#51828](https://github.com/angular/angular/pull/51828)) |
| [7e82df45c5](https://github.com/angular/angular/commit/7e82df45c5bb72ec3dafaa07dc1eaa5d463b006c) | feat | initial skeleton for API doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
| [6795cccbbb](https://github.com/angular/angular/commit/6795cccbbbfc17bbf88fb8197aa172cca67fa2d2) | fix | account for type-only imports in defer blocks ([#52343](https://github.com/angular/angular/pull/52343)) |
| [23bfa10ac8](https://github.com/angular/angular/commit/23bfa10ac809f6b27d32647210c52329f0e4262e) | fix | add diagnostic for inaccessible deferred trigger ([#51922](https://github.com/angular/angular/pull/51922)) |
| [31295a3cf9](https://github.com/angular/angular/commit/31295a3cf907a61e7115d9039a83a232b263a676) | fix | allocating unnecessary slots in conditional instruction ([#51913](https://github.com/angular/angular/pull/51913)) |
| [2aaddd3f64](https://github.com/angular/angular/commit/2aaddd3f64bb8891bb4bdcadf05d427a89338112) | fix | allow comments between switch cases ([#52449](https://github.com/angular/angular/pull/52449)) |
| [ddd9df68bb](https://github.com/angular/angular/commit/ddd9df68bb2e907dd820f239aaf819425cb95df8) | fix | allow decimals in defer block time values ([#52433](https://github.com/angular/angular/pull/52433)) |
| [7dbd47fb30](https://github.com/angular/angular/commit/7dbd47fb3015117c420f984181bfcb48e533525a) | fix | allow newlines in track and let expressions ([#52137](https://github.com/angular/angular/pull/52137)) |
| [0eae992c4e](https://github.com/angular/angular/commit/0eae992c4e03b7c9039476e03b72e92d662293df) | fix | allow nullable values in for loop block ([#51997](https://github.com/angular/angular/pull/51997)) |
| [073ebfe09e](https://github.com/angular/angular/commit/073ebfe09eccd5d01d27fcc46fc5d4465c1851ff) | fix | apply style on :host attributes in prod builds. ([#49118](https://github.com/angular/angular/pull/49118)) |
| [81a287a79a](https://github.com/angular/angular/commit/81a287a79afc16d43c0fd24d7aea54be4414940a) | fix | avoid error in template parser for tag names that can occur in object prototype ([#52225](https://github.com/angular/angular/pull/52225)) |
| [6c58252521](https://github.com/angular/angular/commit/6c582525217197dd777d5bb9e62d6aaa2c70a996) | fix | compilation error when for loop block expression contains new line ([#52447](https://github.com/angular/angular/pull/52447)) |
| [9d19c8e317](https://github.com/angular/angular/commit/9d19c8e31752d211f575246282358b83afe90969) | fix | don't allocate variable to for loop expression ([#52158](https://github.com/angular/angular/pull/52158)) |
| [9acd2ac98b](https://github.com/angular/angular/commit/9acd2ac98bc3b6ffc5a8d6c19f7290d05fe1f896) | fix | enable block syntax in the linker ([#51979](https://github.com/angular/angular/pull/51979)) |
| [1d871c03a5](https://github.com/angular/angular/commit/1d871c03a523e10bb838cb0f9550595cfbd9d14d) | fix | forward referenced dependencies not identified as deferrable ([#52017](https://github.com/angular/angular/pull/52017)) |
| [16ff08ec70](https://github.com/angular/angular/commit/16ff08ec70bfa192041ba050e550676e8d505a05) | fix | narrow the type of expressions in event listeners inside if blocks ([#52069](https://github.com/angular/angular/pull/52069)) |
| [ac0d5dcfd6](https://github.com/angular/angular/commit/ac0d5dcfd6015ec4283ed1a5cf241f130f4c5cf5) | fix | narrow the type of expressions in event listeners inside switch blocks ([#52069](https://github.com/angular/angular/pull/52069)) |
| [02edb43067](https://github.com/angular/angular/commit/02edb4306736e6f12e87a4164c17eca6cbdfe151) | fix | narrow the type of the aliased if block expression ([#51952](https://github.com/angular/angular/pull/51952)) |
| [83067b3ef2](https://github.com/angular/angular/commit/83067b3ef257dbc7b1c20d50645615d19023ba51) | fix | ng-template directive invoke twice at the root of control flow ([#52515](https://github.com/angular/angular/pull/52515)) |
| [17078a3fe1](https://github.com/angular/angular/commit/17078a3fe1e9b90e48952b6c12b6e6b774b97810) | fix | pipes used inside defer triggers not being picked up ([#52071](https://github.com/angular/angular/pull/52071)) |
| [861ce3a7c5](https://github.com/angular/angular/commit/861ce3a7c574340a6164ad0de13f49bda3e172da) | fix | pipes using DI not working in blocks ([#52112](https://github.com/angular/angular/pull/52112)) |
| [1f5039bbd6](https://github.com/angular/angular/commit/1f5039bbd6de8450e5511af00044ddd2f4314016) | fix | project control flow root elements into correct slot ([#52414](https://github.com/angular/angular/pull/52414)) |
| [81c315ec6e](https://github.com/angular/angular/commit/81c315ec6ea37c55d951d3b38b6c551226173be5) | fix | template type checking not reporting diagnostics for incompatible type comparisons ([#52322](https://github.com/angular/angular/pull/52322)) |
| [1beef49d80](https://github.com/angular/angular/commit/1beef49d80809fbb0e7c8e95f17096c39ac8940a) | fix | update the minVersion if component uses block syntax ([#51979](https://github.com/angular/angular/pull/51979)) |
| [386e1e9500](https://github.com/angular/angular/commit/386e1e950033ad98661e5077a4f119df0e7b3008) | fix | work around TypeScript bug when narrowing switch statements ([#52110](https://github.com/angular/angular/pull/52110)) |
| [e5bca43224](https://github.com/angular/angular/commit/e5bca432248add0a19102f6afeae145f1a33ee8a) | perf | further reduce bundle size using arrow functions ([#52010](https://github.com/angular/angular/pull/52010)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [5b66330329](https://github.com/angular/angular/commit/5b66330329fd066a7c347f040a330b4c7f2a0a2b) | fix | allow non-array imports for standalone component in local compilation mode ([#51819](https://github.com/angular/angular/pull/51819)) |
| [377a7abfda](https://github.com/angular/angular/commit/377a7abfda60a6ddd55a41531e3653bcad78b0a2) | fix | bypass static resolving of the component's changeDetection field in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
| [19c3dc18d3](https://github.com/angular/angular/commit/19c3dc18d3c0cfd83efec2c8f81b40860d570346) | fix | fix NgModule injector def in local compilation mode when imports/exports are non-array expressions ([#51819](https://github.com/angular/angular/pull/51819)) |
| [11bb19cafc](https://github.com/angular/angular/commit/11bb19cafcf447b7ce6ade146d431a43c3e2c682) | fix | handle nested qualified names in ctor injection in local compilation mode ([#51947](https://github.com/angular/angular/pull/51947)) |
| [f91f222b55](https://github.com/angular/angular/commit/f91f222b55f249089d267c72a9c0ab5b09d7c932) | fix | resolve component encapsulation enum in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [59b6ec6be8](https://github.com/angular/angular/commit/59b6ec6be8cc464a6e74449ac7e0f2554ac7392b) | docs | Deprecate `ChangeDetectorRef.checkNoChanges` ([#52431](https://github.com/angular/angular/pull/52431)) |
| [4f04d1cdab](https://github.com/angular/angular/commit/4f04d1cdab2fc5217566c0c01b7df10c74a93afa) | feat | add new list reconcilation algorithm ([#51980](https://github.com/angular/angular/pull/51980)) |
| [c7127b98b5](https://github.com/angular/angular/commit/c7127b98b555449f99e24a81c828ab7b1c6c4a4e) | feat | add schematic to escape block syntax characters ([#51905](https://github.com/angular/angular/pull/51905)) |
| [50275e58b8](https://github.com/angular/angular/commit/50275e58b80acfc52239908a6c61523e99f6731c) | feat | Add schematic to migrate control flow syntax ([#52035](https://github.com/angular/angular/pull/52035)) |
| [81b67aa987](https://github.com/angular/angular/commit/81b67aa98767078aebae22150d3441372772c28f) | feat | add support for zone.js 0.14.0 ([#51774](https://github.com/angular/angular/pull/51774)) |
| [048f400efc](https://github.com/angular/angular/commit/048f400efc75011e888ea25d214a0211f91b96d4) | feat | add warnings for oversized images and lazy-lcp ([#51846](https://github.com/angular/angular/pull/51846)) |
| [93675dc797](https://github.com/angular/angular/commit/93675dc797cb9f897c19fe298455dec52b900113) | feat | conditional built-in control flow ([#51346](https://github.com/angular/angular/pull/51346)) |
| [4427e1ebc2](https://github.com/angular/angular/commit/4427e1ebc29f5541cfe6a404f212de4359441812) | feat | create function to assert not running inside reactive context ([#52049](https://github.com/angular/angular/pull/52049)) |
| [e23aaa7d75](https://github.com/angular/angular/commit/e23aaa7d75efdd52be4dd7ca9267bc60d36059c2) | feat | drop support for older TypeScript versions ([#51792](https://github.com/angular/angular/pull/51792)) |
| [43e6fb0606](https://github.com/angular/angular/commit/43e6fb0606e15584dcb4478ad4eaa8e825dda83e) | feat | enable block syntax ([#51994](https://github.com/angular/angular/pull/51994)) |
| [3cbb2a8ecf](https://github.com/angular/angular/commit/3cbb2a8ecf202c814c37ab241ce9f57fb574692e) | feat | implement deferred block interaction triggers ([#51830](https://github.com/angular/angular/pull/51830)) |
| [8be2c48b7c](https://github.com/angular/angular/commit/8be2c48b7cda5e99f3d1efa9f26eb2615fea6a8b) | feat | implement new block syntax ([#51891](https://github.com/angular/angular/pull/51891)) |
| [a54713c831](https://github.com/angular/angular/commit/a54713c8316787eea160cfdb7f2778a087fe59ed) | feat | implement ɵgetInjectorMetadata debug API ([#51900](https://github.com/angular/angular/pull/51900)) |
| [5b88d136af](https://github.com/angular/angular/commit/5b88d136affdaa35e7015c00281b86cae040321b) | feat | mark core signal APIs as stable ([#51821](https://github.com/angular/angular/pull/51821)) |
| [8eef694def](https://github.com/angular/angular/commit/8eef694def3dc660779168925a380179c7e30993) | feat | Provide a diagnostic for missing Signal invocation in template interpolation. ([#49660](https://github.com/angular/angular/pull/49660)) |
| [40113f653c](https://github.com/angular/angular/commit/40113f653c2468315e1dea64f17e545840cc5e22) | feat | Remove deprecated `CompilerOptions.useJit` and`CompilerOptions.missingTranslation`. ([#49672](https://github.com/angular/angular/pull/49672)) |
| [68ba798ae3](https://github.com/angular/angular/commit/68ba798ae3551b789a86d46b0a3bb61d42ef3f87) | feat | revamp the runtime error message for orphan components to include full component info ([#51919](https://github.com/angular/angular/pull/51919)) |
| [1a4aee7e49](https://github.com/angular/angular/commit/1a4aee7e49074e3bc3f3099bff88eaee9b2ab255) | feat | show runtime error for orphan component rendering ([#52061](https://github.com/angular/angular/pull/52061)) |
| [687b96186c](https://github.com/angular/angular/commit/687b96186c7da731927e55e714061ea2de718505) | feat | support deferred hover triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
| [e2e3d69a27](https://github.com/angular/angular/commit/e2e3d69a277990ab79aac7aae43cbdd398e13ed9) | feat | support deferred triggers with implicit triggers ([#51922](https://github.com/angular/angular/pull/51922)) |
| [16f5fc40a4](https://github.com/angular/angular/commit/16f5fc40a49cee0d29711df1783f297ff30b5c6e) | feat | support deferred viewport triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
| [59387ee476](https://github.com/angular/angular/commit/59387ee476dff1a893a01fe5cbee3c95b93c0cdb) | feat | support styles and styleUrl as strings ([#51715](https://github.com/angular/angular/pull/51715)) |
| [9cc52b9b85](https://github.com/angular/angular/commit/9cc52b9b85ffa5cb65c6886e81b5bff10dde8d52) | feat | support TypeScript 5.2 ([#51334](https://github.com/angular/angular/pull/51334)) |
| [7d42dc3c02](https://github.com/angular/angular/commit/7d42dc3c023391e12ea607beb227fd4426e1694d) | feat | the new list reconciliation algorithm for built-in for ([#51980](https://github.com/angular/angular/pull/51980)) |
| [935c1816fd](https://github.com/angular/angular/commit/935c1816fd04caab24be66f8ef67851c88d3d4da) | fix | add `rejectErrors` option to `toSignal` ([#52474](https://github.com/angular/angular/pull/52474)) |
| [5411864c2e](https://github.com/angular/angular/commit/5411864c2e74b52e7df8022719f0fd792b50a849) | fix | adjust toSignal types to handle more common cases ([#51991](https://github.com/angular/angular/pull/51991)) |
| [dcf18dc74c](https://github.com/angular/angular/commit/dcf18dc74c260253bbf394626beb712a831824f3) | fix | allow toSignal calls in reactive context ([#51831](https://github.com/angular/angular/pull/51831)) |
| [dbffdc09c2](https://github.com/angular/angular/commit/dbffdc09c25c93868aa13ae368c9fd21a4c359fb) | fix | avoid duplicated code between entry-points (primary, testing, rxjs-interop) ([#51500](https://github.com/angular/angular/pull/51500)) |
| [4f69d620d9](https://github.com/angular/angular/commit/4f69d620d94663592780b2875acbc2b1918775f9) | fix | deferred blocks not removing content immediately when animations are enabled ([#51971](https://github.com/angular/angular/pull/51971)) |
| [df58c0b714](https://github.com/angular/angular/commit/df58c0b714e37152ddf81855ee31f93f9fa71e30) | fix | disallow `afterRender` in reactive contexts ([#52138](https://github.com/angular/angular/pull/52138)) |
| [5d61221ed7](https://github.com/angular/angular/commit/5d61221ed7b4a5d1ef005183045d45238b19a446) | fix | disallow using `effect` inside reactive contexts ([#52138](https://github.com/angular/angular/pull/52138)) |
| [99e7629159](https://github.com/angular/angular/commit/99e7629159afbb8550957a265c5bd75f7e13f052) | fix | do not remove used ng-template nodes in control flow migration ([#52186](https://github.com/angular/angular/pull/52186)) |
| [c7ff9dff2c](https://github.com/angular/angular/commit/c7ff9dff2c14aba70e92b9e216a2d4d97d6ef71e) | fix | drop mutate function from the signals public API ([#51821](https://github.com/angular/angular/pull/51821)) |
| [00128e3853](https://github.com/angular/angular/commit/00128e38538f12fe9bc72ede9b55149e0be5ead0) | fix | drop mutate function from the signals public API ([#51821](https://github.com/angular/angular/pull/51821)) ([#51986](https://github.com/angular/angular/pull/51986)) |
| [ddef3ac9a4](https://github.com/angular/angular/commit/ddef3ac9a42677b900c998b2af7cd23a8213aa3a) | fix | effects wait for ngOnInit for their first run ([#52473](https://github.com/angular/angular/pull/52473)) |
| [5ead7d412d](https://github.com/angular/angular/commit/5ead7d412d847c85176a321e58d12dcdfc0dab67) | fix | ensure a consumer drops all its stale producers ([#51722](https://github.com/angular/angular/pull/51722)) |
| [1dd8558f82](https://github.com/angular/angular/commit/1dd8558f82aeb5f5819629d5e25b616343d27f1f) | fix | Ensure backwards-referenced transplanted views are refreshed ([#51854](https://github.com/angular/angular/pull/51854)) |
| [50ad074505](https://github.com/angular/angular/commit/50ad074505c15d09fe5d85fb443d9a2775125f87) | fix | framework debug APIs getDependenciesForTokenInInjector and getInjectorMetadata ([#51719](https://github.com/angular/angular/pull/51719)) |
| [80e7a0f8fa](https://github.com/angular/angular/commit/80e7a0f8fa13ad72a32b07cd1722efab3722fa49) | fix | guard usages of `performance.mark` ([#52505](https://github.com/angular/angular/pull/52505)) |
| [b9ea2d6900](https://github.com/angular/angular/commit/b9ea2d690015dd5b919df239a5ded66f2969b97b) | fix | handle aliased index with no space in control flow migration ([#52444](https://github.com/angular/angular/pull/52444)) |
| [ffe9b1fcc2](https://github.com/angular/angular/commit/ffe9b1fcc2ecb8dc0d36c7f9228ac1a052554eef) | fix | handle for alias with as in control flow migration ([#52183](https://github.com/angular/angular/pull/52183)) |
| [e5720edb46](https://github.com/angular/angular/commit/e5720edb460a1bb51475c78d3bd442da52991a46) | fix | handle if alias in control flow migration ([#52181](https://github.com/angular/angular/pull/52181)) |
| [4461cefa4f](https://github.com/angular/angular/commit/4461cefa4f8db21009ab10a2a53de664163a86d2) | fix | handle trackBy and aliased index in control flow migration ([#52423](https://github.com/angular/angular/pull/52423)) |
| [7368b8aaeb](https://github.com/angular/angular/commit/7368b8aaeba2ef0972a8bb261208c7281e050bb9) | fix | host directive validation not picking up duplicate directives on component node ([#52073](https://github.com/angular/angular/pull/52073)) |
| [696f003553](https://github.com/angular/angular/commit/696f003553a0ca6886329728511dd46761de909b) | fix | mutation bug in `getDependenciesFromInjectable` ([#52450](https://github.com/angular/angular/pull/52450)) |
| [d487014785](https://github.com/angular/angular/commit/d48701478518d97a1fd5b4744963530494f93958) | fix | Remove no longer needed build rule related to removed migration ([#52143](https://github.com/angular/angular/pull/52143)) |
| [4da08dc2ef](https://github.com/angular/angular/commit/4da08dc2ef439d3eced7199afb9a104cfd7b54cc) | fix | remove unnecessary migration ([#52141](https://github.com/angular/angular/pull/52141)) |
| [384d7aacd0](https://github.com/angular/angular/commit/384d7aacd04dfbb951d9d4ab493759c12cf35645) | fix | replace assertion with more intentional error ([#52427](https://github.com/angular/angular/pull/52427)) |
| [40bb45f329](https://github.com/angular/angular/commit/40bb45f3297359866cab39044dba06b3e809b096) | fix | Respect OnPush change detection strategy for dynamically created components ([#51356](https://github.com/angular/angular/pull/51356)) |
| [3a19d6b743](https://github.com/angular/angular/commit/3a19d6b7437e1812ae70b3784fd6a8a185b330b1) | fix | run afterRender callbacks outside of the Angular zone ([#51385](https://github.com/angular/angular/pull/51385)) |
| [a2ba5482c3](https://github.com/angular/angular/commit/a2ba5482c3032df808cb684444f76e2825a4fd36) | fix | use TNode instead of LView for mapping injector providers ([#52436](https://github.com/angular/angular/pull/52436)) |
| [d5dad3eb4c](https://github.com/angular/angular/commit/d5dad3eb4cd837032da72899f0796c6d431cb2c9) | fix | viewport trigger deregistering callbacks multiple times ([#52115](https://github.com/angular/angular/pull/52115)) |
| [8e4a7ab52b](https://github.com/angular/angular/commit/8e4a7ab52bc85172efd12e42304e1b8da446ff75) | perf | avoid repeated access to LContainer and trackBy calculation ([#52227](https://github.com/angular/angular/pull/52227)) |
| [1dc14d9853](https://github.com/angular/angular/commit/1dc14d98539b9063b14c6463a534a4129b0a4643) | perf | avoid unnecessary callbacks in after render hooks ([#52292](https://github.com/angular/angular/pull/52292)) |
| [e90694259e](https://github.com/angular/angular/commit/e90694259e31f264f05d4aa9ebe275638577ce1a) | perf | build-in for should update indexes only when views were added / removed ([#52051](https://github.com/angular/angular/pull/52051)) |
| [1032c1e1a5](https://github.com/angular/angular/commit/1032c1e1a5f5de28f38ede1786cf973f8e8b7a53) | perf | cache LiveCollectionLContainerImpl ([#52227](https://github.com/angular/angular/pull/52227)) |
| [685d01e106](https://github.com/angular/angular/commit/685d01e1065dad6dc52eaac9eb9527100994f5ce) | perf | chain template instructions ([#51546](https://github.com/angular/angular/pull/51546)) |
| [88a0af64fd](https://github.com/angular/angular/commit/88a0af64fde58cbf71e8e2a22c39fabb8f0ee8fb) | perf | generate arrow functions for pure function calls ([#51668](https://github.com/angular/angular/pull/51668)) |
| [37d627dbd4](https://github.com/angular/angular/commit/37d627dbd4083662b103de2e28102e6ff31a9192) | perf | minimze trackBy calculations ([#52227](https://github.com/angular/angular/pull/52227)) |
| [3861a73135](https://github.com/angular/angular/commit/3861a73135ca9111c0ec10d52ee7db0a0e95f262) | perf | Update LView consumer to only mark component for check ([#52302](https://github.com/angular/angular/pull/52302)) |
| [9b9e11fcaf](https://github.com/angular/angular/commit/9b9e11fcaf5d8d639ff1d7b8feddb01751b47e14) | refactor | deprecate allowing full context object to be replaced in `EmbeddedViewRef` ([#51887](https://github.com/angular/angular/pull/51887)) |
| [ba9fc2419e](https://github.com/angular/angular/commit/ba9fc2419eee0d72c573463016a872a4b69f71c1) | refactor | deprecate the `NgProbeToken` ([#51396](https://github.com/angular/angular/pull/51396)) |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [7dde42a5df](https://github.com/angular/angular/commit/7dde42a5dfdab30e9420708722e0bef9f1467d1f) | feat | allow customization of the HttpTransferCache. ([#52029](https://github.com/angular/angular/pull/52029)) |
| [8156b3d4ec](https://github.com/angular/angular/commit/8156b3d4ec44a3b0489cc21763790a3be2969f7e) | fix | Don't override the backend when using the InMemoryWebAPI ([#52425](https://github.com/angular/angular/pull/52425)) |
| [bd9e91ecf7](https://github.com/angular/angular/commit/bd9e91ecf7af877e4ecf08a16eda7b4e59707541) | perf | reduce data transfer when using HTTP caching ([#52347](https://github.com/angular/angular/pull/52347)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [08482f2c7d](https://github.com/angular/angular/commit/08482f2c7dcbcd100981dfb266a6e63f64432328) | fix | Retain correct language service when `ts.Project` reloads ([#51912](https://github.com/angular/angular/pull/51912)) |
### service-worker
| [449830f24e](https://github.com/angular/angular/commit/449830f24e78ebd977ca3210ab3541912d959245) | feat | Complete inside @switch ([#52153](https://github.com/angular/angular/pull/52153)) |
| [e2416a284f](https://github.com/angular/angular/commit/e2416a284ff086752c809689ef74588f02e5f0e4) | feat | Enable go to definition of styleUrl ([#51746](https://github.com/angular/angular/pull/51746)) |
| [023a181ba5](https://github.com/angular/angular/commit/023a181ba5f489deb0a47bbc9b290621ad38304a) | feat | Implement outlining spans for control flow blocks ([#52062](https://github.com/angular/angular/pull/52062)) |
| [7c052bb6ef](https://github.com/angular/angular/commit/7c052bb6efde580afc61d6c50e787353c103e3e1) | feat | Support autocompletion for blocks ([#52121](https://github.com/angular/angular/pull/52121)) |
| [9d565cd6d6](https://github.com/angular/angular/commit/9d565cd6d682e5c86ee8d43e1ee1c0f8866eb274) | fix | Autocomplete block keywords in more cases ([#52198](https://github.com/angular/angular/pull/52198)) |
### localize
| Commit | Type | Description |
| -- | -- | -- |
| [cc7973f5a5](https://github.com/angular/angular/commit/cc7973f5a5cddbc5288db7d572757819327a40c3) | fix | throw a critical error when handleFetch fails ([#51960](https://github.com/angular/angular/pull/51960)) |
| [5a20a44c64](https://github.com/angular/angular/commit/5a20a44c64066e47894ca3cbe26327766ca89a42) | fix | ng-add schematics for application builder ([#51777](https://github.com/angular/angular/pull/51777)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [f0da7c2e44](https://github.com/angular/angular/commit/f0da7c2e44a29c5a71cf4880388989d129f4c6e8) | feat | schematic to remove deprecated CompilerOptions properties ([#49672](https://github.com/angular/angular/pull/49672)) |
| [965ce5a8c5](https://github.com/angular/angular/commit/965ce5a8c514237aa8e4c03a5e4b5527a1a19d96) | feat | Schematics for `TransferState`, `StateKey` and `makeStateKey` migration. ([#49594](https://github.com/angular/angular/pull/49594)) |
| [09e905ad67](https://github.com/angular/angular/commit/09e905ad672294d36f36eb2728b3483ab2e729fc) | fix | account for separator characters inside strings ([#52525](https://github.com/angular/angular/pull/52525)) |
| [4c878f90d2](https://github.com/angular/angular/commit/4c878f90d2cba689703d8def9dade3b7f8f6e8f1) | fix | Add support for nested structures inside a switch statement ([#52358](https://github.com/angular/angular/pull/52358)) |
| [d7397fb29b](https://github.com/angular/angular/commit/d7397fb29b400f193a2cda7af70058ac39247e6f) | fix | Ensure control flow migration ignores new block syntax ([#52402](https://github.com/angular/angular/pull/52402)) |
| [6a01d62b9d](https://github.com/angular/angular/commit/6a01d62b9df33732ea3dbef8de1385c8c0cb8a88) | fix | fix broken migration when no control flow is present ([#52399](https://github.com/angular/angular/pull/52399)) |
| [9c2be715a3](https://github.com/angular/angular/commit/9c2be715a3f6ea6b1d0184f5d79f5a3dfef4b576) | fix | Fixes a bug in the ngFor pre-v5 alias translation ([#52531](https://github.com/angular/angular/pull/52531)) |
| [54fed68bbf](https://github.com/angular/angular/commit/54fed68bbfed34a631df64ac6a32e99d2fade50a) | fix | Fixes the root level template offset in control flow migration ([#52355](https://github.com/angular/angular/pull/52355)) |
| [57404d4723](https://github.com/angular/angular/commit/57404d4723d3634f2b5dfdc9af1af50d8f61da70) | fix | handle comma-separated syntax in ngFor ([#52525](https://github.com/angular/angular/pull/52525)) |
| [54bc384661](https://github.com/angular/angular/commit/54bc384661ae7b884dbb822592e75df281aa02dd) | fix | handle nested classes in block entities migration ([#52309](https://github.com/angular/angular/pull/52309)) |
| [c9b1ddff4d](https://github.com/angular/angular/commit/c9b1ddff4d16cca149ca2b1faae729bf454b61e5) | fix | handle nested classes in control flow migration ([#52309](https://github.com/angular/angular/pull/52309)) |
| [6988a0070e](https://github.com/angular/angular/commit/6988a0070e9849b58738bba82d6f9eb9e3b27330) | fix | handle ngIf else condition with no whitespaces ([#52504](https://github.com/angular/angular/pull/52504)) |
| [e40e55d902](https://github.com/angular/angular/commit/e40e55d902e09c36da4cf73b5ab31b6b65584518) | fix | Remove unhelpful parsing errors from the log ([#52401](https://github.com/angular/angular/pull/52401)) |
| [c267f54bc3](https://github.com/angular/angular/commit/c267f54bc36e1c92f526071e2d78455daf8a588c) | fix | Update regex to better match ng-templates ([#52529](https://github.com/angular/angular/pull/52529)) |
### platform-browser
| Commit | Type | Description |
| -- | -- | -- |
| [c340d6e044](https://github.com/angular/angular/commit/c340d6e0440bd982dff6f9f4f4229931c62d2c08) | feat | enable removal of styles on component destroy by default ([#51571](https://github.com/angular/angular/pull/51571)) |
| [c5daa6ce77](https://github.com/angular/angular/commit/c5daa6ce776724d44c02cc97f1a349a85cb2a819) | feat | expose `EventManagerPlugin` in the public API. ([#49969](https://github.com/angular/angular/pull/49969)) |
| [5b375d106f](https://github.com/angular/angular/commit/5b375d106f2e02afadad8f5a60c37558318ea091) | fix | Fire Animations events when using async animations. ([#52087](https://github.com/angular/angular/pull/52087)) |
| [65786b2b96](https://github.com/angular/angular/commit/65786b2b96ba198034ff23bb14571a659a491b50) | fix | prevent duplicate stylesheets from being created ([#52019](https://github.com/angular/angular/pull/52019)) |
| [75d610d420](https://github.com/angular/angular/commit/75d610d420ce3a1ec6429d79c72ec6ef6c2c9a10) | fix | set animation properties when using async animations. ([#52087](https://github.com/angular/angular/pull/52087)) |
| [3c0577f991](https://github.com/angular/angular/commit/3c0577f99140b75688cb8ae969738325cc871548) | perf | disable styles of removed components instead of removing ([#51808](https://github.com/angular/angular/pull/51808)) |
| [c9cde3ab10](https://github.com/angular/angular/commit/c9cde3ab103699bc3f941d8176ee0b0373fcf7e0) | perf | only append style element on creation ([#52237](https://github.com/angular/angular/pull/52237)) |
| [dbc14eb41d](https://github.com/angular/angular/commit/dbc14eb41d540ab3f7509e41cdf64ac6fe33e13a) | refactor | remove `withNoDomReuse` function ([#52057](https://github.com/angular/angular/pull/52057)) |
### platform-server
| Commit | Type | Description |
| -- | -- | -- |
| [0c66e2424c](https://github.com/angular/angular/commit/0c66e2424c84f92765c727a98f8d2199f4b1a809) | fix | resolve relative requests URL ([#52326](https://github.com/angular/angular/pull/52326)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [1da28f4825](https://github.com/angular/angular/commit/1da28f482517ea53a18e4eb526c7c9708e6fcb55) | feat | Add callback to execute when a view transition is created ([#52002](https://github.com/angular/angular/pull/52002)) |
| [73e4bf2ed2](https://github.com/angular/angular/commit/73e4bf2ed2471faf44a49b591e19a390d5867449) | feat | Add feature to support the View Transitions API ([#51314](https://github.com/angular/angular/pull/51314)) |
| [86e91463af](https://github.com/angular/angular/commit/86e91463afc1f3d3d71a669fb2919f2b8bc5a1ca) | feat | Add option to skip the first view transition ([#51825](https://github.com/angular/angular/pull/51825)) |
| [ce1b915868](https://github.com/angular/angular/commit/ce1b915868e654cdb679e9381db9d3bd3d68d5c4) | fix | Allow redirects after an absolute redirect ([#51731](https://github.com/angular/angular/pull/51731)) |
| [37df395be0](https://github.com/angular/angular/commit/37df395be070a11b8cd84c0ff3af9290d15c4e9d) | fix | children of routes with loadComponent should not inherit parent data by default ([#52114](https://github.com/angular/angular/pull/52114)) |
| [4dce8766f8](https://github.com/angular/angular/commit/4dce8766f8a3a33ffab0b3df5981ad209db42c77) | fix | Ensure newly resolved data is inherited by child routes ([#52167](https://github.com/angular/angular/pull/52167)) |
| [f464e39364](https://github.com/angular/angular/commit/f464e39364da6436fc4b5a703f66fe7dee70818c) | fix | Ensure title observable gets latest values ([#51561](https://github.com/angular/angular/pull/51561)) |
| [b2aff43621](https://github.com/angular/angular/commit/b2aff4362129feb746856fc3d0f8e73b1927a037) | fix | Remove `urlHandlingStrategy` from public Router properties ([#51631](https://github.com/angular/angular/pull/51631)) |
| [c62e680098](https://github.com/angular/angular/commit/c62e680098a8c26fb2234336613185f7ab273483) | fix | Remove deprecated Router properties ([#51502](https://github.com/angular/angular/pull/51502)) |
| [3c6258c85b](https://github.com/angular/angular/commit/3c6258c85b37535c1178e84509b7c9ed3a1359e4) | fix | Remove deprecated setupTestingRouter function ([#51826](https://github.com/angular/angular/pull/51826)) |
| [0b3e6a41d0](https://github.com/angular/angular/commit/0b3e6a41d025997d2947125d875ac26ecd1b86d9) | fix | Remove malformedUriErrorHandler from `ExtraOptions` ([#51745](https://github.com/angular/angular/pull/51745)) |
| [c03baed854](https://github.com/angular/angular/commit/c03baed8547c2c1da576307c708d2682dfdf3742) | fix | use DOCUMENT token instead of document directly in view transitions ([#51814](https://github.com/angular/angular/pull/51814)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="16.2.9"></a>
# 16.2.9 (2023-10-11)
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [51a5baace3](https://github.com/angular/angular/commit/51a5baace3b1eee130fa911327773066139cc68e) | fix | reset() call with null values on nested group ([#48830](https://github.com/angular/angular/pull/48830)) |
<!-- CHANGELOG SPLIT MARKER -->
@@ -79,61 +399,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.0.0-next.6"></a>
# 17.0.0-next.6 (2023-09-27)
## Breaking Changes
### router
- Absolute redirects no longer prevent further redirects.
Route configurations may need to be adjusted to prevent infinite
redirects where additional redirects were previously ignored after an
absolute redirect occurred.
- The `setupTestingRouter` function has been removed. Use
`RouterModule.forRoot` or `provideRouter` to setup the `Router` for
tests instead.
## Deprecations
### animations
- The `AnimationDriver.NOOP` symbol is deprecated, use `NoopAnimationDriver` instead.
### animations
| Commit | Type | Description |
| -- | -- | -- |
| [0598613950](https://github.com/angular/angular/commit/0598613950c76f4a13601c6942e30ab4ce1e3b67) | refactor | deprecation of `AnimationDriver.NOOP` ([#51843](https://github.com/angular/angular/pull/51843)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [23bfa10ac8](https://github.com/angular/angular/commit/23bfa10ac809f6b27d32647210c52329f0e4262e) | fix | add diagnostic for inaccessible deferred trigger ([#51922](https://github.com/angular/angular/pull/51922)) |
| [31295a3cf9](https://github.com/angular/angular/commit/31295a3cf907a61e7115d9039a83a232b263a676) | fix | allocating unnecessary slots in conditional instruction ([#51913](https://github.com/angular/angular/pull/51913)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [377a7abfda](https://github.com/angular/angular/commit/377a7abfda60a6ddd55a41531e3653bcad78b0a2) | fix | bypass static resolving of the component's changeDetection field in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
| [f91f222b55](https://github.com/angular/angular/commit/f91f222b55f249089d267c72a9c0ab5b09d7c932) | fix | resolve component encapsulation enum in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [c7127b98b5](https://github.com/angular/angular/commit/c7127b98b555449f99e24a81c828ab7b1c6c4a4e) | feat | add schematic to escape block syntax characters ([#51905](https://github.com/angular/angular/pull/51905)) |
| [3cbb2a8ecf](https://github.com/angular/angular/commit/3cbb2a8ecf202c814c37ab241ce9f57fb574692e) | feat | implement deferred block interaction triggers ([#51830](https://github.com/angular/angular/pull/51830)) |
| [8be2c48b7c](https://github.com/angular/angular/commit/8be2c48b7cda5e99f3d1efa9f26eb2615fea6a8b) | feat | implement new block syntax ([#51891](https://github.com/angular/angular/pull/51891)) |
| [687b96186c](https://github.com/angular/angular/commit/687b96186c7da731927e55e714061ea2de718505) | feat | support deferred hover triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
| [e2e3d69a27](https://github.com/angular/angular/commit/e2e3d69a277990ab79aac7aae43cbdd398e13ed9) | feat | support deferred triggers with implicit triggers ([#51922](https://github.com/angular/angular/pull/51922)) |
| [16f5fc40a4](https://github.com/angular/angular/commit/16f5fc40a49cee0d29711df1783f297ff30b5c6e) | feat | support deferred viewport triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
| [dcf18dc74c](https://github.com/angular/angular/commit/dcf18dc74c260253bbf394626beb712a831824f3) | fix | allow toSignal calls in reactive context ([#51831](https://github.com/angular/angular/pull/51831)) |
### platform-browser
| Commit | Type | Description |
| -- | -- | -- |
| [3c0577f991](https://github.com/angular/angular/commit/3c0577f99140b75688cb8ae969738325cc871548) | perf | disable styles of removed components instead of removing ([#51808](https://github.com/angular/angular/pull/51808)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [86e91463af](https://github.com/angular/angular/commit/86e91463afc1f3d3d71a669fb2919f2b8bc5a1ca) | feat | Add option to skip the first view transition ([#51825](https://github.com/angular/angular/pull/51825)) |
| [ce1b915868](https://github.com/angular/angular/commit/ce1b915868e654cdb679e9381db9d3bd3d68d5c4) | fix | Allow redirects after an absolute redirect ([#51731](https://github.com/angular/angular/pull/51731)) |
| [3c6258c85b](https://github.com/angular/angular/commit/3c6258c85b37535c1178e84509b7c9ed3a1359e4) | fix | Remove deprecated setupTestingRouter function ([#51826](https://github.com/angular/angular/pull/51826)) |
### service-worker
| Commit | Type | Description |
| -- | -- | -- |
| [dcaad169ec](https://github.com/angular/angular/commit/dcaad169ec8bf0a61d032ae1ae68fb90d1face09) | fix | throw a critical error when `handleFetch` fails ([#51885](https://github.com/angular/angular/pull/51885)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="16.2.7"></a>
# 16.2.7 (2023-09-27)
### core
@@ -147,76 +412,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.0.0-next.5"></a>
# 17.0.0-next.5 (2023-09-20)
## Breaking Changes
###
- Node.js v16 support has been removed and the minimum support version has been bumped to 18.13.0.
Node.js v16 is planned to be End-of-Life on 2023-09-11. Angular will stop supporting Node.js v16 in Angular v17. For Node.js release schedule details, please see: https://github.com/nodejs/release#release-schedule
### common
-
### core
- Angular now required `zone.js` version `~0.14.0`
- Versions of TypeScript older than 5.2 are no longer supported.
### router
- `malformedUriErrorHandler` is no longer available in
the `RouterModule.forRoot` options. URL parsing errors should instead be
handled in the `UrlSerializer.parse` method.
### zone.js
- Deep and legacy `dist/` imports like `zone.js/bundles/zone-testing.js` and `zone.js/dist/zone` are no longer allowed. `zone-testing-bundle` and `zone-testing-node-bundle` are also no longer part of the package.
The proper way to import `zone.js` and `zone.js/testing` is:
```js
import 'zone.js';
import 'zone.js/testing';
```
###
| Commit | Type | Description |
| -- | -- | -- |
| [59aa0634f4](https://github.com/angular/angular/commit/59aa0634f4d4694203f2a69c40017fe5a3962514) | build | remove support for Node.js v16 ([#51755](https://github.com/angular/angular/pull/51755)) |
### common
| Commit | Type | Description |
| -- | -- | -- |
| [fe2fd7e1a8](https://github.com/angular/angular/commit/fe2fd7e1a898a4525c219065a6d0908988dfd7e2) | feat | make the warning for lazy-loaded lcp image an error ([#51748](https://github.com/angular/angular/pull/51748)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [7f6d9a73ab](https://github.com/angular/angular/commit/7f6d9a73ab8b658d0d8148080dfefb2550bee6b4) | feat | expand class api doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
| [c7daf7ea16](https://github.com/angular/angular/commit/c7daf7ea1692391f7cac8f794ed777887a2764af) | feat | extract directive docs info ([#51733](https://github.com/angular/angular/pull/51733)) |
| [e0b1bb33d7](https://github.com/angular/angular/commit/e0b1bb33d77babe881f77f52cb1b71e345f5696b) | feat | extract doc info for JsDoc ([#51733](https://github.com/angular/angular/pull/51733)) |
| [b9c70158ab](https://github.com/angular/angular/commit/b9c70158abecd81a5af512c8b4da685851cf159f) | feat | extract docs for accessors, rest params, and types ([#51733](https://github.com/angular/angular/pull/51733)) |
| [a24ae994a0](https://github.com/angular/angular/commit/a24ae994a0470fdac09a69937fd0580cff6c6d68) | feat | extract docs for top level functions and consts ([#51733](https://github.com/angular/angular/pull/51733)) |
| [2e41488296](https://github.com/angular/angular/commit/2e41488296879685b19dfba8d78037690347bda3) | feat | extract docs info for enums, pipes, and NgModules ([#51733](https://github.com/angular/angular/pull/51733)) |
| [34495b3533](https://github.com/angular/angular/commit/34495b35337892ab209d9955ff7fe2897a0c5d41) | feat | extract docs via exports ([#51828](https://github.com/angular/angular/pull/51828)) |
| [7e82df45c5](https://github.com/angular/angular/commit/7e82df45c5bb72ec3dafaa07dc1eaa5d463b006c) | feat | initial skeleton for API doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [5b66330329](https://github.com/angular/angular/commit/5b66330329fd066a7c347f040a330b4c7f2a0a2b) | fix | allow non-array imports for standalone component in local compilation mode ([#51819](https://github.com/angular/angular/pull/51819)) |
| [19c3dc18d3](https://github.com/angular/angular/commit/19c3dc18d3c0cfd83efec2c8f81b40860d570346) | fix | fix NgModule injector def in local compilation mode when imports/exports are non-array expressions ([#51819](https://github.com/angular/angular/pull/51819)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [81b67aa987](https://github.com/angular/angular/commit/81b67aa98767078aebae22150d3441372772c28f) | feat | add support for zone.js 0.14.0 ([#51774](https://github.com/angular/angular/pull/51774)) |
| [e23aaa7d75](https://github.com/angular/angular/commit/e23aaa7d75efdd52be4dd7ca9267bc60d36059c2) | feat | drop support for older TypeScript versions ([#51792](https://github.com/angular/angular/pull/51792)) |
| [5ead7d412d](https://github.com/angular/angular/commit/5ead7d412d847c85176a321e58d12dcdfc0dab67) | fix | ensure a consumer drops all its stale producers ([#51722](https://github.com/angular/angular/pull/51722)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [e2416a284f](https://github.com/angular/angular/commit/e2416a284ff086752c809689ef74588f02e5f0e4) | feat | Enable go to definition of styleUrl ([#51746](https://github.com/angular/angular/pull/51746)) |
### localize
| Commit | Type | Description |
| -- | -- | -- |
| [5a20a44c64](https://github.com/angular/angular/commit/5a20a44c64066e47894ca3cbe26327766ca89a42) | fix | ng-add schematics for application builder ([#51777](https://github.com/angular/angular/pull/51777)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [0b3e6a41d0](https://github.com/angular/angular/commit/0b3e6a41d025997d2947125d875ac26ecd1b86d9) | fix | Remove malformedUriErrorHandler from `ExtraOptions` ([#51745](https://github.com/angular/angular/pull/51745)) |
| [c03baed854](https://github.com/angular/angular/commit/c03baed8547c2c1da576307c708d2682dfdf3742) | fix | use DOCUMENT token instead of document directly in view transitions ([#51814](https://github.com/angular/angular/pull/51814)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="16.2.6"></a>
# 16.2.6 (2023-09-20)
### core
@@ -226,124 +421,16 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.0.0-next.4"></a>
# 17.0.0-next.4 (2023-09-13)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [59387ee476](https://github.com/angular/angular/commit/59387ee476dff1a893a01fe5cbee3c95b93c0cdb) | feat | support styles and styleUrl as strings ([#51715](https://github.com/angular/angular/pull/51715)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [73e4bf2ed2](https://github.com/angular/angular/commit/73e4bf2ed2471faf44a49b591e19a390d5867449) | feat | Add feature to support the View Transitions API ([#51314](https://github.com/angular/angular/pull/51314)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="16.2.5"></a>
# 16.2.5 (2023-09-13)
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.0.0-next.3"></a>
# 17.0.0-next.3 (2023-09-06)
## Breaking Changes
### core
- `OnPush` components that are created dynamically now
only have their host bindings refreshed and `ngDoCheck run` during change
detection if they are dirty.
Previously, a bug in the change detection would result in the `OnPush`
configuration of dynamically created components to be ignored when
executing host bindings and the `ngDoCheck` function. This is
rarely encountered but can happen if code has a handle on the
`ComponentRef` instance and updates values read in the `OnPush`
component template without then calling either `markForCheck` or
`detectChanges` on that component's `ChangeDetectorRef`.
### router
- `urlHandlingStrategy` has been removed from the Router public API.
This should instead be configured through the provideRouter or RouterModule.forRoot APIs.
### core
| Commit | Type | Description |
| -- | -- | -- |
| [40bb45f329](https://github.com/angular/angular/commit/40bb45f3297359866cab39044dba06b3e809b096) | fix | Respect OnPush change detection strategy for dynamically created components ([#51356](https://github.com/angular/angular/pull/51356)) |
| [88a0af64fd](https://github.com/angular/angular/commit/88a0af64fde58cbf71e8e2a22c39fabb8f0ee8fb) | perf | generate arrow functions for pure function calls ([#51668](https://github.com/angular/angular/pull/51668)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [f464e39364](https://github.com/angular/angular/commit/f464e39364da6436fc4b5a703f66fe7dee70818c) | fix | Ensure title observable gets latest values ([#51561](https://github.com/angular/angular/pull/51561)) |
| [b2aff43621](https://github.com/angular/angular/commit/b2aff4362129feb746856fc3d0f8e73b1927a037) | fix | Remove `urlHandlingStrategy` from public Router properties ([#51631](https://github.com/angular/angular/pull/51631)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="16.2.4"></a>
# 16.2.4 (2023-09-06)
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.0.0-next.2"></a>
# 17.0.0-next.2 (2023-08-30)
## Breaking Changes
### platform-browser
- `REMOVE_STYLES_ON_COMPONENT_DESTROY` default value is now `true`. This causes CSS of components to be removed from the DOM when destroyed. You retain the previous behaviour by providing the `REMOVE_STYLES_ON_COMPONENT_DESTROY` injection token.
```ts
import {REMOVE_STYLES_ON_COMPONENT_DESTROY} from '@angular/platform-browser';
...
providers: [{
provide: REMOVE_STYLES_ON_COMPONENT_DESTROY,
useValue: false,
}]
```
### router
- The following Router properties have been removed from
the public API:
- canceledNavigationResolution
- paramsInheritanceStrategy
- titleStrategy
- urlUpdateStrategy
- malformedUriErrorHandler
These should instead be configured through the `provideRouter` or
`RouterModule.forRoot` APIs.
### animations
| Commit | Type | Description |
| -- | -- | -- |
| [698c058e1c](https://github.com/angular/angular/commit/698c058e1c975c573722407f4843a4a774ceb92a) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
| [18be804c03](https://github.com/angular/angular/commit/18be804c038e8d81a60c9a72521cfa640c8a1d5a) | fix | remove unnecessary escaping in regex expressions ([#51554](https://github.com/angular/angular/pull/51554)) |
### common
| Commit | Type | Description |
| -- | -- | -- |
| [86c5e34601](https://github.com/angular/angular/commit/86c5e34601d7901a11688124aa902646524177eb) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [de2550d988](https://github.com/angular/angular/commit/de2550d9886394e1ecde586d72bf2bab5b65cb39) | fix | correct incomplete escaping ([#51557](https://github.com/angular/angular/pull/51557)) |
| [3bca9db4a5](https://github.com/angular/angular/commit/3bca9db4a56d61ac22b4ce87591d8862606177c8) | fix | remove unnecessary escaping in regex expressions ([#51554](https://github.com/angular/angular/pull/51554)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [dbffdc09c2](https://github.com/angular/angular/commit/dbffdc09c25c93868aa13ae368c9fd21a4c359fb) | fix | avoid duplicated code between entry-points (primary, testing, rxjs-interop) ([#51500](https://github.com/angular/angular/pull/51500)) |
| [45d2ded0ea](https://github.com/angular/angular/commit/45d2ded0ea9ef414948256099f8dc9c4598fdc2b) | fix | correct incomplete escaping ([#51557](https://github.com/angular/angular/pull/51557)) |
| [006577f39c](https://github.com/angular/angular/commit/006577f39c0e46e37491e44687142521fe7fab54) | fix | handle hydration of view containers that use component hosts as anchors ([#51456](https://github.com/angular/angular/pull/51456)) |
| [1423bfbf8f](https://github.com/angular/angular/commit/1423bfbf8ffa3b43d0dea41054c8f950e669a697) | fix | remove unnecessary escaping in regex expressions ([#51554](https://github.com/angular/angular/pull/51554)) |
| [3a19d6b743](https://github.com/angular/angular/commit/3a19d6b7437e1812ae70b3784fd6a8a185b330b1) | fix | run afterRender callbacks outside of the Angular zone ([#51385](https://github.com/angular/angular/pull/51385)) |
| [685d01e106](https://github.com/angular/angular/commit/685d01e1065dad6dc52eaac9eb9527100994f5ce) | perf | chain template instructions ([#51546](https://github.com/angular/angular/pull/51546)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [88b1575271](https://github.com/angular/angular/commit/88b157527172d70ed3e5aa11aa8b7963a8612e49) | fix | correct incomplete escaping ([#51557](https://github.com/angular/angular/pull/51557)) |
### platform-browser
| Commit | Type | Description |
| -- | -- | -- |
| [c340d6e044](https://github.com/angular/angular/commit/c340d6e0440bd982dff6f9f4f4229931c62d2c08) | feat | enable removal of styles on component destroy by default ([#51571](https://github.com/angular/angular/pull/51571)) |
| [c5daa6ce77](https://github.com/angular/angular/commit/c5daa6ce776724d44c02cc97f1a349a85cb2a819) | feat | expose `EventManagerPlugin` in the public API. ([#49969](https://github.com/angular/angular/pull/49969)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [c62e680098](https://github.com/angular/angular/commit/c62e680098a8c26fb2234336613185f7ab273483) | fix | Remove deprecated Router properties ([#51502](https://github.com/angular/angular/pull/51502)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="16.2.3"></a>
# 16.2.3 (2023-08-30)
### animations
@@ -369,28 +456,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.0.0-next.1"></a>
# 17.0.0-next.1 (2023-08-23)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [d910bf8a84](https://github.com/angular/angular/commit/d910bf8a843c07a096969d5e47b49f60981b00e9) | fix | Allow safeUrl for ngSrc in NgOptimizedImage ([#51351](https://github.com/angular/angular/pull/51351)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [5bd9fbd2c3](https://github.com/angular/angular/commit/5bd9fbd2c3ab4467074fac5e4d689b3c85bf08cd) | fix | enforce a minimum version to be used when a library uses input transform ([#51413](https://github.com/angular/angular/pull/51413)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [9cc52b9b85](https://github.com/angular/angular/commit/9cc52b9b85ffa5cb65c6886e81b5bff10dde8d52) | feat | support TypeScript 5.2 ([#51334](https://github.com/angular/angular/pull/51334)) |
| [a9b3c006f8](https://github.com/angular/angular/commit/a9b3c006f8593e0187298df21418644070312a40) | fix | guard the jasmine hooks ([#51394](https://github.com/angular/angular/pull/51394)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [96d94ad130](https://github.com/angular/angular/commit/96d94ad13072032326446e8a20658c9f38fd1b8e) | fix | Ensure `canceledNavigationResolution: 'computed'` works on first page ([#51441](https://github.com/angular/angular/pull/51441)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="16.2.2"></a>
# 16.2.2 (2023-08-23)
### common
@@ -412,11 +477,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.0.0-next.0"></a>
# 17.0.0-next.0 (2023-08-16)
<!-- CHANGELOG SPLIT MARKER -->
<a name="16.2.1"></a>
# 16.2.1 (2023-08-16)
### router
+3 -3
View File
@@ -129,7 +129,7 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
Join the conversation and help the community.
- [Twitter][twitter]
- [X (formerly Twitter)][X (formerly Twitter)]
- [Discord][discord]
- [Gitter][gitter]
- [YouTube][youtube]
@@ -152,13 +152,13 @@ Join the conversation and help the community.
[forms]: https://angular.io/guide/forms-overview
[api]: https://angular.io/api
[angularelements]: https://angular.io/guide/elements
[ssr]: https://angular.io/guide/universal
[ssr]: https://angular.io/guide/ssr
[schematics]: https://angular.io/guide/schematics
[lazyloading]: https://angular.io/guide/lazy-loading-ngmodules
[node.js]: https://nodejs.org/
[npm]: https://www.npmjs.com/get-npm
[codeofconduct]: CODE_OF_CONDUCT.md
[twitter]: https://www.twitter.com/angular
[X (formerly Twitter)]: https://www.twitter.com/angular
[discord]: https://discord.gg/angular
[gitter]: https://gitter.im/angular/angular
[stackoverflow]: https://stackoverflow.com/questions/tagged/angular
+4 -4
View File
@@ -80,7 +80,7 @@ yarn_install(
"//:.yarnrc",
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
"//tools:postinstall-patches.js",
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-680aab4562e5bb684518ca496cb449a6c447601d.patch",
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-b2cec2dcba358f9dc7111800a3d65738f57abe8f.patch",
"//tools/esm-interop:patches/npm/@bazel+concatjs+5.8.1.patch",
"//tools/esm-interop:patches/npm/@bazel+esbuild+5.7.1.patch",
"//tools/esm-interop:patches/npm/@bazel+protractor+5.7.1.patch",
@@ -196,10 +196,10 @@ cldr_xml_data_repository(
# sass rules
http_archive(
name = "io_bazel_rules_sass",
sha256 = "923b2c470e981b6e96b17e5b9ec7a6141eab808d52ebffdbd94695f5a42a5184",
strip_prefix = "rules_sass-9a327e5badc3fdd5141479410675c356133e1e41",
sha256 = "7a90567717c3417a23ff4b21963a698a6e9aa669ac9e682121ea33f218a2816a",
strip_prefix = "rules_sass-425ecafa8268bc013d684216363fe75bc802323e",
urls = [
"https://github.com/bazelbuild/rules_sass/archive/9a327e5badc3fdd5141479410675c356133e1e41.zip",
"https://github.com/bazelbuild/rules_sass/archive/425ecafa8268bc013d684216363fe75bc802323e.zip",
],
)
+67
View File
@@ -0,0 +1,67 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
/src/assets/tutorials
/src/assets/previews
/TEMP
# Node
node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# Ignore ng-dev logs and user-config
.ng-dev.err*
.ng-dev.log
.ng-dev.user*
# System files
.DS_Store
Thumbs.db
# Yarn v2+. Zero installs disabled since we are not using PnP.
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
!.yarn/yarn.js
# Firebase
.firebase
*-debug.log
.runtimeconfig.json
+42
View File
@@ -0,0 +1,42 @@
{
// Default state for all rules
"default": true,
// MD013/line-length - Line length
"MD013": {
// Number of characters
"line_length": 750,
// Include code blocks
"code_blocks": false,
// Include tables
"tables": false
},
// MD034/bare-URL-used - Do not lint bare url - we display links in config code that does not match linter styles
"MD034": false,
// MD038 - Spaces inside code span elements - We display inline templates in codeblocks
"MD038": false,
// MD046/code-block-style - Do not lint inline code blocks - we display formatted CLI output in code that does not match linter styles
"MD046": false,
// MD049/emphasis-style - Emphasis style should be consistent
"MD049": false,
// TODO: Enable the following rules as MD improvements:
// MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content
"MD024": false,
// MD033/no-inline-html - Inline HTML
"MD033": false,
// MD051/link-fragments - Link fragments should be valid
"MD051": false,
// MD052/reference-links-images - Reference links and images should use a label that is defined
"MD052": false,
// MD037/no-space-in-emphasis - Inline lists (such as in tables) incorrectly trigger this warning.
"MD037": false
}
+1
View File
@@ -0,0 +1 @@
node_modules/
+2
View File
@@ -0,0 +1,2 @@
.yarn/releases/*.cjs
src/content/examples/**/*.*
+5
View File
@@ -0,0 +1,5 @@
# Ignoring documentation style examples
/src/content/api-examples
/src/content/examples
/src/content/tutorials
node_modules/
+130
View File
@@ -0,0 +1,130 @@
{
"customSyntax": "postcss-scss",
"defaultSeverity": "error",
"reportNeedlessDisables": true,
"reportInvalidScopeDisables": true,
"plugins": [
"./tools/stylelint/loader-rule.js",
"./tools/stylelint/no-concrete-rules.ts",
"./tools/stylelint/no-import.ts",
"./tools/stylelint/no-unused-import.ts",
"./tools/stylelint/selector-no-deep.ts",
"./tools/stylelint/single-line-comment-only.ts"
],
"rules": {
"@angular/selector-no-deep": true,
"@angular/no-unused-import": true,
"@angular/single-line-comment-only": true,
"@angular/no-import": true,
"color-hex-case": "lower",
"color-no-invalid-hex": true,
"function-calc-no-unspaced-operator": true,
"function-comma-space-after": "always-single-line",
"function-comma-space-before": "never",
"function-name-case": "lower",
"function-url-quotes": "always",
"function-url-scheme-disallowed-list": ["data"],
"function-whitespace-after": "always",
"number-leading-zero": "always",
"number-no-trailing-zeros": true,
"length-zero-no-unit": true,
"string-no-newline": true,
"string-quotes": "single",
"unit-case": "lower",
"unit-no-unknown": true,
"unit-allowed-list": [
"px",
"%",
"deg",
"s",
"ms",
"em",
"rem",
"vh",
"vw",
"vmin",
"fr",
"ch",
"lvh",
"svh",
"dvh"
],
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never",
"shorthand-property-no-redundant-values": true,
"property-case": "lower",
"no-duplicate-at-import-rules": true,
"declaration-block-no-duplicate-properties": [
true,
{
"ignore": ["consecutive-duplicates-with-different-values"]
}
],
"declaration-block-trailing-semicolon": "always",
"declaration-block-single-line-max-declarations": 1,
"declaration-block-semicolon-space-before": "never",
"declaration-block-semicolon-space-after": "always-single-line",
"declaration-block-semicolon-newline-before": "never-multi-line",
"declaration-block-semicolon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-property-value-disallowed-list": [
{"/.*/": ["initial"]},
{"message": "The `initial` value is not supported in IE."}
],
"block-closing-brace-newline-after": "always",
"block-closing-brace-newline-before": "always-multi-line",
"block-opening-brace-newline-after": "always-multi-line",
"block-opening-brace-space-before": "always-multi-line",
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-element-case": "lower",
"selector-pseudo-element-colon-notation": "double",
"selector-pseudo-element-no-unknown": true,
"selector-type-case": "lower",
"selector-max-id": 0,
"no-missing-end-of-source-newline": true,
"no-eol-whitespace": true,
"max-line-length": [
100,
{
"ignorePattern": "/https?://.*/"
}
],
"linebreaks": "unix",
"selector-class-pattern": [
"^_?(docs-|adev-|cm-|mat-|mdc-|xterm|ͼ3|ͼ2u|ͼ1|ng-|hljs-|as-|cdk-)",
{
"resolveNestedSelectors": true
}
]
},
"overrides": [
{
"files": ["projects/shared/src/lib/styles/_split.scss"],
"rules": {
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": ["ng-deep"]
}
]
}
}
]
}
+3
View File
@@ -0,0 +1,3 @@
# Angular.dev: The new home for Angular developers (https://angular.dev)
Read more about our initial launch on the [Angular blog](https://goo.gle/angular-dot-dev).
+143
View File
@@ -0,0 +1,143 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angular-dev": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss",
"standalone": true,
"changeDetection": "OnPush"
}
},
"root": "projects/angular-dev",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/angular-dev",
"index": "src/index.html",
"browser": "src/main.ts",
"server": "src/main.server.ts",
"prerender": true,
"polyfills": ["src/polyfills.ts", "zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/robots.txt",
"src/assets"
],
"styles": ["projects/shared/src/lib/styles/global-styles.scss"],
"scripts": [],
"webWorkerTsConfig": "tsconfig.worker.json"
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "5kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "angular-dev:build:production"
},
"development": {
"buildTarget": "angular-dev:build:development"
}
},
"defaultConfiguration": "development",
"options": {
"buildTarget": "web-ui:build",
"headers": {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "angular-dev:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"include": ["src/app"],
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["projects/shared/src/lib/styles/global-styles.scss"],
"scripts": [],
"webWorkerTsConfig": "tsconfig.worker.json"
}
}
}
},
"shared": {
"projectType": "library",
"root": "projects/shared",
"sourceRoot": "projects/shared/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/shared/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/shared/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/shared/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "projects/shared/tsconfig.spec.json",
"polyfills": ["zone.js", "zone.js/testing"]
}
}
}
}
},
"cli": {
"analytics": "dca119a9-da31-47f7-a6cb-b60541037021"
}
}
+122
View File
@@ -0,0 +1,122 @@
{
"name": "blossom",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "yarn generate-all && ng serve",
"build": "ng build angular-dev",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"ng-dev": "cross-env TS_NODE_PROJECT=$PWD/.ng-dev/tsconfig.json TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm node_modules/@angular/ng-dev/bundles/cli.mjs",
"lint": "tslint -c tslint.json --project tsconfig.json && stylelint \"projects/**/*.+(css|scss)\" --config .stylelintrc.json && markdownlint \"projects/**/*.md\"",
"lint-fix": "markdownlint --fix '**/*.md'",
"format": "yarn ng-dev format changed && tslint -c tslint.json --fix --project tsconfig.json",
"serve:prerender": "http-server -c-l dist/angular-dev/browser",
"postinstall": "yarn generate-all",
"generate-all": "yarn generate-tutorials && yarn generate-docs && yarn generate-previews && yarn set-version && yarn generate-api && yarn generate-cli",
"generate-api": "ts-node-esm ./prerender/api-gen/index.ts all ./src/assets/content/api",
"generate-cli": "ts-node-esm ./prerender/api-gen/cli-index.ts all ./src/assets/content/cli",
"generate-docs": "ts-node-esm ./prerender/markdown-pipeline/index.mts",
"generate-previews": "ts-node-esm scripts/examples/generate-previews-components-map.mts",
"generate-tutorials": "ts-node scripts/tutorials/generate-tutorials.ts",
"check-links": "ts-node scripts/checks/check-links.ts",
"new-example": "ts-node-esm scripts/examples/create-example.mts",
"new-tutorial": "ts-node scripts/tutorials/new-tutorial.ts",
"new-playground-template": "ts-node scripts/tutorials/new-playground-template.ts",
"update-tutorials-package-lock": "ts-node scripts/tutorials/update-package-lock.ts",
"set-version": "ts-node-esm scripts/versions/set-current-version.mts",
"prepare": "husky install"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.0.0-rc.1",
"@angular/cdk": "17.0.0-rc.1",
"@angular/common": "^17.0.0-rc.1",
"@angular/compiler": "^17.0.0-rc.1",
"@angular/core": "^17.0.0-rc.1",
"@angular/elements": "^17.0.0-rc.1",
"@angular/forms": "^17.0.0-rc.1",
"@angular/material": "17.0.0-rc.1",
"@angular/platform-browser": "^17.0.0-rc.1",
"@angular/platform-browser-dynamic": "^17.0.0-rc.1",
"@angular/platform-server": "^17.0.0-rc.1",
"@angular/router": "^17.0.0-rc.1",
"@angular/service-worker": "^17.0.0-rc.1",
"@codemirror/autocomplete": "^6.9.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-angular": "^0.1.2",
"@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.3",
"@codemirror/lang-javascript": "^6.1.8",
"@codemirror/lang-sass": "^6.0.1",
"@codemirror/language": "^6.7.0",
"@codemirror/lint": "^6.4.0",
"@codemirror/search": "^6.4.0",
"@codemirror/state": "^6.2.1",
"@codemirror/tooltip": "^0.19.16",
"@codemirror/view": "^6.12.0",
"@lezer/highlight": "^1.1.6",
"@typescript/vfs": "^1.5.0",
"@webcontainer/api": "^1.1.5",
"algoliasearch": "^4.20.0",
"angular-split": "16.2.1",
"diff": "^5.1.0",
"express": "^4.15.2",
"gsap": "^3.12.2",
"highlight.js": "^11.8.0",
"jszip": "^3.10.1",
"ngx-progressbar": "^11.1.0",
"ogl": "^1.0.1",
"preact": "^10.17.1",
"preact-render-to-string": "^6.2.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.0.0-rc.3",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#bb8a1cf74bda7e7a193cc449ca9558bc3ce55ad0",
"@angular/cli": "17.0.0-rc.3",
"@angular/compiler-cli": "^17.0.0-rc.1",
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#09c58eaf121b8e71edbe863c373854c0a221e290",
"@inquirer/prompts": "^3.0.2",
"@mermaid-js/mermaid-cli": "^10.3.1",
"@types/algoliasearch": "^4.0.0",
"@types/diff": "^5.0.3",
"@types/dom-navigation": "~1.0.0",
"@types/dom-view-transitions": "~1.0.1",
"@types/express": "^4.17.0",
"@types/jasmine": "~4.3.0",
"@types/jsdom": "^21.1.2",
"@types/marked": "^5.0.1",
"@types/node": "^18.11.9",
"cross-env": "^7.0.3",
"emoji-regex": "^10.2.1",
"firebase-tools": "latest",
"glob": "^10.2.7",
"html-entities": "^2.4.0",
"http-server": "^14.1.1",
"husky": "^8.0.0",
"jasmine-core": "~4.5.0",
"jsdom": "^22.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"markdownlint-cli": "^0.36.0",
"marked": "^5.1.1",
"ng-packagr": "17.0.0-next.1",
"postcss-scss": "^4.0.4",
"prettier": "^2.8.4",
"stylelint": "^14.14.0",
"ts-node": "^10.8.1",
"tslint": "^6.1.3",
"tslint-eslint-rules": "5.4.0",
"typescript": "~5.2.0",
"uuid": "^9.0.0",
"vrsource-tslint-rules": "6.0.0"
}
}
+116
View File
@@ -0,0 +1,116 @@
/*!
* @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
*/
/* tslint:disable:no-console */
import {NavigationItem} from '@angular/docs-shared';
import {readFileSync} from 'fs';
import {glob} from 'glob';
import {join} from 'path';
import {SUB_NAVIGATION_DATA} from '../../src/app/sub-navigation-data';
// PATHS
const PROJECT_FOLDER_PATH = join(__dirname, '../../src');
// SKIP Files
const SKIPED_ORPHAN = ['kitchen-sink', 'error', 'examples'];
main();
async function main() {
const contentDir = join(PROJECT_FOLDER_PATH, 'content');
const mdFiles = await retrieveAllMarkdownFiles(contentDir);
const allowedPaths = new Set([
...SUB_NAVIGATION_DATA.docs.flatMap((item: any) => getPaths(item)),
SUB_NAVIGATION_DATA.reference.flatMap((item: any) => getPaths(item)),
SUB_NAVIGATION_DATA.footer.flatMap((item: any) => getPaths(item)),
]);
const contentPaths = new Set([
...SUB_NAVIGATION_DATA.docs.flatMap((item: any) => getContentPaths(item)),
...SUB_NAVIGATION_DATA.reference.flatMap((item: any) => getContentPaths(item)),
...SUB_NAVIGATION_DATA.footer.flatMap((item: any) => getContentPaths(item)),
]);
// Regular expression to match relative links
const regex = /\]\((?!https?:\/\/)([^)]+)\)/g;
const orphanFiles: string[] = [];
for (const {path} of mdFiles) {
if (
!contentPaths.has(path.slice(0, -3)) &&
!path.startsWith('tutorials') &&
!SKIPED_ORPHAN.some((skip) => path.startsWith(skip))
) {
orphanFiles.push(path.slice(0, -3));
}
}
if (orphanFiles.length > 0) {
console.log('======== ORPHAN FILES =========\n');
orphanFiles.forEach((file) => console.warn(file));
}
const deadLinks: {path: string; links: string[]}[] = [];
for (const {content, path} of mdFiles) {
let match;
const matches = [];
while ((match = regex.exec(content)) !== null) {
const relativeLink = match[1].split('#')[0];
if (!allowedPaths.has(relativeLink) && relativeLink.startsWith('guide')) {
matches.push(relativeLink);
}
}
if (matches.length) {
deadLinks.push({path, links: matches});
}
}
if (deadLinks.length > 0) {
console.warn('\n\n======== DEAD RELATIVE LINKS =========\n');
deadLinks.forEach(({path, links}) => {
console.warn(path);
links.forEach((path) => console.log(`\t ${path}`));
});
}
if (deadLinks.length > 0 || orphanFiles.length > 0) {
throw new Error('Check fails');
}
}
function getPaths(navItem: NavigationItem): string[] {
return [
...(navItem.path ? [navItem.path] : []),
...(navItem.children ?? []).flatMap((item) => getPaths(item)),
];
}
function getContentPaths(navItem: NavigationItem): string[] {
return [
...(navItem.contentPath ? [navItem.contentPath] : []),
...(navItem.children ?? []).flatMap((item) => getContentPaths(item)),
];
}
/**
* Recursively search the provided directory for all markdown files and
* asyncronously load them.
*/
async function retrieveAllMarkdownFiles(
baseDir: string,
): Promise<{path: string; content: string}[]> {
const files = await glob('**/*.md', {
root: baseDir,
cwd: baseDir,
ignore: ['**/node_modules/**'],
});
return files.map((fullPathToFile) => ({
path: fullPathToFile,
content: readFileSync(join(baseDir, fullPathToFile), {encoding: 'utf-8'}),
}));
}
+135
View File
@@ -0,0 +1,135 @@
/*!
* @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);
}
}
@@ -0,0 +1,158 @@
/*!
* @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);
}
@@ -0,0 +1,23 @@
/*!
* @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}
};
@@ -0,0 +1,221 @@
/*!
* @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
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"ts-node": {
"compilerOptions": {
"module": "es2020"
}
}
}
@@ -0,0 +1,55 @@
/*!
* @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
@@ -0,0 +1,77 @@
/*!
* @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});
}
}
@@ -0,0 +1,35 @@
/*!
* @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';
// FOLDER NAMES
export const ASSETS_FOLDER_NAME = 'assets';
export const CONTENT_FOLDER_NAME = 'content';
export const EXAMPLES_FOLDER_NAME = 'examples';
export const TUTORIALS_FOLDER_NAME = 'tutorials';
// PATHS
export const BASE_PATH = dirname(fileURLToPath(import.meta.url));
export const ASSETS_EXAMPLES_PATH = 'assets/content/examples';
export const PROJECT_FOLDER_PATH = join(BASE_PATH, '../../src');
export const ASSETS_CONTENT_FOLDER_PATH = join(
PROJECT_FOLDER_PATH,
ASSETS_FOLDER_NAME,
CONTENT_FOLDER_NAME,
);
export const CONTENT_FOLDER_PATH = join(PROJECT_FOLDER_PATH, CONTENT_FOLDER_NAME);
export const EXAMPLES_FOLDER_PATH = join(CONTENT_FOLDER_PATH, EXAMPLES_FOLDER_NAME);
export const TUTORIALS_FOLDER_PATH = join(CONTENT_FOLDER_PATH, TUTORIALS_FOLDER_NAME);
// TODO: Update the branch/sha
// URLS
export const GITHUB_CONTENT_URL = 'https://github.com/angular/angular/blob/main/adev/src/content';
export const GITHUB_EDIT_CONTENT_URL =
'https://github.com/angular/angular/edit/main/adev/src/content';
@@ -0,0 +1,146 @@
/*!
* @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 {run} from '@mermaid-js/mermaid-cli';
import {readFile, writeFile} from 'fs/promises';
import {marked} from 'marked';
import {v4 as uuidv4} from 'uuid';
import {
DocsCodeMultifileToken,
DocsCodeToken,
DocsCodeTripleTickBlockToken,
} from './tranformations/docs-code.mjs';
import {generateStackblitzExample} from './../../scripts/examples/stackblitz-builder.mjs';
export const MERMAID_TEMP_FOLDER = 'TEMP';
const MERMAID_INPUT = `${MERMAID_TEMP_FOLDER}/mermaid-input-#{id}.mmd`;
const MERMAID_OUTPUT_SVG = `${MERMAID_TEMP_FOLDER}/mermaid-output-#{id}.svg`;
// If token type is 'docs-code' or 'docs-code-triple-tick' and language is 'mermaid' then we can try to render Mermaid svg.
export async function docsWalkTokens(walkToken: marked.Tokens.Generic): Promise<void> {
const token = walkToken as DocsCodeToken | DocsCodeTripleTickBlockToken | DocsCodeMultifileToken;
await handleMermaid(token);
await handleStackblitzExamples(token);
}
async function handleMermaid(
token: DocsCodeToken | DocsCodeMultifileToken | DocsCodeTripleTickBlockToken,
) {
if (
token.language === 'mermaid' &&
(token.type === 'docs-code' || token.type === 'docs-code-triple-tick')
) {
token.code = await renderMermaidToSvg(token.code);
}
}
async function handleStackblitzExamples(
token: DocsCodeToken | DocsCodeTripleTickBlockToken | DocsCodeMultifileToken,
) {
if (
(token.type !== 'docs-code' && token.type !== 'docs-code-multifile') ||
!token.preview ||
!token.path
) {
return;
}
const defaultCodeToken: marked.Tokens.Generic =
token.type === 'docs-code-multifile' ? token.paneTokens[0] : token;
const path: string = defaultCodeToken['diff'] ?? defaultCodeToken['path'];
const pathToExampleRule = /([^\/]+)\/(.+)/s;
const pathToExampleSegments = path.match(pathToExampleRule);
if (!pathToExampleSegments) {
return;
}
const exampleFolder = pathToExampleSegments[1];
const primaryFilePath = pathToExampleSegments[2];
await generateStackblitzExample(exampleFolder, primaryFilePath, defaultCodeToken['header']);
}
export async function renderMermaidToSvg(content: string): Promise<string> {
// Generate input and output path
const mermaidTempFileId = uuidv4();
const input = MERMAID_INPUT.replace('#{id}', mermaidTempFileId);
const output = MERMAID_OUTPUT_SVG.replace('#{id}', mermaidTempFileId) as `${string}.svg`;
// Create *.mmd file with provided content.
await writeFile(input, content);
// Execute rendering. The generated SVG file will be written in the output path.
await run(input, output, {
quiet: true,
puppeteerConfig: {headless: 1},
parseMMDOptions: {
mermaidConfig: {
themeCSS: `
background-color: var(--page-background) !important; // svg background color
g {
rect {
stroke: black !important; // border around the rectangles, same for dark/light theme
filter: drop-shadow(5px 5px 0px var(--vivid-pink));
}
}
.messageText, .pieTitleText {
fill: var(--primary-contrast) !important; // pie chart title text and line labels
}
.pieOuterCircle {
stroke-width: 1px;
}
.pieCircle {
stroke-width: 1.5px;
}
.legend {
rect {
filter: none;
opacity: 0.7;
}
text {
fill: var(--primary-contrast) !important; // legend label text color
}
}
.slice { // e.g. text on the pie charts
fill: var(--primary-contrast) !important;
}
.flowchart-link, line { // lines
stroke: var(--primary-contrast) !important;
}
.marker,
#statediagram-barbEnd,
.transition,
#arrowhead path { // arrows
stroke: var(--primary-contrast) !important;
fill: var(--primary-contrast) !important;
}
.cluster rect {
stroke: var(--primary-contrast) !important;
fill: var(--page-background) !important;
}
`,
theme: 'base',
themeVariables: {
fontFamily: 'sans-serif',
primaryColor: '#fff',
primaryBorderColor: '#000',
pie1: '#0546ff',
pie2: '#f637e3',
pie3: '#f11653',
pie4: '#8001c6',
pie5: '#00c572',
pie6: '#fe3700',
},
},
},
});
// Read the content of generated SVG file.
return await readFile(output, 'utf8');
}
@@ -0,0 +1,22 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {resetHeaderIdsOfCurrentDocument} from './state.mjs';
/**
* Custom hooks for marked that will be used to post-transform markdown files with parent styles for Angular docs.
*/
export const hooks = {
preprocess(html: string): string {
resetHeaderIdsOfCurrentDocument();
return html;
},
postprocess(html: string): string {
return html;
},
};
+189
View File
@@ -0,0 +1,189 @@
/*!
* @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 {existsSync} from 'fs';
import {copyFile, mkdir, readdir, rm, stat, writeFile} from 'fs/promises';
import {marked} from 'marked';
import {dirname, join} from 'path';
import {PROJECT_FOLDER_PATH} from './constants.mjs';
import {MERMAID_TEMP_FOLDER, docsWalkTokens} from './docs-walk-tokens.mjs';
import {hooks} from './hooks.mjs';
import {renderer} from './renderer.mjs';
import {setCurrentParsedFilePath} from './state.mjs';
import {docsAlertExtension} from './tranformations/docs-alert.mjs';
import {docsCalloutExtension} from './tranformations/docs-callout.mjs';
import {docsCardContainerExtension, docsCardExtension} from './tranformations/docs-card.mjs';
import {
docsCodeExtension,
docsCodeMultifileExtension,
docsTripleTickMarkdownCodeExtension,
} from './tranformations/docs-code.mjs';
import {docsDecorativeHeaderExtension} from './tranformations/docs-decorative-header.mjs';
import {docsPillExtension, docsPillRowExtension} from './tranformations/docs-pill.mjs';
import {docsVideoExtension} from './tranformations/docs-video.mjs';
import {docsStepExtension, docsWorkflowExtension} from './tranformations/docs-workflow.mjs';
import {retrieveAllMarkdownFiles} from './utils.mjs';
declare var logs: string[];
(globalThis as any).logs = [];
/**
* Fully configured processor instance.
*
* Marked was chosen as our processor as it is a popular and well supported by the community. Additionally,
* its processing of custom HTML allows for a more direct interactions rather than being done within a custom
* AST representation as other solutions provide.
*/
marked.use({
mangle: false,
headerIds: false,
renderer,
hooks,
extensions: [
/** Custom Extensions are @type marked.TokenizerAndRendererExtension but the this.renderer uses a custom Token that extends marked.Tokens.Generic which is not exported by @types/marked */
// @ts-ignore @types/marked
docsCardExtension,
// @ts-ignore @types/marked
docsCardContainerExtension,
// @ts-ignore @types/marked
docsCalloutExtension,
// @ts-ignore @types/marked
docsDecorativeHeaderExtension,
// @ts-ignore @types/marked
docsPillRowExtension,
// @ts-ignore @types/marked
docsPillExtension,
// @ts-ignore @types/marked
docsWorkflowExtension,
// @ts-ignore @types/marked
docsStepExtension,
// @ts-ignore @types/marked
docsCodeExtension,
// @ts-ignore @types/marked
docsCodeMultifileExtension,
// @ts-ignore @types/marked
docsTripleTickMarkdownCodeExtension,
// @ts-ignore @types/marked
docsAlertExtension,
// @ts-ignore @types/marked
docsVideoExtension,
],
// Marked will return a promise if the async option is true.
// The async option will tell marked to await any walkTokens functions before parsing the tokens and returning an HTML string.
// More details: https://marked.js.org/using_pro#async
async: true,
walkTokens: docsWalkTokens,
});
main();
async function main() {
/** Create temporary folder which helps to generate mermaid diagrams in the svg format */
await createTempMermaidFolder();
/** Generate Docs Content */
await generateContent(
join(PROJECT_FOLDER_PATH, 'content'),
join(PROJECT_FOLDER_PATH, 'assets', 'content'),
);
/** Copy the images from source content folder to assets */
await copyFolder(
join(PROJECT_FOLDER_PATH, 'content', 'images'),
join(PROJECT_FOLDER_PATH, 'assets', 'content', 'images'),
);
/** Remove temp folder */
await cleanUp();
}
async function generateContent(baseDir: string, distDir: string) {
const consoleTimeLabel = '\nMarkdown files processed in';
console.info(`Processing markdown files...`);
console.time(consoleTimeLabel);
/** List of all markdown files in the content directory. */
const files = await retrieveAllMarkdownFiles(baseDir);
/** These directories have no markdown destined to be in the docs */
const skipDirectories = ['examples'];
const filteredFiles = files.filter((f) => !skipDirectories.some((dir) => f.path.startsWith(dir)));
await Promise.all(
filteredFiles.map((file) =>
mkdir(join(distDir, dirname(file.path)), {recursive: true}).catch(console.error),
),
);
let hadWarnings = false;
for (let index = 0; index < filteredFiles.length; index++) {
const file = filteredFiles[index];
// Basic progress report
process.stdout.write('\r\x1b[K');
process.stdout.write(`${index}/${filteredFiles.length} - Processing ${file.path}`);
// keep in mind write doesn't write a newline like console.log does
// Logging is handcrafted because markedjs provides no solution
// Moreover it has no context of filename as it is only string based
// So we're using a global `logs` array
if (logs.length) {
console.warn('\n');
logs.forEach((log) => {
console.warn(`${file.path}: ${log}`);
});
console.warn('\n');
logs.length = 0;
hadWarnings = true;
}
setCurrentParsedFilePath(file.path);
const processedFile = await marked.parse(file.content, {async: true});
const htmlFilePath = `${file.path}.html`;
await writeFile(join(distDir, htmlFilePath), processedFile.toString());
}
if (hadWarnings) {
console.warn(
`\n\nYou can resolve the ambiguities by editing the mentioned file(s) and adding a specific link using the Markdown syntax.`,
);
}
console.timeEnd(consoleTimeLabel);
}
async function createTempMermaidFolder() {
if (!existsSync(MERMAID_TEMP_FOLDER)) {
await mkdir(MERMAID_TEMP_FOLDER);
}
}
async function cleanUp() {
if (existsSync(MERMAID_TEMP_FOLDER)) {
await rm(MERMAID_TEMP_FOLDER, {recursive: true});
}
}
async function copyFolder(source: string, destination: string) {
if (!existsSync(destination)) {
await mkdir(destination);
}
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);
(await stats.isDirectory()) ? copyFolder(sourcePath, destPath) : copyFile(sourcePath, destPath);
}
}
@@ -0,0 +1,3 @@
# Code Regions
All the files in the regions folder were implemented based on angular.io: <https://github.com/angular/angular/tree/main/aio/tools/transforms/examples-package/services>.
@@ -0,0 +1,13 @@
/*!
* @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
*/
// These kind of comments are used CSS and other languages that do not support inline comments
export const regionStartMatcher = /^\s*\/\*\s*#docregion\s*(.*)\s*\*\/\s*$/;
export const regionEndMatcher = /^\s*\/\*\s*#enddocregion\s*(.*)\s*\*\/\s*$/;
export const plasterMatcher = /^\s*\/\*\s*#docplaster\s*(.*)\s*\*\/\s*$/;
export const createPlasterComment = (plaster: string) => `/* ${plaster} */`;
@@ -0,0 +1,14 @@
/*!
* @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
*/
// These kind of comments are used in HTML
export const regionStartMatcher = /^\s*<!--\s*#docregion\s*(.*?)\s*(?:-->)?\s*$/;
export const regionEndMatcher = /^\s*<!--\s*#enddocregion\s*(.*?)\s*-->\s*$/;
export const plasterMatcher = /^\s*<!--\s*#docplaster\s*(.*?)\s*-->\s*$/;
export const createPlasterComment = (plaster: string) => `<!-- ${plaster} -->`;
@@ -0,0 +1,13 @@
/*!
* @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
*/
// These kind of comments are used in languages that do not support block comments, such as Jade
export const regionStartMatcher = /^\s*\/\/\s*#docregion\s*(.*)\s*$/;
export const regionEndMatcher = /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/;
export const plasterMatcher = /^\s*\/\/\s*#docplaster\s*(.*)\s*$/;
export const createPlasterComment = (plaster: string) => `// ${plaster}`;
@@ -0,0 +1,13 @@
/*!
* @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
*/
// This comment type is used in C like languages such as JS, TS, etc
export const regionStartMatcher = /^\s*\/\/\s*#docregion\s*(.*)\s*$/;
export const regionEndMatcher = /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/;
export const plasterMatcher = /^\s*\/\/\s*#docplaster\s*(.*)\s*$/;
export const createPlasterComment = (plaster: string) => `/* ${plaster} */`;
@@ -0,0 +1,13 @@
/*!
* @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
*/
// These type of comments are used in hash comment based languages such as bash and Yaml
export const regionStartMatcher = /^\s*#\s*#docregion\s*(.*)\s*$/;
export const regionEndMatcher = /^\s*#\s*#enddocregion\s*(.*)\s*$/;
export const plasterMatcher = /^\s*#\s*#docplaster\s*(.*)\s*$/;
export const createPlasterComment = (plaster: string) => `# ${plaster}`;
@@ -0,0 +1,196 @@
/*!
* @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 * as blockC from './region-matchers/block-c.mjs';
import * as html from './region-matchers/html.mjs';
import * as inlineC from './region-matchers/inline-c.mjs';
import * as inlineCOnly from './region-matchers/inline-c-only.mjs';
import * as inlineHash from './region-matchers/inline-hash.mjs';
import {FileType, removeEslintComments} from './removeEslintComments.mjs';
const WHOLE_FILE_REGION_NAME = '';
const DEFAULT_PLASTER = '. . .';
const REGION_MATCHERS = {
ts: inlineC,
js: inlineC,
mjs: inlineCOnly,
es6: inlineC,
html: html,
svg: html,
css: blockC,
conf: inlineHash,
yaml: inlineHash,
yml: inlineHash,
sh: inlineHash,
jade: inlineCOnly,
pug: inlineCOnly,
json: inlineCOnly,
'json.annotated': inlineCOnly,
};
interface Region {
lines: string[];
open?: boolean;
ranges: {from: number; to?: number}[];
}
/**
* NOTE: We assume that the tag defining the beginning and end of the region will be in different lines in each case.
* For example, in HTML, we don't expect to have the following code on one line: <!-- docregion name -->content<!--enddocregion name-->
*/
export const regionParser = (contents: string, filePath: string) => {
const fileType: FileType | undefined = filePath?.split('.').pop() as FileType;
if (!fileType) {
throw new Error(`Incorrect file type for region parser: ${filePath}!`);
}
const regionMatcher = REGION_MATCHERS[fileType];
const openRegions: string[] = [];
const regionMap: Record<string, Region> = {};
let countOfRegionLines = 0;
if (regionMatcher) {
let plaster = regionMatcher.createPlasterComment(DEFAULT_PLASTER);
const lines = removeEslintComments(contents, fileType)
.split(/\r?\n/)
.filter((line, index) => {
const startRegion = line.match(regionMatcher.regionStartMatcher);
const endRegion = line.match(regionMatcher.regionEndMatcher);
const updatePlaster = line.match(regionMatcher.plasterMatcher);
// start region processing
if (startRegion) {
// open up the specified region
handleStartRegion(
startRegion,
regionMap,
index,
countOfRegionLines,
plaster,
openRegions,
);
} else if (endRegion) {
// end region processing
handleEndRegion(openRegions, endRegion, regionMap, index, countOfRegionLines);
} else if (updatePlaster) {
// doc plaster processing
const plasterString = updatePlaster[1].trim();
plaster = plasterString ? regionMatcher.createPlasterComment(plasterString) : '';
} else {
// simple line of content processing
openRegions.forEach((regionName) => regionMap[regionName].lines.push(line));
// do not filter out this line from the content
return true;
}
// this line contained an annotation so let's filter it out
countOfRegionLines++;
return false;
});
if (!regionMap[WHOLE_FILE_REGION_NAME]) {
regionMap[WHOLE_FILE_REGION_NAME] = {lines, ranges: [{from: 1, to: lines.length + 1}]};
}
return {
contents: lines.join('\n'),
regionMap,
totalLinesCount: lines.length,
};
} else {
return {contents, regionMap, totalLinesCount: 0};
}
};
function handleStartRegion(
startRegion: RegExpMatchArray,
regionMap: Record<string, Region>,
index: number,
countOfRegionLines: number,
plaster: string,
openRegions: string[],
) {
const regionNames = getRegionNames(startRegion[1]);
if (regionNames.length === 0) {
regionNames.push(WHOLE_FILE_REGION_NAME);
}
for (const regionName of regionNames) {
const region = regionMap[regionName];
if (region) {
if (region.open) {
throw new Error(`Tried to open a region, named "${regionName}", that is already open`);
}
// Region is opened, set from range value.
region.open = true;
region.ranges.push({from: getFromRangeValue(index, countOfRegionLines)});
if (plaster) {
// Use the same indent as the docregion marker
const indent = startRegion[0].split(/[^ ]/, 1);
region.lines.push(indent + plaster);
}
} else {
regionMap[regionName] = {
lines: [],
open: true,
ranges: [
{
from: getFromRangeValue(index, countOfRegionLines),
},
],
};
}
openRegions.push(regionName);
}
}
function handleEndRegion(
openRegions: string[],
endRegion: RegExpMatchArray,
regionMap: Record<string, Region>,
index: number,
countOfRegionLines: number,
) {
if (openRegions.length === 0) {
throw new Error('Tried to close a region when none are open');
}
// close down the specified region (or most recent if no name is given)
const regionNames = getRegionNames(endRegion[1]);
if (regionNames.length === 0) {
regionNames.push(openRegions[openRegions.length - 1]);
}
for (const regionName of regionNames) {
const region = regionMap[regionName];
if (!region || !region.open) {
throw new Error(`Tried to close a region, named "${regionName}", that is not open`);
}
// Region is closed, we can define the last line number of the region
region.open = false;
region.ranges[region.ranges.length - 1].to = index - countOfRegionLines;
removeLast(openRegions, regionName);
}
}
function getFromRangeValue(index: number, countOfRegionLines: number): number {
return index - countOfRegionLines + 1;
}
function getRegionNames(input: string): string[] {
return input.trim() === '' ? [] : input.split(',').map((name) => name.trim());
}
function removeLast(array: string[], item: string): void {
const index = array.lastIndexOf(item);
array.splice(index, 1);
}
@@ -0,0 +1,58 @@
/*!
* @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
*/
export type FileType =
| 'ts'
| 'js'
| 'mjs'
| 'es6'
| 'html'
| 'svg'
| 'css'
| 'conf'
| 'yaml'
| 'yml'
| 'sh'
| 'jade'
| 'pug'
| 'json'
| 'json.annotated';
export function removeEslintComments(input: string, fileType: FileType) {
if (!input || (fileType !== 'ts' && fileType !== 'js' && fileType !== 'html')) {
return input;
}
const regexForFileType = regexesForFileTypes[fileType];
return input.replace(regexForFileType, '');
}
const jsRegexes = [
/\/\/ *eslint-disable(?:-next-line)?(?: .*)?(?:\n *|$)/,
/\n? *\/\/ *eslint-(?:disable-line|enable)(?: .*)?(?=\n|$)/,
/\/\*\s*eslint-disable(?:-next-line)?(?: [\s\S]*?)?\*\/ *(?:\n *)?/,
/\n? *\/\*\s*eslint-(?:disable-line|enable)(?: [\s\S]*?)?\*\//,
];
const htmlRegexes = [
/<!--\s*eslint-disable(?:-next-line)?(?: [\s\S]*?)?--> *(?:\n *)?/,
/\n? *<!--\s*eslint-(?:disable-line|enable)(?: [\s\S]*?)?-->/,
];
const joinRegexes = (regexes: any) =>
new RegExp(regexes.map((regex: any) => `(?:${regex.source})`).join('|'), 'g');
const htmlRegex = joinRegexes(htmlRegexes);
// Note: the js regex needs to also include the html ones to account for inline templates in @Components
const jsRegex = joinRegexes([...jsRegexes, ...htmlRegexes]);
const regexesForFileTypes = {
js: jsRegex,
ts: jsRegex,
html: htmlRegex,
};
@@ -0,0 +1,61 @@
/*!
* @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 {Renderer as MarkedRenderer, marked} from 'marked';
import {handleCode} from './tranformations/code.mjs';
import {handleEmoji} from './tranformations/emoji.mjs';
import {transformH1} from './tranformations/h1.mjs';
import {transformHeader} from './tranformations/header.mjs';
import {transformImage} from './tranformations/image.mjs';
import {transformLink} from './tranformations/link.mjs';
import {transformDocsList} from './tranformations/lists.mjs';
import {transformDocsTable} from './tranformations/table.mjs';
/**
* Custom renderer for marked that will be used to transform markdown files to HTML
* files that can be used in the Angular docs.
*/
export const renderer: Partial<MarkedRenderer> = {
// All headers have tabindex="-1"
// so that we can focus relevant sections on
// navigation for accessibility purposes.
heading(label: string, level: number) {
switch (level) {
// Custom H1 header element with page breadcrumb and docs edit button link to GitHub
case 1:
return transformH1(label);
// Custom headers include an anchor link and href
default:
return transformHeader(label, level);
}
},
// Custom table styling
table(header: string, body: string) {
return transformDocsTable(header, body);
},
// Custom list styling
list(body: string, ordered: boolean) {
return transformDocsList(body, ordered);
},
// Custom list styling
image(href: string | null, title: string | null, text: string) {
return transformImage(href, title, text);
},
// Handles special behavior for external links
link(href: string | null, title: string | null, text: string) {
return transformLink(href, title, text);
},
// Custom code span styling
codespan(this: marked.RendererThis, code: string): string {
return handleCode(code);
},
// Custom text styling
text(this: marked.RendererThis, code: string): string {
return handleEmoji(code);
},
};
@@ -0,0 +1,58 @@
/*!
* @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 {TUTORIALS_FOLDER_NAME} from './constants.mjs';
export const enum DocumentType {
DOCS,
TUTORIAL,
}
let currentParsedPath: string | null = null;
let currentParsedDocumentType: DocumentType = DocumentType.DOCS;
const headerIds = new Map<string, number>();
export const setCurrentParsedFilePath = (path: string) => {
currentParsedPath = path;
setCurrentParsedDocumentType(
path.startsWith(TUTORIALS_FOLDER_NAME) ? DocumentType.TUTORIAL : DocumentType.DOCS,
);
};
export const getCurrentParsedFilePath = (): string | null => currentParsedPath;
export const setCurrentParsedDocumentType = (type: DocumentType) => {
currentParsedDocumentType = type;
};
export const getCurrentParsedDocumentType = (): DocumentType => currentParsedDocumentType;
export const getHeaderId = (id: string): string => {
const numberOfHeaderOccurrencesInTheDocument = headerIds.get(id) ?? 0;
headerIds.set(id, numberOfHeaderOccurrencesInTheDocument + 1);
const cleanedUpId = id
.toLowerCase()
.replaceAll(/<code>(.*?)<\/code>/g, '$1') // remove <code>
.replaceAll(/<strong>(.*?)<\/strong>/g, '$1') // remove <strong>
.replaceAll(/<em>(.*?)<\/em>/g, '$1') // remove <em>
.replace(/\s|\//g, '-') // remove spaces and slashes
.replace(/gt;|lt;/g, '') // remove escaped < and >
.replace(/&#\d+;/g, '') // remove HTML entities
.replace(/[^0-9a-zA-Z\-]/g, ''); // only keep letters, digits & dashes
const headerId = numberOfHeaderOccurrencesInTheDocument
? `${cleanedUpId}-${numberOfHeaderOccurrencesInTheDocument}`
: cleanedUpId;
return headerId;
};
export const resetHeaderIdsOfCurrentDocument = (): void => {
headerIds.clear();
};
@@ -0,0 +1,120 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {readFileSync, readdirSync} from 'fs';
import {join} from 'path';
import {ClassEntry, DocEntry, EntryType} from '../../api-gen/entities';
import {handleEmoji} from './emoji.mjs';
// global defined in the index
declare const logs: string[];
// API
interface EntryCollection {
moduleName: string;
entries: DocEntry[];
}
function parseEntryData(srcs: string[]): EntryCollection[] {
return srcs.map((jsonDataFilePath) => {
const fileContent = readFileSync(jsonDataFilePath, {encoding: 'utf8'});
return JSON.parse(fileContent) as EntryCollection;
});
}
interface ParsedEntry {
module: string;
members: string[];
type: EntryType;
isDeprecated: boolean;
}
let apiEntries: undefined | Map<string, Array<ParsedEntry>>;
function getApiEntries(): Map<string, Array<ParsedEntry>> {
const DEV_DATA_PATH = join('prerender', 'api-gen', 'dev-data', 'api');
const filePaths = readdirSync(DEV_DATA_PATH).map((fileName) => join(DEV_DATA_PATH, fileName));
const entryCollections: EntryCollection[] = parseEntryData(filePaths);
const entries = new Map<string, Array<ParsedEntry>>();
entryCollections.forEach((module) => {
const moduleName = module.moduleName.slice(9); // removing "@angular/"" from the name
module.entries.forEach((entry) => {
const members = (entry as ClassEntry).members?.map((m) => m.name) ?? [];
const parsedEntry = {
module: moduleName,
members,
type: entry.entryType,
isDeprecated: entry.jsdocTags.some((tag) => tag.name === 'deprecated'),
};
if (!entries.has(entry.name)) {
entries.set(entry.name, [parsedEntry]);
} else {
entries.get(entry.name)?.push(parsedEntry);
}
});
});
return entries;
}
function defaultCodeBlock(text: string) {
return `<code>${handleEmoji(text)}</code>`;
}
export function handleCode(text: string): string {
if (!apiEntries) {
apiEntries = getApiEntries();
}
// strip trailing the braces for functions,
// remove leading @ for decorators
let entity = text.replace(/^@|(\(\))$/g, '');
let method: string | undefined;
// We're parsing a class.method
if (entity.includes('.')) {
[entity, method] = entity.split('.');
}
if (apiEntries.has(entity)) {
const isDecorator = text.startsWith('@');
const entries = apiEntries.get(entity);
const nonDeprecated = entries?.find((e) => e.isDeprecated);
let entry: ParsedEntry | undefined;
if (entries?.length === 1) {
entry = entries[0];
} else if (entries?.length === 2 && nonDeprecated) {
// Sometime we move an entry from one package to another and deprecate the original entry.
// In this case we have 2 entries with one deprecated, we take the non-deprecated one
entry = nonDeprecated;
}
if (entry) {
if (isDecorator && entry.type !== 'decorator') {
return defaultCodeBlock(text);
}
if (entity && method && entry.members.includes(method)) {
return `<a href="api/${entry.module}/${entity}#${method}"><code>${text}</code></a>`;
}
return `<a href="api/${entry.module}/${entity}"><code>${text}</code></a>`;
} else {
logs.push(`Cannot resolve the ambiguity for: ${entity}`);
}
}
return defaultCodeBlock(text);
}
@@ -0,0 +1,66 @@
/*!
* @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 {marked} from 'marked';
import {AlertSeverityLevel} from './enums/levels.mjs';
interface DocsAlertToken extends marked.Tokens.Generic {
type: 'docs-alert';
body: string;
severityLevel: string;
tokens: marked.Token[];
}
interface DocsAlert {
alert: RegExpExecArray | null;
severityLevel: string;
}
export const docsAlertExtension = {
name: 'docs-alert',
level: 'block',
tokenizer(this: marked.TokenizerThis, src: string): DocsAlertToken | undefined {
const match: DocsAlert = {
alert: null,
severityLevel: AlertSeverityLevel.HELPFUL,
};
for (let level in AlertSeverityLevel) {
// Capture group 1: all alert text content after the severity level
const rule = new RegExp('^s*' + level + ': (.*?)\n', 's');
const possibleMatch = rule.exec(src);
if (possibleMatch?.[1]) {
match.alert = possibleMatch;
match.severityLevel = level;
}
}
if (match?.alert) {
const token: DocsAlertToken = {
type: 'docs-alert',
raw: match.alert[0],
body: match.alert[1].trim(),
severityLevel: match.severityLevel,
tokens: [],
};
token.body = `**${
token.severityLevel === AlertSeverityLevel.TLDR ? 'TL;DR' : token.severityLevel
}:** ${token.body}`;
this.lexer.blockTokens(token.body, token.tokens);
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsAlertToken) {
return `
<div class="docs-alert docs-alert-${token.severityLevel.toLowerCase()}">
${this.parser.parse(token.tokens)}
</div>
`;
},
};
@@ -0,0 +1,70 @@
/*!
* @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 {marked} from 'marked';
import {CalloutSeverityLevel} from './enums/levels.mjs';
interface DocsCalloutToken extends marked.Tokens.Generic {
type: 'docs-callout';
title: string;
titleTokens: marked.Token[];
severityLevel: CalloutSeverityLevel;
body: string;
bodyTokens: marked.Token[];
}
// Capture group 1: all attributes on the opening tag
// Capture group 2: all content between the open and close tags
const calloutRule = /^<docs-callout([^>]*)>((?:.(?!\/docs-callout))*)<\/docs-callout>/s;
const titleRule = /title="([^"]*)"/;
const isImportantRule = /important/;
const isCriticalRule = /critical/;
export const docsCalloutExtension = {
name: 'docs-callout',
level: 'block',
start(src: string) {
return src.match(/^\s*<docs-callout/m)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsCalloutToken | undefined {
const match = calloutRule.exec(src);
if (match) {
const attr = match[1].trim();
const title = titleRule.exec(attr);
let severityLevel = CalloutSeverityLevel.HELPFUL;
if (isImportantRule.exec(attr)) severityLevel = CalloutSeverityLevel.IMPORTANT;
if (isCriticalRule.exec(attr)) severityLevel = CalloutSeverityLevel.CRITICAL;
const body = match[2].trim();
const token: DocsCalloutToken = {
type: 'docs-callout',
raw: match[0],
severityLevel: severityLevel,
title: title ? title[1] : '',
titleTokens: [],
body: body ?? '',
bodyTokens: [],
};
this.lexer.inlineTokens(token.title, token.titleTokens);
this.lexer.blockTokens(token.body, token.bodyTokens);
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsCalloutToken) {
return `
<div class="docs-callout docs-callout-${token.severityLevel.toLowerCase()}">
<h3>${this.parser.parseInline(token.titleTokens)}</h3>
<p>${this.parser.parse(token.bodyTokens)}</p>
</div>
`;
},
};
@@ -0,0 +1,169 @@
/*!
* @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 {marked} from 'marked';
import {targetForExternalHref} from './helpers.mjs';
import {PROJECT_FOLDER_PATH} from '../constants.mjs';
import {join} from 'path';
import {readFileSync} from 'fs';
interface DocsCardContainerToken extends marked.Tokens.Generic {
type: 'docs-card-container';
cards: string;
tokens: marked.Token[];
}
const cardContainerRule = /^<docs-card-container>(.*?)<\/docs-card-container>/s;
export const docsCardContainerExtension = {
name: 'docs-card-container',
level: 'block',
start(src: string) {
return src.match(/^\s*<docs-card-container/m)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsCardContainerToken | undefined {
const match = cardContainerRule.exec(src);
if (match) {
const body = match[1];
const token: DocsCardContainerToken = {
type: 'docs-card-container',
raw: match[0],
cards: body ?? '',
tokens: [],
};
this.lexer.blockTokens(token.cards, token.tokens);
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsCardContainerToken) {
return `
<div class="docs-card-grid">
${this.parser.parse(token.tokens)}
</div>
`;
},
};
interface DocsCardToken extends marked.Tokens.Generic {
type: 'docs-card';
title: string;
body: string;
link?: string;
href?: string;
imgSrc?: string;
tokens: marked.Token[];
}
// Capture group 1: all attributes on the opening tag
// Capture group 2: all content between the open and close tags
const cardRule = /^[^<]*<docs-card\s([^>]*)>((?:.(?!\/docs-card))*)<\/docs-card>/s;
const titleRule = /title="([^"]*)"/;
const linkRule = /link="([^"]*)"/;
const hrefRule = /href="([^"]*)"/;
const imgSrcRule = /imgSrc="([^"]*)"/;
export const docsCardExtension = {
name: 'docs-card',
level: 'block',
start(src: string) {
return src.match(/^\s*<docs-card\s*/m)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsCardToken | undefined {
const match = cardRule.exec(src);
if (match) {
const attr = match[1].trim();
const title = titleRule.exec(attr);
const link = linkRule.exec(attr);
const href = hrefRule.exec(attr);
const imgSrc = imgSrcRule.exec(attr);
const body = match[2].trim();
const token: DocsCardToken = {
type: 'docs-card',
raw: match[0],
title: title ? title[1] : '',
body: body ?? '',
href: href ? href[1] : undefined,
link: link ? link[1] : undefined,
imgSrc: imgSrc ? imgSrc[1] : undefined,
tokens: [],
};
this.lexer.blockTokens(token.body, token.tokens);
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsCardToken) {
return token.imgSrc ? getCardWithSvgIllustration(this, token) : getStandardCard(this, token);
},
};
function getStandardCard(renderer: marked.RendererThis, token: DocsCardToken) {
if (token.href) {
return `
<a href="${token.href}" ${targetForExternalHref(token.href)} class="docs-card">
<div>
<h3>${token.title}</h3>
<p>${renderer.parser.parse(token.tokens)}</p>
</div>
<span>${token.link ? token.link : 'Learn more'}</span>
</a>
`;
}
return `
<div class="docs-card">
<div>
<h3>${token.title}</h3>
<p>${renderer.parser.parse(token.tokens)}</p>
</div>
${token.link ? `<span>${token.link}</span>` : ''}
</div>
`;
}
function getCardWithSvgIllustration(renderer: marked.RendererThis, token: DocsCardToken) {
// We can assume that all illustrations are svg files
// We need to read svg content, instead of renering svg with `img`,
// cause we would like to use CSS variables to support dark and light mode.
const illustration = getSvgIllustration(token.imgSrc!);
if (token.href) {
return `
<a href="${token.href}" ${targetForExternalHref(
token.href,
)} class="docs-card docs-card-with-svg">
${illustration}
<div class="docs-card-text-content">
<div>
<h3>${token.title}</h3>
${renderer.parser.parse(token.tokens)}
</div>
<span>${token.link ? token.link : 'Learn more'}</span>
</div>
</a>
`;
}
return `
<div class="docs-card docs-card-with-svg">
${illustration}
<div class="docs-card-text-content">
<h3>${token.title}</h3>
${renderer.parser.parse(token.tokens)}
</div>
</div>
`;
}
function getSvgIllustration(path: string): string {
return readFileSync(join(PROJECT_FOLDER_PATH, path), {encoding: 'utf-8'});
}
@@ -0,0 +1,445 @@
/*!
* @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 {marked} from 'marked';
import hljs from 'highlight.js';
import * as diff from 'diff';
import {join} from 'path';
import {readFileSync} from 'fs';
import {decode} from 'html-entities';
import {regionParser} from '../regions/region-parser.mjs';
import {EXAMPLES_FOLDER_PATH, TUTORIALS_FOLDER_PATH} from '../constants.mjs';
import {DocumentType, getCurrentParsedDocumentType} from '../state.mjs';
/** Marked token for a multifile custom docs element. */
export interface DocsCodeMultifileToken extends marked.Tokens.Generic {
type: 'docs-code-multifile';
// The example path used for linking to Stackblitz or rendering a preview
path: string | undefined;
// The raw nested Markdown of <docs-code> examples in the multifile example
panes: string;
// The DocsCodeToken of the nested <docs-code> examples
paneTokens: marked.Token[];
// True if we should display preview
preview: boolean;
}
/** Marked token for a custom docs element. */
export interface DocsCodeToken extends marked.Tokens.Generic {
type: 'docs-code';
// Nested code OR the code from the optional file path
code: string;
// The example file path
path: string | undefined;
// The example display header
header: string | undefined;
// Code language
language: string | undefined;
// True if styled with line numbers
linenums: boolean | undefined;
// The lines to display highlighting on
highlight: string | undefined;
// The example path to determine diff (lines added/removed)
diff: string | undefined;
// The lines viewable in collapsed view
visibleLines: string | undefined;
// The name of the viewable region in the collapsed view
visibleRegion: string | undefined;
// True if we should display preview
preview: boolean;
}
export interface DocsCodeTripleTickBlockToken extends marked.Tokens.Generic {
type: 'docs-code-triple-tick';
// Nested code
code: string;
// Code language
language: string | undefined;
}
interface DiffMetadata {
code: string;
linesAdded: number[];
linesRemoved: number[];
}
// Capture group 1: all attributes on the opening tag
// Capture group 2: all content between the open and close tags
const multiFileCodeRule = /^\s*<docs-code-multifile(.*?)>(.*?)<\/docs-code-multifile>/s;
const singleFileSelfClosingCodeRule = /^\s*<docs-code\s([^>]*)((?:.(?!\/>))*)\/>/s;
const singleFileCodeRule = /^\s*<docs-code\s([^>]*)>((?:.(?!\/docs-code))*)<\/docs-code>/s;
// TODO: use regex for code implemented in the marked package: https://github.com/markedjs/marked/blob/4e6acc8b8517eafe0036a914f58b6f53d4b12ca6/src/rules.ts#L72C1-L73C1
const tripleTickCodeRule = /^\s*`{3}(\w+)[\r\n]+(.*?)[\r\n]+`{3}/s;
const pathRule = /path="([^"]*)"/;
const headerRule = /header="([^"]*)"/;
const linenumsRule = /linenums/;
const highlightRule = /highlight="([^"]*)"/;
const diffRule = /diff="([^"]*)"/;
const languageRule = /language="([^"]*)"/;
const visibleLinesRule = /visibleLines="([^"]*)"/;
const visibleRegionRule = /visibleRegion="([^"]*)"/;
const previewRule = /preview/;
const lineNumberClassName: string = 'hljs-ln-number';
const lineMultifileClassName: string = 'hljs-ln-line';
const lineAddedClassName: string = 'add';
const lineRemovedClassName: string = 'remove';
const lineHighlightedClassName: string = 'highlighted';
const mermaidClassName = 'mermaid';
export const docsCodeMultifileExtension = {
name: 'docs-code-multifile',
level: 'block',
start(src: string) {
return src.match(/^\s*<docs-code-multifile/)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsCodeMultifileToken | undefined {
const match = multiFileCodeRule.exec(src);
if (match) {
const attr = match[1].trim();
const path = pathRule.exec(attr);
const preview = previewRule.exec(attr) ? true : false;
const token: DocsCodeMultifileToken = {
type: 'docs-code-multifile',
raw: match[0],
path: path?.[1],
panes: match[2].trim(),
paneTokens: [],
preview: preview,
};
this.lexer.blockTokens(token.panes, token.paneTokens);
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsCodeMultifileToken) {
const attributes = getAttribute('path', token.path) + getAttribute('preview', token.preview);
return `
<div class="docs-code-multifile"${attributes}>
${this.parser.parse(token.paneTokens)}
</div>
`;
},
};
export const docsCodeExtension = {
name: 'docs-code',
level: 'block',
start(src: string) {
return src.match(/^<docs-code\s/)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsCodeToken | undefined {
const code = singleFileCodeRule.exec(src);
const selfClosingCode = singleFileSelfClosingCodeRule.exec(src);
const match = selfClosingCode ?? code;
if (match) {
const attr = match[1].trim();
const path = pathRule.exec(attr);
const header = headerRule.exec(attr);
const linenums = linenumsRule.exec(attr);
const highlight = highlightRule.exec(attr);
const diff = diffRule.exec(attr);
const language = languageRule.exec(attr);
const visibleLines = visibleLinesRule.exec(attr);
const visibleRegion = visibleRegionRule.exec(attr);
const preview = previewRule.exec(attr) ? true : false;
let code = match[2].trim();
if (path && path[1]) {
code = getCodeFromPath(path[1]);
}
const token: DocsCodeToken = {
type: 'docs-code',
raw: match[0],
code: code,
path: path?.[1],
header: header?.[1],
linenums: !!linenums,
highlight: highlight?.[1],
diff: diff?.[1],
language: language?.[1],
visibleLines: visibleLines?.[1],
visibleRegion: visibleRegion?.[1],
preview: preview,
};
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsCodeToken) {
return formatDocsCode(token);
},
};
export const docsTripleTickMarkdownCodeExtension = {
name: 'docs-code-triple-tick',
level: 'block',
start(src: string) {
return src.match(/^(```)\s/)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsCodeTripleTickBlockToken | undefined {
const match = tripleTickCodeRule.exec(src);
if (match) {
const token: DocsCodeTripleTickBlockToken = {
raw: match[0],
type: 'docs-code-triple-tick',
code: match[2],
language: match[1],
};
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsCodeTripleTickBlockToken) {
return formatDocsCode({code: token.code, language: token.language});
},
};
function formatDocsCode(token: Partial<DocsCodeToken>): string {
let diffData: DiffMetadata | null = null;
if (!token.code) {
throw new Error('Undefined code in DocsCodeToken!');
}
let code = token.code;
if (token.diff) {
diffData = getDiffData(token.diff, code);
code = diffData.code;
}
const highlightedLines = token.highlight ? getRangeValues(token.highlight) : [];
const extractRegionsResult = extractRegions(token, code);
code = extractRegionsResult.code;
// Add highlighting based on the code language
code = getHighlightedCode(token.language, code, diffData, highlightedLines, !!token.linenums);
const attributes =
getAttribute(
'visibleLines',
getRangeValues(extractRegionsResult.visibleLines ?? token.visibleLines)?.toString(),
) +
getAttribute('path', token.diff ?? token.path) +
getAttribute('preview', token.preview) +
getAttribute('header', token.header) +
getAttribute('mermaid', token.language === mermaidClassName);
const classes = getClass('shell', token.language == 'shell');
const header = token.header ? `<div class="docs-code-header"><h3>${token.header}</h3></div>` : '';
return `
<div class="docs-code${classes}"${attributes}>
${header}
<pre class="adev-mini-scroll-track">
<code>${code}</code>
</pre>
</div>
`;
}
function getCodeFromPath(path: string): string {
const rootFolder =
getCurrentParsedDocumentType() === DocumentType.DOCS
? EXAMPLES_FOLDER_PATH
: TUTORIALS_FOLDER_PATH;
return readFileSync(join(rootFolder, path), {encoding: 'utf-8'});
}
function getHighlightedCode(
language: string | undefined,
code: string,
diffData: DiffMetadata | null,
highlightedLines: number[] | null,
displayLineNums: boolean,
): string {
if (language == 'none' || language == 'file' || language == mermaidClassName || !highlightedLines)
return code;
// Decode the code content to replace HTML entities to characters
const decodedCode = decode(code);
const highlightResult = language
? hljs.highlight(decodedCode, {language})
: hljs.highlightAuto(decodedCode);
return appendModifierClassesToCodeLinesElements(
highlightResult.value,
diffData,
highlightedLines,
displayLineNums,
);
}
function appendModifierClassesToCodeLinesElements(
htmlString: string,
diffData: DiffMetadata | null,
highlightedLines: number[],
displayLineNums: boolean,
): string {
const lines = getLines(htmlString);
let finalHtml = '';
let lineIndex = 0;
let resultFileLineIndex = 1;
const linesCount = lines.length;
if (linesCount === 0) {
return htmlString;
}
do {
const isRemovedLine = diffData?.linesRemoved.includes(lineIndex);
const isAddedLine = diffData?.linesAdded.includes(lineIndex);
const isHighlighted = highlightedLines.includes(lineIndex);
const statusClasses = `${getClass(lineAddedClassName, isAddedLine)}${getClass(
lineRemovedClassName,
isRemovedLine,
)}${getClass(lineHighlightedClassName, isHighlighted)}`;
if (displayLineNums) {
if (isRemovedLine) {
finalHtml += `<span role="presentation" class="${lineNumberClassName}${statusClasses}">-</span>`;
} else {
finalHtml += `<span role="presentation" class="${lineNumberClassName}${statusClasses}">${
isAddedLine ? '+' : resultFileLineIndex
}</span>`;
resultFileLineIndex++;
}
}
finalHtml += `<div class="${lineMultifileClassName}${statusClasses}">${lines[lineIndex]}</div>`;
lineIndex++;
} while (lineIndex < linesCount);
return finalHtml;
}
function getDiffData(diffPath: string, code: string): DiffMetadata {
const changedCode = getCodeFromPath(diffPath);
const change = diff.diffLines(code, changedCode);
const getLinesRange = (start: number, count: number): number[] =>
Array.from(Array(count).keys()).map((i) => i + start);
let processedLines = 0;
return change.reduce(
(prev: DiffMetadata, part: diff.Change) => {
const diff: DiffMetadata = {
code: `${prev.code}${part.value}`,
linesAdded: part.added
? [...prev.linesAdded, ...getLinesRange(processedLines, part.count ?? 0)]
: prev.linesAdded,
linesRemoved: part.removed
? [...prev.linesRemoved, ...getLinesRange(processedLines, part.count ?? 0)]
: prev.linesRemoved,
};
processedLines += part.count ?? 0;
return diff;
},
{
code: '',
linesAdded: [],
linesRemoved: [],
},
);
}
function extractRegions(
token: Partial<DocsCodeToken>,
code: string,
): {code: string; visibleLines?: string} {
if (!token.path) return {code};
const result = regionParser(code, token.path);
if (token.visibleRegion) {
const region = result.regionMap[token.visibleRegion];
if (!region) throw new Error(`Cannot find ${token.visibleRegion} in ${token.path}!`);
return {
code: result.contents,
visibleLines: `[${region.ranges.map(
(range) => `[${range.from}, ${range.to ?? result.totalLinesCount + 1}]`,
)}]`,
};
}
return {
code: result.contents,
};
}
function getLines(text: string): string[] {
if (text.length === 0) {
return [];
}
return text.split(/\r\n|\r|\n/g);
}
function getAttribute(name: string, value: boolean | number | string | undefined): string {
return value ? ` ${name}="${value}"` : '';
}
function getClass(name: string, value: boolean | number | string | undefined): string {
return value ? ` ${name}` : ``;
}
/**
* The function used to generate ranges of highlighted or visible lines in code blocks
*/
function getRangeValues(token?: string): number[] | null {
const getAllValuesFromRange = (range: any[]) => {
const [start, end] = range;
for (let i = start; i <= end; i++) {
result.push(i - 1);
}
};
if (!token) {
return null;
}
let result: number[] = [];
try {
const boundaryValueArray = JSON.parse(token);
if (!Array.isArray(boundaryValueArray)) {
throw new Error('Provided token has wrong format!\n', boundaryValueArray);
}
// Flat Array
if (
boundaryValueArray.length === 2 &&
!Array.isArray(boundaryValueArray[0]) &&
!Array.isArray(boundaryValueArray[1])
) {
getAllValuesFromRange(boundaryValueArray);
} else {
for (const range of boundaryValueArray) {
if (Array.isArray(range) && range.length === 2) {
getAllValuesFromRange(range);
} else if (!Number.isNaN(range)) {
result.push(Number(range - 1));
} else {
throw new Error('Input has wrong format!\n', range);
}
}
}
return result;
} catch {
return null;
}
}
@@ -0,0 +1,84 @@
/*!
* @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 {marked} from 'marked';
import {getPageTitle} from './h1.mjs';
import {readFileSync} from 'fs';
import {join} from 'path';
import {PROJECT_FOLDER_PATH} from '../constants.mjs';
interface DocsDecorativeHeaderToken extends marked.Tokens.Generic {
type: 'docs-decorative-header';
title: string;
imgSrc: string;
body: string;
}
// Capture group 1: all attributes on the opening tag
// Capture group 2: all content between the open and close tags
const decorativeHeaderRule =
/^[^<]*<docs-decorative-header\s([^>]*)>((?:.(?!\/docs-decorative-header))*)<\/docs-decorative-header>/s;
const imgSrcRule = /imgSrc="([^"]*)"/;
const titleRule = /title="([^"]*)"/;
export const docsDecorativeHeaderExtension = {
name: 'docs-decorative-header',
level: 'block',
start(src: string) {
return src.match(/^\s*<docs-decorative-header\s*/m)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsDecorativeHeaderToken | undefined {
const match = decorativeHeaderRule.exec(src);
if (match) {
const attr = match[1].trim();
const body = match[2].trim();
const imgSrc = imgSrcRule.exec(attr);
const title = titleRule.exec(attr);
const token: DocsDecorativeHeaderToken = {
type: 'docs-decorative-header',
raw: match[0],
title: title ? title[1] : '',
imgSrc: imgSrc ? imgSrc[1] : '../assets/images/globe.svg',
body: body ?? '',
};
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsDecorativeHeaderToken) {
// We can assume that all illustrations are svg files
// We need to read svg content, instead of renering svg with `img`,
// cause we would like to use CSS variables to support dark and light mode.
const illustration = getSvgIllustration(token.imgSrc);
return `
<div class="docs-decorative-header-container">
<div class="docs-decorative-header">
<div class="docs-header-content">
<docs-breadcrumb></docs-breadcrumb>
${getPageTitle(token.title)}
<p>${token.body}</p>
</div>
<!-- illustration -->
${illustration}
</div>
</div>
`;
},
};
function getSvgIllustration(path: string): string {
return readFileSync(join(PROJECT_FOLDER_PATH, path), {encoding: 'utf-8'});
}
@@ -0,0 +1,103 @@
/*!
* @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 {marked} from 'marked';
import {isExternalLink, targetForExternalHref} from './helpers.mjs';
interface DocsPillRowToken extends marked.Tokens.Generic {
type: 'docs-pill-row';
pills: string;
tokens: marked.Token[];
}
// Capture group 1: all content between the open and close tags
const pillRowRule = /^\s*<docs-pill-row>((?:.(?!docs-pill-row))*)<\/docs-pill-row>/s;
export const docsPillRowExtension = {
name: 'docs-pill-row',
level: 'block',
start(src: string) {
return src.match(/^\s*<docs-pill-row/m)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsPillRowToken | undefined {
const match = pillRowRule.exec(src);
if (match) {
const body = match[1];
const token: DocsPillRowToken = {
type: 'docs-pill-row',
raw: match[0],
pills: body ?? '',
tokens: [],
};
this.lexer.inlineTokens(token.pills, token.tokens);
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsPillRowToken) {
return `
<nav class="docs-pill-row">
${this.parser.parseInline(token.tokens)}
</nav>
`;
},
};
interface DocsPillToken extends marked.Tokens.Generic {
type: 'docs-pill';
title: string;
href: string;
tokens: marked.Token[];
}
// Capture group 1: all attributes on the docs-pill tag
const pillRule = /^\s*<docs-pill\s((?:.(?!\n))*)\/>/s;
const titleRule = /title="([^"]*)"/;
const hrefRule = /href="([^"]*)"/;
export const docsPillExtension = {
name: 'docs-pill',
level: 'inline',
start(src: string) {
return src.indexOf('<docs-pill ');
},
tokenizer(this: marked.TokenizerThis, src: string): DocsPillToken | undefined {
const match = pillRule.exec(src);
if (match) {
const attr = match[1].trim();
const title = titleRule.exec(attr);
const href = hrefRule.exec(attr);
const token: DocsPillToken = {
type: 'docs-pill',
raw: match[0],
title: title ? title[1] : '',
href: href ? href[1] : '',
tokens: [],
};
this.lexer.inlineTokens(token.title, token.tokens);
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsPillToken) {
return `
<a class="docs-pill" href="${token.href}" ${targetForExternalHref(token.href)}>
${this.parser.parseInline(token.tokens)}
${
isExternalLink(token.href)
? '<docs-icon class="docs-icon-small">open_in_new</docs-icon>'
: ''
}
</a>
`;
},
};
@@ -0,0 +1,69 @@
/*!
* @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 {marked} from 'marked';
interface DocsVideoToken extends marked.Tokens.Generic {
type: 'docs-video';
src: string;
title: string | undefined;
}
// Capture group 1: all attributes on the opening tag
const videoRule = /^<docs-video([^>]*)\/>/s;
const srcRule = /src="([^"]*)"/;
const titleRule = /title="([^"]*)"/;
const validYTUrlPrefix = 'https://www.youtube.com/embed/';
export const docsVideoExtension = {
name: 'docs-video',
level: 'block',
start(src: string) {
return src.match(/^\s*<docs-video/m)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsVideoToken | undefined {
const match = videoRule.exec(src);
if (match) {
const attr = match[1].trim();
const src = srcRule.exec(attr);
const title = titleRule.exec(attr);
if (src) {
return {
type: 'docs-video',
raw: match[0],
src: src?.[1],
title: title?.[1],
};
}
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsVideoToken) {
if (!token.src.startsWith(validYTUrlPrefix)) {
process.stdout.write(
`\n<docs-video> cannot load: ${token.src}. YouTube Player API expects src to begin with ${validYTUrlPrefix}.\n`,
);
}
return `
<div class="docs-video-container">
<iframe
class="docs-video"
src="${token.src}"
${token.title ? `title="${token.title}"` : ''}
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
credentialless
title="Video player"
></iframe>
</div>
`;
},
};
@@ -0,0 +1,99 @@
/*!
* @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 {marked} from 'marked';
import {transformHeader} from './header.mjs';
interface DocsWorkflowToken extends marked.Tokens.Generic {
type: 'docs-workflow';
steps: string;
tokens: marked.Token[];
}
// Capture group 1: all content between the open and close tags
const workflowRule = /^<docs-workflow>(.*?)<\/docs-workflow>/s;
export const docsWorkflowExtension = {
name: 'docs-workflow',
level: 'block',
start(src: string) {
return src.match(/^\s*<docs-workflow/m)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsWorkflowToken | undefined {
const match = workflowRule.exec(src);
if (match) {
const steps = match[1];
const token: DocsWorkflowToken = {
type: 'docs-workflow',
raw: match[0],
steps: steps ?? '',
tokens: [],
};
this.lexer.blockTokens(token.steps, token.tokens);
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsWorkflowToken) {
return `
<ol class="docs-steps">
${this.parser.parse(token.tokens)}
</ol>
`;
},
};
interface DocsStepToken extends marked.Tokens.Generic {
type: 'docs-step';
title: string;
body: string;
tokens: marked.Token[];
}
// Capture group 1: all attributes on the opening tag
// Capture group 2: all content between the open and close tags
const stepRule = /^\s*<docs-step([^>]*)>((?:.(?!\/docs-step))*)<\/docs-step>/s;
const titleRule = /title="([^"]*)"/;
export const docsStepExtension = {
name: 'docs-step',
level: 'block',
start(src: string) {
return src.match(/^\s*<docs-step/m)?.index;
},
tokenizer(this: marked.TokenizerThis, src: string): DocsStepToken | undefined {
const match = stepRule.exec(src);
if (match) {
const attr = match[1].trim();
const title = titleRule.exec(attr);
const body = match[2].trim();
const token: DocsStepToken = {
type: 'docs-step',
raw: match[0],
title: title ? title[1] : '',
body: body,
tokens: [],
};
this.lexer.blockTokens(token.body, token.tokens);
return token;
}
return undefined;
},
renderer(this: marked.RendererThis, token: DocsStepToken) {
return `
<li>
<span class="docs-step-number" aria-hidden="true"></span>
${transformHeader(token.title, 3)}
${this.parser.parse(token.tokens)}
</li>
`;
},
};
@@ -0,0 +1,19 @@
/*!
* @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 emojiRegex from 'emoji-regex';
const UNICODE_EMOJI_REGEX = /&#x[\dA-Fa-f]+;/g;
const regex = emojiRegex();
export function handleEmoji(text: string): string {
return text.match(regex) || text.match(UNICODE_EMOJI_REGEX)
? `<span class="docs-emoji">${text}</span>`
: text;
}
@@ -0,0 +1,26 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
// Enum with the severity levels for alerts and callouts
export enum CalloutSeverityLevel {
HELPFUL = 'HELPFUL',
IMPORTANT = 'IMPORTANT',
CRITICAL = 'CRITICAL',
}
export enum AlertSeverityLevel {
Note = 'Note',
Tip = 'Tip',
TODO = 'TODO',
QUESTION = 'QUESTION',
Summary = 'Summary',
TLDR = 'TLDR',
CRITICAL = 'CRITICAL',
IMPORTANT = 'IMPORTANT',
HELPFUL = 'HELPFUL',
}
@@ -0,0 +1,39 @@
/*!
* @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 {GITHUB_EDIT_CONTENT_URL} from '../constants.mjs';
import {DocumentType, getCurrentParsedDocumentType, getCurrentParsedFilePath} from '../state.mjs';
export function transformH1(text: string) {
// For tutorials pages, we don't have to display docs-breadcrumb
return `
<!-- Document Header -->
<header class="docs-header">
${
getCurrentParsedDocumentType() === DocumentType.TUTORIAL
? ''
: '<docs-breadcrumb></docs-breadcrumb>'
}
${getPageTitle(text)}
</header>
`;
}
// TODO: Add back pencil icon once code is open sourced
export function getPageTitle(text: string): string {
return `
<!-- Page title -->
<div class="docs-page-title">
<h1 tabindex="-1">${text}</h1>
<a class="docs-github-links" target="_blank" href="${GITHUB_EDIT_CONTENT_URL}/${getCurrentParsedFilePath()}" title="Edit this page" aria-label="Edit this page">
<!-- Pencil -->
<!-- <docs-icon role="presentation">edit</docs-icon> -->
</a>
</div>`;
}
@@ -0,0 +1,26 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {getHeaderId} from '../state.mjs';
import {mapBackticksToCodeElement} from '../utils.mjs';
export function transformHeader(text: string, level: number) {
// Nested anchor elements are invalid in HTML
// They might happen when we have a code block in a heading
// regex aren't perfect for that but this one should be "good enough"
const regex = /<a\s+(?:[^>]*?\s+)?href.*?>(.*?)<\/a>/gi;
const anchorLessText = text.replace(regex, '$1');
const link = getHeaderId(anchorLessText);
const label = mapBackticksToCodeElement(anchorLessText);
return `
<h${level} id="${link}">
<a href="#${link}" class="docs-anchor" tabindex="-1">${label}</a>
</h${level}>`;
}
@@ -0,0 +1,13 @@
/*!
* @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
*/
export const isExternalLink = (href: string | undefined | null) =>
href?.startsWith('http') ?? false;
export const targetForExternalHref = (href: string | undefined | null) =>
isExternalLink(href) ? `target="_blank"` : '';
@@ -0,0 +1,17 @@
/*!
* @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 {normalize} from 'path';
// TODO: Make sure we're setting up correct image optimization
export function transformImage(href: string | null, title: string | null, text: string) {
const src = href?.startsWith('./') ? `assets/content/${normalize(href)}` : href;
return `
<img src="${src}" alt="${text}" title="${title}" class="docs-image">
`;
}
@@ -0,0 +1,15 @@
/*!
* @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 {targetForExternalHref} from './helpers.mjs';
export function transformLink(href: string | null, title: string | null, text: string) {
return `<a href="${href}" ${title ? `title=${title}` : ''} ${targetForExternalHref(
href,
)}>${text}</a>`;
}
@@ -0,0 +1,22 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
export function transformDocsList(body: string, ordered: boolean) {
if (ordered) {
return `
<ol class="docs-ordered-list">
${body}
</ol>
`;
}
return `
<ul class="docs-list">
${body}
</ul>
`;
}
@@ -0,0 +1,22 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
export function transformDocsTable(header: string, body: string) {
return `
<div class="docs-table adev-scroll-track-transparent">
<table>
<thead>
${header}
</thead>
<tbody>
${body}
</tbody>
</table>
</div>
`;
}
@@ -0,0 +1,31 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {readFileSync} from 'fs';
import {glob} from 'glob';
import {join} from 'path';
export function mapBackticksToCodeElement(text: string): string {
return text.replaceAll(/`(.*?)`/g, '<code>$1</code>');
}
/** Recursively search the provided directory for all markdown files and asyncronously load them. */
export async function retrieveAllMarkdownFiles(
baseDir: string,
): Promise<{path: string; content: string}[]> {
const files = await glob('**/*.md', {
root: baseDir,
cwd: baseDir,
ignore: ['**/node_modules/**'],
});
return files.map((filePath) => ({
path: filePath,
content: readFileSync(join(baseDir, filePath), {encoding: 'utf-8'}),
}));
}
+19
View File
@@ -0,0 +1,19 @@
# Tutorial Scripts
The tutorials scripts are responsible for generating the tutorials source code and metadata by reading the tutorials content from [`src/content/tutorials`](/src/content/tutorials).
_See [the tutorials content README](/src/content/tutorials/README.md) for more information about the tutorials content._
For each tutorial, the following JSON files are generated and stored at `src/assets/tutorials`:
- `source-code/<tutorial-pathname>.json`: contains the tutorial source code in the [WebContainer](https://webcontainers.io) `FileSystemTree` format. It's
used to mount the tutorial files in the WebContainer. See the WebContainer docs for more info: <https://webcontainers.io/guides/working-with-the-file-system>
- `metadata/<tutorial-pathname>.json`: the tutorial metadata has all the information necessary to manage the tutorial in app.
It contains the tutorial `config.json`, the previous and next step, the next tutorial, and the code editor file paths and contents.
- `routes.json`: defines all routes for the tutorials, used at [`src/app/sub-navigation-data.ts`](/src/app/sub-navigation-data.ts) to populate the tutorials nav.
## Usage
```bash
yarn generate-tutorials
```
@@ -0,0 +1,84 @@
/*!
* @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 {NavigationItem} from '@angular/docs-shared';
import {PagePrefix} from '../../src/app/core/enums/pages';
import type {TutorialNavigationData, TutorialNavigationItem, TutorialStep} from './tutorials-types';
import {createDirectoryAndWriteFile} from './utils/filesystem';
import {TUTORIALS_ROUTES_NODE_PATH} from './utils/node-constants';
/** the step is used only in this function to sort the nav items */
export type TutorialNavigationItemWithStep = TutorialNavigationItem & {
tutorialData: TutorialNavigationData & {
step: TutorialStep['step'];
};
};
export async function buildTutorialsNavigationItems(
initialNavigationItems: TutorialNavigationItemWithStep[],
) {
const sortedInitialNavigationItems = initialNavigationItems.sort((a, b) => {
if (!a.tutorialData?.step || !b.tutorialData?.step) return 0;
if (a.tutorialData.step < b.tutorialData.step) return -1;
else if (a.tutorialData.step > b.tutorialData.step) return 1;
return 0;
});
const addTutorialPagePrefixToNavItemPath = (path: string) => `${PagePrefix.TUTORIALS}/${path}`;
const navItemMap = new Map<NavigationItem['path'], TutorialNavigationItem>();
for (const navItem of sortedInitialNavigationItems) {
const pathParts = navItem.path.split('/');
const topLevelPath = pathParts[0];
const topLevelNavItem = navItemMap.get(topLevelPath);
const isNavItemTopLevelPath = pathParts.length === 1;
const newNavItem: TutorialNavigationItem = {
path: addTutorialPagePrefixToNavItemPath(navItem.path),
label: navItem.label,
contentPath: navItem.contentPath,
tutorialData: navItem.tutorialData,
};
if (!topLevelNavItem) {
// create new map entry
navItemMap.set(
topLevelPath,
isNavItemTopLevelPath ? newNavItem : ({children: [newNavItem]} as TutorialNavigationItem),
);
} else if (isNavItemTopLevelPath) {
// add top level path to existing map entry
navItemMap.set(topLevelPath, {
...newNavItem,
...topLevelNavItem,
});
} else if (!topLevelNavItem.children) {
// create children in existing map entry
topLevelNavItem.children = [newNavItem];
navItemMap.set(topLevelPath, topLevelNavItem);
} else {
// add children to existing map entry
topLevelNavItem.children.push(newNavItem);
navItemMap.set(topLevelPath, topLevelNavItem);
}
}
const tutorialNavigationItems = Array.from(navItemMap.values());
await createDirectoryAndWriteFile(
TUTORIALS_ROUTES_NODE_PATH,
JSON.stringify(tutorialNavigationItems),
);
}
@@ -0,0 +1,204 @@
/*!
* @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 {mkdir, rm, writeFile} from 'fs/promises';
import {basename, join, sep} from 'path';
import {
GLOB_OPTIONS,
INTRO_DIRECTORY,
PLAYGROUND_ROUTE_NODE_PATH,
STEPS_DIRECTORY,
TUTORIALS_ASSETS_NODE_PATH,
TUTORIALS_COMMON_NODE_PATH,
TUTORIALS_METADATA_NODE_PATH,
TUTORIALS_PROJECT_PATH,
TUTORIALS_SOURCE_CODE_NODE_PATH,
} from './utils/node-constants';
import {createDirectoryAndWriteFile, getFilesContents, globWithCwdPath} from './utils/filesystem';
import {
TutorialNavigationItemWithStep,
buildTutorialsNavigationItems,
} from './generate-tutorials-routes';
import {copyCommonAssetsToAppAssets, getTutorialCommonFiles} from './tutorial-type/common';
import {getHomepagePlaygroundFiles} from './tutorial-type/homepage';
import {getIntroAndStepsTutorialFiles} from './tutorial-type/intro-and-steps';
import {getTutorialPlaygroundFiles, isPlaygroundRouteData} from './tutorial-type/playground';
import {PlaygroundFiles, TutorialFiles} from './tutorials-types';
import {
TUTORIALS_COMMON_DIRECTORY,
TUTORIALS_HOMEPAGE_DIRECTORY,
TUTORIALS_PLAYGROUND_DIRECTORY,
} from './utils/web-constants';
main();
/**
* Builds and stores the tutorials source codes and metadata into JSON files.
* These files are later used to load the tutorials into the WebContainer file system
* and manage the tutorials in the embedded editor.
*
* @see https://webcontainers.io/guides/working-with-the-file-system
*/
export async function main() {
const consoleTimeLabel = 'Tutorials files build done in';
// Informs the developers that this script is running and how much time it took
console.info('Building tutorials files...');
console.time(consoleTimeLabel);
const tutorialsFiles = await getTutorialsFiles();
// Clean up the directory before writing the files
await rm(TUTORIALS_ASSETS_NODE_PATH, {recursive: true}).catch(() => {
// `rm` throws if the directory doesn't exist, meaning this script is
// being executed for the first time
});
await Promise.all([
mkdir(TUTORIALS_ASSETS_NODE_PATH, {recursive: true}),
mkdir(TUTORIALS_SOURCE_CODE_NODE_PATH, {recursive: true}),
mkdir(TUTORIALS_METADATA_NODE_PATH, {recursive: true}),
]);
const writeFilesPromises: Promise<void>[] = [];
const tutorialsRoutes: TutorialNavigationItemWithStep[] = [];
tutorialsFiles.forEach(({sourceCode, metadata, route, sourceCodeZip}, tutorialName) => {
if (sourceCode) {
const filePath = join(TUTORIALS_SOURCE_CODE_NODE_PATH, tutorialName + '.json');
const fileContents = JSON.stringify(sourceCode);
if (tutorialName.includes(sep)) {
writeFilesPromises.push(createDirectoryAndWriteFile(filePath, fileContents));
} else {
writeFilesPromises.push(writeFile(filePath, fileContents));
}
}
if (metadata) {
const filePath = join(TUTORIALS_METADATA_NODE_PATH, tutorialName + '.json');
const fileContents = JSON.stringify(metadata);
if (tutorialName.includes(sep)) {
writeFilesPromises.push(createDirectoryAndWriteFile(filePath, fileContents));
} else {
writeFilesPromises.push(writeFile(filePath, fileContents));
}
}
if (route) {
if (isPlaygroundRouteData(route)) {
writeFilesPromises.push(
createDirectoryAndWriteFile(PLAYGROUND_ROUTE_NODE_PATH, JSON.stringify(route)),
);
} else {
tutorialsRoutes.push({
...route,
path: tutorialName,
});
}
}
if (sourceCodeZip) {
const filePath = join(TUTORIALS_SOURCE_CODE_NODE_PATH, tutorialName + '.zip');
writeFilesPromises.push(createDirectoryAndWriteFile(filePath, sourceCodeZip));
}
});
await Promise.all([
...writeFilesPromises,
copyCommonAssetsToAppAssets(),
buildTutorialsNavigationItems(tutorialsRoutes),
]);
console.timeEnd(consoleTimeLabel);
}
/**
* Retrieve the tutorials source code and generate a Map representing the files
* and contents that will be stored in the filesystem.
*
* Those files are later used by the `/tutorials` pages.
*
* The map key is the project name and the value contain the contents for each
* .json file.
*/
async function getTutorialsFiles(): Promise<Map<string, TutorialFiles | PlaygroundFiles>> {
const [tutorialsProjects, commonTutorialFiles] = await Promise.all([
globWithCwdPath('**', {
...GLOB_OPTIONS,
ignore: [...(GLOB_OPTIONS.ignore as string[]), '*.md', TUTORIALS_COMMON_DIRECTORY],
maxDepth: 1,
cwd: TUTORIALS_PROJECT_PATH,
}),
globWithCwdPath('**', {
...GLOB_OPTIONS,
cwd: TUTORIALS_COMMON_NODE_PATH,
nodir: true,
}),
]);
const tutorialsFiles = new Map<string, TutorialFiles | PlaygroundFiles>();
// tutorials depends on the common project files so here we handle it first
const commonFilesContents = await getFilesContents(commonTutorialFiles);
tutorialsFiles.set(
TUTORIALS_COMMON_DIRECTORY,
await getTutorialCommonFiles(commonTutorialFiles, commonFilesContents),
);
await Promise.all(
tutorialsProjects.map(async (tutorialPath) => {
const tutorialName = basename(tutorialPath);
const [tutorialDirectories, tutorialFiles] = await Promise.all([
globWithCwdPath('**/', {...GLOB_OPTIONS, cwd: tutorialPath}),
globWithCwdPath('**', {...GLOB_OPTIONS, nodir: true, cwd: tutorialPath}),
]);
try {
if (tutorialName === TUTORIALS_PLAYGROUND_DIRECTORY) {
const playgroundFiles = await getTutorialPlaygroundFiles(commonFilesContents);
playgroundFiles.forEach((playgroundFiles, playgroundName) => {
tutorialsFiles.set(playgroundName, playgroundFiles);
});
} else if (tutorialName === TUTORIALS_HOMEPAGE_DIRECTORY) {
const homepageFiles = await getHomepagePlaygroundFiles(commonFilesContents);
tutorialsFiles.set(TUTORIALS_HOMEPAGE_DIRECTORY, homepageFiles);
} else if (
tutorialDirectories.some((directory) => directory.includes(STEPS_DIRECTORY)) &&
tutorialDirectories.some((directory) => directory.includes(INTRO_DIRECTORY))
) {
const tutorialStepsFiles = await getIntroAndStepsTutorialFiles(
tutorialName,
tutorialDirectories,
tutorialFiles,
commonFilesContents,
);
tutorialStepsFiles.forEach((stepFiles, stepName) => {
tutorialsFiles.set(stepName, stepFiles);
});
} else {
throw `Could not find "${INTRO_DIRECTORY}" and/or "${STEPS_DIRECTORY}" directories.`;
}
} catch (error) {
console.error(`\n❌ Failed to parse tutorial at ${TUTORIALS_PROJECT_PATH}/${tutorialName}`);
console.error(`\t${error}\n`);
process.exit(1);
}
}),
);
return tutorialsFiles;
}
@@ -0,0 +1,56 @@
/*!
* @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 {join} from 'path';
import {glob} from 'glob';
import {input} from '@inquirer/prompts';
import {TutorialType} from './utils/web-constants';
import {parseConfig} from './tutorials-config';
import {createDirectoryAndWriteFile} from './utils/filesystem';
import {CONFIG_FILE, GLOB_OPTIONS, TUTORIALS_PLAYGROUND_NODE_PATH} from './utils/node-constants';
import {validatePathAnswer} from './utils/cli';
createTemplate();
async function createTemplate() {
const existingTemplates = await glob('**/', {
...GLOB_OPTIONS,
absolute: false,
cwd: TUTORIALS_PLAYGROUND_NODE_PATH,
maxDepth: 1,
});
const path = await askTemplatePath(existingTemplates);
const title = await input({message: 'What is the title for the template?'});
const templatePath = `${TUTORIALS_PLAYGROUND_NODE_PATH}/${path}`;
const config = await parseConfig({
type: TutorialType.EDITOR_ONLY,
title,
openFiles: ['src/main.ts'],
});
await Promise.all([
createDirectoryAndWriteFile(join(templatePath, CONFIG_FILE), JSON.stringify(config, null, 2)),
createDirectoryAndWriteFile(join(templatePath, 'src', 'main.ts'), ''),
]);
console.info(`\n✅ Template created at ${templatePath}`);
}
function askTemplatePath(existingTemplates: string[]) {
const placeholder = 'path-to-template';
return input({
message: 'What is the path of the template?',
default: placeholder,
validate: (answer) => validatePathAnswer(answer, existingTemplates, placeholder),
});
}
+203
View File
@@ -0,0 +1,203 @@
/*!
* @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 {join} from 'path';
import {select, input, confirm} from '@inquirer/prompts';
import {glob} from 'glob';
import {createDirectoryAndWriteFile} from './utils/filesystem';
import {
CONFIG_FILE,
CONTENT_PLACEHOLDER,
GLOB_OPTIONS,
INTRO_DIRECTORY,
STEPS_DIRECTORY,
TUTORIALS_PROJECT_PATH,
TUTORIAL_CONTENT_FILENAME,
} from './utils/node-constants';
import type {TutorialConfig} from './tutorials-types';
import {parseConfig} from './tutorials-config';
import {
TUTORIALS_COMMON_DIRECTORY,
TUTORIALS_PLAYGROUND_DIRECTORY,
TutorialType,
} from './utils/web-constants';
import {validatePathAnswer} from './utils/cli';
type TutorialFields = TutorialConfig & {
path: string;
};
type TutorialToCreate = {
intro: TutorialFields;
steps: Array<TutorialFields & {step: number}>;
};
createTutorial();
async function createTutorial() {
const existingTutorials = await glob('**/', {
ignore: [
...(GLOB_OPTIONS.ignore as string[]),
TUTORIALS_COMMON_DIRECTORY,
TUTORIALS_PLAYGROUND_DIRECTORY,
],
cwd: TUTORIALS_PROJECT_PATH,
maxDepth: 1,
});
const introPath = await askTutorialPath(existingTutorials);
const introType = await askTutorialType('What is the type of the tutorial intro?');
const introTitle = await input({message: 'What is the title for the tutorial intro?'});
const introNextTutorial = await askAboutNextTutorial(existingTutorials);
const tutorialToCreate: TutorialToCreate = {
intro: {
type: introType as any, // Note: casting to any here as a workaround for the enum type error
title: introTitle,
path: introPath,
openFiles: [''],
nextTutorial: introNextTutorial,
},
steps: [],
};
let addMoreSteps = true;
let step = 1;
while (addMoreSteps) {
console.info(`\nStep ${step}`);
const path = await askTutorialStepPath(tutorialToCreate.steps);
const type = await askTutorialType('What is the type of the step?');
const title = await input({message: 'What is the title of the step?'});
tutorialToCreate.steps.push({
step,
type: type as any, // Note: casting to any here as a workaround for the enum type error
path,
title,
openFiles: [''],
});
step++;
console.info();
addMoreSteps = await confirm({message: 'Would you like to add another step?', default: true});
}
const tutorialPath = `${TUTORIALS_PROJECT_PATH}/${tutorialToCreate.intro.path}`;
await Promise.all([
createIntroFiles(tutorialToCreate.intro),
...createStepsFiles(tutorialPath, tutorialToCreate.steps),
]);
console.info(`\n✅ Tutorial created at ${tutorialPath}`);
}
function askTutorialType(message: string = 'What is the type of the tutorial?') {
return select<TutorialConfig['type']>({
message: message,
choices: [
{
name: 'CLI only: a tutorial for the Angular CLI in-browser unsing the embedded terminal',
value: TutorialType.CLI,
},
{name: 'Local app: a tutorial for building an app locally', value: TutorialType.LOCAL},
{
name: 'Embedded editor: a tutorial for building an app in-browser using the embedded editor',
value: TutorialType.EDITOR,
},
],
});
}
function askAboutNextTutorial(
existingTutorials: string[],
message: string = 'What should be the next tutorial?',
) {
return select({
message,
choices: [
...existingTutorials.map((tutorial) => ({name: tutorial, value: tutorial})),
{name: 'None', value: undefined},
],
});
}
function askTutorialPath(existingTutorials: string[]) {
const placeholder = 'path-to-tutorial';
return input({
message: 'What is the path of the tutorial?',
default: placeholder,
validate: (answer) => validatePathAnswer(answer, existingTutorials, placeholder),
});
}
function askTutorialStepPath(createdSteps: TutorialToCreate['steps']) {
const placeholder = 'path-to-step';
const existingSteps = createdSteps.map((step) => step.path);
return input({
message: 'What is the path of the step?',
default: placeholder,
validate: (answer) => validatePathAnswer(answer, existingSteps, placeholder),
});
}
async function createIntroFiles(intro: TutorialToCreate['intro']): Promise<Promise<void>[]> {
const tutorialPath = `${TUTORIALS_PROJECT_PATH}/${intro.path}`;
const config = await parseConfig({
type: intro.type as any, // Note: casting to any here as a workaround for the enum type error
title: intro.title,
openFiles: intro.openFiles,
nextTutorial: intro.nextTutorial,
});
return [
createDirectoryAndWriteFile(
join(tutorialPath, INTRO_DIRECTORY, CONFIG_FILE),
JSON.stringify(config, null, 2),
),
createDirectoryAndWriteFile(
join(tutorialPath, INTRO_DIRECTORY, TUTORIAL_CONTENT_FILENAME),
CONTENT_PLACEHOLDER,
),
];
}
function createStepsFiles(
tutorialPath: string,
steps: TutorialToCreate['steps'],
): Promise<Promise<void>[]>[] {
const tutorialStepsPath = join(tutorialPath, STEPS_DIRECTORY);
return steps
.map(async (step) => {
const stepPath = join(tutorialStepsPath, `${step.step}-${step.path}`);
const config = await parseConfig({
type: step.type as any, // Note: casting to any here as a workaround for the enum type error
title: step.title,
openFiles: step.openFiles,
});
return [
createDirectoryAndWriteFile(`${stepPath}/${CONFIG_FILE}`, JSON.stringify(config, null, 2)),
createDirectoryAndWriteFile(
`${stepPath}/${TUTORIAL_CONTENT_FILENAME}`,
CONTENT_PLACEHOLDER,
),
];
})
.flat();
}
@@ -0,0 +1,62 @@
/*!
* @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 {copyFile, mkdir} from 'fs/promises';
import {glob} from 'glob';
import {filesAndContentsToRecord} from '../utils/filesystem';
import {TUTORIALS_COMMON_DIRECTORY} from '../utils/web-constants';
import {getFileSystemTree, shouldUseFileInWebContainer} from '../utils/webcontainers';
import type {FileAndContent, TutorialFiles} from '../tutorials-types';
import {TUTORIALS_COMMON_ASSETS_DEST, TUTORIALS_COMMON_ASSETS_SRC} from '../utils/node-constants';
const BEFORE_COMMON_PATH_REGEX = new RegExp(`.*\/${TUTORIALS_COMMON_DIRECTORY}\/`);
export async function getTutorialCommonFiles(
files: string[],
filesContents: FileAndContent[],
): Promise<TutorialFiles> {
const filesPath = files.filter(shouldUseFileInWebContainer).map(getCleanFilePath);
const filesContentsWithoutPath = filesContents.map(({path, content}) => ({
path: getCleanFilePath(path),
content,
}));
const fileSystemTree = getFileSystemTree(
filesPath,
filesAndContentsToRecord(filesContentsWithoutPath),
);
return {sourceCode: fileSystemTree};
}
export function getCleanFilePath(path: string) {
return path.replace(BEFORE_COMMON_PATH_REGEX, '');
}
export async function copyCommonAssetsToAppAssets() {
const [commonAssets] = await Promise.all([
// List of files for the assets from the tutorial common directory/app.
glob(['**/*.png', '**/*.jpg'], {
cwd: TUTORIALS_COMMON_ASSETS_SRC,
nodir: true,
absolute: true,
}),
// Ensure the common directory for tutorial assets exists.
mkdir(TUTORIALS_COMMON_ASSETS_DEST, {recursive: true}),
]);
await Promise.all(
commonAssets.map((asset) =>
copyFile(asset, asset.replace(TUTORIALS_COMMON_ASSETS_SRC, TUTORIALS_COMMON_ASSETS_DEST)),
),
);
}
@@ -0,0 +1,146 @@
/*!
* @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 {basename} from 'path';
import {
filesAndContentsToRecord,
getFilesContents,
getPackageJsonFromFiles,
globWithCwdPath,
} from '../utils/filesystem';
import {
GLOB_OPTIONS,
TUTORIALS_COMMON_NODE_PATH,
TUTORIALS_HOMEPAGE_NODE_PATH,
} from '../utils/node-constants';
import {getCleanFilePath as getCleanCommonFilePath} from './common';
import {getTutorialConfig, validateOpenFilesConfig} from '../tutorials-config';
import {
FileAndContent,
FileAndContentRecord,
TutorialFiles,
TutorialMetadata,
} from '../tutorials-types';
import {getAllFiles, validatePackageJson} from '../utils/metadata';
import {TUTORIALS_HOMEPAGE_DIRECTORY, TutorialType} from '../utils/web-constants';
import {getFileSystemTree, shouldUseFileInWebContainer} from '../utils/webcontainers';
const BEFORE_HOMEPAGE_PATH_REGEX = new RegExp(`.*\/${TUTORIALS_HOMEPAGE_DIRECTORY}\/`);
export async function getHomepagePlaygroundFiles(
commonFiles: FileAndContent[],
): Promise<TutorialFiles> {
const homepagePlaygroundFiles = await globWithCwdPath('**', {
...GLOB_OPTIONS,
cwd: TUTORIALS_HOMEPAGE_NODE_PATH,
nodir: true,
});
const config = await getTutorialConfig(homepagePlaygroundFiles);
if (config.type !== TutorialType.EDITOR_ONLY)
throw new Error(`Homepage must be of type "${TutorialType.EDITOR_ONLY}"`);
const filesContents = await getFilesContents(homepagePlaygroundFiles);
const tutorialFiles = getTutorialFiles(filesContents, commonFiles);
if (config.openFiles) {
validateOpenFilesConfig(
TUTORIALS_HOMEPAGE_NODE_PATH,
config.openFiles,
Object.keys(tutorialFiles),
);
} else {
config.openFiles = Object.keys(tutorialFiles);
}
const hasPackageJson = homepagePlaygroundFiles.some((file) => basename(file) === 'package.json');
let dependencies: TutorialMetadata['dependencies'];
if (hasPackageJson) {
const packageJson = getPackageJsonFromFiles(filesContents);
dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
validatePackageJson(homepagePlaygroundFiles, packageJson, getPackageJsonFromFiles(commonFiles));
}
const allFiles = getAllFiles(
homepagePlaygroundFiles,
commonFiles.map(({path}) => path),
getCleanFilePath,
);
return {
sourceCode: getSourceCode(homepagePlaygroundFiles, filesContents),
metadata: {
type: config.type,
allFiles,
dependencies,
tutorialFiles,
openFiles: config.openFiles ?? Object.keys(tutorialFiles),
hiddenFiles: config.openFiles
? Object.keys(tutorialFiles).filter((filename) => !config.openFiles!.includes(filename))
: [],
},
};
}
function getSourceCode(tutorialFiles: string[], filesContents: FileAndContent[]) {
// TODO: reuse this as it's duplicated in getTutorialFiles
const fileSystemTreeContents = filesContents
.filter(({path}) => shouldUseFileInWebContainer(path))
// remove steps paths from file contents
.map(({path, content}) => ({
path: getCleanFilePath(path),
content,
}));
const fileSystemTreeFiles = tutorialFiles
.filter((filename) => shouldUseFileInWebContainer(filename))
.map(getCleanFilePath);
return getFileSystemTree(fileSystemTreeFiles, filesAndContentsToRecord(fileSystemTreeContents));
}
function getTutorialFiles(
filesContents: FileAndContent[],
commonFiles: FileAndContent[],
): FileAndContentRecord {
const playgroundFilesForCodeEditor = filesContents
.filter(({path}) => shouldUseFileInWebContainer(getCleanFilePath(path)))
.map(({path, content}) => ({
path: getCleanFilePath(path),
content,
}));
const commonFilesForCodeEditor = commonFiles
.filter(({path}) => shouldUseFileInWebContainer(path))
.map(({path, content}) => ({
path: getCleanCommonFilePath(path),
content,
}));
return {
...filesAndContentsToRecord(commonFilesForCodeEditor),
...filesAndContentsToRecord(playgroundFilesForCodeEditor),
};
}
function getCleanFilePath(path: string) {
if (path.includes(TUTORIALS_COMMON_NODE_PATH)) return getCleanCommonFilePath(path);
return path.replace(BEFORE_HOMEPAGE_PATH_REGEX, '');
}
@@ -0,0 +1,402 @@
/*!
* @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 {basename, dirname, join} from 'path';
import {
filesAndContentsToRecord,
getFilesContents,
getPackageJsonFromFiles,
getZipBuffer,
globWithCwdPath,
recordToFilesAndContents,
} from '../utils/filesystem';
import {
ANSWER_DIRECTORY,
GLOB_OPTIONS,
INTRO_DIRECTORY,
STEPS_DIRECTORY,
TUTORIALS_PROJECT_PATH,
} from '../utils/node-constants';
import {getFileSystemTree, shouldUseFileInWebContainer} from '../utils/webcontainers';
import {getCleanFilePath as getCleanCommonFilePath} from './common';
import {TutorialNavigationItemWithStep} from '../generate-tutorials-routes';
import {getTutorialConfig, validateOpenFilesConfig} from '../tutorials-config';
import type {
FileAndContent,
FileAndContentRecord,
TutorialConfig,
TutorialFiles,
TutorialMetadata,
TutorialStep,
} from '../tutorials-types';
import {
TUTORIALS_COMMON_DIRECTORY,
TUTORIALS_DIST_PATH,
TUTORIALS_SOURCE_CODE_WEB_PATH,
TutorialType,
} from '../utils/web-constants';
import {getAllFiles, validatePackageJson} from '../utils/metadata';
export const TUTORIALS_DIRECTORY_REGEX = /^[a-z-0-9]*$/;
export const STEPS_DIRECTORY_REGEX = /^([0-9]|[0-9][0-9])-[A-Za-z-0-9]*$/;
const BEFORE_STEP_PATH_REGEX = new RegExp(`.*\/${STEPS_DIRECTORY}\/[^\/]+\/`);
const BEFORE_INTRO_PATH_REGEX = new RegExp(`.*\/${INTRO_DIRECTORY}\/`);
export async function getIntroAndStepsTutorialFiles(
tutorialName: string,
tutorialDirectories: string[],
tutorialFiles: string[],
commonTutorialFiles: FileAndContent[],
): Promise<Map<string, TutorialFiles>> {
const tutorialSteps = getTutorialSteps(tutorialName, tutorialDirectories);
const introAndStepsFiles = new Map<TutorialStep['url'], TutorialFiles>();
const tutorialCommonFiles = tutorialFiles.filter((file) =>
isCommonTutorialFile(tutorialName, file),
);
const introFiles = await getTutorialIntroFiles(
tutorialName,
[
...tutorialCommonFiles,
...tutorialFiles.filter((file) => file.includes(`/${INTRO_DIRECTORY}/`)),
],
tutorialSteps,
commonTutorialFiles,
);
introAndStepsFiles.set(tutorialName, introFiles);
await Promise.all(
tutorialSteps.map(async (step) => {
const stepFiles = tutorialFiles.filter((file) => file.includes(step.path));
let nextTutorial;
if (introFiles.route && 'tutorialData' in introFiles.route) {
nextTutorial = introFiles.route.tutorialData.nextTutorial;
}
const stepSourceCodeAndMetadata = await getStepFiles(
[...tutorialCommonFiles, ...stepFiles],
{
...step,
nextTutorial,
},
commonTutorialFiles,
);
introAndStepsFiles.set(step.url, stepSourceCodeAndMetadata);
}),
);
return introAndStepsFiles;
}
async function getTutorialIntroFiles(
tutorialName: string,
introTutorialFiles: string[],
tutorialSteps: TutorialStep[],
commonFilesContents: FileAndContent[],
) {
return getStepFiles(
introTutorialFiles,
{
step: 0,
name: tutorialName,
path: dirname(introTutorialFiles[0]),
nextStep: tutorialSteps.find((step) => step.step === 0 || step.step === 1)?.url, // assume the first tutorialSteps array item is the first step
url: tutorialName,
},
commonFilesContents,
);
}
async function getStepFiles(
stepFiles: string[],
step: TutorialStep,
commonFilesContents: FileAndContent[],
): Promise<TutorialFiles> {
const config = await getTutorialConfig(stepFiles);
// if src is present in the config, add files to the step files
if (config.src) {
const srcFiles = await getExternalTutorialFiles(step, config.src);
stepFiles.push(...srcFiles);
}
const filesContents = await getFilesContents(stepFiles);
const route: Omit<TutorialNavigationItemWithStep, 'path'> = {
label: config.title,
contentPath: getTutorialContentPath(stepFiles),
tutorialData: {
type: config.type,
title: config.title,
step: step.step,
nextStep: step.nextStep,
previousStep: step.previousStep,
nextTutorial: config.nextTutorial ?? step.nextTutorial,
},
};
if (config.type === TutorialType.LOCAL) {
const hasFilesToDownload =
filesContents.filter(({path}) => shouldUseFileInWebContainer(path)).length > 0;
if (!hasFilesToDownload) {
return {route};
} else {
const filesToDownload = getFilesToDownload(commonFilesContents, filesContents);
route.tutorialData.sourceCodeZipPath = join(
TUTORIALS_SOURCE_CODE_WEB_PATH,
step.url + '.zip',
);
return {
sourceCodeZip: await getZipBuffer(filesToDownload),
route,
};
}
}
const tutorialFiles = getTutorialFiles(filesContents);
if (config.openFiles) {
validateOpenFilesConfig(step.path, config.openFiles, Object.keys(tutorialFiles));
} else if (config.type === TutorialType.EDITOR) {
config.openFiles = Object.keys(tutorialFiles);
}
const hasPackageJson = stepFiles.some((file) => basename(file) === 'package.json');
let dependencies: TutorialMetadata['dependencies'];
if (hasPackageJson) {
const packageJson = getPackageJsonFromFiles(filesContents);
dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
validatePackageJson(stepFiles, packageJson, getPackageJsonFromFiles(commonFilesContents));
}
const fileSystemTreeContents = filesContents
.filter(({path}) => shouldUseFileInWebContainer(path))
.map(({path, content}) => ({path: getCleanFilePath(path), content}));
const fileSystemTreeFiles = stepFiles.filter(shouldUseFileInWebContainer).map(getCleanFilePath);
const allFiles = getAllFiles(
stepFiles,
commonFilesContents.map(({path}) => path),
getCleanFilePath,
);
const metadata: TutorialMetadata = {
type: config.type,
allFiles,
tutorialFiles,
answerFiles: await getAnswerFiles(step, filesContents, config.answerSrc),
openFiles: config.openFiles ?? Object.keys(tutorialFiles),
hiddenFiles: config.openFiles
? Object.keys(tutorialFiles).filter((filename) => !config.openFiles!.includes(filename))
: [],
dependencies,
};
return {
metadata,
sourceCode: getFileSystemTree(
fileSystemTreeFiles,
filesAndContentsToRecord(fileSystemTreeContents),
),
route,
};
}
export function getTutorialSteps(
tutorialName: string,
tutorialDirectories: string[],
): TutorialStep[] {
const stepsDirectories = tutorialDirectories.filter((directory) => {
const isStepDirectory = directory.includes(`${tutorialName}/${STEPS_DIRECTORY}/`);
if (!isStepDirectory) return false;
const stepPathParts = directory.split('/');
// steps directory is always right after STEPS_DIRECTORY in the path, e.g. `learn-angular/steps/1-getting-started`
const stepDirectoryIndex = stepPathParts.indexOf(STEPS_DIRECTORY) + 1;
const step = stepPathParts[stepDirectoryIndex];
return step === stepPathParts.at(-1);
});
const steps: TutorialStep[] = [];
for (const stepDirectoryPath of stepsDirectories) {
const stepDirectory = basename(stepDirectoryPath);
// force the tutorials steps to follow the pattern steps/{step}-{name}
if (!STEPS_DIRECTORY_REGEX.test(stepDirectory))
throw `Invalid step directory '${stepDirectory}'. The step directory must start with a number > 0, followed by a hyphen and a name. For example: '1-intro'`;
const [step, ...nameParts] = stepDirectory.replace(`${STEPS_DIRECTORY}/`, '').split('-');
const name = nameParts.join('-');
steps.push({
step: Number(step),
name,
path: stepDirectoryPath,
url: join(tutorialName, name),
});
}
return steps.map((step) => {
const previousStep = steps.find(({step: previousStep}) => previousStep === step.step - 1);
const nextStep = steps.find(({step: nextStep}) => nextStep === step.step + 1);
return {
...step,
nextStep: nextStep?.url,
previousStep: previousStep?.url ?? tutorialName,
};
});
}
/**
* Filter files that won't be used in the WebContainer.
*/
function getTutorialFiles(
tutorialFilesContents: FileAndContent[],
): Record<FileAndContent['path'], FileAndContent['content']> {
const tutorialFiles = tutorialFilesContents
.filter(({path}) => shouldUseFileInWebContainer(path))
.map(({path, content}) => ({
path: getCleanFilePath(path),
content,
}));
return filesAndContentsToRecord(tutorialFiles);
}
async function getAnswerFiles(
step: TutorialStep,
filesContents: FileAndContent[],
answerSrc: TutorialConfig['answerSrc'],
): Promise<FileAndContentRecord> {
const removeAnswerPathFromFiles = (filesAndContents: FileAndContent[]) =>
filesAndContents.map(({path, content}) => ({
path: path.replace(`${ANSWER_DIRECTORY}/`, ''),
content,
}));
if (answerSrc) {
const answerSrcFiles = await getExternalTutorialFiles(step, answerSrc);
const answerFiles = await getFilesContents(answerSrcFiles);
return getTutorialFiles(removeAnswerPathFromFiles(answerFiles));
}
const answerFiles = filesContents.filter(({path}) => path.includes(`${ANSWER_DIRECTORY}/`));
return getTutorialFiles(removeAnswerPathFromFiles(answerFiles));
}
/**
* Get files external to the current tutorial directory.
*/
async function getExternalTutorialFiles(
step: TutorialStep,
relativePath: string,
): Promise<string[]> {
const externalFilesPath = join(step.path, relativePath);
const files = await globWithCwdPath('**', {
...GLOB_OPTIONS,
cwd: externalFilesPath,
nodir: true,
});
return files;
}
function getTutorialContentPath(stepFiles: string[]) {
const tutorialContentPath = stepFiles.find((filename) => filename.endsWith('README.md'));
if (!tutorialContentPath) {
const tutorialPath = dirname(stepFiles[0]);
throw `Tutorial at ${tutorialPath} is missing a README.md for content`;
}
const contentPathWithDistPath = tutorialContentPath.replace(
TUTORIALS_PROJECT_PATH,
TUTORIALS_DIST_PATH,
);
return contentPathWithDistPath.replace(/\.[^.]+$/, '');
}
/**
* Clean the file path by making it relative to the project root.
*
* @example
* ```ts
* getCleanFilePath('path-to-tutorial/intro/src/app/app.component.ts') => 'src/app/app.component.ts'
* getCleanFilePath('path-to-tutorial/steps/1-step/src/app/app.component.ts') => 'src/app/app.component.ts'
* ```
*/
function getCleanFilePath(path: string) {
if (path.includes(TUTORIALS_COMMON_DIRECTORY)) return getCleanCommonFilePath(path);
if (path.includes(STEPS_DIRECTORY)) return path.replace(BEFORE_STEP_PATH_REGEX, '');
return path.replace(BEFORE_INTRO_PATH_REGEX, '');
}
/**
* Merge common project files with the step files to get the final files to download.
*/
function getFilesToDownload(
commonFilesContents: FileAndContent[],
stepFilesWithoutAnswer: FileAndContent[],
) {
const commonFilesWithoutPath = commonFilesContents.map(({path, content}) => ({
path: getCleanCommonFilePath(path),
content,
}));
const stepFilesWithoutPath = stepFilesWithoutAnswer
.filter(({path}) => shouldUseFileInWebContainer(path))
.map(({path, content}) => ({
path: getCleanFilePath(path),
content,
}));
// use object to easily override common files with step files
const filesToDownload = {
...filesAndContentsToRecord(commonFilesWithoutPath),
...filesAndContentsToRecord(stepFilesWithoutPath),
};
return recordToFilesAndContents(filesToDownload);
}
function isCommonTutorialFile(tutorialPath: string, filePath: string) {
return filePath.includes(join(tutorialPath, TUTORIALS_COMMON_DIRECTORY));
}
@@ -0,0 +1,289 @@
/*!
* @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 {glob} from 'glob';
import {basename, join, sep} from 'path';
import {
filesAndContentsToRecord,
getFilesContents,
getPackageJsonFromFiles,
globWithCwdPath,
} from '../utils/filesystem';
import {CONFIG_FILE, GLOB_OPTIONS, TUTORIALS_PLAYGROUND_NODE_PATH} from '../utils/node-constants';
import {
DEFAULT_PLAYGROUND_TEMPLATE,
STARTER_PLAYGROUND_TEMPLATE,
TUTORIALS_COMMON_DIRECTORY,
TUTORIALS_PLAYGROUND_DIRECTORY,
TutorialType,
} from '../utils/web-constants';
import {getFileSystemTree, shouldUseFileInWebContainer} from '../utils/webcontainers';
import {getTutorialConfig, validateOpenFilesConfig} from '../tutorials-config';
import {
FileAndContent,
FileAndContentRecord,
PlaygroundFiles,
PlaygroundRouteData,
PlaygroundTemplate,
TutorialConfig,
TutorialFiles,
TutorialMetadata,
} from '../tutorials-types';
import {getAllFiles, validatePackageJson} from '../utils/metadata';
import {getCleanFilePath as getCleanCommonFilePath} from './common';
// used to avoid recreating a regex in loops
const beforePlaygroundPathRegexCache = new Map<string, RegExp>();
export async function getTutorialPlaygroundFiles(
commonFiles: FileAndContent[],
): Promise<Map<string, PlaygroundFiles>> {
const cwd = TUTORIALS_PLAYGROUND_NODE_PATH;
const [playgroundProjects, playgroundFiles] = await Promise.all([
glob('*/', {
ignore: [TUTORIALS_COMMON_DIRECTORY],
cwd,
}),
globWithCwdPath('**', {
...GLOB_OPTIONS,
nodir: true,
cwd,
}),
]);
const playgroundCommonFiles = playgroundFiles.filter(isPlaygroundCommonFile);
const playgroundFilesMap = new Map<string, PlaygroundFiles>();
const templates: PlaygroundTemplate[] = [];
await Promise.all(
playgroundProjects.map(async (project) => {
const projectFiles = playgroundFiles.filter((file) =>
file.startsWith(`${cwd}${sep}${project}${sep}`),
);
if (!projectFiles.length) {
throw new Error(
`Project at ${join(TUTORIALS_PLAYGROUND_NODE_PATH, project)} has no files.`,
);
}
const projectFilesAndPlaygroundCommonFiles = [...playgroundCommonFiles, ...projectFiles];
const projectPath = join(TUTORIALS_PLAYGROUND_DIRECTORY, project);
const {sourceCode, metadata, route} = await getPlaygroundFiles(
projectPath,
projectFilesAndPlaygroundCommonFiles,
commonFiles,
);
// store only sourceCode and metadata per playground template
playgroundFilesMap.set(projectPath, {sourceCode, metadata});
// compose playground templates from each project route object
if (route) templates.push(...route!.templates);
}),
);
const defaultTemplate = templates.find((template) =>
template.path.endsWith(DEFAULT_PLAYGROUND_TEMPLATE),
);
if (!defaultTemplate) {
throw new Error(`Invalid default playground template path '${DEFAULT_PLAYGROUND_TEMPLATE}'`);
}
const starterTemplate = templates.find((template) =>
template.path.endsWith(STARTER_PLAYGROUND_TEMPLATE),
);
if (!starterTemplate) {
throw new Error(`Invalid starter playground template path '${STARTER_PLAYGROUND_TEMPLATE}'`);
}
playgroundFilesMap.set(TUTORIALS_PLAYGROUND_DIRECTORY, {
route: {
templates: templates.sort((a, b) => {
const aPath = a.path.toLowerCase();
const bPath = b.path.toLowerCase();
if (aPath < bPath) return -1;
if (aPath > bPath) return 1;
return 0;
}),
defaultTemplate,
starterTemplate,
},
});
return playgroundFilesMap;
}
async function getPlaygroundFiles(
project: string,
playgroundFiles: string[],
commonFiles: FileAndContent[],
): Promise<PlaygroundFiles> {
const config = await getTutorialConfig(playgroundFiles);
if (config.type !== TutorialType.EDITOR_ONLY)
throw new Error(`Playground must be of type "${TutorialType.EDITOR_ONLY}"`);
const filesContents = await getFilesContents(playgroundFiles);
const projectFiles = getProjectFiles(project, config.openFiles, filesContents, commonFiles);
if (config.openFiles) {
validateOpenFilesConfig(
TUTORIALS_PLAYGROUND_NODE_PATH,
config.openFiles,
Object.keys(projectFiles),
);
}
const hasPackageJson = playgroundFiles.some((file) => basename(file) === 'package.json');
let dependencies: TutorialMetadata['dependencies'];
if (hasPackageJson) {
const packageJson = getPackageJsonFromFiles(filesContents);
dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
validatePackageJson(playgroundFiles, packageJson, getPackageJsonFromFiles(commonFiles));
}
const allFiles = getAllFiles(
playgroundFiles,
commonFiles.map(({path}) => path),
(path: string) => getCleanFilePath(project, path),
);
return {
sourceCode: getSourceCode(project, playgroundFiles, filesContents),
metadata: {
type: config.type,
allFiles,
dependencies,
tutorialFiles: projectFiles,
openFiles: config.openFiles ?? Object.keys(projectFiles),
hiddenFiles: config.openFiles
? Object.keys(projectFiles).filter((filename) => !config.openFiles!.includes(filename))
: [],
},
route: {
templates: [{path: project, label: config.title}],
},
};
}
function getProjectFiles(
project: string,
openFiles: TutorialConfig['openFiles'],
playgroundFilesContents: FileAndContent[],
commonFiles: FileAndContent[],
): FileAndContentRecord {
const playgroundFilesForCodeEditor = playgroundFilesContents
// remove files that should not be shown in the code editor
.filter(({path}) => shouldUseFileInWebContainer(path))
// remove playground path from file paths
.map(({path, content}) => ({
path: getCleanFilePath(project, path),
content,
}));
if (!openFiles) return filesAndContentsToRecord(playgroundFilesForCodeEditor);
const commonFilesForCodeEditor = commonFiles
// remove files that should not be shown in the code editor
.filter(({path}) => shouldUseFileInWebContainer(getCleanCommonFilePath(path)))
// remove common path from file paths
.map(({path, content}) => ({
path: getCleanCommonFilePath(path),
content,
}));
const commonAndPlaygroundFiles = {
...filesAndContentsToRecord(commonFilesForCodeEditor),
...filesAndContentsToRecord(playgroundFilesForCodeEditor),
};
const openFilesConfigWithContents: FileAndContentRecord = {};
for (const openFile of openFiles) {
if (commonAndPlaygroundFiles[openFile]) {
openFilesConfigWithContents[openFile] = commonAndPlaygroundFiles[openFile];
} else {
throw `At ${TUTORIALS_PLAYGROUND_NODE_PATH}/${CONFIG_FILE}.\n\tCould not find file "${openFile}" in playground or common files`;
}
}
return openFilesConfigWithContents;
}
export function getSourceCode(
project: string,
tutorialFiles: string[],
filesContents: FileAndContent[],
) {
const fileSystemTreeContents = filesContents
// filter out config files
.filter(({path}) => shouldUseFileInWebContainer(path))
// remove steps paths from file contents
.map(({path, content}) => ({
path: getCleanFilePath(project, path),
content,
}));
const fileSystemTreeFiles = tutorialFiles
.filter(shouldUseFileInWebContainer)
.map((filename) => getCleanFilePath(project, filename));
return getFileSystemTree(fileSystemTreeFiles, filesAndContentsToRecord(fileSystemTreeContents));
}
/**
* Create a regex that matches the string before the project name in a path.
*
* This is used to remove the project name from the path, keeping only the path
* relative to the project root.
*/
function getBeforePlaygroundProjectRegex(project: string): RegExp {
const projectRegex = beforePlaygroundPathRegexCache.get(project);
if (projectRegex) return projectRegex;
const newRegex = new RegExp(`.*\/${project}\/`);
beforePlaygroundPathRegexCache.set(project, newRegex);
return newRegex;
}
function getCleanFilePath(project: string, path: string) {
if (isPlaygroundCommonFile(path)) {
return getCleanCommonFilePath(path);
}
return path.replace(getBeforePlaygroundProjectRegex(project), '');
}
export function isPlaygroundRouteData(
routeData: PlaygroundRouteData | TutorialFiles['route'],
): routeData is PlaygroundRouteData {
return (routeData as PlaygroundRouteData).templates !== undefined;
}
function isPlaygroundCommonFile(path: string) {
return path.includes(TUTORIALS_COMMON_DIRECTORY);
}
@@ -0,0 +1,145 @@
/*!
* @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 {basename, join} from 'path';
import {readdir} from 'fs/promises';
import {glob} from 'glob';
import {getFileContents} from './utils/filesystem';
import {
CONFIG_FILE,
CONFIG_KEYS,
REQUIRED_CONFIGS,
TUTORIALS_PROJECT_PATH,
} from './utils/node-constants';
import type {TutorialConfig} from './tutorials-types';
import {TutorialType} from './utils/web-constants';
export async function getTutorialConfig(tutorialFiles: string[]): Promise<TutorialConfig> {
const configFilePath = tutorialFiles.find((file) => basename(file) === CONFIG_FILE);
if (!configFilePath) throw `Can't find ${CONFIG_FILE}`;
const configFileContents = await getFileContents(configFilePath);
let config: TutorialConfig;
try {
config = JSON.parse(String(configFileContents));
config = await parseConfig(config);
return config;
} catch (err) {
throw `${configFilePath.replace(`${TUTORIALS_PROJECT_PATH}/`, '')}: ${err}`;
}
}
export async function parseConfig(config: TutorialConfig): Promise<TutorialConfig> {
const tutorialConfigKeys = Object.keys(config);
REQUIRED_CONFIGS.forEach((requiredConfig) => {
if (!tutorialConfigKeys.includes(requiredConfig)) {
throw `missing required config '${requiredConfig}'`;
}
});
for (const key of tutorialConfigKeys) {
// @ts-expect-error
if (!CONFIG_KEYS.includes(key)) throw `invalid config '${key}'`;
}
if (config.nextTutorial) {
if (typeof config.nextTutorial !== 'string') {
throw '"nextTutorial" must be a string';
}
const path = join(TUTORIALS_PROJECT_PATH, config.nextTutorial);
// check if nextTutorial directory exists
try {
await readdir(path);
} catch (error) {
throw `the nextTutorial "${config.nextTutorial}" doesn't exist at ${path}`;
}
}
if (config.src) {
if (typeof config.src !== 'string') {
throw '"src" must be a string';
}
// check if src directory exists
const srcFiles = await glob(`**/${config.src}`, {cwd: TUTORIALS_PROJECT_PATH});
if (!srcFiles.length) throw `could not find "src" "${config.src}"`;
}
if (config.answerSrc) {
if (typeof config.answerSrc !== 'string') {
throw '"answerSrc" must be a string';
}
// check if answerSrc directory exists
const answerSrcFiles = await glob(`**/${config.answerSrc}`, {cwd: TUTORIALS_PROJECT_PATH});
if (!answerSrcFiles.length) throw `could not find "answerSrc" "${config.answerSrc}"`;
}
switch (config.type) {
case TutorialType.EDITOR:
assertOpenFiles(config.openFiles);
return config;
case TutorialType.CLI:
return config;
case TutorialType.LOCAL:
return config;
case TutorialType.EDITOR_ONLY:
assertOpenFiles(config.openFiles);
return config;
default:
throw 'invalid config';
}
}
function assertOpenFiles(
openFiles: TutorialConfig['openFiles'],
): asserts openFiles is string[] | undefined {
if (openFiles === undefined) return;
const errorMessage = '"openFiles" must be an array of strings';
if (!Array.isArray(openFiles)) {
throw errorMessage;
}
for (const openFile of openFiles) {
if (typeof openFile !== 'string') throw errorMessage;
}
}
/**
* Check if openFiles exist in tutorial files.
*/
export function validateOpenFilesConfig(
tutorialPath: string,
openFiles: NonNullable<TutorialConfig['openFiles']>,
tutorialFiles: string[],
) {
for (const openFile of openFiles) {
if (!tutorialFiles.includes(openFile)) {
throw `At: ${tutorialPath}/${CONFIG_FILE}\n\tCould not find "${openFile}" in the tutorial files`;
}
}
}
+152
View File
@@ -0,0 +1,152 @@
/*!
* @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 type {FileSystemTree} from '@webcontainer/api';
import type {TutorialNavigationItemWithStep} from './generate-tutorials-routes';
import {NavigationItem} from '@angular/docs-shared';
import {TutorialType} from './utils/web-constants';
/**
* Represents the contents of the tutorial files to be generated by the build script
*/
export type TutorialFiles = {
sourceCode?: FileSystemTree;
metadata?: TutorialMetadata;
sourceCodeZip?: Buffer;
route?: Omit<TutorialNavigationItemWithStep, 'path'>;
};
export type PlaygroundFiles = {
sourceCode?: FileSystemTree;
metadata?: TutorialMetadata;
sourceCodeZip?: undefined;
route?: PlaygroundRouteData;
};
/** Represents the contents of the tutorial config file */
export type TutorialMetadata = {
type: TutorialConfig['type'];
/** a record of all tutorials filenames and its contents */
tutorialFiles: FileAndContentRecord;
/** a record of filenames and contents for the tutorial answer */
answerFiles?: FileAndContentRecord;
/** files that are part of the project but are not visible in the code editor */
hiddenFiles: string[];
/**
* All files in the project, used to find the difference between new and old projects
* when changing projects
*/
allFiles: string[];
openFiles: NonNullable<TutorialConfig['openFiles']>;
/** whether a package.json exists */
dependencies?: Record<string, string>;
};
export type TutorialStep = {
step: number;
name: string;
path: string;
url: string;
nextStep?: TutorialStep['url'];
previousStep?: TutorialStep['url'];
nextTutorial?: string;
};
export type TutorialConfig =
| EditorTutorialConfig
| LocalTutorialConfig
| CliTutorialConfig
| EditorOnlyTutorialConfig;
export interface TutorialConfigBase {
type: TutorialType;
/** The tutorial title */
title: string;
/** The name of the tutorial folder that will be started after the current one ends. */
nextTutorial?: string;
/** The path to the tutorial src folder when it's external to the tutorial */
src?: string;
/** The path to the tutorial answer folder when it's external to the tutorial */
answerSrc?: string;
/** An array of files to be open in the editor */
openFiles?: string[];
}
/** Represents a tutorial config with all the embedded editor components enabled */
export interface EditorTutorialConfig extends TutorialConfigBase {
type: TutorialType.EDITOR;
}
/** Represents a tutorial config that won't use the embedded editor */
export interface LocalTutorialConfig extends TutorialConfigBase {
type: TutorialType.LOCAL;
// fields that must be undefined for local app tutorials
openFiles?: undefined;
src?: undefined;
answerSrc?: undefined;
}
/** Represents a tutorial config that supports only the interactive terminal for the Angular CLI */
export type CliTutorialConfig = Omit<LocalTutorialConfig, 'type'> & {
type: TutorialType.CLI;
};
export type EditorOnlyTutorialConfig = Omit<EditorTutorialConfig, 'type'> & {
type: TutorialType.EDITOR_ONLY;
};
export type FileAndContent = {
path: string;
content: string | Buffer;
};
export type FileAndContentRecord = Record<FileAndContent['path'], FileAndContent['content']>;
export type TutorialNavigationItem = {
path: NonNullable<NavigationItem['path']>;
label: NonNullable<NavigationItem['label']>;
children?: TutorialNavigationItem[];
parent?: TutorialNavigationItem;
contentPath?: string;
tutorialData: TutorialNavigationData;
};
export type TutorialNavigationData = {
type: TutorialConfig['type'];
title: TutorialConfig['title'];
nextStep?: string;
previousStep?: string;
nextTutorial?: string;
sourceCodeZipPath?: string;
};
export type PlaygroundRouteData = {
templates: PlaygroundTemplate[];
defaultTemplate?: PlaygroundTemplate;
starterTemplate?: PlaygroundTemplate;
};
export type PlaygroundTemplate = Required<Pick<NavigationItem, 'path' | 'label'>>;
// Note: only the fields being used are defined in this type
export interface PackageJson {
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
}
@@ -0,0 +1,36 @@
/*!
* @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 {glob} from 'glob';
import {TUTORIALS_CONTENT_NODE_PATH} from './utils/node-constants';
import {dirname} from 'path';
import {execSync} from 'child_process';
main();
/**
* This script updates the package-lock.json files for all tutorials
* after manually changing the dependencies in the package.json files
*/
async function main() {
const tutorialsPackageJsons = await glob('**/package.json', {
ignore: ['**/node_modules'],
absolute: true,
cwd: TUTORIALS_CONTENT_NODE_PATH,
});
for (const path of tutorialsPackageJsons) {
const directory = dirname(path);
console.info(`\nUpdating lock file at ${directory}\n`);
execSync('npm install --package-lock-only', {
cwd: directory,
stdio: [null, null, 'inherit'],
});
}
}
+29
View File
@@ -0,0 +1,29 @@
/*!
* @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 {TUTORIALS_DIRECTORY_REGEX} from '../tutorial-type/intro-and-steps';
export function validatePathAnswer(
answer: string,
existingPaths: string[],
placeholder: string,
): string | true {
if (answer === placeholder) {
return 'Please define the path';
}
if (!TUTORIALS_DIRECTORY_REGEX.test(answer)) {
return `The path must be in the format of "${placeholder}"`;
}
if (existingPaths.includes(answer)) {
return `'${answer}' already exists`;
}
return true;
}
@@ -0,0 +1,150 @@
/*!
* @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 {basename, dirname} from 'path';
import {mkdir, readFile, writeFile} from 'fs/promises';
import {GlobOptionsWithFileTypesFalse, glob} from 'glob';
import JSZip from 'jszip';
import type {FileAndContent, FileAndContentRecord, PackageJson} from '../tutorials-types';
const fileContentsCache = new Map<string, string | Buffer>();
export async function getFileContents(path: string): Promise<string | Buffer> {
if (fileContentsCache.get(path)) {
return fileContentsCache.get(path)!;
}
const fileBuffer = await readFile(path);
if (isBufferImage(fileBuffer)) {
fileContentsCache.set(path, fileBuffer);
return fileBuffer;
}
fileContentsCache.set(path, fileBuffer.toString());
return fileBuffer.toString();
}
export async function getFilesContents(files: string[]): Promise<FileAndContent[]> {
const filesContents: FileAndContent[] = [];
await Promise.all(
files.map((path) =>
getFileContents(path).then((fileContents) => {
filesContents.push({path, content: fileContents});
}),
),
);
return filesContents;
}
export async function createDirectoryAndWriteFile(path: string, fileContents: string | Buffer) {
await mkdir(dirname(path), {recursive: true}).catch(() => {
// ignore error if directory already exists
});
await writeFile(path, fileContents);
}
export function filesAndContentsToRecord(filesAndContents: FileAndContent[]): FileAndContentRecord {
return Object.fromEntries(filesAndContents.map(({path, content}) => [path, content]));
}
export function recordToFilesAndContents(filesRecord: FileAndContentRecord): FileAndContent[] {
return Object.entries(filesRecord).map(([path, content]) => ({path, content}));
}
export async function getZipBuffer(files: FileAndContent[]): Promise<Buffer> {
const zip = new JSZip();
for (const file of files) {
zip.file(file.path, file.content, {binary: true});
}
return await zip.generateAsync({type: 'nodebuffer'});
}
export async function globWithCwdPath(
pattern: string | string[],
optionsWithCwd: Omit<GlobOptionsWithFileTypesFalse, 'cwd'> & {cwd: string},
) {
const files = await glob(pattern, optionsWithCwd);
return getFilesWithCwdPath(optionsWithCwd.cwd, files).sort();
}
export function getFilesWithCwdPath(cwdPath: string, files: string[]) {
return files.map((file) => file.substring(file.indexOf(cwdPath)));
}
function isBufferImage(buffer: Buffer) {
return isBufferPng(buffer) || isBufferJpg(buffer) || isBufferGif(buffer);
}
/**
* Check initial bytes of the buffer(aka magic numbers) to see if it's a PNG file.
*
* See https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files
*/
function isBufferPng(buffer: Buffer) {
const pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
return bufferMatchInitialBytes(buffer, pngSignature);
}
/**
* Check initial bytes of the buffer(aka magic numbers) to see if it's a JPG file.
*
* See https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files
*/
function isBufferJpg(buffer: Buffer) {
const jpgSignature = [0xff, 0xd8, 0xff];
return bufferMatchInitialBytes(buffer, jpgSignature);
}
/**
* Check initial bytes of the buffer(aka magic numbers) to see if it's a GIF file.
*
* See https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files
*/
function isBufferGif(buffer: Buffer) {
const gifSignature = [0x47, 0x49, 0x46];
return bufferMatchInitialBytes(buffer, gifSignature);
}
/**
* Check if the initial bytes of a buffer matches the expected bytes.
*/
function bufferMatchInitialBytes(buffer: Buffer, expectedInitialBytes: number[]) {
for (const [index, byte] of expectedInitialBytes.entries()) {
if (buffer[index] !== byte) return false;
}
return true;
}
export function getPackageJsonFromFiles(filesAndContents: FileAndContent[]): PackageJson {
const packageJson = filesAndContents.find(({path}) => basename(path) === 'package.json');
if (!packageJson)
throw new Error(`Could not find ${dirname(filesAndContents[0].path)}/package.json`);
let parsedPackageJsonContent: PackageJson;
try {
parsedPackageJsonContent = JSON.parse(packageJson.content as string);
} catch (e) {
throw new Error(`Could not JSON.parse ${dirname(filesAndContents[0].path)}/package.json`);
}
return parsedPackageJsonContent;
}
@@ -0,0 +1,74 @@
/*!
* @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 {basename, dirname} from 'path';
import {PackageJson} from '../tutorials-types';
import {shouldUseFileInWebContainer} from './webcontainers';
export function validatePackageJson(
files: string[],
projectPackageJson: PackageJson,
commonPackageJson: PackageJson,
): void {
if (!files.some((file) => basename(file) === 'package-lock.json')) {
throw new Error(
`Tutorial at ${dirname(
files[0],
)} has a package.json but is missing a package-lock.json. A lock file is required for faster installs.`,
);
}
const commonDependencies = {
...commonPackageJson.dependencies,
...commonPackageJson.devDependencies,
};
const projectDependencies = {
...projectPackageJson.dependencies,
...projectPackageJson.devDependencies,
};
const projectDependenciesNames = Object.keys(projectDependencies);
for (const dependency of Object.keys(commonDependencies)) {
if (!projectDependenciesNames.includes(dependency)) {
throw new Error(
`Tutorial at ${dirname(files[0])} is missing '${dependency}' as a dependency.`,
);
} else {
const commonDependencyVersion = commonDependencies[dependency];
const projectDependencyVersion = projectDependencies[dependency];
if (commonDependencyVersion !== projectDependencyVersion) {
throw new Error(
`${dirname(
files[0],
)} has a different version of '${dependency}' than the common package.json.
The common version is ${commonDependencyVersion}.
The project version is ${projectDependencyVersion}.
Please update the project package.json to match the common package.json, or update both.`,
);
}
}
}
}
export function getAllFiles(
projectFiles: string[],
commonFiles: string[],
getCleanFilePath: (file: string) => string,
) {
const commonAndProjectPaths = [...commonFiles, ...projectFiles]
.map((file) => getCleanFilePath(file))
.filter(shouldUseFileInWebContainer);
const uniquePaths = Array.from(new Set(commonAndProjectPaths));
return uniquePaths;
}
@@ -0,0 +1,115 @@
/*!
* @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 {join} from 'path';
import type {TutorialConfigBase} from '../tutorials-types';
import {
TUTORIALS_ASSETS_METADATA_DIRECTORY,
TUTORIALS_ASSETS_ROUTES_DIRECTORY,
TUTORIALS_ASSETS_SOURCE_CODE_DIRECTORY,
TUTORIALS_COMMON_DIRECTORY,
TUTORIALS_HOMEPAGE_DIRECTORY,
TUTORIALS_PLAYGROUND_DIRECTORY,
} from './web-constants';
import {GlobOptionsWithFileTypesFalse} from 'glob';
export const CONTENT_PATH = join('projects', 'angular-dev', 'src', 'content');
export const TUTORIALS_PROJECT_PATH = join(CONTENT_PATH, 'tutorials');
export const TUTORIALS_ASSETS_NODE_PATH = join(
'projects',
'angular-dev',
'src',
'assets',
'tutorials',
);
export const TUTORIALS_PLAYGROUND_NODE_PATH = join(
TUTORIALS_PROJECT_PATH,
TUTORIALS_PLAYGROUND_DIRECTORY,
);
export const TUTORIALS_HOMEPAGE_NODE_PATH = join(
TUTORIALS_PROJECT_PATH,
TUTORIALS_HOMEPAGE_DIRECTORY,
);
export const TUTORIALS_COMMON_NODE_PATH = join(TUTORIALS_PROJECT_PATH, TUTORIALS_COMMON_DIRECTORY);
/** The common location where the tutorial assets are stored. */
export const TUTORIALS_COMMON_ASSETS_SRC = join(TUTORIALS_COMMON_NODE_PATH, 'src', 'assets');
/** The common location for the tutorial assets to be served from. */
export const TUTORIALS_COMMON_ASSETS_DEST = join(TUTORIALS_ASSETS_NODE_PATH, 'common');
export const TUTORIALS_SOURCE_CODE_NODE_PATH = join(
TUTORIALS_ASSETS_NODE_PATH,
TUTORIALS_ASSETS_SOURCE_CODE_DIRECTORY,
);
export const TUTORIALS_METADATA_NODE_PATH = join(
TUTORIALS_ASSETS_NODE_PATH,
TUTORIALS_ASSETS_METADATA_DIRECTORY,
);
export const TUTORIALS_CONTENT_NODE_PATH = join(
'projects',
'angular-dev',
'src',
'content',
'tutorials',
);
export const TUTORIALS_ROUTES_NODE_PATH = join(
TUTORIALS_ASSETS_NODE_PATH,
TUTORIALS_ASSETS_ROUTES_DIRECTORY,
'tutorials.json',
);
export const PLAYGROUND_ROUTE_NODE_PATH = join(
TUTORIALS_ASSETS_NODE_PATH,
TUTORIALS_ASSETS_ROUTES_DIRECTORY,
'playground.json',
);
export const TUTORIALS_COMMON_ASSETS_PATH = join(TUTORIALS_COMMON_NODE_PATH, 'src', 'assets');
export const IGNORED_BASENAMES = [
'.DS_Store',
'LICENSE',
'.gitignore',
'.editorconfig',
'.gitkeep',
'favicon.ico',
];
export const IGNORED_EXTENSIONS = ['.map'];
export const IGNORED_DIRECTORIES = ['.git'];
export const TUTORIAL_CONTENT_FILENAME = 'README.md';
export const ANSWER_DIRECTORY = 'answer';
export const INTRO_DIRECTORY = 'intro';
export const STEPS_DIRECTORY = 'steps';
export const CONFIG_FILE = 'config.json';
export const CONFIG_KEYS: Array<keyof TutorialConfigBase> = [
'title',
'type',
'nextTutorial',
'src',
'answerSrc',
'openFiles',
];
export const REQUIRED_CONFIGS: Array<keyof Pick<TutorialConfigBase, 'title' | 'type'>> = [
'title',
'type',
];
export const CONTENT_PLACEHOLDER = 'Insert content here';
export const GLOB_OPTIONS: GlobOptionsWithFileTypesFalse = {
dot: false,
absolute: true,
posix: true,
ignore: [
'.',
'**/node_modules/**',
...IGNORED_BASENAMES.map((ignoredBasename) => `**/${ignoredBasename}`),
...IGNORED_EXTENSIONS.map((ext) => `**/*${ext}`),
...IGNORED_DIRECTORIES.map((dir) => `**/${dir}/**`),
],
};
@@ -0,0 +1,31 @@
/*!
* @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
*/
export const TUTORIALS_ASSETS_WEB_PATH = '/assets/tutorials';
export const TUTORIALS_DIST_PATH = 'tutorials';
export const TUTORIALS_PLAYGROUND_DIRECTORY = 'playground';
/** the playground template that loads by default when entering the /playground page */
export const DEFAULT_PLAYGROUND_TEMPLATE = '0-minigame';
export const STARTER_PLAYGROUND_TEMPLATE = '3-hello-world';
export const TUTORIALS_HOMEPAGE_DIRECTORY = 'homepage';
export const TUTORIALS_COMMON_DIRECTORY = 'common';
export const TUTORIALS_ASSETS_SOURCE_CODE_DIRECTORY = 'source-code';
export const TUTORIALS_ASSETS_METADATA_DIRECTORY = 'metadata';
export const TUTORIALS_ASSETS_ROUTES_DIRECTORY = 'routes';
export const TUTORIALS_SOURCE_CODE_WEB_PATH = `${TUTORIALS_ASSETS_WEB_PATH}/${TUTORIALS_ASSETS_SOURCE_CODE_DIRECTORY}`;
export const TUTORIALS_METADATA_WEB_PATH = `${TUTORIALS_ASSETS_WEB_PATH}/${TUTORIALS_ASSETS_METADATA_DIRECTORY}`;
export const enum TutorialType {
CLI = 'cli',
LOCAL = 'local',
EDITOR = 'editor',
EDITOR_ONLY = 'editor-only',
}
@@ -0,0 +1,91 @@
/*!
* @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 {basename, dirname, extname} from 'path';
import {DirectoryNode, FileNode, FileSystemTree} from '@webcontainer/api';
import {ANSWER_DIRECTORY, CONFIG_FILE} from './node-constants';
import {FileAndContent, FileAndContentRecord} from '../tutorials-types';
/**
* Create a WebContainer's FileSystemTree from a list of files and its contents
*/
export function getFileSystemTree(files: string[], filesContents: FileAndContentRecord) {
const fileSystemTree: FileSystemTree = {};
for (let filepath of files) {
const dir = dirname(filepath);
const filename = basename(filepath);
if (dir === '.') {
const fileNode: FileNode = {file: {contents: filesContents[filepath]}};
fileSystemTree[filename] = fileNode;
} else {
const dirParts = dir.split('/');
buildFileSystemTree(fileSystemTree, dirParts, filename, filesContents[filepath]);
}
}
return fileSystemTree;
}
/**
* Builds a WebContainer's file system tree object recursively, mutating the
* `fileSystemTree` parameter.
*
* @see https://webcontainers.io/api#filesystemtree
*/
export function buildFileSystemTree(
fileSystemTree: FileSystemTree,
fileDirectories: string[],
filename: FileAndContent['path'],
fileContents: FileAndContent['content'],
): void {
if (fileDirectories.length === 1) {
const directory = fileDirectories[0];
const fileNode: FileNode = {file: {contents: fileContents}};
fileSystemTree[directory] = {
...fileSystemTree[directory],
directory: {
...(fileSystemTree[directory]
? (fileSystemTree[directory] as DirectoryNode).directory
: undefined),
[filename]: fileNode,
},
};
return;
}
const nextDirectory = fileDirectories.shift();
if (!nextDirectory) return;
if (!fileSystemTree[nextDirectory]) {
fileSystemTree[nextDirectory] = {directory: {}};
}
buildFileSystemTree(
(fileSystemTree[nextDirectory] as DirectoryNode).directory,
fileDirectories,
filename,
fileContents,
);
}
export function shouldUseFileInWebContainer(filename: string) {
return (
basename(filename) !== CONFIG_FILE &&
['.md', '.png', '.jpg'].includes(extname(filename)) === false &&
!filename.includes(`${ANSWER_DIRECTORY}/`)
);
}
@@ -0,0 +1,57 @@
/*!
* @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 {readFile, writeFile} from 'fs/promises';
import {dirname, join} from 'path';
import {argv} from 'process';
import {fileURLToPath} from 'url';
import {VersionsConfig, VersionMode} from './types.mjs';
// CONSTANTS
const MODE_ARG = 'v=';
const VERSIONS_CONFIG = 'versions.json';
// PATHS
const ASSETS = join(
dirname(fileURLToPath(import.meta.url)),
'../../src/assets',
);
const VERSIONS_CONFIG_PATH = join(ASSETS, VERSIONS_CONFIG);
main();
async function main() {
console.info('Updating version...');
const versionsConfig: VersionsConfig = JSON.parse(await readFile(VERSIONS_CONFIG_PATH, 'utf-8'));
const currentVersion = (await getCurrentVersion()) ?? 'stable';
if (
currentVersion !== 'next' &&
currentVersion !== 'rc' &&
currentVersion !== 'stable' &&
Number.isNaN(currentVersion)
) {
throw new Error('Invalid mode value provided!');
}
versionsConfig.currentVersion = currentVersion;
await updateConfig(versionsConfig);
console.info('Updating version successfully done!');
}
async function updateConfig(config: VersionsConfig): Promise<void> {
await writeFile(VERSIONS_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`);
}
async function getCurrentVersion(): Promise<VersionMode | undefined> {
return argv.find((arg) => arg.startsWith(MODE_ARG))?.replaceAll(MODE_ARG, '') as VersionMode;
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"ts-node": {
"compilerOptions": {
"module": "es2020"
}
}
}
+20
View File
@@ -0,0 +1,20 @@
/*!
* @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
*/
export interface VersionUrl {
version: VersionMode;
url: string;
}
export type VersionMode = 'stable' | 'rc' | 'next' | number;
export interface VersionsConfig {
currentVersion: VersionMode;
currentVersions: VersionUrl[];
historicalVersions: VersionUrl[];
}
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/shared",
"lib": {
"entryFile": "src/public-api.ts"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "shared",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^16.0.1",
"@angular/core": "^16.0.1"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}
@@ -0,0 +1,52 @@
<!-- Algolia logo -->
<svg
id="Layer_1"
class="adev-algolia-logo"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2196.2 500"
>
<defs>
<style>
.cls-1,
.cls-2 {
fill: #003dff;
}
.cls-2 {
fill-rule: evenodd;
}
</style>
</defs>
<path
class="cls-2"
d="M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
/>
<rect class="cls-1" x="1845.88" y="104.73" width="62.58" height="277.9" rx="5.9" ry="5.9" />
<path
class="cls-2"
d="M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"
/>
<path
class="cls-2"
d="M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
/>
<path
class="cls-2"
d="M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"
/>
<path
class="cls-2"
d="M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
/>
<path
class="cls-2"
d="M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
/>
<path
class="cls-2"
d="M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"
/>
<path
class="cls-1"
d="M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -0,0 +1,17 @@
/*!
* @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 {Component} from '@angular/core';
@Component({
selector: 'docs-algolia-icon',
standalone: true,
imports: [],
templateUrl: './algolia-icon.component.html',
})
export class AlgoliaIcon {}
@@ -0,0 +1,13 @@
@for (breadcrumb of breadcrumbItems(); track breadcrumb) {
<div class="docs-breadcrumb">
@if (breadcrumb.path) {
@if (breadcrumb.isExternal) {
<a [href]="breadcrumb.path">{{ breadcrumb.label }}</a>
} @else {
<a [routerLink]="'/' + breadcrumb.path">{{ breadcrumb.label }}</a>
}
} @else {
<span>{{ breadcrumb.label }}</span>
}
</div>
}
@@ -0,0 +1,25 @@
:host {
display: flex;
align-items: center;
padding-block-end: 1.5rem;
}
.docs-breadcrumb {
span {
color: var(--quaternary-contrast);
font-size: 0.875rem;
display: flex;
align-items: center;
}
&:not(:last-child) {
span {
&::after {
content: 'chevron_right';
font-family: var(--icons);
margin-inline: 0.5rem;
color: var(--quinary-contrast);
}
}
}
}
@@ -0,0 +1,29 @@
/*!
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {Breadcrumb} from './breadcrumb.component';
describe('Breadcrumb', () => {
let component: Breadcrumb;
let fixture: ComponentFixture<Breadcrumb>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [Breadcrumb],
});
fixture = TestBed.createComponent(Breadcrumb);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,50 @@
/*!
* @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 {ChangeDetectionStrategy, Component, OnInit, inject, signal} from '@angular/core';
import {NavigationState} from '../../services';
import {NavigationItem} from '../../interfaces';
import {NgFor, NgIf} from '@angular/common';
import {RouterLink} from '@angular/router';
@Component({
selector: 'docs-breadcrumb',
standalone: true,
imports: [NgIf, NgFor, RouterLink],
templateUrl: './breadcrumb.component.html',
styleUrls: ['./breadcrumb.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Breadcrumb implements OnInit {
private readonly navigationState = inject(NavigationState);
breadcrumbItems = signal<NavigationItem[]>([]);
ngOnInit(): void {
this.setBreadcrumbItemsBasedOnNavigationStructure();
}
private setBreadcrumbItemsBasedOnNavigationStructure(): void {
let breadcrumbs: NavigationItem[] = [];
const traverse = (node: NavigationItem | null) => {
if (!node) {
return;
}
if (node.parent) {
breadcrumbs = [node.parent, ...breadcrumbs];
traverse(node.parent);
}
};
traverse(this.navigationState.activeNavigationItem());
this.breadcrumbItems.set(breadcrumbs);
}
}
@@ -0,0 +1,22 @@
@if (!hasAccepted()) {
<div class="docs-cookies-popup adev-invert-mode">
<p>This site uses cookies from Google to deliver its services and to analyze traffic.</p>
<div>
<a href="https://policies.google.com/technologies/cookies" target="_blank" rel="noopener">
<button class="adev-primary-btn" [attr.text]="'Learn more'" aria-label="Learn More">
Learn more
</button>
</a>
<button
type="button"
(click)="accept()"
class="adev-primary-btn"
[attr.text]="'Ok, Got it'"
aria-label="Ok, Got it"
>
Ok, Got it
</button>
</div>
</div>
}

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