Compare commits

...

79 Commits

Author SHA1 Message Date
Joey Perrott 60dbc2b39d fix(docs-infra): remove part aio infra
Remove parts of the aio infra
2024-03-18 19:03:18 +00:00
Joey Perrott 293298a235 refactor(docs-infra): don't rely on aio_npm in adev
Use @npm// instead of @aio_npm// in adev BUILD files
2024-03-18 18:29:55 +00:00
Joey Perrott 0be64c1628 ci: move saucelabs to regular CI job (#54926)
Move the saucelabs job to regular CI instead of the priviledged CI to prevent cache poisoning/overload

PR Close #54926
2024-03-18 11:02:37 -07:00
Joey Perrott f50afb10c3 build: update to latest angular/dev-infra/github-actions/create-pr-for-changes (#54904)
Update to latest to correct node verison issue

PR Close #54904
2024-03-17 00:41:30 +00:00
Doug Parker 5328be6660 refactor(zone.js): formatting recently changed files (#53443)
For some reason CI started complaining about lack of formatting here.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker cc1b0f4e8e refactor(zone.js): update imports for Zone setup to invoke patches (#53443)
I initially tried switching to use public entry points under `zone.js/plugins/*`, however this file is both manually compiled for Saucelabs and also built with Bazel for a number of tests. The Bazel integration doesn't work well with depending on real NPM packages, so importing `zone.js/plugins/*` in that context doesn't really work. Instead we need to depend on the internals and manually call the `patch*` functions.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 635227b881 refactor(zone.js): package Zone *.d.ts files (#53443)
This was a bit complicated, but the typings test (`packages/zone.js/test/typings/...`) was failing due to an unresolved import on `./zone-impl`.

The main cause is that `//packages/zone.js:zone_js_d_ts` was generating the output `zone.d.ts` file by _concatenating_ `zone.d.ts` with `zone.api.extensions.d.ts` and `zone.configurations.api.d.ts`. Now that `zone.d.ts` imports `zone-impl.d.ts`, concatenation is no longer a viable means of bundling this content.

To fix this, I created a new `packages/zone.js/zone.ts` entry point and imported the underlying `zone.ts` file as well as the two extensions. I added `extract_types` to pull the `*.d.ts` files out of the target (because all the JS is bundled separately) and used those files in the final NPM package. This is sufficient to pass the typings test and should be equivalent to what exists today.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 1081c51d1a refactor(zone.js): only treat the electron package (not paths with electron) as external (#53443)
The `rxjs|electron` regex was causing `import { /* ... */ } from './electron';` to be treated as external and not bundled, which is an issue for the Electron entry point which just happens to be named `electron.ts`. Now only the actual `electron` package should be treated as external, while internal files of any name should not.

I actually tried updating to `^rxjs` as well, however this appears to break some tests and isn't necessary to fix the failing Electron test, so I'm leaving it alone.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 79308d1668 refactor(zone.js): fix Node fs tests (#53443)
Not sure why this becomes a problem now. These errors appear to be preexisting. Most likely the file wasn't loaded previously and now it is.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker cb0379c8df refactor(zone.js): fix Node error disable/lazy policy tests (#53443)
The lazy policy tests call the same error policy entry point, so this fixes both at the same time.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker e8e1178e42 refactor(zone.js): fix Bluebird test by updating the entry point and calling patches appropriately (#53443)
This executes the patches in the top-level scope of the Bluebird test.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 54a1fe05df refactor(zone.js): fix fake async test by reading parentUnresolved symbol lazily (#53443)
As this was, `__symbol__` was being called as a static field initializer, which runs during module evaluation, meaning it happened at import time. However for tests, the Zone prefix is overridden which changes the result of `__symbol__`. This change happens too late to be picked up by `__symbol__` at top-level execution, so instead we defer it until `symbolParentUnresolved` is actually read.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker b334e29d59 refactor(zone.js): read patched timers after they are patched (#53443)
This moves timer patching from a top-level side effect into the `patchFakeAsyncTest` function. Top-level statements are evaluated before the Node patches run and have a chance to patch them with the Zone versions of these timers, meaning `FakeAsyncTestZoneSpec` was repatching the native versions between tests. The fix here is to grab the patched versions of these timers during the `patchFakeAsyncTest` function where we can be confident Node patches have already run.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 6ca1911967 refactor(zone.js): export fake-async-test.ts functions (#53443)
This moves the internals of `fake-async-test.ts` outside `patchFakeAsyncTest` and exports them. This way they can be imported without depending on the top-level side effects of loading Zone.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 8af7d3a50b refactor(zone.js): update Node test entry point to call patch functions (#53443)
This executes the patches in the top-level scope of Node tests.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 33c08e5aba refactor(zone.js): switch async-test.ts to use __symbol__ from an import (#53443)
This removes a dependency on the top-level side effect of `Zone` global initialization.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 181121dd9e refactor(zone.js): rename __global variable (#53443)
For some reason the `_global` name appears to conflict with another `_global` name. Not entirely sure how or why, but the easiest fix seems to be to just give the variable a unique name.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 5e45bb771b refactor(zone.js): delay reading __Zone_symbol_prefix until needed (#53443)
While reading this is not a top-level side effect, it does _depend_ on a top-level side effect. Specifically, `node-env-setup.ts` set this value. Now that its side effect is moved into a function, we can't read it as the top-level of `zone-impl.ts` and need to wait until `__symbol__` is actually called outside of top-level scope.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 417cd71830 refactor(zone.js): updates fake polyfill to remove top-level side effect (#53443)
This shifts the side effect from `test_Fake_polyfill.ts` into `browser-env-setup.ts` instead.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker 73969ae9d4 refactor(zone.js): extracts TaskTrackingZoneSpec and exports it (#53443)
While `TaskTrackingZoneSpec` was implicitly global previously, it does not need to be exposed in a `declare global {}` block. This is because classic scripts in TypeScript are only implicitly global within the same compilation. `TaskTrackingZoneSpec` was not exposed in the existing `.d.ts` files shipped with the `zone.js` package. Within google3, this is also a separate compilation and was not accessible. As a result, `TaskTrackingZoneSpec` was always private and we do not need a global to maintain compatibility.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker cf61126ef3 refactor(zone.js): extracts ProxyZoneSpec and exports it (#53443)
While `ProxyZoneSpec` was implicitly global previously, it does not need to be exposed in a `declare global {}` block. This is because classic scripts in TypeScript are only implicitly global within the same compilation. `ProxyZoneSpec` was not exposed in the existing `.d.ts` files shipped with the `zone.js` package. Within google3, this is also a separate compilation and was not accessible. As a result, `ProxyZoneSpec` was always private and we do not need a global to maintain compatibility.

PR Close #53443
2024-03-15 18:11:34 -07:00
Doug Parker d2e5f23c38 refactor(zone.js): update utils.ts to import __symbol__ (#53443)
This removes a dependency on a top-level side effect of global Zone initialization.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 1b9c807efa refactor(zone.js): update Zone bundles to call patch* functions (#53443)
Since each patch no longer contains top-level side effects, each bundled entry point needs to import and call its associated patch. For the most part this just means that each entry point imports the associated patch and invokes it at the top-level scope.

Note that many of these entry points did not actually have a dependency on `Zone` and had no guarantee that it was loaded prior to execution. To maintain consistency, the missing dependencies on `Zone` are left as-is. They will use the global instance of `Zone` and if users fail to load it prior to importing a specific patch, then the patch will fail just as it did previously.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 829a10937a refactor(zone.js): wrap Zone.js global initialization in a loadZone function (#53443)
This drop the top-level side effects in `zone.ts` and allows the initialization to be reused across the various entry points in the package.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker a75d9de874 refactor(zone.js): wrap browser-legacy.ts in a patchBrowserLegacy function. (#53443)
This particular is slightly different from the others because it does not have a hard dependency on `Zone`. It doesn't actually use `Zone` directly during the patch because the patch just sets a `legacyPatch` global. To maintain consistency, this continues to use global `Zone` and does _not_ accept `Zone` as a parameter. While it's using a global in an inconvenient way, this isn't a problem right now because it doesn't cause or require a dependency on a top-level side effect.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 7c1991048b refactor(zone.js): wrap Zone.__load_patch calls in exported functions (#53443)
This removes top-level side effects from each of these files and drops the dependency on global `Zone`, instead allowing it to be provided to each patch as a parameter.

Most of these are pure mechanical transformations. A couple notable files which were somewhat unique:

* `async-test.ts`, `fake-async-test.ts`, and `wtf.ts` had unique IIFE usage and patch `Zone` itself. This removes the IIFE and exports the function instead.
* `jest.ts` and `jasmine.ts` have a unique `jest` global usage which needs to be declared.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 52d672dd13 refactor(zone.js): move Error type into declare global block in zone.ts (#53443)
This helps consolidate globals into a single declaration in a single file.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker d8db402833 refactor(zone.js): move duplicate Zone check into zone.ts file (#53443)
`zone.ts` contains the global variable definition and is the appropriate place for an error side effect when Zone is initialized twice.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker c5cdd8954c refactor(zone.js): export remaining Zone types as globals (#53443)
These are all the symbols originally under `declare global {}` with the same names. Unfortunatley TypeScript treats a `declare global {}` scope as the parent scope, so we need to import with a different name than the global, meaning everything gets an `_` prefix.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 4876ed9510 refactor(zone.js): rename types to drop the _ prefix convention (#53443)
Now that we have real ES modules, there's no need for the prefix convention.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker d90b6844a1 refactor(zone.js): move Zone global declaration and assignment to zone.ts (#53443)
Global state is to be managed in `zone.ts` rather than `zone-impl.ts`.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 1a4d783bf8 refactor(zone.js): clear zone.ts file (#53443)
All its content has been copied to `zone-impl.ts`, so this file will become a wrapper for those symbols.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 6c7e1e61c6 refactor(zone.js): export Zone initialization function instead of invoking it as an IIFE (#53443)
This removes the top-level side effects from this file. Indenting is left as-is to minimize changes.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 17c59eb962 refactor(zone.js): rename Zone class to ZoneImpl (#53443)
This removes a name collision on `Zone` with the interface. Otherwise putting the `interface Zone {}` and `class Zone {}` in the same scope when the IIFE is removed will cause a conflict.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 482335657b refactor(zone.js): moves global constant outside IIFE (#53443)
The IIFE will be removed shortly to support exporting symbols inside it.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 4feec728c0 refactor(zone.js): export previously global types from zone-impl.ts. (#53443)
This allows the types to be directly imported when used.

PR Close #53443
2024-03-15 18:11:33 -07:00
Doug Parker 76d8c7344e refactor(zone.js): remove declare global from zone-impl.ts (#53443)
This drops global declarations from this file (as they will be limited to `zone.ts`. It also deletes the `/** @internal */` annotation on `AmbientZone` which is no longer necessary since it isn't in the `declare global` and deletes the global Zone declaration which is also unnecessary.

PR Close #53443
2024-03-15 18:11:32 -07:00
Doug Parker a760470b20 refactor(zone.js): copy zone.ts to zone-impl.ts (#53443)
This is just a straight copy, no code is altered. The new `zone-impl.ts` file will contain the actual definitions exported directly without any global declarations or top-level side effects. The existing `zone.ts` will remain as an entry point with those globals and side-effects.

PR Close #53443
2024-03-15 18:11:32 -07:00
Paul Gschwendtner 5a63a475fb test(compiler-cli): add unit tests for output() JIT transform (#54841)
We are already testing the JIT transforms via integration tests, but
this commit adds dedicated unit tests for the transform behavior for
proper test coverage (planned follow-up).

PR Close #54841
2024-03-15 15:14:32 -07:00
Matthieu Riegler c3da79710d docs(docs-infra): bump xterm to 5.4.0 to fix a layout issue on Firefox. (#54897)
With 5.4.0 `xterm` has migrated to scoped packages. We're now using `@xterm/xterm`.

Fixes #54894

PR Close #54897
2024-03-15 15:13:39 -07:00
Kristiyan Kostadinov 879bd80b57 fix(compiler): capture data bindings for content projection purposes in blocks (#54876)
Fixes a regression in the template pipeline where data bindings weren't being captured for content projection purposes.

Fixes #54872.

PR Close #54876
2024-03-15 15:11:19 -07:00
Chinthoorie b961075f92 refactor(compiler-cli): move DelegatingPerfRecorder initialization into constructor (#54834)
Move the initialization of class field `DelegatingPerfRecorder` into the constructor.

This fixes the error : `TypeError: Cannot read properties of undefined (reading 'eventCount')`

This is blocking the roll-out of public class.

PR Close #54834
2024-03-15 15:10:46 -07:00
Paul Gschwendtner d01576b104 refactor(compiler-cli): properly preserve file overview comments (#54819)
This commit updates the logic for preserving file overview comments
to be more reliable and less dependent on previous transforms.

Previously, with the old import manager, we had a utility called
`addImport` that always separated import statements and non-import
statements. This meant that the non-emitted statement from Tsickle
for the synthetic file-overview comments no longer lived at the
beginning of the file.

`addImports` tried to overcome this by adding another new non-emitted
statement *before* all imports. This then was later used by the
transform (or was assumed!) to attach the synthetic file overview
comments if the original tsickle AST Node is no longer at the top.

This logic can be improved, because the import manager shouldn't need to
bother about this fileoverview non-emitted statement, and the logic for
re-attaching the fileoverview comment should be local. This commit fixes
this and makes it a local transform.

PR Close #54819
2024-03-15 15:09:41 -07:00
Paul Gschwendtner de75fe0358 test(compiler-cli): add tests to verify import generation in TCB files/blocks (#54819)
This commit adds some unit tests verifying the import generation in TCB
files and inline blocks. We don't seem to have any unit tests for these
in general. This commit adds some, verifying some characteristics we
would like to guarantee.

PR Close #54819
2024-03-15 15:09:41 -07:00
Paul Gschwendtner 0e9c075cac refactor(compiler-cli): rename ImportManagerV2 to ImportManager (#54819)
To ease review and to allow for both instances to co-exist, `ImportManagerV2`
was introduced. This commit renames it to `ImportManager` now that we
deleted the older one.

PR Close #54819
2024-03-15 15:09:41 -07:00
Paul Gschwendtner 922f950e45 refactor(compiler-cli): delete old unused ImportManager (#54819)
This commit deletes the older and now unused `ImportManager`.

PR Close #54819
2024-03-15 15:09:40 -07:00
Paul Gschwendtner bec0179afe test(compiler-cli): enable incremental re-use type checking with signal inputs (#54819)
Enables the incremental type-checking test that we never enabled when we
landed signal inputs. Now that we fixed incremental re-use by re-using
the existing user imports for inline type check blocks, the test is
passing and can be enabled.

PR Close #54819
2024-03-15 15:09:40 -07:00
Paul Gschwendtner 94bc3afc23 refactor(compiler-cli): switch jit transforms to use new import manager (#54819)
Switches the JIT transforms to use the new import manager.

PR Close #54819
2024-03-15 15:09:40 -07:00
Paul Gschwendtner dc80046132 refactor(compiler-cli): update type check generation code to use new import manager (#54819)
Updates the type-check block generation code (also for inline type check
blocks) to use the new import manager.

This is now a requirement because the translator utilities from the
reference emit environment expect an import manager that follows the
new contract established via `ImportGenerator<TFile, TExpression>`.

For type check files, we can simply print new imports as we don't expect
existing imports to be updated. That is because type check files do not
have any _original_ source files (or in practice— those are empty).

For type check blocks inline, or constructors, imports _may_ be re-used.
This is great as it helps fixing some incrementality bugs that we were
seeing in the type check code. That is, sometimes the type check block
code may generate imports conditionally for e.g. `TemplateRef`, or
animations. Those then **prevent** incremental re-use if TCB code
switches between those continously. We tried to account for that with
signal inputs by always pre-generating such imports. This fixed the
issue for type-check files, but for inline type check blocks this is
different as we would introduce new imports in user code that would then
be changed back in subsequential edit iterations. See:
https://github.com/angular/angular/pull/53521#pullrequestreview-1778130879.

In practice, the assumption was that we would be fine since user code is
most likely containing imports to `@angular/core` already. That is a
true assumption, but unfortunately it doesn't help with incremental
re-use because TypeScript's structural change detection does not dedupe
and expects 1:1 exact imports from their old source files.

https://github.com/microsoft/TypeScript/pull/56845

To improve incremental re-use for the type check integration, we should
re-use original source file imports when possible. This commit enables
this.

To update imports and execute inline operations, we are now uisng
`magic-string` (which is then bundled) as it simplifies the string
manipulatuons.

PR Close #54819
2024-03-15 15:09:40 -07:00
Paul Gschwendtner 6288f1c679 refactor(compiler-cli): use new import manager for ngtsc transforms (#54819)
This commit switches ngtsc's JS and DTS transform to use the new import
manager. This is a drop-in replacement as we've updated the translator
helpers in the previous commit to align with the new API suggested by
the `ImportManagerV2` (to be renamed then).

PR Close #54819
2024-03-15 15:09:40 -07:00
Paul Gschwendtner 0482b6b4c7 refactor(compiler-cli): update ImportGenerator abstraction for new manager (#54819)
`ImportGenerator` is the abstraction used by the translator functions to
insert imports for `ExternalExpr` in an AST-agnostic way.

This was built specifically for the linker which does not use any of the
complex import managers- but rather re-uses `ngImport` or uses
`ngImport.Bla`.

This commit also switches the linker AST-agnostic generator to follow
the new signatures. This was rather trivial.

PR Close #54819
2024-03-15 15:09:40 -07:00
Paul Gschwendtner 9a56bd50f5 refactor(compiler-cli): introduce new implementation of ImportManager (#54819)
This commit introduces a new implementation of `ImportManager` that has
numerous benefits:

- It allows efficient re-use of original source file imports.
  * either fully re-using original imports if matching
  * updating existing import declarations to include new symbols.
- It allows efficient re-use of previous generated imports.
- The manager can be used for schematics and migrations.

The implementation is a rework of the import manager that we originally
built for schematics in Angular Material, but this commit improved it
to be more flexible, more readable, and "correct".

In follow-ups we can use this for schematics/migrations.

PR Close #54819
2024-03-15 15:09:40 -07:00
Alex Rickabaugh 5901b0ca80 Revert "fix(router): Routed components never inherit RouterOutlet EnvironmentInjector (#54265)" (#54895)
This reverts commit da906fdafc.

This change causes some test failures in google3.

PR Close #54895
2024-03-15 15:08:45 -07:00
Angular Robot 369157063c build: update dependency google-closure-compiler to v20231112 (#54893)
See associated pull request for more information.

PR Close #54893
2024-03-15 18:31:22 +00:00
Angular Robot 406448a09a build: update dependency archiver to v7 (#54892)
See associated pull request for more information.

PR Close #54892
2024-03-15 18:18:34 +00:00
Joey Perrott 9574edaa44 build: lock file maintenance (#54890)
See associated pull request for more information.

PR Close #54890
2024-03-15 18:01:45 +00:00
Angular Robot e56c86f93c build: update actions/cache digest to ab5e6d0 (#54886)
See associated pull request for more information.

PR Close #54886
2024-03-15 17:28:14 +00:00
Angular Robot d3309d7d9c build: update dependency @octokit/graphql to v8 (#54888)
See associated pull request for more information.

PR Close #54888
2024-03-15 17:24:51 +00:00
Angular Robot dbe2e3898b build: update dependency lighthouse to v11 (#54292)
See associated pull request for more information.

PR Close #54292
2024-03-15 17:22:55 +00:00
Angular Robot 8cee78d25b build: update all non-major dependencies (#54616)
See associated pull request for more information.

PR Close #54616
2024-03-15 17:21:28 +00:00
Angular Robot 97da5f7804 build: update io_bazel_rules_sass digest to 79bd239 (#54887)
See associated pull request for more information.

PR Close #54887
2024-03-15 17:16:23 +00:00
Angular Robot 0472a92bc6 build: update cross-repo angular dependencies (#54883)
See associated pull request for more information.

PR Close #54883
2024-03-15 17:06:43 +00:00
Angular Robot e473b709bc docs: update events (#54749)
Generated `events.json` with the latest events retrieved from the Firebase DB.

PR Close #54749
2024-03-15 16:38:14 +00:00
Angular Robot 70e2431b75 build: update dependency puppeteer-core to v22 (#54298)
See associated pull request for more information.

PR Close #54298
2024-03-15 16:31:25 +00:00
Angular Robot 17199b9214 build: update peter-evans/create-or-update-comment action to v4 (#54299)
See associated pull request for more information.

PR Close #54299
2024-03-15 16:30:51 +00:00
Angular Robot 1f8a30dca9 build: update eslint dependencies (#50993)
See associated pull request for more information.

PR Close #50993
2024-03-15 16:30:11 +00:00
Andrew Scott da906fdafc fix(router): Routed components never inherit RouterOutlet EnvironmentInjector (#54265)
This commit ensures components in the route config predictably always
get their providers from the hierarchy available to routes rather than
sometimes being dependent on where they are inserted.

fixes #53369

BREAKING CHANGE: Providers available to the routed components always
come from the injector heirarchy of the routes and never inherit from
the `RouterOutlet`. This means that providers available only to the
component that defines the `RouterOutlet` will no longer be available to
route components in any circumstances. This was already the case
whenever routes defined providers, either through lazy loading an
`NgModule` or through explicit `providers` on the route config.

PR Close #54265
2024-03-14 12:33:13 -07:00
Andrew Scott 2b802587f2 feat(router): Allow Route.redirectTo to be a function which returns a string or UrlTree (#52606)
This commit updates the logic around `Route.redirectTo` to enable using a
function to create the redirect. This function can return a string,
ands acts the same as previous string redirects, or a `UrlTree`, which
will act as an absolute redirect.

To be useful, the redirect function needs access to the params and data.
Today, developers can access these in their redirect strings, for
example `{path: ':id', redirectTo: '/user/:id'}`. Unfortunately,
developers only have access to params and data on the _current route_
today in the redirect strings. The params and data in the `RedirectFn`
give developers access to anything from the matched parent routes as
well. This is done as the same way as param and data aggregation later
on (https://github.com/angular/angular/blob/897f014785578d87bc655ea6ae9e113653960f50/packages/router/src/router_state.ts#L236-L278).
In order to accomplish this, we inherit params and data while
matching, after the `ActivatedRouteSnapshot` is created for the matched
route rather than waiting until the end.

The `RedirectFunction` does not return the full
`ActivatedRouteSnapshot` interface. Some things are not accurately known
at the route matching phase. For example, resolvers are not run until
later, so any resolved title would not be populated. The same goes for lazy
loaded components. The is also true for all the snapshots up to the
root, so properties that include parents (root, parent, pathFromRoot)
are also excluded. And naturally, the full route matching hasn't yet
happened so firstChild and children are not available either.

fixes #13373
resolves #28661 (though not for the redirect string - you would return a
`UrlTree` from the function)

BREAKING CHANGE: This change allows `Route.redirectTo` to be a function
in addition to the previous string. Code which expects `redirectTo` to
only be a string on `Route` objects will need to be adjusted.

PR Close #52606
2024-03-14 11:19:01 -07:00
Angular Robot b71a32324e docs: update Angular CLI help [main] (#54861)
Updated Angular CLI help contents.

PR Close #54861
2024-03-14 08:55:42 -07:00
Andrew Scott 77a397b382 release: cut the v18.0.0-next.0 release 2024-03-14 08:44:29 -07:00
Andrew Scott 700c0520bb fix(core): Update ApplicationRef.tick loop to only throw in dev mode (#54848)
This commit updates the loop in ApplicationRef.tick to only throw in dev
mode. In addition, it reduces the reruns to 10 from 100 in order to
reduce the impact on production applications that encounter this error.
That is, the loop will bail out much earlier and prevent prolonged
unresponsiveness.

PR Close #54848
2024-03-13 15:35:34 -07:00
Andrew Scott 1b19d2d430 docs: release notes for the v17.3.0 release 2024-03-13 14:09:11 -07:00
Andrew Scott 2d47329444 build: update symbol goldens (#54843)
fix goldens after recent commit

PR Close #54843
2024-03-13 11:36:06 -07:00
Andrew Scott 0dd441d08f refactor(core): Remove cancelation funtion from scheduler return value (#54578)
Canceling a scheduled callback is not used.

PR Close #54578
2024-03-13 11:09:56 -07:00
Andrew Scott 10c5cdb49c fix(core): ensure change detection runs in a reasonable timeframe with zone coalescing (#54578)
Zone event and run coalescing now races `requestAnimationFrame` and
`setTimeout`. There are tradeoffs to both functions and racing the two
gives us the benefits of both. This is explained in more detail in the
jsdoc comment. This change also aligns the timing of zone coalescing
with what will be used for zoneless.

BREAKING CHANGE: The exact timing of change detection execution when
using event or run coalescing with `NgZone` is now the first of either
`setTimeout` or `requestAnimationFrame`. Code which relies on this
timing (usually by accident) will need to be adjusted. If a callback
needs to execute after change detection, we recommend `afterNextRender`
instead of something like `setTimeout`.

fixes #54544
fixes #44314
fixes #39854 (unverified but seems likely)

PR Close #54578
2024-03-13 11:09:55 -07:00
Andrew Scott 8cad4e8cbe fix(core): ComponentFixture autoDetect respects OnPush flag of host view (#54824)
This is a follow-up to #53718 that applies the same logic to the
`autoDetect` feature of the fixture's host view. This now unifies the
logic between `ApplicationRef` and `ComponentFixture` autodetect.

BREAKING CHANGE: The `ComponentFixture` `autoDetect` feature will no
longer refresh the component's host view when the component is `OnPush`
and not marked dirty. This exposes existing issues in components which
claim to be `OnPush` but do not correctly call `markForCheck` when they
need to be refreshed. If this change causes test failures, the easiest
fix is to change the component to `ChangeDetectionStrategy.Default`.

PR Close #54824
2024-03-13 08:32:03 -07:00
JoostK edc1740d7e refactor(core): restructure logic in isCssClassMatching function (#54800)
The logic in `isCssClassMatching` is only interested in two areas in the attributes:
implicit attributes and the `AttributeMarker.Classes` area, with the first area only
of interest for projection matching, not directive matching. This commit splits these
two searches to make this more apparent.

PR Close #54800
2024-03-12 14:05:16 -07:00
JoostK e6ee6d25f9 fix(core): exclude class attribute intended for projection matching from directive matching (#54800)
This commit resolves a regression that was introduced when the compiler switched from
`TemplateDefinitionBuilder` (TDB) to the template pipeline (TP) compiler. The TP compiler
has changed the output of

```html
if (false) { <div class="test"></div> }
```

from

```ts
defineComponent({
  consts: [['class', 'test'], [AttributeMarker.Classes, 'test']],
  template: function(rf) {
    if (rf & 1) {
      ɵɵtemplate(0, App_Conditional_0_Template, 2, 0, "div", 0)
    }
  }
});
```

to

```ts
defineComponent({
  consts: [[AttributeMarker.Classes, 'test']],
  template: function(rf) {
    if (rf & 1) {
      ɵɵtemplate(0, App_Conditional_0_Template, 2, 0, "div", 0)
    }
  }
});
```

The last argument to the `ɵɵtemplate` instruction (0 in both compilation outputs) corresponds with
the index in `consts` of the element's attribute's, and we observe how TP has allocated only a single
attribute array for the `div`, where there used to be two `consts` entries with TDB. Consequently,
the `ɵɵtemplate` instruction is now effectively referencing a different attributes array, where the
distinction between the `"class"` attribute vs. the `AttributeMarker.Classes` distinction affects
the behavior: TP's emit causes the runtime to incorrectly match a directive with `selector: '.foo'` to
be instantiated on the `ɵɵtemplate` instruction as if it corresponds with a structural directive!

Instead of changing TP to align with TDB's emit, this commit updates the runtime instead. This uncovered
an inconsistency in selector matching for class names, where there used to be two paths dealing with
class matching:

1. The first check was commented to be a special-case for class matching, implemented in `isCssClassMatching`.
2. The second path was part of the main selector matching algorithm, where `findAttrIndexInNode` was being used
   to find the start position in `tNode.attrs` to match the selector's value against.

The second path only considers `AttributeMarker.Classes` values if matching for content projection, OR of the
`TNode` is not an inline template. The special-case in path 1 however does not make that distinction, so it
would consider the `AttributeMarker.Classes` binding as a selector match, incorrectly causing a directive to
match on the `ɵɵtemplate` itself.

The second path was also buggy for class bindings, as the return value of `classIndexOf` was incorrectly
negated: it considered a matching class attribute as non-matching and vice-versa. This bug was not observable
because of another issue, where the class-handling in part 2 was never relevant because of the special-case
in part 1.

This commit separates path 1 entirely from path 2 and removes the buggy class-matching logic in part 2, as
that is entirely handled by path 1 anyway. `isCssClassMatching` is updated to exclude class bindings from
being matched for inline templates.

Fixes #54798

PR Close #54800
2024-03-12 14:05:16 -07:00
Ben Hong 0f7bbd0fc5 docs: fix missing security guide in navigation (#54830)
The paths for the security guide were flipped in the original PR. As a result, it looked for a markdown file in the best-practices directory when it should have looked for it in the guide directory instead.

PR Close #54830
2024-03-12 13:12:58 -07:00
582 changed files with 14963 additions and 53488 deletions
-17
View File
@@ -56,11 +56,6 @@ build --enable_runfiles
build:release --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=release"
build:release --stamp
# Building AIO against local Angular deps requires stamping
# versions in Angular packages due to CLI version checks.
build:aio_local_deps --stamp
build:aio_local_deps --workspace_status_command="yarn -s --cwd aio local-workspace-status"
# Snapshots should also be stamped with version control information.
build:snapshot-build --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=snapshot"
build:snapshot-build --stamp
@@ -74,14 +69,6 @@ build:snapshot-build --stamp
##########################################################
build --flag_alias=aio_build_config=//aio:flag_aio_build_config
####################################
# AIO first party dep substitution #
# Turn on with #
# --config=aio_local_deps #
####################################
build:aio_local_deps --//aio:flag_aio_local_deps
###############################
# Output #
###############################
@@ -118,10 +105,6 @@ common:remote --jobs=200
build:remote --google_default_credentials
# Limit the number of test jobs for on an AIO local deps build. The example tests running
# concurrently pushes the circleci executor RAM usage to its limits.
test:aio_local_deps --jobs=24
# Force remote exeuctions to consider the entire run as linux
build:remote --cpu=k8
build:remote --host_cpu=k8
+1 -1
View File
@@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn'
runs:
using: 'composite'
steps:
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
- uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4
with:
path: |
./node_modules/
+5 -5
View File
@@ -21,16 +21,16 @@ jobs:
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel build --config=aio_local_deps //adev:build
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@c83e99a12397014162531ca125c94549db55dd84
run: yarn bazel build //adev:build
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
workflow-artifact-name: 'adev-preview'
pull-number: '${{github.event.pull_request.number}}'
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c83e99a12397014162531ca125c94549db55dd84
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'adev-preview'
@@ -16,6 +16,6 @@ jobs:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
- uses: angular/dev-infra/github-actions/branch-manager@c83e99a12397014162531ca125c94549db55dd84
- uses: angular/dev-infra/github-actions/branch-manager@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+3 -3
View File
@@ -19,7 +19,7 @@ jobs:
exit: true
# Indicate that the benchmark command was received.
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
with:
comment-id: ${{github.event.comment.id}}
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
@@ -38,7 +38,7 @@ jobs:
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
bazelrc: ./.bazelrc.user
@@ -53,7 +53,7 @@ jobs:
id: benchmark
name: Running benchmark
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
with:
issue-number: ${{github.event.issue.number}}
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
-64
View File
@@ -1,64 +0,0 @@
name: CI
on:
pull_request_target:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
permissions: {}
defaults:
run:
shell: bash
jobs:
saucelabs:
runs-on: ubuntu-latest-4core
env:
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
# Checking out the pull request commit is intended here as we need to run the changed code tests.
ref: ${{ github.event.pull_request.head.sha }}
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- 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
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
# test in all supported browsers on Saucelabs anyway until this job can be removed.
- name: Preparing Bazel-generated fixtures required in legacy tests
run: |
yarn bazel build //packages/core/test:downleveled_es5_fixture //packages/common/locales
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
mkdir -p dist/legacy-test-out/core/test/reflection/
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
dist/legacy-test-out/core/test/reflection/es5_downleveled_inheritance_fixture.js
# Locale files are needed for i18n tests running within Saucelabs. These are added
# directly as sources so that the TypeScript compilation of `/packages/tsconfig.json`
# can succeed. Note that the base locale and currencies files are checked-in, so
# we do not need to re-generate those through Bazel.
mkdir -p packages/common/locales/extra
cp dist/bin/packages/common/locales/*.ts packages/common/locales
cp dist/bin/packages/common/locales/extra/*.ts packages/common/locales/extra
- name: Build bundle of tests to run on Saucelabs
run: node tools/legacy-saucelabs/build-saucelabs-test-bundle.mjs
- name: Wait and confirm Saucelabs tunnel has connected
run: ./tools/saucelabs/sauce-service.sh ready-wait
timeout-minutes: 3
- name: Running tests on Saucelabs.
run: KARMA_WEB_TEST_MODE=SL_REQUIRED yarn karma start ./karma-js.conf.js --single-run
- name: Stop Saucelabs tunnel service
run: ./tools/saucelabs/sauce-service.sh stop
+73 -26
View File
@@ -5,6 +5,7 @@ on:
branches:
- main
- '[0-9]+.[0-9]+.x'
- 'remove-aio-stuff'
pull_request:
types: [opened, synchronize, reopened]
@@ -23,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
cache-node-modules: true
node-module-directories: |
@@ -58,13 +59,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Run unit tests
@@ -76,13 +77,13 @@ jobs:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Install node modules
run: yarn install --frozen-lockfile --network-timeout 100000
- name: Run CI tests for framework
@@ -93,15 +94,15 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel test --config=aio_local_deps //adev:test
run: yarn bazel test //adev:test
publish-snapshots:
if: github.event_name == 'push'
@@ -109,13 +110,13 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Install node modules
run: yarn install --frozen-lockfile
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
@@ -127,7 +128,7 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
cache-node-modules: true
node-module-directories: |
@@ -135,9 +136,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@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Install node modules
run: yarn install --frozen-lockfile
- run: |
@@ -176,7 +177,7 @@ jobs:
JOBS: 2
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
cache-node-modules: true
# Checking out the pull request commit is intended here as we need to run the changed code tests.
@@ -184,20 +185,66 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Saucelabs Variables
uses: angular/dev-infra/github-actions/saucelabs@50bb71e51d3372ca2e359adb9069f89beeac4ae7
- name: Set up Sauce Tunnel Daemon
run: yarn bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
env:
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
- name: Run all saucelabs bazel tests
run: |
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
yarn bazel test --config=saucelabs --jobs=$JOBS ${TESTS}
saucelabs:
runs-on: ubuntu-latest-4core
env:
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
cache-node-modules: true
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Saucelabs Variables
uses: angular/dev-infra/github-actions/saucelabs@50bb71e51d3372ca2e359adb9069f89beeac4ae7
- 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
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
# test in all supported browsers on Saucelabs anyway until this job can be removed.
- name: Preparing Bazel-generated fixtures required in legacy tests
run: |
yarn bazel build //packages/core/test:downleveled_es5_fixture //packages/common/locales
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
mkdir -p dist/legacy-test-out/core/test/reflection/
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
dist/legacy-test-out/core/test/reflection/es5_downleveled_inheritance_fixture.js
# Locale files are needed for i18n tests running within Saucelabs. These are added
# directly as sources so that the TypeScript compilation of `/packages/tsconfig.json`
# can succeed. Note that the base locale and currencies files are checked-in, so
# we do not need to re-generate those through Bazel.
mkdir -p packages/common/locales/extra
cp dist/bin/packages/common/locales/*.ts packages/common/locales
cp dist/bin/packages/common/locales/extra/*.ts packages/common/locales/extra
- name: Build bundle of tests to run on Saucelabs
run: node tools/legacy-saucelabs/build-saucelabs-test-bundle.mjs
- name: Wait and confirm Saucelabs tunnel has connected
run: ./tools/saucelabs/sauce-service.sh ready-wait
timeout-minutes: 3
- name: Running tests on Saucelabs.
run: KARMA_WEB_TEST_MODE=SL_REQUIRED yarn karma start ./karma-js.conf.js --single-run
- name: Stop Saucelabs tunnel service
run: ./tools/saucelabs/sauce-service.sh stop
adev-deploy:
needs: [adev]
if: needs.adev.result == 'success' && github.event_name == 'push' && github.ref_name == 'main'
@@ -207,15 +254,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel build --config=aio_local_deps //adev:build
run: yarn bazel build //adev:build
- name: Move generated project files into deployment directory
run: cp -r dist/bin/adev/build adev/build
- name: Setup firebase context
+2 -2
View File
@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/commit-message-based-labels@c83e99a12397014162531ca125c94549db55dd84
- uses: angular/dev-infra/github-actions/commit-message-based-labels@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
post_approval_changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/post-approval-changes@c83e99a12397014162531ca125c94549db55dd84
- uses: angular/dev-infra/github-actions/post-approval-changes@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/google-internal-tests@c83e99a12397014162531ca125c94549db55dd84
- uses: angular/dev-infra/github-actions/google-internal-tests@5774b71c01a55c4c998f858ee37d3b77ae704c31
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@c83e99a12397014162531ca125c94549db55dd84
- uses: angular/dev-infra/github-actions/unified-status-check@5774b71c01a55c4c998f858ee37d3b77ae704c31
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@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
- 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@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
cache-node-modules: true
node-module-directories: |
+1 -1
View File
@@ -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@c83e99a12397014162531ca125c94549db55dd84
uses: angular/dev-infra/github-actions/create-pr-for-changes@1421ff04811ecb45812930c5d1f61d912880e75f
with:
branch-prefix: update-cli-help
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
+96 -47
View File
@@ -1,25 +1,114 @@
<a name="17.3.0-rc.0"></a>
# 17.3.0-rc.0 (2024-03-06)
<a name="18.0.0-next.0"></a>
# 18.0.0-next.0 (2024-03-14)
## Breaking Changes
### core
- `OnPush` views at the root of the application need to
be marked dirty for their host bindings to refresh. Previously, the host
bindings were refreshed for all root views without respecting the
`OnPush` change detection strategy.
- The `ComponentFixture` `autoDetect` feature will no
longer refresh the component's host view when the component is `OnPush`
and not marked dirty. This exposes existing issues in components which
claim to be `OnPush` but do not correctly call `markForCheck` when they
need to be refreshed. If this change causes test failures, the easiest
fix is to change the component to `ChangeDetectionStrategy.Default`.
- When Angular runs change detection, it will continue to
refresh any views attached to `ApplicationRef` that are still marked for
check after one round completes. In rare cases, this can result in infinite
loops when certain patterns continue to mark views for check using
`ChangeDetectorRef.detectChanges`. This will be surfaced as a runtime
error with the `NG0103` code.
- The `ComponentFixture.autoDetect` feature now executes
change detection for the fixture within `ApplicationRef.tick`. This more
closely matches the behavior of how a component would refresh in
production. The order of component refresh in tests may be slightly
affected as a result, especially when dealing with additional components
attached to the application, such as dialogs. Tests sensitive to this
type of change (such as screenshot tests) may need to be updated.
Concretely, this change means that the component will refresh _before_
additional views attached to `ApplicationRef` (i.e. dialog components).
Prior to this change, the fixture component would refresh _after_ other
views attached to the application.
- The exact timing of change detection execution when
using event or run coalescing with `NgZone` is now the first of either
`setTimeout` or `requestAnimationFrame`. Code which relies on this
timing (usually by accident) will need to be adjusted. If a callback
needs to execute after change detection, we recommend `afterNextRender`
instead of something like `setTimeout`.
- Newly created and views marked for check and reattached
during change detection are now guaranteed to be refreshed in that same
change detection cycle. Previously, if they were attached at a location
in the view tree that was already checked, they would either throw
`ExpressionChangedAfterItHasBeenCheckedError` or not be refreshed until
some future round of change detection. In rare circumstances, this
correction can cause issues. We identified one instance that relied on
the previous behavior by reading a value on initialization which was
queued to be updated in a microtask instead of being available in the
current change detection round. The component only read this value during
initialization and did not read it again after the microtask updated it.
### router
- Guards can now return `RedirectCommand` for redirects
in addition to `UrlTree`. Code which expects only `boolean` or `UrlTree`
values in `Route` types will need to be adjusted.
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [5afa4f0ec1](https://github.com/angular/angular/commit/5afa4f0ec1b64b88ef875d48bd143e0f36e0a955) | fix | support `ModuleWithProviders` literal detection with `typeof` ([#54650](https://github.com/angular/angular/pull/54650)) |
| [ae7dbe42de](https://github.com/angular/angular/commit/ae7dbe42de5779e578fb636938699b8ba85b33f6) | fix | unwrap expressions with type parameters in query read property ([#54647](https://github.com/angular/angular/pull/54647)) |
| [9b424d7224](https://github.com/angular/angular/commit/9b424d7224db46edb16c81979c7e231d5e3db5e9) | fix | preserve original reference to non-deferrable dependency ([#54759](https://github.com/angular/angular/pull/54759)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [64f870c12b](https://github.com/angular/angular/commit/64f870c12bae1ad66509f0d65f8d3e051aae6eaa) | fix | `ApplicationRef.tick` should respect OnPush for host bindings ([#53718](https://github.com/angular/angular/pull/53718)) ([#53718](https://github.com/angular/angular/pull/53718)) |
| [8cad4e8cbe](https://github.com/angular/angular/commit/8cad4e8cbe2baf20dae7b7ef1f4253a4940cbba0) | fix | `ComponentFixture` `autoDetect` respects `OnPush` flag of host view ([#54824](https://github.com/angular/angular/pull/54824)) |
| [ba8e465974](https://github.com/angular/angular/commit/ba8e46597435a827670f10b971b2c58f7033b180) | fix | Change Detection will continue to refresh views while marked for check ([#54734](https://github.com/angular/angular/pull/54734)) |
| [24bc0ed4f2](https://github.com/angular/angular/commit/24bc0ed4f2de47bd998338d73cba394fb45dd497) | fix | ComponentFixture autodetect should detect changes within ApplicationRef.tick ([#54733](https://github.com/angular/angular/pull/54733)) |
| [10c5cdb49c](https://github.com/angular/angular/commit/10c5cdb49c51c95086febd37f4d88a9b944d7e1c) | fix | ensure change detection runs in a reasonable timeframe with zone coalescing ([#54578](https://github.com/angular/angular/pull/54578)) |
| [ad045efd4b](https://github.com/angular/angular/commit/ad045efd4b1565e01c14399998143538ebfbfd99) | fix | Ensure views marked for check are refreshed during change detection ([#54735](https://github.com/angular/angular/pull/54735)) |
| [700c0520bb](https://github.com/angular/angular/commit/700c0520bb638952ba41a8d8260cf12afb078c0e) | fix | Update ApplicationRef.tick loop to only throw in dev mode ([#54848](https://github.com/angular/angular/pull/54848)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [8735af08b9](https://github.com/angular/angular/commit/8735af08b976b30cf236a83f9e8b64b5ff62e9f3) | feat | Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards ([#45023](https://github.com/angular/angular/pull/45023)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.3.0"></a>
# 17.3.0 (2024-03-13)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [1a6beae8a2](https://github.com/angular/angular/commit/1a6beae8a2bdcff27d4c1e402f98246a52247906) | feat | Enable template pipeline by default. ([#54571](https://github.com/angular/angular/pull/54571)) |
| [f386a04c9d](https://github.com/angular/angular/commit/f386a04c9ddc2951c8105d5cafcce7b4bedea569) | fix | handle two-way bindings to signal-based template variables in instruction generation ([#54714](https://github.com/angular/angular/pull/54714)) |
| [1f129f114e](https://github.com/angular/angular/commit/1f129f114edb21137d74471dd6b652848d2ffb68) | fix | not catching for loop empty tracking expressions ([#54772](https://github.com/angular/angular/pull/54772)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [12dc4d074e](https://github.com/angular/angular/commit/12dc4d074e63edaff626003ad6136a8d122b2ba6) | fix | account for as expression in docs extraction ([#54414](https://github.com/angular/angular/pull/54414)) |
| [da7fbb40f0](https://github.com/angular/angular/commit/da7fbb40f06e6e37504f69e7b335f8219f424de2) | fix | detect when the linker is working in unpublished angular and widen supported versions ([#54439](https://github.com/angular/angular/pull/54439)) |
| [492e03f699](https://github.com/angular/angular/commit/492e03f699ca95df4cf854885253f63a5a1fb5c1) | fix | flag two-way bindings to non-signal values in templates ([#54714](https://github.com/angular/angular/pull/54714)) |
| [5afa4f0ec1](https://github.com/angular/angular/commit/5afa4f0ec1b64b88ef875d48bd143e0f36e0a955) | fix | support `ModuleWithProviders` literal detection with `typeof` ([#54650](https://github.com/angular/angular/pull/54650)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [331b16efd2](https://github.com/angular/angular/commit/331b16efd2f5af876e6dc0ad2474ee7a87b00de5) | feat | add API to inject attributes on the host node ([#54604](https://github.com/angular/angular/pull/54604)) |
| [fb540e169a](https://github.com/angular/angular/commit/fb540e169a78a61f38d611f538eea8fdb0971f1d) | feat | add migration for invalid two-way bindings ([#54630](https://github.com/angular/angular/pull/54630)) |
| [c687b8f453](https://github.com/angular/angular/commit/c687b8f4531252cd1c3dfbb9a7bd42bdbe666a36) | feat | expose new `output()` API ([#54650](https://github.com/angular/angular/pull/54650)) |
| [c809069f21](https://github.com/angular/angular/commit/c809069f213244afd0e2d803a6a43510b218e6f5) | feat | introduce `outputFromObservable()` interop function ([#54650](https://github.com/angular/angular/pull/54650)) |
| [aff65fd1f4](https://github.com/angular/angular/commit/aff65fd1f4a61ed76a6f9b623852f197eb3500e4) | feat | introduce `outputToObservable` interop helper ([#54650](https://github.com/angular/angular/pull/54650)) |
| [33a6fab094](https://github.com/angular/angular/commit/33a6fab094205cc74aceb916e45c8afa22293cf4) | fix | apply TestBed provider overrides to `@defer` dependencies ([#54667](https://github.com/angular/angular/pull/54667)) |
| [78e69117f0](https://github.com/angular/angular/commit/78e69117f07e8ae11aab3abe0343df815b3649be) | fix | generic inference for signal inputs may break with `--strictFunctionTypes` ([#54652](https://github.com/angular/angular/pull/54652)) |
| [974958913c](https://github.com/angular/angular/commit/974958913ca632971f878a045537472f2c99c665) | feat | support TypeScript 5.4 ([#54414](https://github.com/angular/angular/pull/54414)) |
| [39a50f9a8d](https://github.com/angular/angular/commit/39a50f9a8df5afc4968d18924f3d9d7d6b649d3a) | fix | ensure all initializer functions run in an injection context ([#54761](https://github.com/angular/angular/pull/54761)) |
| [243ccce624](https://github.com/angular/angular/commit/243ccce62475ae03a2e727d2b3cb2d51a595c4a7) | fix | exclude class attribute intended for projection matching from directive matching ([#54800](https://github.com/angular/angular/pull/54800)) |
| [2909e9817d](https://github.com/angular/angular/commit/2909e9817daf69b6478d7d09229491a9a48cff16) | fix | prevent infinite loops in clobbered elements check ([#54425](https://github.com/angular/angular/pull/54425)) |
| [7243c704cf](https://github.com/angular/angular/commit/7243c704cf8a4986fae419793027458e142658f0) | fix | return a readonly signal on `asReadonly`. ([#54706](https://github.com/angular/angular/pull/54706)) |
| [ffad7b8ea9](https://github.com/angular/angular/commit/ffad7b8ea9d1286ddb3ee7a2dac7dd33fd76b3aa) | fix | untrack various core operations ([#54614](https://github.com/angular/angular/pull/54614)) |
| [bb35414a38](https://github.com/angular/angular/commit/bb35414a38f8a8447d03c66dc5bf60b84409f9e3) | perf | speed up retrieval of `DestroyRef` in `EventEmitter` ([#54748](https://github.com/angular/angular/pull/54748)) |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [8d37ed035c](https://github.com/angular/angular/commit/8d37ed035c9e9796ba9a7b1f055404ea220dbb3b) | fix | exclude caching for authenticated HTTP requests ([#54746](https://github.com/angular/angular/pull/54746)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [c1c7384e02](https://github.com/angular/angular/commit/c1c7384e02becc623c6a42985f7178ca98137264) | feat | Add reusable types for router guards ([#54580](https://github.com/angular/angular/pull/54580)) |
| [7225485311](https://github.com/angular/angular/commit/722548531108e247660ebe6966b99b57d510b615) | fix | Navigations triggered by cancellation events should cancel previous navigation ([#54710](https://github.com/angular/angular/pull/54710)) |
<!-- CHANGELOG SPLIT MARKER -->
@@ -63,46 +152,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.3.0-next.1"></a>
# 17.3.0-next.1 (2024-02-27)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [707bfc9b32](https://github.com/angular/angular/commit/707bfc9b326f321f302dccfdfebef7380c914eb5) | perf | `AsyncPipe` should not call `markForCheck` on subscription ([#54554](https://github.com/angular/angular/pull/54554)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [1a6beae8a2](https://github.com/angular/angular/commit/1a6beae8a2bdcff27d4c1e402f98246a52247906) | feat | Enable template pipeline by default. ([#54571](https://github.com/angular/angular/pull/54571)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [12dc4d074e](https://github.com/angular/angular/commit/12dc4d074e63edaff626003ad6136a8d122b2ba6) | fix | account for as expression in docs extraction ([#54414](https://github.com/angular/angular/pull/54414)) |
| [f578889ca2](https://github.com/angular/angular/commit/f578889ca2dbb22f2e050d4c4af6aa2d597bf70d) | fix | catch function instance properties in interpolated signal diagnostic ([#54325](https://github.com/angular/angular/pull/54325)) |
| [f04ecc0cda](https://github.com/angular/angular/commit/f04ecc0cdaeb3a292a748b1ccc94ce70a573fc79) | fix | identify aliased initializer functions ([#54480](https://github.com/angular/angular/pull/54480)) |
| [f5c566c079](https://github.com/angular/angular/commit/f5c566c0793eacf9ca146c8a6b8da15b0e8f4c4d) | fix | identify aliased initializer functions ([#54609](https://github.com/angular/angular/pull/54609)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [331b16efd2](https://github.com/angular/angular/commit/331b16efd2f5af876e6dc0ad2474ee7a87b00de5) | feat | add API to inject attributes on the host node ([#54604](https://github.com/angular/angular/pull/54604)) |
| [974958913c](https://github.com/angular/angular/commit/974958913ca632971f878a045537472f2c99c665) | feat | support TypeScript 5.4 ([#54414](https://github.com/angular/angular/pull/54414)) |
| [dcb9deb363](https://github.com/angular/angular/commit/dcb9deb3631ff4e839f738f2fb97ca6b894256ef) | fix | collect providers from NgModules while rendering `@defer` block ([#52881](https://github.com/angular/angular/pull/52881)) |
| [ff40c9f762](https://github.com/angular/angular/commit/ff40c9f762b2c14870ac2859201ece9660087e79) | fix | fix typo in injectors.svg file ([#54596](https://github.com/angular/angular/pull/54596)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [71e0c7df69](https://github.com/angular/angular/commit/71e0c7df695f274a54d5ffb9dfc0587d06d3a953) | fix | resolve infinite loop for a single line element with a long tag name and angle bracket on a new line ([#54588](https://github.com/angular/angular/pull/54588)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.3.0-next.0"></a>
# 17.3.0-next.0 (2024-02-21)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [da7fbb40f0](https://github.com/angular/angular/commit/da7fbb40f06e6e37504f69e7b335f8219f424de2) | fix | detect when the linker is working in unpublished angular and widen supported versions ([#54439](https://github.com/angular/angular/pull/54439)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.2.2"></a>
# 17.2.2 (2024-02-21)
### common
+3 -28
View File
@@ -2,7 +2,6 @@ workspace(
name = "angular",
managed_directories = {
"@npm": ["node_modules"],
"@aio_npm": ["aio/node_modules"],
},
)
@@ -109,30 +108,6 @@ yarn_install(
yarn_lock = "//:yarn.lock",
)
yarn_install(
name = "aio_npm",
# Note that we add the postinstall scripts here so that the dependencies are re-installed
# when the postinstall patches are modified.
data = [
YARN_LABEL,
"//:.yarnrc",
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
"//aio:tools/cli-patches/bazel-architect-output.patch",
"//aio:tools/cli-patches/patch.js",
],
# Currently disabled due to:
# 1. Missing Windows support currently.
# 2. Incompatibilites with the `ts_library` rule.
exports_directories_only = False,
manual_build_file_contents = npm_package_archives(),
package_json = "//aio:package.json",
# We prefer to symlink the `node_modules` to only maintain a single install.
# See https://github.com/angular/dev-infra/pull/446#issuecomment-1059820287 for details.
symlink_node_modules = True,
yarn = YARN_LABEL,
yarn_lock = "//aio:yarn.lock",
)
yarn_install(
name = "aio_example_deps",
# Rename the default js_library target from "node_modules" as this obscures the
@@ -206,10 +181,10 @@ cldr_xml_data_repository(
# sass rules
http_archive(
name = "io_bazel_rules_sass",
sha256 = "974f5e14c54d300ca63a67910ebfcea6d838d8811fefc61184f2872107152705",
strip_prefix = "rules_sass-888dd9c02709e228b60a8b3e51e2b16b2ca5279d",
sha256 = "5633816e996a79c77f44306f6228b9ef18fc2ecb412d63b4e314a132225facba",
strip_prefix = "rules_sass-79bd239ce77c101ea1aed575678020bd2999f17d",
urls = [
"https://github.com/bazelbuild/rules_sass/archive/888dd9c02709e228b60a8b3e51e2b16b2ca5279d.zip",
"https://github.com/bazelbuild/rules_sass/archive/79bd239ce77c101ea1aed575678020bd2999f17d.zip",
],
)
+2 -2
View File
@@ -1,5 +1,5 @@
load("//:packages.bzl", "link_packages")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
@@ -73,7 +73,7 @@ APPLICATION_DEPS = link_packages([
"@npm//@lezer/highlight",
"@npm//@lezer/javascript",
"@npm//@lezer/common",
"@npm//xterm",
"@npm//@xterm/xterm",
"@npm//xterm-addon-fit",
"@npm//angular-split",
])
@@ -8,7 +8,7 @@
import {inject} from '@angular/core';
import {Subject} from 'rxjs';
import {Terminal} from 'xterm';
import {Terminal} from '@xterm/xterm';
import {WINDOW} from '@angular/docs';
@@ -7,7 +7,7 @@
*/
import {Injectable} from '@angular/core';
import {Terminal} from 'xterm';
import {Terminal} from '@xterm/xterm';
import {FitAddon} from 'xterm-addon-fit';
import {InteractiveTerminal} from './interactive-terminal';
+2 -2
View File
@@ -766,8 +766,8 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
{
label: 'Security',
path: 'guide/security', // Have not refactored due to build issues
contentPath: 'best-practices/security',
path: 'best-practices/security',
contentPath: 'guide/security', // Have not refactored due to build issues
},
{
label: 'Accessibility',
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
File diff suppressed because it is too large Load Diff
@@ -9,21 +9,21 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^17.2.0",
"@angular/common": "^17.2.0",
"@angular/compiler": "^17.2.0",
"@angular/core": "^17.2.0",
"@angular/forms": "^17.2.0",
"@angular/platform-browser": "^17.2.0",
"@angular/router": "^17.2.0",
"@angular/animations": "^17.3.0-rc",
"@angular/common": "^17.3.0-rc",
"@angular/compiler": "^17.3.0-rc",
"@angular/core": "^17.3.0-rc",
"@angular/forms": "^17.3.0-rc",
"@angular/platform-browser": "^17.3.0-rc",
"@angular/router": "^17.3.0-rc",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.2.0",
"@angular/cli": "^17.2.0",
"@angular/compiler-cli": "^17.2.0",
"@angular-devkit/build-angular": "^17.3.0-rc",
"@angular/cli": "^17.3.0-rc",
"@angular/compiler-cli": "^17.3.0-rc",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
"copyfiles": "^2.4.1",
File diff suppressed because it is too large Load Diff
@@ -9,19 +9,19 @@
},
"private": true,
"dependencies": {
"@angular/common": "^17.2.0",
"@angular/compiler": "^17.2.0",
"@angular/core": "^17.2.0",
"@angular/forms": "^17.2.0",
"@angular/platform-browser": "^17.2.0",
"@angular/common": "^17.3.0-rc",
"@angular/compiler": "^17.3.0-rc",
"@angular/core": "^17.3.0-rc",
"@angular/forms": "^17.3.0-rc",
"@angular/platform-browser": "^17.3.0-rc",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.2.0",
"@angular/cli": "^17.2.0",
"@angular/compiler-cli": "^17.2.0",
"@angular-devkit/build-angular": "^17.3.0-rc",
"@angular/cli": "^17.3.0-rc",
"@angular/compiler-cli": "^17.3.0-rc",
"typescript": "~5.2.0"
}
}
File diff suppressed because it is too large Load Diff
@@ -9,20 +9,20 @@
},
"private": true,
"dependencies": {
"@angular/common": "^17.2.0",
"@angular/compiler": "^17.2.0",
"@angular/core": "^17.2.0",
"@angular/forms": "^17.2.0",
"@angular/platform-browser": "^17.2.0",
"@angular/router": "^17.2.0",
"@angular/common": "^17.3.0-rc",
"@angular/compiler": "^17.3.0-rc",
"@angular/core": "^17.3.0-rc",
"@angular/forms": "^17.3.0-rc",
"@angular/platform-browser": "^17.3.0-rc",
"@angular/router": "^17.3.0-rc",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.2.0",
"@angular/cli": "^17.2.0",
"@angular/compiler-cli": "^17.2.0",
"@angular-devkit/build-angular": "^17.3.0-rc",
"@angular/cli": "^17.3.0-rc",
"@angular/compiler-cli": "^17.3.0-rc",
"typescript": "~5.2.0"
}
}
File diff suppressed because it is too large Load Diff
@@ -9,22 +9,22 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^17.2.0",
"@angular/cdk": "^17.2.0",
"@angular/common": "^17.2.0",
"@angular/compiler": "^17.2.0",
"@angular/core": "^17.2.0",
"@angular/forms": "^17.2.0",
"@angular/material": "^17.2.0",
"@angular/platform-browser": "^17.2.0",
"@angular/animations": "^17.3.0-rc",
"@angular/cdk": "^17.3.0-rc",
"@angular/common": "^17.3.0-rc",
"@angular/compiler": "^17.3.0-rc",
"@angular/core": "^17.3.0-rc",
"@angular/forms": "^17.3.0-rc",
"@angular/material": "^17.3.0-rc",
"@angular/platform-browser": "^17.3.0-rc",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.2.0",
"@angular/cli": "^17.2.0",
"@angular/compiler-cli": "^17.2.0",
"@angular-devkit/build-angular": "^17.3.0-rc",
"@angular/cli": "^17.3.0-rc",
"@angular/compiler-cli": "^17.3.0-rc",
"typescript": "~5.2.0"
}
}
-246
View File
@@ -1,246 +0,0 @@
{
"root": true,
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json",
"tests/e2e/tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/template/process-inline-templates"
],
"plugins": [
"@typescript-eslint",
"@angular-eslint",
"eslint-plugin-jsdoc",
"eslint-plugin-import",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow"
],
"rules": {
"@angular-eslint/component-class-suffix": "error",
"@angular-eslint/component-selector": [
"error",
{
"prefix": "aio",
"style": "kebab-case",
"type": "element"
}
],
"@angular-eslint/contextual-lifecycle": "error",
"@angular-eslint/directive-class-suffix": "error",
"@angular-eslint/directive-selector": [
"error",
{
"prefix": "aio",
"style": "camelCase",
"type": "attribute"
}
],
"@angular-eslint/no-conflicting-lifecycle": "error",
"@angular-eslint/no-host-metadata-property": "off",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-native": "error",
"@angular-eslint/no-output-on-prefix": "error",
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/no-outputs-metadata-property": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": "error",
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/dot-notation": "error",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": [
"error",
{
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@typescript-eslint/member-ordering": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": [
"error",
{
"ignoreParameters": true
}
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-shadow": [
"error"
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"@typescript-eslint/semi": [
"error",
"always"
],
"@typescript-eslint/triple-slash-reference": [
"error",
{
"lib": "always",
"path": "always",
"types": "prefer-import"
}
],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"arrow-parens": "off",
"comma-dangle": "off",
"complexity": "off",
"constructor-super": "error",
"curly": "error",
"eol-last": "error",
"eqeqeq": [
"error",
"smart"
],
"guard-for-in": "error",
"id-denylist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
],
"id-match": "error",
"import/no-deprecated": "warn",
"indent": "off",
"jsdoc/check-alignment": "error",
"jsdoc/no-types": "error",
"max-classes-per-file": "off",
"max-len": [
"error",
120
],
"new-parens": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-cond-assign": "error",
"no-console": [
"error",
{
"allow": [
"log",
"warn",
"error"
]
}
],
"no-debugger": "error",
"no-empty": "off",
"no-empty-function": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-invalid-this": "off",
"no-multiple-empty-lines": "off",
"no-new-wrappers": "error",
"no-restricted-imports": [
"error",
{
"message": "Please import directly from 'rxjs' instead",
"name": "rxjs/Rx"
}
],
"no-restricted-syntax": [
"error",
{
"message": "Don't keep jasmine focus methods.",
"selector": "CallExpression[callee.name=/^(fdescribe|fit)$/]"
}
],
"no-shadow": "off",
"no-tabs": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-unsafe-finally": "error",
"no-unused-expressions": "off",
"no-unused-labels": "error",
"no-use-before-define": "off",
"no-var": "error",
"object-shorthand": "error",
"one-var": [
"error",
"never"
],
"prefer-arrow/prefer-arrow-functions": "off",
"prefer-const": "error",
"quote-props": [
"error",
"as-needed"
],
"quotes": "off",
"radix": "error",
"sort-keys": "off",
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"asyncArrow": "always",
"named": "never"
}
],
"use-isnan": "error",
"valid-typeof": "off"
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {
"@angular-eslint/template/alt-text": "error",
"@angular-eslint/template/elements-content": "error",
"@angular-eslint/template/label-has-associated-control": "error",
"@angular-eslint/template/table-scope": "error",
"@angular-eslint/template/valid-aria": "error",
"@angular-eslint/template/click-events-have-key-events": "error",
"@angular-eslint/template/eqeqeq": "off",
"@angular-eslint/template/mouse-events-have-key-events": "error",
"@angular-eslint/template/no-autofocus": "error",
"@angular-eslint/template/no-distracting-elements": "error",
"@angular-eslint/template/no-positive-tabindex": "error"
}
}
]
}
-1
View File
@@ -1 +0,0 @@
engine-strict = true
+43 -43
View File
@@ -1,11 +1,11 @@
load("@aio_npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin", "npm_package_bin")
load("//tools:defaults.bzl", "nodejs_binary")
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
load(":local_packages_util.bzl", "link_local_packages", "substitute_local_package_deps")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag")
load("//aio/scripts:local_server_test.bzl", "local_server_test")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load(":aio_targets.bzl", "aio_test")
load("@bazel_skylib//lib:collections.bzl", "collections")
@@ -91,7 +91,7 @@ npm_package_bin(
tags = [
"requires-network",
],
tool = "@aio_npm//dgeni/bin:dgeni",
tool = "@npm//dgeni/bin:dgeni",
toolchains = [
"@npm//@angular/build-tooling/bazel/git-toolchain:current_git_toolchain",
],
@@ -148,29 +148,29 @@ APPLICATION_DEPS = [
":stackblitz",
":example-zips",
"//aio/src/generated:ngsw-config",
"@aio_npm//@angular-devkit/build-angular",
"@aio_npm//@angular-eslint/builder",
"@aio_npm//@angular/animations",
"@aio_npm//@angular/cdk",
"@aio_npm//@angular/cli",
"@aio_npm//@angular/common",
"@aio_npm//@angular/compiler",
"@aio_npm//@angular/compiler-cli",
"@aio_npm//@angular/core",
"@aio_npm//@angular/elements",
"@aio_npm//@angular/forms",
"@aio_npm//@angular/material",
"@aio_npm//@angular/platform-browser",
"@aio_npm//@angular/platform-browser-dynamic",
"@aio_npm//@angular/router",
"@aio_npm//@angular/service-worker",
"@aio_npm//@types/lunr",
"@aio_npm//@types/trusted-types",
"@aio_npm//lunr",
"@aio_npm//rxjs",
"@aio_npm//safevalues",
"@aio_npm//tslib",
"@aio_npm//zone.js",
"@npm//@angular-devkit/build-angular",
"@npm//@angular-eslint/builder",
"@npm//@angular/animations",
"@npm//@angular/cdk",
"@npm//@angular/cli",
"@npm//@angular/common",
"@npm//@angular/compiler",
"@npm//@angular/compiler-cli",
"@npm//@angular/core",
"@npm//@angular/elements",
"@npm//@angular/forms",
"@npm//@angular/material",
"@npm//@angular/platform-browser",
"@npm//@angular/platform-browser-dynamic",
"@npm//@angular/router",
"@npm//@angular/service-worker",
"@npm//@types/lunr",
"@npm//@types/trusted-types",
"@npm//lunr",
"@npm//rxjs",
"@npm//safevalues",
"@npm//tslib",
"@npm//zone.js",
]
# All sources, specs, and config files required to test the docs app
@@ -183,16 +183,16 @@ TEST_FILES = APPLICATION_FILES + [
# Dependencies required to test the docs app
TEST_DEPS = APPLICATION_DEPS + [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@aio_npm//@types/jasmine",
"@aio_npm//@types/node",
"@aio_npm//assert",
"@aio_npm//jasmine",
"@aio_npm//jasmine-core",
"@aio_npm//karma-chrome-launcher",
"@aio_npm//karma-coverage",
"@aio_npm//karma-jasmine",
"@aio_npm//karma-jasmine-html-reporter",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//assert",
"@npm//jasmine",
"@npm//jasmine-core",
"@npm//karma-chrome-launcher",
"@npm//karma-coverage",
"@npm//karma-jasmine",
"@npm//karma-jasmine-html-reporter",
"//aio/tools:windows-chromium-path",
]
@@ -202,12 +202,12 @@ E2E_FILES = APPLICATION_FILES + glob(["tests/e2e/**"])
# Dependencies required to run the e2e tests
E2E_DEPS = APPLICATION_DEPS + [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@aio_npm//@types/jasmine",
"@aio_npm//@types/node",
"@aio_npm//jasmine-spec-reporter",
"@aio_npm//protractor",
"@aio_npm//ts-node",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//jasmine-spec-reporter",
"@npm//protractor",
"@npm//ts-node",
"//aio/tools:windows-chromium-path",
]
@@ -322,7 +322,7 @@ architect_test(
# (presumably after AIO migration to Bazel)
flaky = True,
toolchains = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
],
)
-114
View File
@@ -1,114 +0,0 @@
# Angular documentation project (https://angular.io)
Everything in this folder is part of the documentation project. This includes:
* the web site for displaying the documentation.
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
* the tooling for setting up examples for development; and generating live-example and zip files from the examples.
<a name="developer-tasks"></a>
## Developer tasks
We use [Yarn](https://yarnpkg.com) to manage the dependencies and development tasks. Behind the scenes, [Bazel](https://bazel.build/) is used to build targets and run tests.
You should run all these tasks from the `angular/aio` folder.
Here are the most important tasks you might need to use:
* `yarn` - install all the dependencies.
* `yarn build` - create a development build of the application.
* `yarn build-prod` - create a production build of the application.
* `yarn build-local` - same as `build`, but uses locally built Angular packages from source code rather than from npm.
* `yarn start` - run a development web server that watches, rebuilds, and reloads the page when there are changes to the source code or docs.
* `yarn start-local` - same as `start`, but uses local Angular packages.
* `yarn test` - run all the unit tests for the doc-viewer once.
* `yarn test-local` - similar to `test`, but tests against locally built Angular packages.
* `yarn test-and-watch` - watch all the source files for the doc-viewer, and run all the unit tests when any change.
* `yarn e2e` - run all the e2e tests for the doc-viewer.
* `yarn e2e-local` - similar to `e2e`, but tests against locally built Angular packages.
* `yarn lint` - check that the doc-viewer code follows our style rules.
* `yarn docs-watch` - similar to `start`, but only watches for docs changes and uses a faster, low-fidelity build ideal for quick editing.
* `yarn docs-test` - run the unit tests for the doc generation code.
* `yarn docs-lint` - check that the doc gen code follows our style rules.
* `yarn create-example` - create a new example directory containing initial source files.
* `yarn example-playground <exampleName>` - set up a playground to manually test an example combined with its boilerplate files
- `--local` - link locally build Angular packages as deps
- `--watch` - update the playground when sources change
* `yarn example-e2e` - run all e2e tests for examples. Available options:
- `--local`: run e2e tests against locally built Angular packages.
- `--filter=foo`: limit e2e tests to those containing the word "foo".
- `--exclude=bar`: exclude e2e tests containing the word "bar".
> **Note for Windows users**
>
> The underlying Bazel build requires creating [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) (see [here](./tools/examples/README.md#symlinked-node_modules) for details). On Windows, this requires to either have [Developer Mode enabled](https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10) (supported on Windows 10 or newer) or run the setup commands as administrator.
## Using ServiceWorker locally
Running `yarn start` (even when explicitly targeting production mode) does not set up the
ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then
serve the files with `yarn http-server ../dist/bin/aio/build -p 4200`.
## Guide to authoring
There are two types of content in the documentation:
* **API docs**: descriptions of all that make up the Angular platform, such as the modules, classes, interfaces or decorators.
API docs are generated directly from the source code.
The source code is contained in TypeScript files, located in the `angular/packages` folder.
Each API item may have a preceding comment, which contains JSDoc style tags and content.
The content is written in markdown. To generate docs, each package's files need to be explicitly included in the [packages/BUILD.bazel](../packages/BUILD.bazel) file under the `files_for_docgen` target.
* **Other content**: guides, tutorials, and other marketing material.
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
* **Code examples**: code examples need to be testable to ensure their accuracy.
Also, our examples have a specific look and feel and allow the user to copy the source code. For larger
examples they are rendered in a tabbed interface (e.g. template, HTML, and TypeScript on separate
tabs). Additionally, some are live examples, which provide links where the code can be edited, executed, and/or downloaded. For details on working with code examples, please read the [Code snippets](https://angular.io/guide/docs-style-guide#code-snippets), [Source code markup](https://angular.io/guide/docs-style-guide#source-code-markup), and [Live examples](https://angular.io/guide/docs-style-guide#live-examples) pages of the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
The [Authors Style Guide](https://angular.io/guide/docs-style-guide) prescribes guidelines for
writing guide pages, explains how to use the documentation classes and components, and how to markup sample source code to produce code snippets.
### Generating the complete docs
Running the `yarn build` or `yarn start` tasks will automatically generate the docs. This will process all the source files (API and other),
extracting the documentation and generating JSON files that can be consumed by the doc-viewer.
### Partial doc generation for editors
Full doc generation can take up to one minute. That's too slow for efficient document creation and editing.
You can make small changes in a smart editor that displays formatted markdown:
>In VS Code, _Cmd-K, V_ opens markdown preview in side pane; _Cmd-B_ toggles left sidebar
You also want to see those changes displayed properly in the doc viewer
with a quick, edit/view cycle time.
For this purpose, use the `yarn docs-watch` task, which watches for changes to source files and only
re-processes the files necessary to generate the docs that are related to the file that has changed.
Since this task takes shortcuts, it is much faster (often less than 1 second) but it won't produce full
fidelity content. For example, links to other docs and code examples may not render correctly. This is
most particularly noticed in links to other docs and in the embedded examples, which may not always render
correctly.
The general setup is as follows:
* Open a terminal, ensure the dependencies are installed, then start the doc-viewer:
```bash
yarn docs-watch
```
* A browser will open automatically at https://localhost:4200/. Navigate to the document on which you want to work.
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
be regenerated, the app will rebuild and the page will reload.
* If you get a build error complaining about examples or any other odd behavior, be sure to consult
the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
-42
View File
@@ -1,42 +0,0 @@
# Update the angular.io app
The dependencies of the angular.io app (including Angular, Angular Material and Angular CLI) are automatically updated using [Renovate](https://renovatebot.com/).
However, it is useful to periodically also manually update the app to more closely match (in file layout, configs, etc.) what a new Angular CLI app would look like.
This is typically only needed once for each new major Angular version.
Since angular.io is an Angular CLI app, we can take advantage of `ng update` to apply migrations.
Follow these steps to align the angular.io app with new CLI apps.
> **Note:**
> The following steps assume that the related Angular dependencies have already been updated in [aio/package.json](./package.json) (for example, automatically by Renovate).
> **Note:**
> All commands shown below are expected to be executed from inside the [aio/](./) directory (unless specified otherwise).
- Determine (for example, by examining git history) what is the last versions for which this process was performed.
These will be referred to as `<FROM_VERSION_*>`.
If you can't determine these, use arbitrary versions, such as the previous major version.
- Run the following commands to automatically apply any available migrations to the project:
```sh
# Ensure dependencies are installed.
yarn install
# Migrate project to new versions.
yarn ng update @angular/cli --allow-dirty --migrate-only --from=<FROM_VERSION_CLI>
yarn ng update @angular/core --allow-dirty --migrate-only --from=<FROM_VERSION_ANGULAR>
yarn ng update @angular/material --allow-dirty --migrate-only --from=<FROM_VERSION_MATERIAL>
```
> **Note:**
> Depending on the number of changes generated from each `ng update` command, it might make sense to create a separate commit for each update.
- Inspect [package.json](./package.json) to determine what is the current version of Angular CLI (i.e. `@angular/cli`) used in the app.
This will be referred to as `<TO_VERSION_CLI>`.
- Use the [angular-cli-diff](https://github.com/cexbrayat/angular-cli-diff) repository to discover more changes (which are not automatically applied via `ng update` migrations) between Angular CLI apps of different versions.
Visit https://github.com/cexbrayat/angular-cli-diff/compare/<FROM_VERSION_CLI>...<TO_VERSION_CLI>, inspect the changes between the two versions and apply the ones that make sense to the angular.io source code.
- Commit all changes and [submit a pull request](../CONTRIBUTING.md#submit-pr).
+2 -2
View File
@@ -1,4 +1,4 @@
load("@aio_npm//@angular-devkit/architect-cli:index.bzl", "architect_test")
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect_test")
def aio_test(name, data, args, **kwargs):
architect_test(
@@ -10,7 +10,7 @@ def aio_test(name, data, args, **kwargs):
"CHROME_BIN": "../$(CHROMIUM)",
},
toolchains = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
],
**kwargs
)
-264
View File
@@ -1,264 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "yarn",
"warnings": {
"typescriptMismatch": false
},
"analytics": false,
"cache": {
// Disable build caching as the cache folder will just be dropped
// when run under Bazel sandboxed execution.
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"site": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
},
"@schematics/angular:component": {
"inlineStyle": true,
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "aio",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": ["zone.js"],
"ngswConfigPath": "src/generated/ngsw-config.json",
"tsConfig": "tsconfig.app.json",
"webWorkerTsConfig": "tsconfig.worker.json",
"optimization": {
"fonts": {
"inline": false
},
"scripts": true,
"styles": {
"inlineCritical": false,
"minify": true
}
},
"outputHashing": "all",
"sourceMap": true,
"namedChunks": true,
"assets": [
// Architect on Windows has difficulty service assets files within a
// Bazel runfiles symlink forest. Using "followSymlinks" seems to
// fix this. Note that the assets below don't need this workaround as
// symlinked tree artifacts pointing to the output tree don't appear
// to have the same issue.
{
"followSymlinks": true,
"input": "src/assets",
"glob": "**/*",
"output": "/assets/"
},
{
"followSymlinks": true,
"input": "src/",
"glob": "**/pwa-manifest.json",
"output": "/"
},
{
"followSymlinks": true,
"input": "src/",
"glob": "**/google385281288605d160.html",
"output": "/"
},
{
"input": "stackblitz/generated",
"output": "generated",
"glob": "**"
},
{
"input": "example-zips/generated",
"output": "generated",
"glob": "**"
},
{
"input": "dgeni/generated",
"output": "generated",
"glob": "**"
},
{
"input": "dgeni-fast/generated",
"output": "generated",
"glob": "**"
}
],
"styles": [
"src/styles/main.scss",
{
"inject": false,
"input": "src/styles/custom-themes/dark-theme.scss",
"bundleName": "dark-theme"
},
{
"inject": false,
"input": "src/styles/custom-themes/light-theme.scss",
"bundleName": "light-theme"
}
],
"scripts": [],
"budgets": [
{
"type": "initial",
"maximumWarning": "850kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
},
"configurations": {
"next": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.next.ts"
}
],
"serviceWorker": true
},
"rc": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.rc.ts"
}
],
"serviceWorker": true
},
"stable": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.stable.ts"
}
],
"serviceWorker": true
},
"archive": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.archive.ts"
}
],
"serviceWorker": true
},
"ci": {
"progress": false
},
"development": {
"buildOptimizer": false,
"optimization": false,
"outputHashing": "none",
"vendorChunk": true,
"extractLicenses": false
}
},
"defaultConfiguration": "stable"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"headers": {
// Keep in sync with the `firebase.json` file.
// Note: Unlike in `firebase.json` we omit the `report-uri` for development.
"Content-Security-Policy": "require-trusted-types-for 'script'; trusted-types angular angular#bundler angular#unsafe-bypass aio#analytics google#safe goog#html"
}
},
"configurations": {
"next": {
"browserTarget": "site:build:next"
},
"rc": {
"browserTarget": "site:build:rc"
},
"stable": {
"browserTarget": "site:build:stable"
},
"archive": {
"browserTarget": "site:build:archive"
},
"ci": {
"browserTarget": "site:build:ci"
},
"development": {
"browserTarget": "site:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "site:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"webWorkerTsConfig": "tsconfig.worker.json",
"karmaConfig": "karma.conf.js",
"assets": ["src/assets", "src/pwa-manifest.json", "src/google385281288605d160.html"],
"styles": [
"src/styles/main.scss",
{
"inject": false,
"input": "src/styles/custom-themes/dark-theme.scss",
"bundleName": "dark-theme"
},
{
"inject": false,
"input": "src/styles/custom-themes/light-theme.scss",
"bundleName": "light-theme"
}
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/!(generated)/**/*.ts",
"src/!(generated)/**/*.html",
"tests/**/*.ts"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "tests/e2e/protractor.conf.js",
"devServerTarget": "site:serve"
},
"configurations": {
"ci": {
"devServerTarget": "site:serve:ci"
}
}
}
}
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
{
"branchName": "refs/heads/main",
"sha": "6452ed70f1464c97e1ed4551dcf9f1346c5452f1"
"sha": "76a340c895b3fb124b07aa8a12f78202f5b8ea98"
}
+1
View File
@@ -3,6 +3,7 @@
"command": "ng serve [project]",
"shortDescription": "Builds and serves your application, rebuilding on file changes.",
"aliases": [
"dev",
"s"
],
"deprecated": false,
+2 -2
View File
@@ -206,7 +206,7 @@ def docs_example(name, test = True, test_tags = [], test_exec_properties = {}, f
data = [
":%s" % name,
YARN_LABEL,
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
"//aio/tools/examples:run-example-e2e",
"//aio/tools:windows-chromium-path",
# We install the whole node modules for runtime deps of e2e tests
@@ -229,7 +229,7 @@ def docs_example(name, test = True, test_tags = [], test_exec_properties = {}, f
"CHROMEDRIVER_BIN": "$(CHROMEDRIVER)",
},
toolchains = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
],
exec_properties = test_exec_properties,
flaky = flaky,
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
@@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])
+36
View File
@@ -1313,6 +1313,12 @@
},
"name": "JSDayIE"
},
{
"date": {
"start": "2024-01-30"
},
"name": "THAT Conf TX"
},
{
"date": {
"start": "2024-02-29"
@@ -1325,12 +1331,30 @@
},
"name": "ng-conf"
},
{
"date": {
"start": "2024-04-10"
},
"name": "Cloud Next"
},
{
"date": {
"start": "2024-05-24"
},
"name": "NG Belgrade Conf"
},
{
"date": {
"start": "2024-05-30"
},
"name": "Craft"
},
{
"date": {
"start": "2024-06-12"
},
"name": "RenderATL"
},
{
"date": {
"start": "2024-06-13"
@@ -1342,5 +1366,17 @@
"start": "2024-06-27"
},
"name": "dotJS"
},
{
"date": {
"start": "2024-07-29"
},
"name": "THAT Conf WI"
},
{
"date": {
"start": "2024-10-10"
},
"name": "NG-DE"
}
]
-11
View File
@@ -1,11 +0,0 @@
{
"rules": {
".read": false,
".write": false,
"events": {
".read": true,
".write": false
}
}
}
-287
View File
@@ -1,287 +0,0 @@
{
// Docs on Firebase hosting configuration: https://firebase.google.com/docs/hosting/full-config
"hosting": {
"target": "aio",
"public": "dist",
"cleanUrls": true,
//////////////////////////////////////////////////////////////////////////////////////////////
// ADDING REDIRECTS
//
// In order to permanently redirect a URL to a new address, add an new entry in the `redirects`
// list:
// ```json
// {"type": 301, "source": "/old/url", "destination": "/new/url"}
// ```
//
// This will cause the server to redirect any clients that visit `/old/url` to `/new/url`. At
// the same time, the ServiceWorker will be automatically configured to let requests for
// `/old/url` pass through to the server (instead of serving them locally with the cached
// `index.html`).
//
// In addition, you should also add a corresponding testcase in `URLS_TO_REDIRECT.txt` (inside
// `tests/deployment/shared/`) to ensure that the new redirect behaves as expected.
//
// NOTE 1:
// On clients that still have an old ServiceWorker version installed the next time they visit a
// recently redirected URL, the ServiceWorker will not yet know to let the request pass through
// to the server (and get redirected). In such cases, those clients will get a "Page not found"
// error, as the app tries to load `generated/docs/old/url.json` (which no longer exists). Such
// errors would typically go away after reloading the page (since the ServiceWorker will have
// updated to the latest version in the background).
//
// This temporary error is usually not a concern, because it only affects clients with an old
// ServiceWorker version at the time of the first visit and it goes away on reload. If, however,
// it is important to avoid this error for a specific redirect, you can add an additional
// redirect entry for the requested JSON file:
// ```json
// {"type": 302, "source": "/generated/docs/old/url.json", "destination": "/generated/docs/new/url.json"}
// ```
//
// In order to avoid bloating the redirects config, it is recommended to remove this additional
// entry after some time has passed, at which point the large majority of existing clients will
// have switched to an updated ServiceWorker version.
//
// NOTE 2:
// If you decide to add the additional entry described above, keep in mind that the JSON file
// URL is derived from the page URL by prepending `/generated/docs`, replacing any upper-case
// letters with the corresponding lower-case ones followed by an underscore (`_`) and appending
// `.json`.
//
// For example, the page URL `/api/forms/FormBuilder` would result in requesting the JSON file
// `/generated/docs/api/forms/f_ormb_uilder.json`.
//////////////////////////////////////////////////////////////////////////////////////////////
"redirects": [
// A random bad indexed page that used `api/api`
{"type": 301, "source": "/api/api/:rest*", "destination": "/api/:rest*"},
// Guide renames/removals
{"type": 301, "source": "/docs/*/latest/cli-quickstart.html", "destination": "/start"},
{"type": 301, "source": "/docs/*/latest/glossary.html", "destination": "/guide/glossary"},
{"type": 301, "source": "/docs/*/latest/quickstart.html", "destination": "/start"},
{"type": 301, "source": "/docs/*/latest/guide/server-communication.html", "destination": "/guide/understanding-communicating-with-http"},
{"type": 301, "source": "/docs/*/latest/guide/style-guide.html", "destination": "/guide/styleguide"},
{"type": 301, "source": "/guide/bazel", "destination": "https://github.com/angular/angular/blob/main/packages/bazel/docs/BAZEL_SCHEMATICS.md"},
{"type": 301, "source": "/guide", "destination": "/docs"},
{"type": 301, "source": "/guide/cli-quickstart", "destination": "/start"},
{"type": 301, "source": "/guide/i18n", "destination": "/guide/i18n-overview"},
{"type": 301, "source": "/guide/service-worker-getstart", "destination": "/guide/service-worker-getting-started"},
{"type": 301, "source": "/guide/service-worker-comm", "destination": "/guide/service-worker-communications"},
{"type": 301, "source": "/guide/service-worker-configref", "destination": "/guide/service-worker-config"},
{"type": 301, "source": "/guide/webpack", "destination": "https://v5.angular.io/guide/webpack"},
{"type": 301, "source": "/guide/http", "destination": "/guide/understanding-communicating-with-http"},
{"type": 301, "source": "/guide/setup", "destination": "/guide/setup-local"},
{"type": 301, "source": "/guide/setup-systemjs-anatomy", "destination": "/guide/file-structure"},
{"type": 301, "source": "/guide/change-log", "destination": "https://github.com/angular/angular/blob/main/CHANGELOG.md"},
{"type": 301, "source": "/guide/quickstart", "destination": "/start"},
{"type": 301, "source": "/getting-started", "destination": "/start"},
{"type": 301, "source": "/getting-started/:rest*", "destination": "/start/:rest*"},
{"type": 301, "source": "/guide/displaying-data", "destination": "/start#template-syntax"},
{"type": 301, "source": "/guide/ivy", "destination": "https://v12.angular.io/guide/ivy"},
{"type": 301, "source": "/guide/updating-to-version-10", "destination": "https://v10.angular.io/guide/updating-to-version-10"},
{"type": 301, "source": "/guide/updating-to-version-11", "destination": "https://v11.angular.io/guide/updating-to-version-11"},
{"type": 301, "source": "/guide/updating-to-version-12", "destination": "https://v12.angular.io/guide/updating-to-version-12"},
{"type": 301, "source": "/guide/updating-to-version-13", "destination": "https://v13.angular.io/guide/update-to-latest-version"},
{"type": 301, "source": "/guide/set-document-title", "destination": "/guide/router#setting-the-page-title"},
// Update Angular guide refactor to name individual update topics.
// This redirects the generic link to the current version's topic.
// The destination value must be updated with each new major version.
// When changing this value, be sure to update or add the corresponding
// test in angular/aio/tests/deployment/shared/URLS_TO_REDIRECT.txt
{"type": 301, "source": "/guide/update-to-latest-version", "destination": "/guide/update-to-version-17"},
// Renaming of Getting Started topics
{"type": 301, "source": "/start/data", "destination": "/start/start-data"},
{"type": 301, "source": "/start/deployment", "destination": "/start/start-deployment"},
{"type": 301, "source": "/start/forms", "destination": "/start/start-forms"},
{"type": 301, "source": "/start/routing", "destination": "/start/start-routing"},
// Tutorials moved to subdirectories
{"type": 301, "source": "/tutorial/index", "destination": "/tutorial/tour-of-heroes/index"},
{"type": 301, "source": "/tutorial/toh-pt0", "destination": "/tutorial/tour-of-heroes/toh-pt0"},
{"type": 301, "source": "/tutorial/toh-pt1", "destination": "/tutorial/tour-of-heroes/toh-pt1"},
{"type": 301, "source": "/tutorial/toh-pt2", "destination": "/tutorial/tour-of-heroes/toh-pt2"},
{"type": 301, "source": "/tutorial/toh-pt3", "destination": "/tutorial/tour-of-heroes/toh-pt3"},
{"type": 301, "source": "/tutorial/toh-pt4", "destination": "/tutorial/tour-of-heroes/toh-pt4"},
{"type": 301, "source": "/tutorial/toh-pt5", "destination": "/tutorial/tour-of-heroes/toh-pt5"},
{"type": 301, "source": "/tutorial/toh-pt6", "destination": "/tutorial/tour-of-heroes/toh-pt6"},
// some top level guide pages on old site were moved below the guide folder
{"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"},
{"type": 301, "source": "/docs/styleguide", "destination": "/guide/styleguide"},
// news is now blog
{"type": 301, "source": "/news*", "destination": "https://blog.angular.io/"},
// cookbook guides were moved (and sometime renamed or removed)
{"type": 301, "source": "/docs/*/latest/cookbook", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/cookbook/", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/cookbook/index.html", "destination": "/docs"},
{"type": 301, "source": "/**/cookbook/ts-to-js*", "destination": "https://v2.angular.io/docs/ts/latest/cookbook/ts-to-js.html"},
{"type": 301, "source": "/docs/*/latest/cookbook/a1-a2-quick-reference.html", "destination": "/guide/ajs-quick-reference"},
{"type": 301, "source": "/docs/*/latest/cookbook/component-communication.html", "destination": "/guide/component-interaction"},
{"type": 301, "source": "/docs/*/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"},
{"type": 301, "source": "/docs/*/latest/cookbook/:cookbook.html", "destination": "/guide/:cookbook"},
// Forms related code was moved from the `common` to `forms` package (and NgFor was renamed to NgForOf)
{"type": 301, "source": "/**/NgFor-*", "destination": "/api/common/NgForOf"},
{"type": 301, "source": "/**/api/common/index/MaxLengthValidator-*", "destination": "/api/forms/MaxLengthValidator"},
{"type": 301, "source": "/**/api/common/ControlGroup*", "destination": "/api/forms/FormGroup"},
{"type": 301, "source": "/**/api/common/Control*", "destination": "/api/forms/FormControl"},
{"type": 301, "source": "/**/api/common/SelectControlValueAccessor-*", "destination": "/api/forms/SelectControlValueAccessor"},
{"type": 301, "source": "/**/api/common/NgModel", "destination": "/api/forms/NgModel"},
// `@angular/http` package was removed, and new `HttpClient` APIs are available under `@angular/common/http` package
{"type": 301, "source": "/api/http", "destination": "/guide/deprecations#http"},
{"type": 301, "source": "/api/http/**", "destination": "/guide/deprecations#http"},
// Animations moves, renames and removals
{"type": 301, "source": "/api/animate/:rest*", "destination": "/api/animations/:rest*"},
// AnimationStateDeclarationMetadata was removed
{"type": 301, "source": "/**/AnimationStateDeclarationMetadata*", "destination": "/api/animations"},
// `AnimationDriver` was moved to the `animations/browser` package
{"type": 301, "source": "/api/platform-browser/AnimationDriver", "destination": "/api/animations/browser/AnimationDriver"},
// The `testing` package was renamed to `core/testing`
{"type": 301, "source": "/api/testing/:api-*", "destination": "/api/core/testing/:api"},
// CORE_DIRECTIVES & PLATFORM_PIPES were removed and are now in the CommonModule
{"type": 301, "source": "/**/CORE_DIRECTIVES*", "destination": "/api/common/CommonModule"},
{"type": 301, "source": "/**/PLATFORM_PIPES*", "destination": "/api/common/CommonModule"},
// DirectiveMetadata is now covered by the Directive decorator
{"type": 301, "source": "/**/DirectiveMetadata-*", "destination": "/api/core/Directive"},
// OptionalMetadata is now covered by the Optional decorator
{"type": 301, "source": "/**/OptionalMetadata-*", "destination": "/api/core/Optional"},
// HTTP_PROVIDERS was removed and is now provided in HttpModule
{"type": 301, "source": "/**/HTTP_PROVIDERS*", "destination": "/api/http/HttpModule"},
// URLs that use the old scheme of adding the type to the end (e.g. `SomeClass-class`)
// (Exclude disambiguated URLs that might be suffixed with `-\d+` (e.g. `SomeClass-1`))
{"type": 301, "source": "/api/:package/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/:package/:api"},
{"type": 301, "source": "/api/:package/testing/index/:api", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/api/:package/testing/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/api/upgrade/:package/index/:api", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/api/upgrade/:package/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/upgrade/:package/:api"},
// URLs that use the old scheme before we moved the docs to the angular/angular repo
{"type": 301, "source": "/docs/*/latest", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/api/", "destination": "/api"},
{"type": 301, "source": "/docs/*/latest/api/testing/:api-*", "destination": "/api/core/testing/:api"},
{"type": 301, "source": "/docs/*/latest/api/:package/:api-*", "destination": "/api/:package/:api"},
{"type": 301, "source": "/docs/*/latest/api/:package/index/:api-*", "destination": "/api/:package/:api"},
{"type": 301, "source": "/docs/*/latest/api/:package/testing", "destination": "/api/:package/testing"},
{"type": 301, "source": "/docs/*/latest/api/:package/testing/index/:api-*", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/docs/*/latest/api/platform-browser/animations/index/:api-*", "destination": "/api/platform-browser/animations/:api"},
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/:api-*", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/index/:api-*", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/docs/*/latest/glossary", "destination": "/guide/glossary"},
{"type": 301, "source": "/docs/*/latest/guide/", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/guide/lifecycle-hooks", "destination": "/guide/lifecycle-hooks"},
{"type": 301, "source": "/docs/*/latest/:rest*", "destination": "/:rest*"},
{"type": 301, "source": "/docs/latest/:rest*", "destination": "/:rest*"},
{"type": 301, "source": "/docs/styleguide*", "destination": "/guide/styleguide"},
{"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"},
{"type": 301, "source": "/guide/ngmodule", "destination": "/guide/ngmodules"},
{"type": 301, "source": "/guide/learning-angular*", "destination": "/start"},
{"type": 301, "source": "/testing", "destination": "/guide/testing"},
{"type": 301, "source": "/testing/**", "destination": "/guide/testing"},
// Work around Firebase hosting bug with `/index.html?<query>` leading to infinite redirects.
// See https://github.com/angular/angular/issues/42518#issuecomment-858545483 for details.
{"type": 301, "source": "/index.html:query*", "destination": "/:query*"},
// Strip off the `.html` extension, because Firebase will not do this automatically any more
// (unless the new URL points to an existing file, which is not necessarily the case here).
{"type": 301, "source": "/:topLevelFile.html", "destination": "/:topLevelFile"},
{"type": 301, "source": "/:somePath*/:file.html", "destination": "/:somePath*/:file"},
// The below paths are referenced in users projects generated by the CLI
{"type": 301, "source": "/config/tsconfig", "destination": "/guide/typescript-configuration"},
{"type": 301, "source": "/config/solution-tsconfig", "destination": "https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig"},
{"type": 301, "source": "/strict", "destination": "/guide/strict-mode"},
// Use discoverable link for Angular DevTools and redirect to the guide page
{"type": 301, "source": "/devtools", "destination": "/guide/devtools"},
// Use discoverable link for Angular Package Format and redirect to the guide page
{"type": 301, "source": "/apf", "destination": "/guide/angular-package-format"},
// Temporarily serve the guide at `/devtools` as well as `/guide/devtools` until users have
// their SW updated to a version that knows about the `/devtools` redirect.
{"type": 302, "source": "/generated/docs/devtools.json", "destination": "/generated/docs/guide/devtools.json"},
// Analytics pages that have been deleted in v15
{"type": 301, "source": "/cli/usage-analytics-gathering", "destination": "/cli/analytics"},
{"type": 301, "source": "/analytics", "destination": "/cli/analytics"},
// Deprecated error messages
{"type": 301, "source": "/errors/NG6999", "destination": "https://v13.angular.io/errors/NG6999"},
// Universal has been renamed to ssr
{"type": 301, "source": "/guide/universal", "destination": "/guide/ssr"}
],
"rewrites": [
{
"source": "**/!(*.*)",
"destination": "/index.html"
}
],
"headers": [
{
// All URLs
"source": "**",
"headers": [
// Report Trusted Types violations
{
"key": "Content-Security-Policy-Report-Only",
// The following Trusted Types policies are allowed:
// - angular: Angular's main internal policy. Defined in the `@angular/core` package.
// - angular#bundler: Used by Angular's bundler. Defined in the `webpack` package, enabled by `@angular-devkit/build-angular`.
// - angular#unsafe-bypass: For bypassSecurityTrust* usage. Defined in the `@angular/core` package.
// - aio#analytics: For the Legacy Google Analytics snippet. Defined in `index.html`.
// - google#safe: Used by the safevalues library. Defined in the `safevalues` package.
// - goog#html: Used by the Global Site Tag (`gtag.js`).
// csp.withgoogle.com is Google's CSP report collecting
// infrastructure.
// NOTE: Keep in sync with the header in `angular.json`.
"value": "require-trusted-types-for 'script'; trusted-types angular angular#bundler angular#unsafe-bypass aio#analytics google#safe goog#html; report-uri https://csp.withgoogle.com/csp/angular.io"
},
{
"key": "X-Frame-Options",
"value": "DENY"
}
]
},
{
// All paths (URLs without a file extension).
"source": "**/!(*.*)",
"headers": [
{"key": "Cache-Control", "value": "no-cache"},
{"key": "Link", "value": "</generated/navigation.json>;rel=preload;as=fetch,</generated/docs/index.json>;rel=preload;as=fetch"}
]
},
{
// Images, fonts, (non-hashed) CSS/JS files.
"source": "**/*.@(gif|jpg|jpeg|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|woff2)",
"headers": [
{"key": "Cache-Control", "value": "max-age=86400"} // 1 day
]
},
{
// Hashed CSS/JS files...
"source": "**/*.+([0-9a-f]).@(css|js)",
"headers": [
{"key": "Cache-Control", "value": "max-age=2592000"} // 30 days
]
}
]
},
"database": {
"rules": "database.rules.json"
}
}
-74
View File
@@ -1,74 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const {getAdjustedChromeBinPathForWindows} = require('./tools/windows-chromium-path');
process.env.CHROME_BIN = getAdjustedChromeBinPathForWindows();
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma'),
{'reporter:jasmine-seed': ['type', JasmineSeedReporter]},
],
proxies: {
'/dummy/image': 'src/assets/images/logos/angular/angular.png',
},
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
random: true,
seed: '',
},
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/site'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
],
},
reporters: ['progress', 'kjhtml', 'jasmine-seed'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
// See /integration/README.md#browser-tests for more info on these args
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
},
},
browsers: ['ChromeHeadlessNoSandbox'],
browserNoActivityTimeout: 60000,
singleRun: false,
restartOnFileChange: true,
});
};
// Helpers
function JasmineSeedReporter(baseReporterDecorator) {
baseReporterDecorator(this);
this.onBrowserComplete = (browser, result) => {
const seed = result.order && result.order.random && result.order.seed;
if (seed) this.write(`${browser}: Randomized with seed ${seed}.\n`);
};
this.onRunComplete = () => undefined;
}
+1 -1
View File
@@ -43,7 +43,7 @@ def link_local_packages(all_aio_deps):
# Special case deps that must be testonly
testonly_deps = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
]
# Stamp a corresponding target for each AIO dep that filters out transitive
-150
View File
@@ -1,150 +0,0 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app-shell",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/index.html",
"/pwa-manifest.json",
"/assets/images/favicons/favicon.ico",
"/assets/js/*.js",
"/*.css",
"/*.js"
],
"urls": [
"https://fonts.googleapis.com/**",
"https://fonts.gstatic.com/s/**"
]
}
},
{
"name": "assets-eager",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/**",
"/generated/images/marketing/**",
"!/assets/images/favicons/**",
"!/**/_unused/**"
]
}
},
{
"name": "assets-lazy",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/favicons/**",
"!/**/_unused/**"
]
}
},
{
"name": "docs-index",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/generated/*.json",
"/generated/docs/*.json",
"/generated/docs/api/api-list.json",
"/generated/docs/app/search-data.json"
]
}
},
{
"name": "docs-lazy",
"installMode": "lazy",
"updateMode": "lazy",
"resources": {
"files": [
"/generated/docs/**/*.json",
"/generated/images/**",
"!/**/_unused/**"
]
}
}
],
"navigationUrls": [
"/**",
"!/**/*.*",
"!/**/*__*",
"!/**/*__*/**",
"!/**/stackblitz/{0,1}",
"!/**/AnimationStateDeclarationMetadata*",
"!/**/CORE_DIRECTIVES*",
"!/**/DirectiveMetadata-*",
"!/**/HTTP_PROVIDERS*",
"!/**/NgFor-*",
"!/**/OptionalMetadata-*",
"!/**/PLATFORM_PIPES*",
"!/**/api/common/Control*",
"!/**/api/common/ControlGroup*",
"!/**/api/common/NgModel/{0,1}",
"!/**/api/common/SelectControlValueAccessor-*",
"!/**/api/common/index/MaxLengthValidator-*",
"!/**/cookbook/ts-to-js*",
"!/analytics/{0,1}",
"!/apf/{0,1}",
"!/api/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
"!/api/*/testing/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
"!/api/*/testing/index/*",
"!/api/animate/**",
"!/api/api/**",
"!/api/http/**",
"!/api/http/{0,1}",
"!/api/platform-browser/AnimationDriver/{0,1}",
"!/api/testing/*-*",
"!/api/upgrade/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
"!/api/upgrade/*/index/*",
"!/cli/usage-analytics-gathering/{0,1}",
"!/config/solution-tsconfig/{0,1}",
"!/config/tsconfig/{0,1}",
"!/devtools/{0,1}",
"!/docs/*/latest/**",
"!/docs/*/latest/{0,1}",
"!/docs/latest/**",
"!/docs/styleguide*",
"!/docs/styleguide/{0,1}",
"!/getting-started/**",
"!/getting-started/{0,1}",
"!/guide/bazel/{0,1}",
"!/guide/change-log/{0,1}",
"!/guide/cli-quickstart/{0,1}",
"!/guide/displaying-data/{0,1}",
"!/guide/i18n/{0,1}",
"!/guide/ivy/{0,1}",
"!/guide/learning-angular*",
"!/guide/metadata/{0,1}",
"!/guide/ngmodule/{0,1}",
"!/guide/quickstart/{0,1}",
"!/guide/service-worker-comm/{0,1}",
"!/guide/service-worker-configref/{0,1}",
"!/guide/service-worker-getstart/{0,1}",
"!/guide/set-document-title/{0,1}",
"!/guide/setup-systemjs-anatomy/{0,1}",
"!/guide/setup/{0,1}",
"!/guide/update-to-latest-version/{0,1}",
"!/guide/updating-to-version-10/{0,1}",
"!/guide/updating-to-version-11/{0,1}",
"!/guide/updating-to-version-12/{0,1}",
"!/guide/updating-to-version-13/{0,1}",
"!/guide/webpack/{0,1}",
"!/guide/{0,1}",
"!/news*",
"!/start/data/{0,1}",
"!/start/deployment/{0,1}",
"!/start/forms/{0,1}",
"!/start/routing/{0,1}",
"!/strict/{0,1}",
"!/styleguide/{0,1}",
"!/testing/**",
"!/testing/{0,1}"
]
}
-81
View File
@@ -1,81 +0,0 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app-shell",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/index.html",
"/pwa-manifest.json",
"/assets/images/favicons/favicon.ico",
"/assets/js/*.js",
"/*.css",
"/*.js"
],
"urls": [
"https://fonts.googleapis.com/**",
"https://fonts.gstatic.com/s/**"
]
}
},
{
"name": "assets-eager",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/**",
"/generated/images/marketing/**",
"!/assets/images/favicons/**",
"!/**/_unused/**"
]
}
},
{
"name": "assets-lazy",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/favicons/**",
"!/**/_unused/**"
]
}
},
{
"name": "docs-index",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/generated/*.json",
"/generated/docs/*.json",
"/generated/docs/api/api-list.json",
"/generated/docs/app/search-data.json"
]
}
},
{
"name": "docs-lazy",
"installMode": "lazy",
"updateMode": "lazy",
"resources": {
"files": [
"/generated/docs/**/*.json",
"/generated/images/**",
"!/**/_unused/**"
]
}
}
],
"navigationUrls": [
"/**",
"!/**/*.*",
"!/**/*__*",
"!/**/*__*/**",
"!/**/stackblitz/{0,1}"
]
}
-171
View File
@@ -1,171 +0,0 @@
{
"name": "angular.io",
"version": "0.0.0",
"main": "index.js",
"repository": "git@github.com:angular/angular.git",
"author": "Angular",
"license": "MIT",
"scripts": {
"postinstall": "node tools/cli-patches/patch.js && patch-package --patch-dir ../tools/npm-patches/",
"prebuild": "yarn setup",
"build": "bazel build //aio:build",
"build-prod": "yarn build --config=release",
"build-local": "yarn build --config=aio_local_deps",
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint && yarn security-lint",
"test": "bazel test //aio:test --test_output=streamed --test_tag_filters=-broken",
"test-and-watch": "ibazel test //aio:test-and-watch --test_output=streamed --test_tag_filters=-broken",
"test-local": "yarn test --config=aio_local_deps",
"test:ci": "bazel test //aio/... --test_tag_filters=-broken",
"test-local:ci": "yarn test:ci --config=aio_local_deps",
"e2e": "bazel test //aio:e2e",
"e2e-local": "yarn e2e --config=aio_local_deps",
"setup": "yarn --cwd .. install && yarn install --frozen-lockfile && yarn ~~check-env",
"set-opensearch-url": "node --eval \"const sh = require('shelljs'); sh.set('-e'); sh.sed('-i', /PLACEHOLDER_URL/g, process.argv[1], '../dist/bin/aio/build/assets/opensearch.xml');\"",
"smoke-tests": "protractor tests/deployment/e2e/protractor.conf.js --suite smoke --baseUrl",
"test-a11y-score": "bazel run //aio/scripts:test-aio-a11y",
"test-a11y-score-localhost": "bazel test //aio:test-a11y-score-localhost",
"test-pwa-score": "sh -c 'bazel run //aio/scripts:audit-web-app ${0} all:0,pwa:${1}'",
"test-pwa-score-localhost": "bazel test //aio:test-pwa-score-localhost",
"test-production-url": "bazel test //aio/tests/deployment/e2e",
"example-e2e": "node --experimental-import-meta-resolve tools/examples/run-filtered-example-e2es.mjs",
"example-list-overrides": "bazel run //aio/tools/examples:example-boilerplate list-overrides",
"example-lint": "eslint content/examples",
"example-playground": "node ./tools/examples/create-example-playground-wrapper.mjs",
"deploy-production": "node ./scripts/deploy-to-firebase/index.mjs",
"check-env": "yarn ~~check-env",
"payload-size": "scripts/payload.sh",
"predocs": "node scripts/contributors/validate-data && bazel build -- //aio:stackblitz //aio:example-zips",
"docs-watch": "bazel run //aio:docs-watch --config=release",
"docs": "bazel build //aio:dgeni --config=release",
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
"docs-test": "bazel test //aio/tools/transforms/...",
"redirects-test": "bazel test //aio/tests/deployment/unit:test",
"firebase-utils-test": "bazel test //aio/tools/firebase-test-utils:test",
"tools-lint": "eslint --ext=.mjs scripts/deploy-to-firebase && eslint tools/firebase-test-utils",
"tools-test": "bazel test -- //aio/tools/... //aio/scripts/deploy-to-firebase:test",
"start": "ibazel run //aio:serve --config=release",
"start-local": "yarn start --config=aio_local_deps",
"boilerplate:test": "bazel test //aio/tools/examples:example-boilerplate-test",
"create-example": "bazel run //aio/tools/examples:create-example",
"security-lint": "tsec -p tsconfig.app.json",
"~~audit-web-app": "node scripts/audit-web-app.mjs",
"~~check-env": "node scripts/check-environment",
"~~light-server": "light-server --bind=localhost --historyindex=/index.html --no-reload",
"local-workspace-status": "node scripts/local-workspace-status.mjs"
},
"//engines-comment": "If applicable, also update /package.json and /aio/tools/examples/shared/package.json",
"engines": {
"node": ">=16.14.0",
"yarn": ">=1.22.4 <2",
"npm": "Please use yarn instead of NPM to install dependencies"
},
"private": true,
"dependencies": {
"@angular/animations": "17.2.0-rc.1",
"@angular/cdk": "17.2.0-rc.0",
"@angular/common": "17.2.0-rc.1",
"@angular/compiler": "17.2.0-rc.1",
"@angular/core": "17.2.0-rc.1",
"@angular/elements": "17.2.0-rc.1",
"@angular/forms": "17.2.0-rc.1",
"@angular/material": "17.2.0-rc.0",
"@angular/platform-browser": "17.2.0-rc.1",
"@angular/platform-browser-dynamic": "17.2.0-rc.1",
"@angular/router": "17.2.0-rc.1",
"@angular/service-worker": "17.2.0-rc.1",
"rxjs": "~7.8.0",
"safevalues": "^0.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/architect-cli": "0.1702.0-rc.0",
"@angular-devkit/build-angular": "17.2.0-rc.0",
"@angular-eslint/builder": "16.0.3",
"@angular-eslint/eslint-plugin": "16.0.3",
"@angular-eslint/eslint-plugin-template": "16.0.3",
"@angular-eslint/template-parser": "^16.0.3",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#7c4cf003cb4ac849986beaa243d7e85a893612f2",
"@angular/cli": "17.2.0-rc.0",
"@angular/compiler-cli": "17.2.0-rc.1",
"@bazel/bazelisk": "^1.7.5",
"@bazel/buildozer": "^6.0.0",
"@bazel/ibazel": "^0.16.2",
"@bazel/jasmine": "^5.4.1",
"@bazel/runfiles": "5.8.1",
"@bazel/typescript": "5.8.1",
"@types/jasmine": "~5.1.0",
"@types/lunr": "^2.3.3",
"@types/node": "^12.7.9",
"@types/trusted-types": "^2.0.2",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"archiver": "^6.0.0",
"assert": "^2.0.0",
"canonical-path": "1.0.0",
"chalk": "^4.1.0",
"cjson": "^0.5.0",
"cross-spawn": "^7.0.3",
"css-selector-parser": "^3.0.0",
"dgeni": "^0.4.14",
"dgeni-packages": "^0.30.0",
"entities": "^4.0.0",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jasmine": "4.1.3",
"eslint-plugin-jsdoc": "48.2.0",
"eslint-plugin-prefer-arrow": "1.2.3",
"find-free-port": "^2.0.0",
"firebase-tools": "^13.0.0",
"fs-extra": "^11.0.0",
"get-port": "^7.0.0",
"globby": "^14.0.0",
"hast-util-has-property": "^1.0.4",
"hast-util-is-element": "^1.1.0",
"hast-util-to-string": "^1.0.4",
"html": "^1.0.0",
"ignore": "^5.1.8",
"image-size": "^1.0.0",
"jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"jasmine-spec-reporter": "~7.0.0",
"jsdom": "^24.0.0",
"json-schema-traverse": "^1.0.0",
"json5": "^2.2.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"light-server": "^2.9.1",
"lighthouse": "^9.0.0",
"lighthouse-logger": "^2.0.0",
"lodash": "^4.17.21",
"lunr": "^2.3.9",
"npm-run-all": "^4.1.5",
"patch-package": "^7.0.0",
"protractor": "~7.0.0",
"puppeteer-core": "21.3.8",
"rehype-slug": "^4.0.1",
"remark": "^12.0.0",
"remark-html": "^13.0.0",
"rimraf": "^5.0.0",
"semver": "^7.3.5",
"shelljs": "^0.8.5",
"source-map-support": "0.5.21",
"stemmer": "^2.0.0",
"tree-kill": "^1.1.0",
"ts-node": "^10.8.1",
"tsec": "^0.2.2",
"tslint": "6.1.3",
"typescript": "~5.0.2",
"uglify-js": "^3.13.3",
"unist-util-filter": "^2.0.3",
"unist-util-source": "^3.0.0",
"unist-util-visit": "^2.0.3",
"unist-util-visit-parents": "^3.1.1",
"watchr": "^3.0.1",
"xregexp": "^5.0.2",
"yargs": "^17.0.0"
}
}
+10 -10
View File
@@ -1,5 +1,5 @@
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("@aio_npm//@angular/build-tooling/bazel:filter_outputs.bzl", "filter_first_output")
load("@npm//@angular/build-tooling/bazel:filter_outputs.bzl", "filter_first_output")
load("//tools:defaults.bzl", "nodejs_binary", "nodejs_test")
package(default_visibility = ["//visibility:public"])
@@ -13,8 +13,8 @@ nodejs_binary(
data = [
"//aio:firebase.json",
"//aio:ngsw-config.template.json",
"@aio_npm//canonical-path",
"@aio_npm//json5",
"@npm//canonical-path",
"@npm//json5",
],
entry_point = "build-ngsw-config.js",
)
@@ -26,7 +26,7 @@ js_library(
],
deps = [
"//aio/tools/transforms/authors-package:watchdocs",
"@aio_npm//@angular-devkit/architect-cli",
"@npm//@angular-devkit/architect-cli",
],
)
@@ -40,17 +40,17 @@ nodejs_binary(
testonly = True,
data = [
"//aio/tools:windows-chromium-path",
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@aio_npm//lighthouse",
"@aio_npm//lighthouse-logger",
"@aio_npm//puppeteer-core",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//lighthouse",
"@npm//lighthouse-logger",
"@npm//puppeteer-core",
],
entry_point = "audit-web-app.mjs",
env = {
"CHROME_BIN": "$(CHROMIUM)",
},
toolchains = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
],
)
@@ -85,7 +85,7 @@ nodejs_test(
data = [
":audit-web-app",
":audit-web-app-script",
"@aio_npm//shelljs",
"@npm//shelljs",
],
entry_point = "test-aio-a11y.mjs",
env = {
+3 -3
View File
@@ -1,5 +1,5 @@
load("@aio_npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
DEPLOY_TO_FIREBASE_SOURCES = glob(
["**/*.mjs"],
@@ -7,7 +7,7 @@ DEPLOY_TO_FIREBASE_SOURCES = glob(
)
DEPLOY_TO_FIREBASE_DEPS = [
"@aio_npm//shelljs",
"@npm//shelljs",
"//aio:build",
"//:package.json",
]
+5 -5
View File
@@ -16,16 +16,16 @@ def local_server_test(name, entry_point, serve_target, data = [], args = [], **k
name = name,
testonly = True,
args = [
"$(rootpath @aio_npm//light-server/bin:light-server)",
"$(rootpath @npm//light-server/bin:light-server)",
"$(rootpath %s)" % serve_target,
"$(rootpath %s)" % entry_point,
] + args,
data = [
"//aio/scripts:run-with-local-server.mjs",
"@aio_npm//get-port",
"@aio_npm//shelljs",
"@aio_npm//tree-kill",
"@aio_npm//light-server/bin:light-server",
"@npm//get-port",
"@npm//shelljs",
"@npm//tree-kill",
"@npm//light-server/bin:light-server",
serve_target,
entry_point,
] + data,
-11
View File
@@ -1,11 +0,0 @@
{
"ban-reviewed-conversions": [
"src/app/custom-elements/code/code.component.ts",
"src/app/custom-elements/code/pretty-printer.service.ts",
"src/app/documents/document.service.ts",
"src/app/shared/security.ts"
],
"ban-worker-calls": [
"src/app/search/search.service.ts"
]
}
-52
View File
@@ -1,52 +0,0 @@
<!--
This content replaces the `<body>` content of `index.html` to generate our custom `404.html` page.
The content must visually and structurally resemble the resulting HTML of the main app for not
found pages (e.g. https://angular.io/not/exist.ing).
-->
<style>
.mat-toolbar-row{display:flex;box-sizing:border-box;padding:0 16px;width:100%;flex-direction:row;align-items:center;white-space:nowrap}
.mat-toolbar-row{height:64px}
@media (max-width:600px){.mat-toolbar-row{height:56px}}
.nav-link.home{margin-left:24px}
</style>
<aio-shell class="mode-stable page-file-not-found folder-file-not-found view- aio-notification-hide">
<mat-toolbar class="app-toolbar no-print mat-toolbar mat-primary">
<mat-toolbar-row class="mat-toolbar-row">
<a class="nav-link home" href="/">
<img alt="Home" height="40" src="assets/images/logos/angular/logo-nav@2x.png" title="Home" width="150">
</a>
</mat-toolbar-row>
</mat-toolbar>
<mat-sidenav-container class="sidenav-container mat-drawer-container mat-sidenav-container" role="main">
<mat-sidenav-content class="mat-drawer-content mat-sidenav-content">
<section class="sidenav-content" role="article">
<aio-doc-viewer>
<div class="content">
<div class="nf-container l-flex-wrap flex-center">
<img src="assets/images/support/angular-404.svg" width="300" height="300" alt="not found"/>
<div class="nf-response l-flex-wrap center">
<h1 class="no-toc" id="page-not-found">Resource Not Found</h1>
<p>We're sorry. The resource you are looking for cannot be found.</p>
</div>
</div>
</div>
</aio-doc-viewer>
</section>
</mat-sidenav-content>
</mat-sidenav-container>
<footer class="no-print">
<aio-footer>
<p>
Powered by Google ©2010-2022.
Code licensed under an <a href="license" title="License text">MIT-style License</a>.
Documentation licensed under <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>.
</p>
</aio-footer>
</footer>
</aio-shell>
-87
View File
@@ -1,87 +0,0 @@
<div id="top-of-page"></div>
<aio-cookies-popup></aio-cookies-popup>
<a class="skip-to-content-link" href="#main-content">Skip to main content</a>
<div *ngIf="isFetching" class="progress-bar-container">
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
</div>
<header>
<mat-toolbar #appToolbar color="primary" class="app-toolbar no-print" [class.transitioning]="isTransitioning">
<mat-toolbar-row class="notification-container">
<aio-notification notificationId="state-of-js-2023" expirationDate="2023-12-12" [dismissOnContentClick]="true" (dismissed)="notificationDismissed()">
<a href="https://survey.devographics.com/survey/state-of-js/2023?source=angular_homepage">
<mat-icon class="icon" svgIcon="insert_comment" aria-label="Announcement"></mat-icon>
<span class="message">Share your experience with Angular in <b>The State of JavaScript</b></span>
<span class="action-button">Go to survey</span>
</a>
</aio-notification>
</mat-toolbar-row>
<mat-toolbar-row>
<button mat-button class="hamburger" [class.no-animations]="disableAnimations" (click)="sidenav.toggle()" title="Docs menu">
<mat-icon svgIcon="menu"></mat-icon>
</button>
<a class="nav-link home" href="/" [ngSwitch]="showTopMenu">
<img *ngSwitchCase="true" src="assets/images/logos/angular/logo-nav@2x.png" width="150" height="40" title="Home" alt="Home">
<img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
</a>
<aio-top-menu *ngIf="showTopMenu" [nodes]="topMenuNodes" [currentNode]="currentNodes.TopBar"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event, true)"></aio-search-box>
<aio-theme-toggle #themeToggle></aio-theme-toggle>
<div #externalIcons class="toolbar-external-icons-container">
<a mat-icon-button href="https://twitter.com/angular" title="Twitter" aria-label="Angular on Twitter">
<mat-icon svgIcon="logos:twitter"></mat-icon>
</a>
<a mat-icon-button href="https://github.com/angular/angular" title="GitHub" aria-label="Angular on GitHub">
<mat-icon svgIcon="logos:github"></mat-icon>
</a>
<a mat-icon-button href="https://youtube.com/angular" title="YouTube" aria-label="Angular on YouTube">
<mat-icon svgIcon="logos:youtube"></mat-icon>
</a>
</div>
</mat-toolbar-row>
</mat-toolbar>
</header>
<aio-search-results *ngIf="showSearchResults"
#searchResultsView
[searchResults]="searchResults | async"
(resultSelected)="hideSearchResults()"
(closeButtonClick)="hideSearchResults()">
</aio-search-results>
<mat-sidenav-container class="sidenav-container" [class.no-animations]="disableAnimations" [class.has-floating-toc]="hasFloatingToc">
<mat-sidenav [class.collapsed]="!dockSideNav" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()">
<aio-nav-menu *ngIf="!showTopMenu" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes.TopBarNarrow" [isWide]="dockSideNav" navLabel="primary"></aio-nav-menu>
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes.SideNav" [isWide]="dockSideNav" navLabel="guides and docs"></aio-nav-menu>
<div class="doc-version">
<aio-nav-menu [nodes]="docVersions" [isWide]="true" [currentNode]="currentDocsVersionNode" navLabel="docs versions"></aio-nav-menu>
</div>
</mat-sidenav>
<section class="sidenav-content-container">
<main class="sidenav-content" [id]="pageId">
<div id="main-content" tabindex="-1"></div>
<aio-mode-banner *ngIf="versionInfo" [mode]="deployment.mode" [version]="versionInfo"></aio-mode-banner>
<aio-doc-viewer [class.no-animations]="disableAnimations" [doc]="currentDocument" (docReady)="onDocReady()" (docRemoved)="onDocRemoved()" (docInserted)="onDocInserted()" (docRendered)="onDocRendered()">
</aio-doc-viewer>
<aio-dt *ngIf="dtOn" [(doc)]="currentDocument"></aio-dt>
</main>
<div *ngIf="hasFloatingToc" class="toc-container no-print" [style.max-height.px]="tocMaxHeight" (wheel)="restrainScrolling($event)">
<aio-lazy-ce selector="aio-toc"></aio-lazy-ce>
</div>
</section>
<footer class="no-print">
<aio-footer [nodes]="footerNodes" [versionInfo]="versionInfo"></aio-footer>
</footer>
</mat-sidenav-container>
File diff suppressed because it is too large Load Diff
-480
View File
@@ -1,480 +0,0 @@
import {
Component,
ElementRef,
HostBinding,
HostListener,
OnInit,
QueryList,
ViewChild,
ViewChildren,
} from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { DocumentContents, DocumentService } from 'app/documents/document.service';
import { NotificationComponent } from 'app/layout/notification/notification.component';
import { CurrentNodes, NavigationNode, NavigationService, VersionInfo } from 'app/navigation/navigation.service';
import { SearchResults } from 'app/search/interfaces';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchService } from 'app/search/search.service';
import { Deployment } from 'app/shared/deployment.service';
import { LocationService } from 'app/shared/location.service';
import { ScrollService } from 'app/shared/scroll.service';
import { TocService } from 'app/shared/toc.service';
import { SwUpdatesService } from 'app/sw-updates/sw-updates.service';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { first, map } from 'rxjs/operators';
const sideNavView = 'SideNav';
export const showTopMenuWidth = 1150;
export const dockSideNavWidth = 992;
export const showFloatingTocWidth = 800;
@Component({
selector: 'aio-shell',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
static reducedMotion = window.matchMedia('(prefers-reduced-motion)').matches;
// Disable all Angular animations if the user prefers reduced motion or for the initial render.
@HostBinding('@.disabled')
get disableAnimations(): boolean { return AppComponent.reducedMotion || this.isStarting; }
currentDocument: DocumentContents;
currentDocVersion: NavigationNode;
currentNodes: CurrentNodes = {};
currentPath: string;
docVersions: NavigationNode[];
dtOn = false;
footerNodes: NavigationNode[];
/**
* An HTML friendly identifier for the currently displayed page.
* This is computed from the `currentDocument.id` by replacing `/` with `-`
*/
pageId: string;
/**
* An HTML friendly identifier for the "folder" of the currently displayed page.
* This is computed by taking everything up to the first `/` in the `currentDocument.id`
*/
folderId: string;
/**
* These CSS classes are computed from the current state of the application
* (e.g. what document is being viewed) to allow for fine grain control over
* the styling of individual pages.
* You will get three classes:
*
* * `page-...`: computed from the current document id (e.g. events, guide-security, tutorial-toh-pt2)
* * `folder-...`: computed from the top level folder for an id (e.g. guide, tutorial, etc)
* * `view-...`: computed from the navigation view (e.g. SideNav, TopBar, etc)
*/
@HostBinding('class')
hostClasses = '';
private isStarting = true;
isTransitioning = true;
isFetching = false;
showTopMenu = false;
dockSideNav = false;
private isFetchingTimeout: any;
private isSideNavDoc = false;
sideNavNodes: NavigationNode[];
topMenuNodes: NavigationNode[];
topMenuNarrowNodes: NavigationNode[];
hasFloatingToc = false;
private showFloatingToc = new BehaviorSubject(false);
tocMaxHeight: string;
private tocMaxHeightOffset = 0;
currentDocsVersionNode?: NavigationNode;
versionInfo: VersionInfo | undefined;
get isOpened() { return this.dockSideNav && this.isSideNavDoc; }
get mode() { return this.isOpened ? 'side' : 'over'; }
// Search related properties
showSearchResults = false;
searchResults: Observable<SearchResults>;
@ViewChildren('searchBox, searchResultsView', { read: ElementRef })
searchElements: QueryList<ElementRef>;
@ViewChild(SearchBoxComponent, { static: true })
searchBox: SearchBoxComponent;
@ViewChild('searchResultsView', { read: ElementRef })
searchResultsView: ElementRef;
@ViewChild(MatSidenav, { static: true })
sidenav: MatSidenav;
@ViewChild(NotificationComponent, { static: true })
notification: NotificationComponent;
notificationAnimating = false;
@ViewChild('appToolbar', { read: ElementRef }) toolbar: ElementRef;
@ViewChildren('themeToggle, externalIcons', { read: ElementRef }) toolbarIcons: QueryList<ElementRef>;
constructor(
public deployment: Deployment,
private documentService: DocumentService,
private hostElement: ElementRef,
private locationService: LocationService,
private navigationService: NavigationService,
private scrollService: ScrollService,
private searchService: SearchService,
private swUpdatesService: SwUpdatesService,
private tocService: TocService
) { }
ngOnInit() {
// Do not initialize the search on browsers that lack web worker support
if ('Worker' in window) {
// Delay initialization by up to 2 seconds
this.searchService.initWorker(2000);
}
this.onResize(window.innerWidth);
/* No need to unsubscribe because this root component never dies */
this.documentService.currentDocument.subscribe(doc => this.currentDocument = doc);
this.locationService.currentPath.subscribe(path => {
if (path === this.currentPath) {
// scroll only if on same page (most likely a change to the hash)
this.scrollService.scroll();
} else {
// don't scroll; leave that to `onDocRendered`
this.currentPath = path;
// Start progress bar if doc not rendered within brief time
clearTimeout(this.isFetchingTimeout);
this.isFetchingTimeout = setTimeout(() => this.isFetching = true, 200);
}
});
this.navigationService.currentNodes.subscribe(currentNodes => {
this.currentNodes = currentNodes;
// Redirect to docs if we are in archive mode and are not hitting a docs page
// (i.e. we have arrived at a marketing page)
if (this.deployment.mode === 'archive' && !currentNodes[sideNavView]) {
this.locationService.replace('docs');
}
});
// Compute the version picker list from the current version and the versions in the navigation map
combineLatest([
this.navigationService.versionInfo,
this.navigationService.navigationViews.pipe(map(views => views.docVersions)),
this.locationService.currentUrl,
]).subscribe(([versionInfo, versions, currentUrl]) => {
// TODO(pbd): consider whether we can lookup the stable and next versions from the internet
const computedVersions: NavigationNode[] = [
{ title: 'next', url: 'https://next.angular.io/' },
{ title: 'rc', url: 'https://rc.angular.io/' },
{ title: 'stable', url: 'https://angular.io/' },
];
if (this.deployment.mode === 'archive') {
computedVersions.push({ title: `v${versionInfo.major}` });
}
const allDocsVersionNodes = [...computedVersions, ...versions].map(version => ({
...version,
// Update the urls so that they point to the same page the user is currently at
url: `${version.url}${(version.url?.endsWith('/') ? '' : '/' )}${currentUrl}`,
}));
// Find the current version - either title matches the current deployment mode
// or its title matches the major version of the current version info
this.currentDocsVersionNode = allDocsVersionNodes.find(
version => version.title === this.deployment.mode || version.title === `v${versionInfo.major}`
);
this.docVersions = [
{
title: 'Docs Versions',
children : allDocsVersionNodes
}
];
});
this.navigationService.navigationViews.subscribe(views => {
this.footerNodes = views.Footer || [];
this.sideNavNodes = views.SideNav || [];
this.topMenuNodes = views.TopBar || [];
this.topMenuNarrowNodes = views.TopBarNarrow || this.topMenuNodes;
});
this.navigationService.versionInfo.subscribe(vi => this.versionInfo = vi);
const hasNonEmptyToc = this.tocService.tocList.pipe(map(tocList => tocList.length > 0));
combineLatest([hasNonEmptyToc, this.showFloatingToc])
.subscribe(([hasToc, showFloatingToc]) => this.hasFloatingToc = hasToc && showFloatingToc);
// Generally, we want to delay updating the shell (e.g. host classes, sidenav state) for the new
// document, until after the leaving document has been removed (to avoid having the styles for
// the new document applied prematurely).
// For the first document, though, (when we know there is no previous document), we want to
// ensure the styles are applied as soon as possible to avoid flicker.
combineLatest([
this.documentService.currentDocument, // ...needed to determine host classes
this.navigationService.currentNodes, // ...needed to determine `sidenav` state
]).pipe(first())
.subscribe(() => this.updateShell());
// Start listening for SW version update events.
this.swUpdatesService.enable();
}
onDocReady() {
// About to transition to new view.
this.isTransitioning = true;
// Stop fetching timeout (which, when render is fast, means progress bar never shown)
clearTimeout(this.isFetchingTimeout);
// If progress bar has been shown, keep it for at least 500ms (to avoid flashing).
setTimeout(() => this.isFetching = false, 500);
}
onDocRemoved() {
this.scrollService.removeStoredScrollInfo();
}
onDocInserted() {
// Update the shell (host classes, sidenav state) to match the new document.
// This may be called as a result of actions initiated by view updates.
// In order to avoid errors (e.g. `ExpressionChangedAfterItHasBeenChecked`), updating the view
// (e.g. sidenav, host classes) needs to happen asynchronously.
setTimeout(() => this.updateShell());
// Scroll the good position depending on the context
this.scrollService.scrollAfterRender(500);
}
onDocRendered() {
if (this.isStarting) {
// In order to ensure that the initial sidenav-content left margin
// adjustment happens without animation, we need to ensure that
// `isStarting` remains `true` until the margin change is triggered.
// (Apparently, this happens with a slight delay.)
setTimeout(() => this.isStarting = false, 100);
}
this.isTransitioning = false;
}
@HostListener('window:resize', ['$event.target.innerWidth'])
onResize(width: number) {
this.showTopMenu = width >= showTopMenuWidth;
this.dockSideNav = width >= dockSideNavWidth;
this.showFloatingToc.next(width > showFloatingTocWidth);
if (this.showTopMenu && !this.isSideNavDoc) {
// If this is a non-sidenav doc and the screen is wide enough so that we can display menu
// items in the top-bar, ensure the sidenav is closed.
// (This condition can only be met when the resize event changes the value of `showTopMenu`
// from `false` to `true` while on a non-sidenav doc.)
this.sidenav.toggle(false);
}
}
@HostListener('focusin', ['$event.target'])
onFocus(eventTarget: HTMLElement) {
// Implement a focus trap starting at the input search and ending after the search results
if (this.showSearchResults) {
const insideFocusLoop = [
...this.toolbarIcons,
...this.searchElements
].some(element => element.nativeElement.contains(eventTarget));
const insideToolbar = this.toolbar.nativeElement.contains(eventTarget);
if (!insideFocusLoop) {
if (!insideToolbar) {
// the user is focusing forward at the last search result element,
// loop it back to the search input
this.focusSearchBox();
} else {
// the user is focusing backward from the search input,
// loop it back to the results' close button
const closeBtn: HTMLButtonElement =
this.searchResultsView.nativeElement.querySelector('button.close-button');
closeBtn.focus();
}
}
}
}
@HostListener('click', ['$event.target', '$event.button', '$event.ctrlKey', '$event.metaKey', '$event.altKey'])
onClick(eventTarget: HTMLElement, button: number, ctrlKey: boolean, metaKey: boolean, altKey: boolean): boolean {
// Hide the search results if we clicked outside both the "search box" and the "search results"
if (
this.showSearchResults &&
!this.searchElements.some(element => element.nativeElement.contains(eventTarget))
) {
this.hideSearchResults();
}
// Show developer source view if the footer is clicked while holding the meta and alt keys
if (eventTarget.tagName === 'FOOTER' && metaKey && altKey) {
this.dtOn = !this.dtOn;
return false;
}
// Deal with anchor clicks; climb DOM tree until anchor found (or null)
let target: HTMLElement | null = eventTarget;
while (target && !(target instanceof HTMLAnchorElement)) {
target = target.parentElement;
}
if (target instanceof HTMLAnchorElement) {
return this.locationService.handleAnchorClick(target, button, ctrlKey, metaKey);
}
// Allow the click to pass through
return true;
}
setPageId(id: string) {
// Special case the home page
this.pageId = (id === 'index') ? 'home' : id.replace('/', '-');
}
setFolderId(id: string) {
// Special case the home page
this.folderId = (id === 'index') ? 'home' : id.split('/', 1)[0];
}
notificationDismissed() {
this.notificationAnimating = true;
// this should be kept in sync with the animation durations in:
// - aio/src/styles/2-modules/_notification.scss
// - aio/src/app/layout/notification/notification.component.ts
setTimeout(() => this.notificationAnimating = false, 250);
this.updateHostClasses();
}
updateHostClasses() {
const mode = `mode-${this.deployment.mode}`;
const sideNavOpen = `sidenav-${this.sidenav.opened ? 'open' : 'closed'}`;
const pageClass = `page-${this.pageId}`;
const folderClass = `folder-${this.folderId}`;
const viewClasses = Object.keys(this.currentNodes).map(view => `view-${view}`).join(' ');
const notificationClass = `aio-notification-${this.notification.showNotification}`;
const notificationAnimatingClass = this.notificationAnimating ? 'aio-notification-animating' : '';
this.hostClasses = [
mode,
sideNavOpen,
pageClass,
folderClass,
viewClasses,
notificationClass,
notificationAnimatingClass
].join(' ');
}
updateShell() {
// Update the SideNav state (if necessary).
this.updateSideNav();
// Update the host classes.
this.setPageId(this.currentDocument.id);
this.setFolderId(this.currentDocument.id);
this.updateHostClasses();
}
updateSideNav() {
// Preserve current sidenav open state by default.
let openSideNav = this.sidenav.opened;
const isSideNavDoc = !!this.currentNodes[sideNavView];
if (this.isSideNavDoc !== isSideNavDoc) {
// View type changed. Is it now a sidenav view (e.g, guide or tutorial)?
// Open if changed to a sidenav doc; close if changed to a marketing doc.
openSideNav = this.isSideNavDoc = isSideNavDoc;
}
// May be open or closed when wide; always closed when narrow.
this.sidenav.toggle(this.dockSideNav && openSideNav);
}
// Dynamically change height of table of contents container
@HostListener('window:scroll')
onScroll() {
if (!this.tocMaxHeightOffset) {
// Must wait until `mat-toolbar` is measurable.
const el = this.hostElement.nativeElement as Element;
const headerEl = el.querySelector('.app-toolbar');
const footerEl = el.querySelector('footer');
if (headerEl && footerEl) {
this.tocMaxHeightOffset =
headerEl.clientHeight +
footerEl.clientHeight +
24; // fudge margin
}
}
this.tocMaxHeight = (document.body.scrollHeight - window.scrollY - this.tocMaxHeightOffset).toFixed(2);
}
// Restrain scrolling inside an element, when the cursor is over it
restrainScrolling(evt: WheelEvent) {
const elem = evt.currentTarget as Element;
const scrollTop = elem.scrollTop;
if (evt.deltaY < 0) {
// Trying to scroll up: Prevent scrolling if already at the top.
if (scrollTop < 1) {
evt.preventDefault();
}
} else {
// Trying to scroll down: Prevent scrolling if already at the bottom.
const maxScrollTop = elem.scrollHeight - elem.clientHeight;
if (maxScrollTop - scrollTop < 1) {
evt.preventDefault();
}
}
}
// Search related methods and handlers
hideSearchResults() {
this.showSearchResults = false;
const oldSearch = this.locationService.search();
if (oldSearch.search !== undefined) {
this.locationService.setSearch('', { ...oldSearch, search: undefined });
}
}
focusSearchBox() {
if (this.searchBox) {
this.searchBox.focus();
}
}
doSearch(query: string, fromFocus = false) {
if (this.showSearchResults && fromFocus) {
// the results where already being displayed so there is no
// need to perform the search until the input actually changes
return;
}
this.searchResults = this.searchService.search(query);
this.showSearchResults = !!query;
}
@HostListener('document:keyup', ['$event.key', '$event.which'])
onKeyUp(key: string, keyCode: number) {
// forward slash "/"
if (key === '/' || keyCode === 191) {
this.focusSearchBox();
}
if (key === 'Escape' || keyCode === 27) {
// escape key
if (this.showSearchResults) {
this.hideSearchResults();
this.focusSearchBox();
}
}
}
}
-194
View File
@@ -1,194 +0,0 @@
import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatIconRegistry } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { svg } from 'app/shared/security';
import { trustedResourceUrl, unwrapResourceUrl } from 'safevalues';
import { AppComponent } from 'app/app.component';
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
import { Deployment } from 'app/shared/deployment.service';
import { CookiesPopupComponent } from 'app/layout/cookies-popup/cookies-popup.component';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component';
import { AnalyticsService } from 'app/shared/analytics.service';
import { Logger } from 'app/shared/logger.service';
import { LocationService } from 'app/shared/location.service';
import { STORAGE_PROVIDERS } from 'app/shared/storage.service';
import { NavigationService } from 'app/navigation/navigation.service';
import { DocumentService } from 'app/documents/document.service';
import { SearchService } from 'app/search/search.service';
import { TopMenuComponent } from 'app/layout/top-menu/top-menu.component';
import { FooterComponent } from 'app/layout/footer/footer.component';
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
import { ReportingErrorHandler } from 'app/shared/reporting-error-handler';
import { ScrollService } from 'app/shared/scroll.service';
import { ScrollSpyService } from 'app/shared/scroll-spy.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { NotificationComponent } from 'app/layout/notification/notification.component';
import { TocService } from 'app/shared/toc.service';
import { CurrentDateToken, currentDateProvider } from 'app/shared/current-date';
import { WindowToken, windowProvider } from 'app/shared/window';
import { CustomElementsModule } from 'app/custom-elements/custom-elements.module';
import { SharedModule } from 'app/shared/shared.module';
import { ThemeToggleComponent } from 'app/shared/theme-picker/theme-toggle.component';
import { environment } from '../environments/environment';
// These are the hardcoded inline svg sources to be used by the `<mat-icon>` component.
/* eslint-disable max-len */
export const svgIconProviders = [
{
provide: SVG_ICONS,
useValue: {
name: 'close',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
name: 'insert_comment',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
name: 'keyboard_arrow_right',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
name: 'menu',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
</svg>`,
},
multi: true,
},
// Namespace: logos
{
provide: SVG_ICONS,
useValue: {
namespace: 'logos',
name: 'github',
svgSource:
svg`<svg focusable="false" viewBox="0 0 51.8 50.4" xmlns="http://www.w3.org/2000/svg">
<path d="M25.9,0.2C11.8,0.2,0.3,11.7,0.3,25.8c0,11.3,7.3,20.9,17.5,24.3c1.3,0.2,1.7-0.6,1.7-1.2c0-0.6,0-2.6,0-4.8
c-7.1,1.5-8.6-3-8.6-3c-1.2-3-2.8-3.7-2.8-3.7c-2.3-1.6,0.2-1.6,0.2-1.6c2.6,0.2,3.9,2.6,3.9,2.6c2.3,3.9,6,2.8,7.5,2.1
c0.2-1.7,0.9-2.8,1.6-3.4c-5.7-0.6-11.7-2.8-11.7-12.7c0-2.8,1-5.1,2.6-6.9c-0.3-0.7-1.1-3.3,0.3-6.8c0,0,2.1-0.7,7,2.6
c2-0.6,4.2-0.9,6.4-0.9c2.2,0,4.4,0.3,6.4,0.9c4.9-3.3,7-2.6,7-2.6c1.4,3.5,0.5,6.1,0.3,6.8c1.6,1.8,2.6,4.1,2.6,6.9
c0,9.8-6,12-11.7,12.6c0.9,0.8,1.7,2.4,1.7,4.7c0,3.4,0,6.2,0,7c0,0.7,0.5,1.5,1.8,1.2c10.2-3.4,17.5-13,17.5-24.3
C51.5,11.7,40.1,0.2,25.9,0.2z" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
namespace: 'logos',
name: 'twitter',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
namespace: 'logos',
name: 'youtube',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M21.58 7.19c-.23-.86-.91-1.54-1.77-1.77C18.25 5 12 5 12 5s-6.25 0-7.81.42c-.86.23-1.54.91-1.77 1.77
C2 8.75 2 12 2 12s0 3.25.42 4.81c.23.86.91 1.54 1.77 1.77C5.75 19 12 19 12 19s6.25 0 7.81-.42
c.86-.23 1.54-.91 1.77-1.77C22 15.25 22 12 22 12s0-3.25-.42-4.81zM10 15V9l5.2 3-5.2 3z" />
</svg>`,
},
multi: true,
},
];
/* eslint-enable max-len */
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule.withConfig({disableAnimations: AppComponent.reducedMotion}),
CustomElementsModule,
HttpClientModule,
MatButtonModule,
MatProgressBarModule,
MatSidenavModule,
MatToolbarModule,
SharedModule,
ServiceWorkerModule.register(
// Make sure service worker is loaded with a TrustedScriptURL
unwrapResourceUrl(trustedResourceUrl`/ngsw-worker.js`) as string,
{enabled: environment.production}),
],
declarations: [
AppComponent,
CookiesPopupComponent,
DocViewerComponent,
DtComponent,
FooterComponent,
ModeBannerComponent,
NavMenuComponent,
NavItemComponent,
SearchBoxComponent,
NotificationComponent,
TopMenuComponent,
ThemeToggleComponent,
],
providers: [
AnalyticsService,
Deployment,
DocumentService,
{ provide: ErrorHandler, useClass: ReportingErrorHandler },
Logger,
Location,
{ provide: LocationStrategy, useClass: PathLocationStrategy },
LocationService,
{ provide: MatIconRegistry, useClass: CustomIconRegistry },
NavigationService,
ScrollService,
ScrollSpyService,
SearchService,
STORAGE_PROVIDERS,
svgIconProviders,
TocService,
{ provide: CurrentDateToken, useFactory: currentDateProvider },
{ provide: WindowToken, useFactory: windowProvider },
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
@@ -1,116 +0,0 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { AnnouncementBarComponent } from './announcement-bar.component';
const today = new Date();
const lastWeek = changeDays(today, -7);
const yesterday = changeDays(today, -1);
const tomorrow = changeDays(today, 1);
const nextWeek = changeDays(today, 7);
describe('AnnouncementBarComponent', () => {
let element: HTMLElement;
let fixture: ComponentFixture<AnnouncementBarComponent>;
let component: AnnouncementBarComponent;
let httpMock: HttpTestingController;
let mockLogger: MockLogger;
beforeEach(() => {
const injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [AnnouncementBarComponent],
providers: [{ provide: Logger, useClass: MockLogger }]
});
httpMock = injector.inject(HttpTestingController);
mockLogger = injector.inject(Logger) as any;
fixture = TestBed.createComponent(AnnouncementBarComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});
it('should have no announcement when first created', () => {
expect(component.announcement).toBeUndefined();
});
describe('ngOnInit', () => {
it('should make a single request to the server', () => {
component.ngOnInit();
httpMock.expectOne('generated/announcements.json');
expect().nothing(); // Prevent jasmine from complaining about no expectations.
});
it('should set the announcement to the first "live" one in the list loaded from `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.flush([
{ startDate: lastWeek, endDate: yesterday, message: 'Test Announcement 0' },
{ startDate: tomorrow, endDate: nextWeek, message: 'Test Announcement 1' },
{ startDate: yesterday, endDate: tomorrow, message: 'Test Announcement 2' },
{ startDate: yesterday, endDate: tomorrow, message: 'Test Announcement 3' }
]);
expect(component.announcement.message).toEqual('Test Announcement 2');
});
it('should set the announcement to `undefined` if there are no announcements in `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.flush([]);
expect(component.announcement).toBeUndefined();
});
it('should handle invalid data in `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.flush('some random response');
expect(component.announcement).toBeUndefined();
expect(mockLogger.output.error).toEqual([
[jasmine.any(Error)]
]);
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json contains invalid data:/);
});
it('should handle a failed request for `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.error(new ProgressEvent('404'));
expect(component.announcement).toBeUndefined();
expect(mockLogger.output.error).toEqual([
[jasmine.any(Error)]
]);
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json request failed:/);
});
});
describe('rendering', () => {
beforeEach(() => {
component.announcement = {
imageUrl: 'dummy/image',
linkUrl: 'link/to/website',
message: 'this is an <b>important</b> message',
endDate: '2018-03-01',
startDate: '2018-02-01'
};
fixture.detectChanges();
});
it('should display the message as HTML', () => {
expect(element.innerHTML).toContain('this is an <b>important</b> message');
});
it('should display an image', () => {
expect(element.querySelector('img')?.src).toContain('dummy/image');
});
it('should display a link', () => {
expect(element.querySelector('a')?.href).toContain('link/to/website');
});
});
});
function changeDays(initial: Date, days: number) {
return (new Date(initial.valueOf()).setDate(initial.getDate() + days));
}
@@ -1,85 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, map } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
const announcementsPath = CONTENT_URL_PREFIX + 'announcements.json';
export interface Announcement {
imageUrl: string;
message: string;
linkUrl: string;
startDate: string;
endDate: string;
}
/**
* Display the latest live announcement. This is used on the homepage.
*
* The data for the announcements is kept in `aio/content/marketing/announcements.json`.
*
* The format for that data file looks like:
*
* ```
* [
* {
* "startDate": "2018-02-01",
* "endDate": "2018-03-01",
* "message": "This is an <b>important</b> announcement",
* "imageUrl": "url/to/image",
* "linkUrl": "url/to/website"
* },
* ...
* ]
* ```
*
* Only one announcement will be shown at any time. This is determined as the first "live"
* announcement in the file, where "live" means that its start date is before today, and its
* end date is after today.
*
* **Security Note:**
* The `message` field can contain unsanitized HTML but this field should only updated by
* verified members of the Angular team.
*/
@Component({
selector: 'aio-announcement-bar',
template: `
<div class="homepage-container" *ngIf="announcement">
<div class="announcement-bar">
<img [src]="announcement.imageUrl" alt="">
<p [innerHTML]="announcement.message"></p>
<a class="button" [href]="announcement.linkUrl">Learn More</a>
</div>
</div>`
})
export class AnnouncementBarComponent implements OnInit {
announcement: Announcement;
constructor(private http: HttpClient, private logger: Logger) {}
ngOnInit() {
this.http.get<Announcement[]>(announcementsPath)
.pipe(
catchError(error => {
this.logger.error(new Error(`${announcementsPath} request failed: ${error.message}`));
return [];
}),
map(announcements => this.findCurrentAnnouncement(announcements)),
catchError(error => {
this.logger.error(new Error(`${announcementsPath} contains invalid data: ${error.message}`));
return [];
}),
)
.subscribe(announcement => this.announcement = announcement);
}
/**
* Get the first date in the list that is "live" now
*/
private findCurrentAnnouncement(announcements: Announcement[]) {
return announcements
.filter(announcement => new Date(announcement.startDate).valueOf() < Date.now())
.filter(announcement => new Date(announcement.endDate).valueOf() > Date.now())
[0];
}
}
@@ -1,14 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { SharedModule } from '../../shared/shared.module';
import { AnnouncementBarComponent } from './announcement-bar.component';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, SharedModule, HttpClientModule ],
declarations: [ AnnouncementBarComponent ]
})
export class AnnouncementBarModule implements WithCustomElementComponent {
customElementComponent: Type<any> = AnnouncementBarComponent;
}
@@ -1,37 +0,0 @@
<div class="l-flex-wrap api-filter">
<aio-select (change)="setType($event.option)"
[options]="types"
[selected]="type"
[showSymbol]="true"
label="Type:">
</aio-select>
<aio-select (change)="setStatus($event.option)"
[options]="statuses"
[selected]="status"
[disabled]="type.value === 'package'"
label="Status:">
</aio-select>
<div class="form-search">
<input #filter placeholder="Filter" (input)="setQuery($any($event.target).value)" aria-label="Filter Search">
<em class="material-icons">search</em>
</div>
</div>
<article class="api-list-container">
<div *ngFor="let section of filteredSections | async" >
<h2 *ngIf="section.items"><a [href]="section.path" [class.deprecated-api-item]="section.deprecated">{{section.title}}</a></h2>
<ul class="api-list" *ngIf="section.items && section.items.length">
<ng-container *ngFor="let item of section.items">
<li class="api-item">
<a [href]="item.path" [class.deprecated-api-item]="item.stability === 'deprecated'">
<span class="symbol {{item.docType}}"></span>
{{item.title}}
</a>
</li>
</ng-container>
</ul>
</div>
</article>
@@ -1,317 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BehaviorSubject } from 'rxjs';
import { ApiListComponent } from './api-list.component';
import { ApiItem, ApiSection, ApiService } from './api.service';
import { LocationService } from 'app/shared/location.service';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { ApiListModule } from './api-list.module';
describe('ApiListComponent', () => {
let component: ApiListComponent;
let fixture: ComponentFixture<ApiListComponent>;
let sections: ApiSection[];
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ ApiListModule ],
providers: [
{ provide: ApiService, useClass: TestApiService },
{ provide: Logger, useClass: MockLogger },
{ provide: LocationService, useClass: TestLocationService }
]
});
fixture = TestBed.createComponent(ApiListComponent);
component = fixture.componentInstance;
sections = getApiSections();
});
/**
* Expectation Utility: Assert that filteredSections has the expected result for this test.
*
* @param itemTest - return true if the item passes the match test
*
* Subscribes to `filteredSections` and performs expectation within subscription callback.
*/
function expectFilteredResult(label: string, itemTest: (item: ApiItem) => boolean) {
component.filteredSections.subscribe(filtered => {
filtered = filtered.filter(section => section.items);
expect(filtered.length).withContext('expected something').toBeGreaterThan(0);
expect(filtered.every(section => section.items?.every(itemTest))).withContext(label).toBe(true);
});
}
describe('#filteredSections', () => {
beforeEach(() => {
fixture.detectChanges();
});
it('should return all complete sections when no criteria', () => {
let filtered: ApiSection[]|undefined;
component.filteredSections.subscribe(f => filtered = f);
expect(filtered).toEqual(sections);
});
it('item.show should be true for all queried items', () => {
component.setQuery('class');
expectFilteredResult('query: class', item => /class/.test(item.name));
});
it('items should be an array for every item in section when query matches section name', () => {
component.setQuery('core');
component.filteredSections.subscribe(filtered => {
filtered = filtered.filter(section => Array.isArray(section.items));
expect(filtered.length).withContext('only one section').toBe(1);
expect(filtered[0].name).toBe('core');
expect(filtered[0].items).toEqual(sections.find(section => section.name === 'core')?.items as ApiItem[]);
});
});
describe('section.items', () => {
it('should null if there are no matching items and the section itself does not match', () => {
component.setQuery('core');
component.filteredSections.subscribe(filtered => {
const commonSection = filtered.find(section => section.name === 'common');
expect(commonSection?.items).toBe(null);
});
});
it('should be visible if they have the selected stability status', () => {
component.setStatus({value: 'stable', title: 'Stable'});
expectFilteredResult('status: stable', item => item.stability === 'stable');
});
it('should be visible if they have the selected security status', () => {
component.setStatus({value: 'security-risk', title: 'Security Risk'});
expectFilteredResult('status: security-risk', item => item.securityRisk);
});
it('should be visible if they have the selected developer preview status', () => {
component.setStatus({value: 'developer-preview', title: 'Developer Preview'});
expectFilteredResult('status: developer-preview', item => item.developerPreview);
});
it('should be visible if they match the selected API type', () => {
component.setType({value: 'class', title: 'Class'});
expectFilteredResult('type: class', item => item.docType === 'class');
});
});
it('should have no sections and no items visible when there is no match', () => {
component.setQuery('fizbuzz');
component.filteredSections.subscribe(filtered => {
expect(filtered.some(section => !!section.items)).toBeFalsy();
});
});
});
describe('initial criteria from location', () => {
let locationService: TestLocationService;
beforeEach(() => {
locationService = fixture.componentRef.injector.get<any>(LocationService);
});
function expectOneItem(name: string, section: string, type: string, stability: string) {
fixture.detectChanges();
component.filteredSections.subscribe(filtered => {
filtered = filtered.filter(s => s.items);
expect(filtered.length).withContext('sections').toBe(1);
expect(filtered[0].name).withContext('section name').toBe(section);
const items = filtered[0].items as ApiItem[];
expect(items.length).withContext('items').toBe(1);
const item = items[0];
const badItem = 'Wrong item: ' + JSON.stringify(item, null, 2);
expect(item.docType).withContext(badItem).toBe(type);
expect(item.stability).withContext(badItem).toBe(stability);
expect(item.name).withContext(badItem).toBe(name);
});
}
it('should filter as expected for ?query', () => {
locationService.query = {query: '_3'};
expectOneItem('class_3', 'core', 'class', 'experimental');
});
it('should filter as expected for ?status', () => {
locationService.query = {status: 'deprecated'};
expectOneItem('function_1', 'core', 'function', 'deprecated');
});
it('should filter as expected when status is security-risk', () => {
locationService.query = {status: 'security-risk'};
fixture.detectChanges();
expectFilteredResult('security-risk', item => item.securityRisk);
});
it('should filter as expected when status is developer-preview', () => {
locationService.query = {status: 'developer-preview'};
fixture.detectChanges();
expectFilteredResult('developer-preview', item => item.developerPreview);
});
it('should filter as expected for ?type', () => {
locationService.query = {type: 'pipe'};
expectOneItem('pipe_1', 'common', 'pipe', 'stable');
});
it('should filter as expected for ?query&status&type', () => {
locationService.query = {
query: 's_1',
status: 'experimental',
type: 'class'
};
fixture.detectChanges();
expectOneItem('class_1', 'common', 'class', 'experimental');
});
it('should ignore case for ?query&status&type', () => {
locationService.query = {
query: 'S_1',
status: 'ExperiMental',
type: 'CLASS'
};
fixture.detectChanges();
expectOneItem('class_1', 'common', 'class', 'experimental');
});
});
describe('location path after criteria change', () => {
let locationService: TestLocationService;
beforeEach(() => {
locationService = fixture.componentRef.injector.get<any>(LocationService);
});
it('should have query', () => {
component.setQuery('foo');
// `setSearch` 2nd param is a query/search params object
const search = locationService.setSearch.calls.mostRecent().args[1];
expect(search.query).toBe('foo');
});
it('should keep last of multiple query settings (in lowercase)', () => {
component.setQuery('foo');
component.setQuery('fooBar');
const search = locationService.setSearch.calls.mostRecent().args[1];
expect(search.query).toBe('foobar');
});
it('should have query, status, and type', () => {
component.setQuery('foo');
component.setStatus({value: 'stable', title: 'Stable'});
component.setType({value: 'class', title: 'Class'});
const search = locationService.setSearch.calls.mostRecent().args[1];
expect(search.query).toBe('foo');
expect(search.status).toBe('stable');
expect(search.type).toBe('class');
});
});
});
////// Helpers ////////
class TestLocationService {
query: {[index: string]: string } = {};
setSearch = jasmine.createSpy('setSearch');
search() { return this.query; }
}
class TestApiService {
sectionsSubject = new BehaviorSubject(getApiSections());
sections = this.sectionsSubject.asObservable();
}
const apiSections: ApiSection[] = [
{
name: 'common',
title: 'common',
path: 'api/common',
deprecated: false,
items: [
{
name: 'class_1',
title: 'Class 1',
path: 'api/common/class_1',
docType: 'class',
stability: 'experimental',
securityRisk: false,
developerPreview: false
},
{
name: 'class_2',
title: 'Class 2',
path: 'api/common/class_2',
docType: 'class',
stability: 'stable',
securityRisk: false,
developerPreview: false
},
{
name: 'directive_1',
title: 'Directive 1',
path: 'api/common/directive_1',
docType: 'directive',
stability: 'stable',
securityRisk: true,
developerPreview: false
},
{
name: 'pipe_1',
title: 'Pipe 1',
path: 'api/common/pipe_1',
docType: 'pipe',
stability: 'stable',
securityRisk: true,
developerPreview: false
},
],
},
{
name: 'core',
title: 'core',
path: 'api/core',
deprecated: false,
items: [
{
name: 'class_3',
title: 'Class 3',
path: 'api/core/class_3',
docType: 'class',
stability: 'experimental',
securityRisk: false,
developerPreview: false
},
{
name: 'function_1',
title: 'Function 1',
path: 'api/core/function 1',
docType: 'function',
stability: 'deprecated',
securityRisk: true,
developerPreview: false
},
{
name: 'const_1',
title: 'Const 1',
path: 'api/core/const_1',
docType: 'const',
stability: 'stable',
securityRisk: false,
developerPreview: true
},
],
},
];
function getApiSections() { return apiSections; }
@@ -1,173 +0,0 @@
/*
* API List & Filter Component
*
* A page that displays a formatted list of the public Angular API entities.
* Clicking on a list item triggers navigation to the corresponding API entity document.
* Can add/remove API entity links based on filter settings.
*/
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { combineLatest, Observable, ReplaySubject } from 'rxjs';
import { LocationService } from 'app/shared/location.service';
import { ApiItem, ApiSection, ApiService } from './api.service';
import { Option } from 'app/shared/select/select.component';
import { map } from 'rxjs/operators';
class SearchCriteria {
query = '';
status = 'all';
type = 'all';
}
@Component({
selector: 'aio-api-list',
templateUrl: './api-list.component.html',
})
export class ApiListComponent implements OnInit {
filteredSections: Observable<ApiSection[]>;
showStatusMenu = false;
showTypeMenu = false;
private criteriaSubject = new ReplaySubject<SearchCriteria>(1);
private searchCriteria = new SearchCriteria();
status: Option;
type: Option;
// API types
types: Option[] = [
{ value: 'all', title: 'All' },
{ value: 'class', title: 'Class' },
{ value: 'const', title: 'Const'},
{ value: 'decorator', title: 'Decorator' },
{ value: 'directive', title: 'Directive' },
{ value: 'element', title: 'Element'},
{ value: 'block', title: 'Block'},
{ value: 'enum', title: 'Enum' },
{ value: 'function', title: 'Function' },
{ value: 'interface', title: 'Interface' },
{ value: 'package', title: 'Package'},
{ value: 'pipe', title: 'Pipe'},
{ value: 'ngmodule', title: 'NgModule'},
{ value: 'type-alias', title: 'Type alias' },
];
statuses: Option[] = [
{ value: 'all', title: 'All' },
{ value: 'stable', title: 'Stable'},
{ value: 'developer-preview', title: 'Developer Preview'},
{ value: 'deprecated', title: 'Deprecated' },
{ value: 'security-risk', title: 'Security Risk' }
];
@ViewChild('filter', { static: true }) queryEl: ElementRef;
constructor(
private apiService: ApiService,
private locationService: LocationService) { }
ngOnInit() {
this.filteredSections =
combineLatest([
this.apiService.sections,
this.criteriaSubject,
]).pipe(
map( results => ({ sections: results[0], criteria: results[1]})),
map( results => (
results.sections
.map(section => ({ ...section, items: this.filterSection(section, results.criteria) }))
))
);
this.initializeSearchCriteria();
}
// TODO: may need to debounce as the original did
// although there shouldn't be any perf consequences if we don't
setQuery(query: string) {
this.setSearchCriteria({query: (query || '').toLowerCase().trim() });
}
setStatus(status: Option) {
this.toggleStatusMenu();
this.status = status;
this.setSearchCriteria({status: status.value});
}
setType(type: Option) {
this.toggleTypeMenu();
this.type = type;
this.setSearchCriteria({type: type.value});
}
toggleStatusMenu() {
this.showStatusMenu = !this.showStatusMenu;
}
toggleTypeMenu() {
this.showTypeMenu = !this.showTypeMenu;
}
//////// Private //////////
private filterSection(section: ApiSection, { query, status, type }: SearchCriteria) {
const sectionNameMatches = !query || section.name.indexOf(query) !== -1;
const matchesQuery = (item: ApiItem) =>
sectionNameMatches || item.name.indexOf(query) !== -1;
const matchesStatus = (item: ApiItem) =>
status === 'all' ||
status === item.stability ||
(status === 'security-risk' && item.securityRisk) ||
(status === 'developer-preview' && item.developerPreview);
const matchesType = (item: ApiItem) =>
type === 'all' || type === item.docType;
const items: ApiItem[] = (section.items || []).filter(item =>
matchesType(item) && matchesStatus(item) && matchesQuery(item));
// If there are no items we still return an empty array if the section name matches and the type is 'package'
return items.length ? items : (sectionNameMatches && type === 'package') ? [] : null;
}
// Get initial search criteria from URL search params
private initializeSearchCriteria() {
const {query, status, type} = this.locationService.search();
const q = (query || '').toLowerCase();
// Hack: can't bind to query because input cursor always forced to end-of-line.
this.queryEl.nativeElement.value = q;
this.status = this.statuses.find(x => x.value === status) || this.statuses[0];
this.type = this.types.find(x => x.value === type) || this.types[0];
this.searchCriteria = {
query: q,
status: this.status.value,
type: this.type.value
};
this.criteriaSubject.next(this.searchCriteria);
}
private setLocationSearch() {
const {query, status, type} = this.searchCriteria;
const params = {
query: query ? query : undefined,
status: status !== 'all' ? status : undefined,
type: type !== 'all' ? type : undefined
};
this.locationService.setSearch('API Search', params);
}
private setSearchCriteria(criteria: Partial<SearchCriteria>) {
this.criteriaSubject.next(Object.assign(this.searchCriteria, criteria));
this.setLocationSearch();
}
}
@@ -1,16 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { SharedModule } from '../../shared/shared.module';
import { ApiListComponent } from './api-list.component';
import { ApiService } from './api.service';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, SharedModule, HttpClientModule ],
declarations: [ ApiListComponent ],
providers: [ ApiService ]
})
export class ApiListModule implements WithCustomElementComponent {
customElementComponent: Type<any> = ApiListComponent;
}
@@ -1,125 +0,0 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';
import { ApiService } from './api.service';
describe('ApiService', () => {
let injector: Injector;
let service: ApiService;
let httpMock: HttpTestingController;
beforeEach(() => {
injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
ApiService,
{ provide: Logger, useClass: TestLogger }
]
});
service = injector.get<ApiService>(ApiService);
httpMock = injector.get(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('should not immediately connect to the server', () => {
httpMock.expectNone({});
expect().nothing(); // Prevent jasmine from complaining about no expectations.
});
it('subscribers should be completed/unsubscribed when service destroyed', () => {
let completed = false;
service.sections.subscribe({complete: () => completed = true});
service.ngOnDestroy();
expect(completed).toBe(true);
// Stop `httpMock.verify()` from complaining.
httpMock.expectOne({});
});
describe('#sections', () => {
it('first subscriber should fetch sections', done => {
const data = [
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
];
service.sections.subscribe(sections => {
expect(sections).toEqual(data);
done();
});
httpMock.expectOne({}).flush(data);
});
it('second subscriber should get previous sections and NOT trigger refetch', done => {
const data = [
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
];
let subscriptions = 0;
service.sections.subscribe(sections => {
subscriptions++;
expect(sections).toEqual(data);
});
service.sections.subscribe(sections => {
subscriptions++;
expect(sections).toEqual(data);
expect(subscriptions).toBe(2);
done();
});
httpMock.expectOne({}).flush(data);
});
});
describe('#fetchSections', () => {
it('should connect to the server w/ expected URL', () => {
service.fetchSections();
httpMock.expectOne('generated/docs/api/api-list.json');
expect().nothing(); // Prevent jasmine from complaining about no expectations.
});
it('should refresh the #sections observable w/ new content on second call', () => {
let call = 0;
let data = [
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
];
service.sections.subscribe(sections => {
// called twice during this test
// (1) during subscribe
// (2) after refresh
expect(sections).withContext('call ' + call++).toEqual(data);
});
httpMock.expectOne({}).flush(data);
// refresh/refetch
data = [{name: 'c', title: 'C', path: '', items: [], deprecated: false}];
service.fetchSections();
httpMock.expectOne({}).flush(data);
expect(call).withContext('should be called twice').toBe(2);
});
});
});
class TestLogger {
log = jasmine.createSpy('log');
error = jasmine.createSpy('error');
}
@@ -1,90 +0,0 @@
import { Injectable, OnDestroy } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { ReplaySubject, Subject } from 'rxjs';
import { takeUntil, tap } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service';
import { DOC_CONTENT_URL_PREFIX } from 'app/documents/document.service';
export interface ApiItem {
name: string;
title: string;
path: string;
docType: string;
stability: string;
securityRisk: boolean;
developerPreview: boolean;
}
export interface ApiSection {
path: string;
name: string;
title: string;
deprecated: boolean;
items: ApiItem[]|null;
}
@Injectable()
export class ApiService implements OnDestroy {
private apiBase = DOC_CONTENT_URL_PREFIX + 'api/';
private apiListJsonDefault = 'api-list.json';
private firstTime = true;
private onDestroy = new Subject<void>();
private sectionsSubject = new ReplaySubject<ApiSection[]>(1);
private _sections = this.sectionsSubject.pipe(takeUntil(this.onDestroy));
/**
* Return a cached observable of API sections from a JSON file.
* API sections is an array of Angular top modules and metadata about their API documents (items).
*/
get sections() {
if (this.firstTime) {
this.firstTime = false;
this.fetchSections(); // TODO: get URL for fetchSections by configuration?
// makes sectionsSubject hot; subscribe ensures stays alive (always refCount > 0);
this._sections.subscribe(sections => this.logger.log(`ApiService got API ${sections.length} section(s)`));
}
return this._sections.pipe(tap(sections => {
sections.forEach(section => {
section.deprecated = !!section.items &&
section.items.every(item => item.stability === 'deprecated');
});
}));
}
constructor(private http: HttpClient, private logger: Logger) { }
ngOnDestroy() {
this.onDestroy.next();
}
/**
* Fetch API sections from a JSON file.
* API sections is an array of Angular top modules and metadata about their API documents (items).
* Updates `sections` observable
*
* @param [src] - Name of the api list JSON file
*/
fetchSections(src?: string) {
// TODO: get URL by configuration?
const url = this.apiBase + (src || this.apiListJsonDefault);
this.http.get<ApiSection[]>(url)
.pipe(
takeUntil(this.onDestroy),
tap(() => this.logger.log(`Got API sections from ${url}`)),
)
.subscribe({
next: sections => this.sectionsSubject.next(sections),
error: (err: HttpErrorResponse) => {
// TODO: handle error
this.logger.error(err);
throw err; // rethrow for now.
},
});
}
}
@@ -1,106 +0,0 @@
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CodeExampleComponent } from './code-example.component';
import { CodeExampleModule } from './code-example.module';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { PrettyPrinter } from './pretty-printer.service';
describe('CodeExampleComponent', () => {
let hostComponent: HostComponent;
let codeExampleComponent: CodeExampleComponent;
let fixture: ComponentFixture<HostComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ CodeExampleModule ],
declarations: [
HostComponent,
],
providers: [
{ provide: Logger, useClass: MockLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
]
});
fixture = TestBed.createComponent(HostComponent);
fixture.detectChanges();
hostComponent = fixture.componentInstance;
codeExampleComponent = hostComponent.codeExampleComponent;
});
it('should be able to capture the code snippet provided in content', () => {
expect(codeExampleComponent.aioCode.code.toString().trim()).toBe('const foo = "bar";');
});
it('should clean-up the projected code snippet once captured', () => {
expect(codeExampleComponent.content.nativeElement.innerHTML).toBe('');
});
it('should change aio-code classes based on header presence', () => {
expect(codeExampleComponent.header).toBe('Great Example');
expect(fixture.nativeElement.querySelector('header')).toBeTruthy();
const aioCodeEl = fixture.nativeElement.querySelector('aio-code');
expect(aioCodeEl).toHaveClass('headed-code');
expect(aioCodeEl).not.toHaveClass('simple-code');
codeExampleComponent.header = '';
fixture.detectChanges();
expect(codeExampleComponent.header).toBe('');
expect(fixture.nativeElement.querySelector('header')).toBeFalsy();
expect(aioCodeEl).not.toHaveClass('headed-code');
expect(aioCodeEl).toHaveClass('simple-code');
});
it('should set avoidFile class if path has .avoid.', () => {
const codeExampleComponentElement: HTMLElement =
fixture.nativeElement.querySelector('code-example');
expect(codeExampleComponent.path).toBe('code-path');
expect(codeExampleComponentElement.className.indexOf('avoidFile') === -1).toBe(true);
codeExampleComponent.path = 'code-path.avoid.';
fixture.detectChanges();
expect(codeExampleComponentElement.className.indexOf('avoidFile') === -1).toBe(false);
});
it('should coerce hidecopy', () => {
expect(codeExampleComponent.hidecopy).toBe(false);
hostComponent.hidecopy = true;
fixture.detectChanges();
expect(codeExampleComponent.hidecopy).toBe(true);
hostComponent.hidecopy = 'false';
fixture.detectChanges();
expect(codeExampleComponent.hidecopy).toBe(false);
hostComponent.hidecopy = 'true';
fixture.detectChanges();
expect(codeExampleComponent.hidecopy).toBe(true);
});
});
@Component({
selector: 'aio-host-comp',
template: `
<code-example [header]="header" [path]="path" [hidecopy]="hidecopy">
{{code}}
</code-example>
`
})
class HostComponent {
code = 'const foo = "bar";';
header = 'Great Example';
path = 'code-path';
hidecopy: boolean | string = false;
@ViewChild(CodeExampleComponent, {static: true}) codeExampleComponent: CodeExampleComponent;
}
@@ -1,86 +0,0 @@
/* eslint-disable @angular-eslint/component-selector */
import { Component, HostBinding, ElementRef, ViewChild, Input, AfterViewInit } from '@angular/core';
import { fromInnerHTML } from 'app/shared/security';
import { CodeComponent } from './code.component';
/**
* An embeddable code block that displays nicely formatted code.
* Example usage:
*
* ```
* <code-example language="ts" linenums="2" class="special" header="Do Stuff">
* // a code block
* console.log('do stuff');
* </code-example>
* ```
*/
@Component({
selector: 'code-example',
template: `
<!-- Content projection is used to get the content HTML provided to this component -->
<div #content style="display: none"><ng-content></ng-content></div>
<header *ngIf="header">{{header}}</header>
<aio-code [class.headed-code]="!!this.header"
[class.simple-code]="!this.header"
[language]="language"
[linenums]="linenums"
[path]="path"
[region]="region"
[hideCopy]="hidecopy"
[header]="header">
</aio-code>
`,
})
export class CodeExampleComponent implements AfterViewInit {
classes: { 'headed-code': boolean, 'simple-code': boolean };
@Input() language: string;
@Input() linenums: string;
@Input() region: string;
@Input() header: string;
@Input()
set path(path: string) {
this._path = path;
this.isAvoid = this.path.indexOf('.avoid.') !== -1;
}
get path(): string { return this._path; }
private _path = '';
@Input()
set hidecopy(hidecopy: boolean) {
// Coerce the boolean value.
this._hidecopy = hidecopy != null && `${hidecopy}` !== 'false';
}
get hidecopy(): boolean { return this._hidecopy; }
private _hidecopy: boolean;
/* eslint-disable-next-line @angular-eslint/no-input-rename */
@Input('hide-copy')
set hyphenatedHideCopy(hidecopy: boolean) {
this.hidecopy = hidecopy;
}
/* eslint-disable-next-line @angular-eslint/no-input-rename */
@Input('hideCopy')
set capitalizedHideCopy(hidecopy: boolean) {
this.hidecopy = hidecopy;
}
@HostBinding('class.avoidFile') isAvoid = false;
@ViewChild('content', { static: true }) content: ElementRef<HTMLDivElement>;
@ViewChild(CodeComponent, { static: true }) aioCode: CodeComponent;
ngAfterViewInit() {
const contentElem = this.content.nativeElement;
this.aioCode.code = fromInnerHTML(contentElem);
contentElem.textContent = ''; // Remove DOM nodes that are no longer needed.
}
}
@@ -1,14 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CodeExampleComponent } from './code-example.component';
import { CodeModule } from './code.module';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, CodeModule ],
declarations: [ CodeExampleComponent ],
exports: [ CodeExampleComponent ]
})
export class CodeExampleModule implements WithCustomElementComponent {
customElementComponent: Type<any> = CodeExampleComponent;
}
@@ -1,104 +0,0 @@
import { Component, ViewChild, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CodeTabsComponent } from './code-tabs.component';
import { CodeTabsModule } from './code-tabs.module';
import { PrettyPrinter } from './pretty-printer.service';
describe('CodeTabsComponent', () => {
let fixture: ComponentFixture<HostComponent>;
let hostComponent: HostComponent;
let codeTabsComponent: CodeTabsComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ HostComponent ],
imports: [ CodeTabsModule, NoopAnimationsModule ],
schemas: [ NO_ERRORS_SCHEMA ],
providers: [
{ provide: Logger, useClass: MockLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
]
});
fixture = TestBed.createComponent(HostComponent);
fixture.detectChanges();
hostComponent = fixture.componentInstance;
codeTabsComponent = hostComponent.codeTabsComponent;
});
it('should get correct tab info', () => {
const tabs = codeTabsComponent.tabs;
expect(tabs.length).toBe(2);
// First code pane expectations
expect(tabs[0].class).toBe('class-A');
expect(tabs[0].language).toBe('language-A');
expect(tabs[0].linenums).toBe('linenums-A');
expect(tabs[0].path).toBe('path-A');
expect(tabs[0].region).toBe('region-A');
expect(tabs[0].header).toBe('header-A');
expect(tabs[0].code.toString().trim()).toBe('Code example 1');
// Second code pane expectations
expect(tabs[1].class).toBe('class-B');
expect(tabs[1].language).toBe('language-B');
expect(tabs[1].linenums).withContext('Default linenums should have been used')
.toBe('default-linenums');
expect(tabs[1].path).toBe('path-B');
expect(tabs[1].region).toBe('region-B');
expect(tabs[1].header).toBe('header-B');
expect(tabs[1].code.toString().trim()).toBe('Code example 2');
});
it('should create the right number of tabs with the right labels and classes', () => {
const matTabs = fixture.nativeElement.querySelectorAll('.mdc-tab__text-label');
expect(matTabs.length).toBe(2);
expect(matTabs[0].textContent.trim()).toBe('header-A');
expect(matTabs[0].querySelector('.class-A')).toBeTruthy();
expect(matTabs[1].textContent.trim()).toBe('header-B');
expect(matTabs[1].querySelector('.class-B')).toBeTruthy();
});
it('should show the first tab with the right code', () => {
const codeContent = fixture.nativeElement.querySelector('aio-code').textContent;
expect(codeContent.indexOf('Code example 1') !== -1).toBeTruthy();
});
it('should clean-up the projected tabs content once captured', () => {
expect(codeTabsComponent.content.nativeElement.innerHTML).toBe('');
});
});
@Component({
selector: 'aio-host-comp',
template: `
<code-tabs linenums="default-linenums">
<code-pane class="class-A"
language="language-A"
linenums="linenums-A"
path="path-A"
region="region-A"
header="header-A">
Code example 1
</code-pane>
<code-pane class="class-B"
language="language-B"
path="path-B"
region="region-B"
header="header-B">
Code example 2
</code-pane>
</code-tabs>
`
})
class HostComponent {
@ViewChild(CodeTabsComponent, {static: true}) codeTabsComponent: CodeTabsComponent;
}
@@ -1,89 +0,0 @@
/* eslint-disable @angular-eslint/component-selector */
import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { fromInnerHTML } from 'app/shared/security';
import { CodeComponent } from './code.component';
export interface TabInfo {
class: string;
code: TrustedHTML;
path: string;
region: string;
header?: string;
language?: string;
linenums?: string;
}
/**
* Renders a set of tab group of code snippets.
*
* The innerHTML of the `<code-tabs>` component should contain `<code-pane>` elements.
* Each `<code-pane>` has the same interface as the embedded `<code-example>` component.
* The optional `linenums` attribute is the default `linenums` for each code pane.
*/
@Component({
selector: 'code-tabs',
template: `
<!-- Use content projection so that the provided HTML's code-panes can be split into tabs -->
<div #content style="display: none"><ng-content></ng-content></div>
<mat-card>
<mat-tab-group class="code-tab-group" [disableRipple]="true">
<mat-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
<ng-template mat-tab-label>
<span class="{{ tab.class }}">{{ tab.header }}</span>
</ng-template>
<aio-code class="{{ tab.class }}"
[language]="tab.language"
[linenums]="tab.linenums"
[path]="tab.path"
[region]="tab.region"
[header]="tab.header">
</aio-code>
</mat-tab>
</mat-tab-group>
</mat-card>
`,
})
export class CodeTabsComponent implements OnInit, AfterViewInit {
tabs: TabInfo[];
@Input() linenums: string | undefined;
@ViewChild('content', { static: true }) content: ElementRef<HTMLDivElement>;
@ViewChildren(CodeComponent) codeComponents: QueryList<CodeComponent>;
ngOnInit() {
this.tabs = [];
const contentElem = this.content.nativeElement;
const codeExamples = Array.from(contentElem.querySelectorAll('code-pane'));
// Remove DOM nodes that are no longer needed.
contentElem.textContent = '';
for (const tabContent of codeExamples) {
this.tabs.push(this.getTabInfo(tabContent));
}
}
ngAfterViewInit() {
this.codeComponents.toArray().forEach((codeComponent, i) => {
codeComponent.code = this.tabs[i].code;
});
}
/** Gets the extracted TabInfo data from the provided code-pane element. */
private getTabInfo(tabContent: Element): TabInfo {
return {
class: tabContent.getAttribute('class') || '',
code: fromInnerHTML(tabContent),
path: tabContent.getAttribute('path') || '',
region: tabContent.getAttribute('region') || '',
header: tabContent.getAttribute('header') || undefined,
language: tabContent.getAttribute('language') || undefined,
linenums: tabContent.getAttribute('linenums') || this.linenums,
};
}
}
@@ -1,16 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CodeTabsComponent } from './code-tabs.component';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { CodeModule } from './code.module';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, MatCardModule, MatTabsModule, CodeModule ],
declarations: [ CodeTabsComponent ],
exports: [ CodeTabsComponent ]
})
export class CodeTabsModule implements WithCustomElementComponent {
customElementComponent: Type<any> = CodeTabsComponent;
}
@@ -1,324 +0,0 @@
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatSnackBar } from '@angular/material/snack-bar';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Clipboard } from '@angular/cdk/clipboard';
import { CodeComponent } from './code.component';
import { CodeModule } from './code.module';
import { Logger } from 'app/shared/logger.service';
import { htmlEscape } from 'safevalues';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { PrettyPrinter } from './pretty-printer.service';
const oneLineCode = 'const foo = "bar";';
const smallMultiLineCode = `<hero-details>
<h2>Bah Dah Bing</h2>
<hero-team>
<h3>NYC Team</h3>
</hero-team>
</hero-details>`;
const bigMultiLineCode = `${smallMultiLineCode}\n${smallMultiLineCode}\n${smallMultiLineCode}`;
describe('CodeComponent', () => {
let hostComponent: HostComponent;
let fixture: ComponentFixture<HostComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ NoopAnimationsModule, CodeModule ],
declarations: [ HostComponent ],
providers: [
{ provide: Logger, useClass: TestLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
]
});
fixture = TestBed.createComponent(HostComponent);
hostComponent = fixture.componentInstance;
fixture.detectChanges();
});
describe('pretty printing', () => {
const getFormattedCode = () => fixture.nativeElement.querySelector('code').innerHTML;
it('should format a one-line code sample without linenums by default', () => {
hostComponent.setCode(oneLineCode);
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${oneLineCode}`);
});
it('should add line numbers to one-line code sample when linenums is `true`', () => {
hostComponent.setCode(oneLineCode);
hostComponent.linenums = true;
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${oneLineCode}`);
});
it('should add line numbers to one-line code sample when linenums is `\'true\'`', () => {
hostComponent.setCode(oneLineCode);
hostComponent.linenums = 'true';
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${oneLineCode}`);
});
it('should format a small multi-line code sample without linenums by default', () => {
hostComponent.setCode(smallMultiLineCode);
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${htmlEscape(smallMultiLineCode)}`);
});
it('should add line numbers to a small multi-line code sample when linenums is `true`', () => {
hostComponent.setCode(smallMultiLineCode);
hostComponent.linenums = true;
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${htmlEscape(smallMultiLineCode)}`);
});
it('should add line numbers to a small multi-line code sample when linenums is `\'true\'`', () => {
hostComponent.setCode(smallMultiLineCode);
hostComponent.linenums = 'true';
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${htmlEscape(smallMultiLineCode)}`);
});
it('should format a big multi-line code without linenums by default', () => {
hostComponent.setCode(bigMultiLineCode);
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${htmlEscape(bigMultiLineCode)}`);
});
it('should add line numbers to a big multi-line code sample when linenums is `true`', () => {
hostComponent.setCode(bigMultiLineCode);
hostComponent.linenums = true;
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${htmlEscape(bigMultiLineCode)}`);
});
it('should add line numbers to a big multi-line code sample when linenums is `\'true\'`', () => {
hostComponent.setCode(bigMultiLineCode);
hostComponent.linenums = 'true';
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${htmlEscape(bigMultiLineCode)}`);
});
it('should skip prettify if language is `\'none\'`', () => {
hostComponent.setCode(bigMultiLineCode);
hostComponent.language = 'none';
fixture.detectChanges();
expect(getFormattedCode()).toBe(htmlEscape(bigMultiLineCode).toString());
});
});
describe('whitespace handling', () => {
it('should remove common indentation from the code before rendering', () => {
hostComponent.linenums = false;
fixture.detectChanges();
hostComponent.setCode(`
abc
let x = text.split('\\n');
ghi
jkl
`);
const codeContent = fixture.nativeElement.querySelector('code').textContent;
expect(codeContent).toEqual(
'Formatted code (language: auto, linenums: false): abc\n let x = text.split(\'\\n\');\nghi\n\njkl');
});
it('should trim whitespace from the code before rendering', () => {
hostComponent.linenums = false;
fixture.detectChanges();
hostComponent.setCode('\n\n\n' + smallMultiLineCode + '\n\n\n');
const codeContent = fixture.nativeElement.querySelector('code').textContent;
expect(codeContent).toEqual(codeContent.trim());
});
it('should trim whitespace from code before computing whether to format linenums', () => {
hostComponent.setCode('\n\n\n' + oneLineCode + '\n\n\n');
// `<li>`s are a tell-tale for line numbers
const lis = fixture.nativeElement.querySelectorAll('li');
expect(lis.length).withContext('should be no linenums').toBe(0);
});
});
describe('error message', () => {
function getErrorMessage() {
const missing: HTMLElement = fixture.nativeElement.querySelector('.code-missing');
return missing ? missing.textContent : null;
}
it('should not display "code-missing" class when there is some code', () => {
expect(getErrorMessage()).withContext('should not have element with "code-missing" class').toBeNull();
});
it('should display error message when there is no code (after trimming)', () => {
hostComponent.setCode(' \n ');
expect(getErrorMessage()).toContain('missing');
});
it('should show path and region in missing-code error message', () => {
hostComponent.path = 'fizz/buzz/foo.html';
hostComponent.region = 'something';
fixture.detectChanges();
hostComponent.setCode(' \n ');
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html#something$/);
});
it('should show path only in missing-code error message when no region', () => {
hostComponent.path = 'fizz/buzz/foo.html';
fixture.detectChanges();
hostComponent.setCode(' \n ');
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html$/);
});
it('should show simple missing-code error message when no path/region', () => {
hostComponent.setCode(' \n ');
expect(getErrorMessage()).toMatch(/missing.$/);
});
});
describe('copy button', () => {
function getButton() {
const btnDe = fixture.debugElement.query(By.css('button'));
return btnDe ? btnDe.nativeElement : null;
}
it('should be hidden if the `hideCopy` input is true', () => {
hostComponent.hideCopy = true;
fixture.detectChanges();
expect(getButton()).toBe(null);
});
it('should have title', () => {
expect(getButton().title).toBe('Copy code snippet');
});
it('should have no aria-label by default', () => {
expect(getButton().getAttribute('aria-label')).toBe('');
});
it('should have aria-label explaining what is being copied when header passed in', () => {
hostComponent.header = 'a/b/c/foo.ts';
fixture.detectChanges();
expect(getButton().getAttribute('aria-label')).toContain(hostComponent.header);
});
it('should call copier service when clicked', () => {
const clipboard = TestBed.inject(Clipboard);
const spy = spyOn(clipboard, 'copy');
expect(spy.calls.count()).withContext('before click').toBe(0);
getButton().click();
expect(spy.calls.count()).withContext('after click').toBe(1);
});
it('should copy code text when clicked', () => {
const clipboard = TestBed.inject(Clipboard);
const spy = spyOn(clipboard, 'copy');
getButton().click();
expect(spy.calls.argsFor(0)[0]).withContext('after click').toBe(oneLineCode);
});
it('should preserve newlines in the copied code', () => {
const clipboard = TestBed.inject(Clipboard);
const spy = spyOn(clipboard, 'copy');
const expectedCode = smallMultiLineCode.trim();
let actualCode;
hostComponent.setCode(smallMultiLineCode);
[false, true, 42].forEach(linenums => {
hostComponent.linenums = linenums;
fixture.detectChanges();
getButton().click();
actualCode = spy.calls.mostRecent().args[0];
expect(actualCode).withContext(`when linenums=${linenums}`).toBe(expectedCode);
expect(actualCode.match(/\r?\n/g)?.length).toBe(5);
spy.calls.reset();
});
});
it('should display a message when copy succeeds', () => {
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
const clipboard = TestBed.inject(Clipboard);
spyOn(snackBar, 'open');
spyOn(clipboard, 'copy').and.returnValue(true);
getButton().click();
expect(snackBar.open).toHaveBeenCalledWith('Code Copied', '', { duration: 800 });
});
it('should display an error when copy fails', () => {
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
const clipboard = TestBed.inject(Clipboard);
const logger = TestBed.inject(Logger) as unknown as TestLogger;
spyOn(snackBar, 'open');
spyOn(clipboard, 'copy').and.returnValue(false);
getButton().click();
expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 });
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(jasmine.any(Error));
expect(logger.error.calls.mostRecent().args[0].message).toMatch(/^ERROR copying code to clipboard:/);
});
});
});
//// Test helpers ////
@Component({
selector: 'aio-host-comp',
template: `
<aio-code [language]="language"
[linenums]="linenums" [path]="path" [region]="region"
[hideCopy]="hideCopy" [header]="header"></aio-code>
`
})
class HostComponent implements AfterViewInit {
hideCopy: boolean;
language: string;
linenums: boolean | number | string;
path: string;
region: string;
header: string;
@ViewChild(CodeComponent, {static: false}) codeComponent: CodeComponent;
ngAfterViewInit() {
this.setCode(oneLineCode);
}
/** Changes the displayed code on the code component. */
setCode(code: string) {
this.codeComponent.code = htmlEscape(code);
}
}
class TestLogger {
log = jasmine.createSpy('log');
error = jasmine.createSpy('error');
}
@@ -1,204 +0,0 @@
import { Component, ElementRef, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core';
import { Clipboard } from '@angular/cdk/clipboard';
import { Logger } from 'app/shared/logger.service';
import { PrettyPrinter } from './pretty-printer.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { unwrapHtml } from 'safevalues';
import { htmlSafeByReview } from 'safevalues/restricted/reviewed';
import { fromOuterHTML } from 'app/shared/security';
/**
* Formatted Code Block
*
* Pretty renders a code block, used in the docs and API reference by the code-example and
* code-tabs embedded components.
* It includes a "copy" button that will send the content to the clipboard when clicked
*
* Example usage:
*
* ```
* <aio-code
* [language]="ts"
* [linenums]="true"
* [path]="router/src/app/app.module.ts"
* [region]="animations-module">
* </aio-code>
* ```
*
*
* Renders code provided through the `updateCode` method.
*/
@Component({
selector: 'aio-code',
template: `
<pre class="prettyprint lang-{{language}}">
<button *ngIf="!hideCopy" class="material-icons copy-button no-print"
title="Copy code snippet"
[attr.aria-label]="ariaLabel"
(click)="doCopy()">
<span aria-hidden="true">content_copy</span>
</button>
<code class="animated fadeIn" #codeContainer></code>
</pre>
`
})
export class CodeComponent implements OnChanges {
ariaLabel = '';
/** The code to be copied when clicking the copy button, this should not be HTML encoded */
private codeText: string;
/** Code that should be formatted with current inputs and displayed in the view. */
set code(code: TrustedHTML) {
this._code = code;
if (!this._code.toString().trim()) {
this.showMissingCodeMessage();
} else {
this.formatDisplayedCode();
}
}
get code(): TrustedHTML {
return this._code;
}
_code: TrustedHTML;
/** Whether the copy button should be shown. */
@Input() hideCopy: boolean;
/** Language to render the code (e.g. javascript, typescript). */
@Input() language: string | undefined;
/**
* Whether to display line numbers:
* - If false: hide
* - If true: show
* - If number: show but start at that number
*/
@Input() linenums: boolean | number | string | undefined;
/** Path to the source of the code. */
@Input() path: string;
/** Region of the source of the code being displayed. */
@Input() region: string;
/** Optional header to be displayed above the code. */
@Input()
set header(header: string | undefined) {
this._header = header;
this.ariaLabel = this.header ? `Copy code snippet from ${this.header}` : '';
}
get header(): string|undefined { return this._header; }
private _header: string | undefined;
@Output() codeFormatted = new EventEmitter<void>();
/** The element in the template that will display the formatted code. */
@ViewChild('codeContainer', { static: true }) codeContainer: ElementRef;
constructor(
private snackbar: MatSnackBar,
private pretty: PrettyPrinter,
private clipboard: Clipboard,
private logger: Logger) {}
ngOnChanges() {
// If some inputs have changed and there is code displayed, update the view with the latest
// formatted code.
if (this.code) {
this.formatDisplayedCode();
}
}
private formatDisplayedCode() {
const linenums = this.getLinenums();
const leftAlignedCode = leftAlign(this.code);
this.setCodeHtml(leftAlignedCode); // start with unformatted code
this.codeText = this.getCodeText(); // store the unformatted code as text (for copying)
const skipPrettify = of(undefined);
const prettifyCode = this.pretty
.formatCode(leftAlignedCode, this.language, linenums)
.pipe(tap(formattedCode => this.setCodeHtml(formattedCode)));
if (linenums !== false && this.language === 'none') {
this.logger.warn("Using 'linenums' with 'language: none' is currently not supported.");
}
((this.language === 'none' ? skipPrettify : prettifyCode) as Observable<unknown>)
.subscribe({
next: () => this.codeFormatted.emit(),
error: () => { /* ignore failure to format */ },
});
}
/** Sets the message showing that the code could not be found. */
private showMissingCodeMessage() {
const src = this.path ? this.path + (this.region ? '#' + this.region : '') : '';
const msg = `The code sample is missing${src ? ` for\n${src}` : '.'}`;
const el = document.createElement('p');
el.className = 'code-missing';
el.textContent = msg;
this.setCodeHtml(fromOuterHTML(el));
}
/** Sets the innerHTML of the code container to the provided code string. */
private setCodeHtml(formattedCode: TrustedHTML) {
// **Security:** Code example content is provided by docs authors and as such its considered to
// be safe for innerHTML purposes.
this.codeContainer.nativeElement.innerHTML = unwrapHtml(formattedCode);
}
/** Gets the textContent of the displayed code element. */
private getCodeText() {
// `prettify` may remove newlines, e.g. when `linenums` are on. Retrieve the content of the
// container as text, before prettifying it.
// We take the textContent because we don't want it to be HTML encoded.
return this.codeContainer.nativeElement.textContent;
}
/** Copies the code snippet to the user's clipboard. */
doCopy() {
const code = this.codeText;
const successfullyCopied = this.clipboard.copy(code);
if (successfullyCopied) {
this.logger.log('Copied code to clipboard:', code);
this.snackbar.open('Code Copied', '', { duration: 800 });
} else {
this.logger.error(new Error(`ERROR copying code to clipboard: "${code}"`));
this.snackbar.open('Copy failed. Please try again!', '', { duration: 800 });
}
}
/** Gets the calculated value of linenums (boolean/number). */
getLinenums() {
const linenums =
typeof this.linenums === 'boolean' ? this.linenums :
this.linenums === 'true' ? true :
this.linenums === 'false' ? false :
typeof this.linenums === 'string' ? parseInt(this.linenums, 10) :
this.linenums;
return (linenums != null) && !isNaN(linenums as number) && linenums;
}
}
function leftAlign(text: TrustedHTML): TrustedHTML {
let indent = Number.MAX_VALUE;
const lines = text.toString().split('\n');
lines.forEach(line => {
const lineIndent = line.search(/\S/);
if (lineIndent !== -1) {
indent = Math.min(lineIndent, indent);
}
});
return htmlSafeByReview(
lines.map(line => line.slice(indent)).join('\n').trim(),
'safe manipulation of existing trusted HTML');
}
@@ -1,13 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CodeComponent } from './code.component';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { PrettyPrinter } from './pretty-printer.service';
@NgModule({
imports: [ CommonModule, MatSnackBarModule ],
declarations: [ CodeComponent ],
exports: [ CodeComponent ],
providers: [ PrettyPrinter ]
})
export class CodeModule { }
@@ -1,65 +0,0 @@
import { Injectable } from '@angular/core';
import { htmlSafeByReview } from 'safevalues/restricted/reviewed';
import { from, Observable } from 'rxjs';
import { first, map, share } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service';
type PrettyPrintOne = (code: TrustedHTML, language?: string, linenums?: number|boolean) => string;
/**
* Wrapper around the prettify.js library
*/
@Injectable()
export class PrettyPrinter {
private prettyPrintOne: Observable<PrettyPrintOne>;
constructor(private logger: Logger) {
this.prettyPrintOne = from(this.getPrettyPrintOne()).pipe(share());
}
private getPrettyPrintOne(): Promise<PrettyPrintOne> {
const ppo = (window as any).prettyPrintOne;
return ppo ? Promise.resolve(ppo) :
// `prettyPrintOne` is not on `window`, which means `prettify.js` has not been loaded yet.
// Import it; ad a side-effect it will add `prettyPrintOne` on `window`.
import('assets/js/prettify.js' as any)
.then(
() => (window as any).prettyPrintOne,
err => {
const msg = `Cannot get prettify.js from server: ${err.message}`;
this.logger.error(new Error(msg));
// return a pretty print fn that always fails.
return () => { throw new Error(msg); };
});
}
/**
* Format code snippet as HTML.
*
* @param code - the code snippet to format; should already be HTML encoded
* @param [language] - The language of the code to render (could be javascript, html, typescript, etc)
* @param [linenums] - Whether to display line numbers:
* - false: don't display
* - true: do display
* - number: do display but start at the given number
* @returns Observable<string> - Observable of formatted code
*/
formatCode(code: TrustedHTML, language?: string, linenums?: number|boolean) {
return this.prettyPrintOne.pipe(
map(ppo => {
try {
return htmlSafeByReview(
ppo(code, language, linenums), 'prettify.js modifies already trusted HTML inline');
} catch (err) {
const msg = `Could not format code that begins '${code.toString().slice(0, 50)}...'.`;
console.error(msg, err);
throw new Error(msg);
}
}),
first(), // complete immediately
);
}
}
@@ -1,109 +0,0 @@
import { Injector } from '@angular/core';
import { of } from 'rxjs';
import { ContributorGroup } from './contributors.model';
import { ContributorListComponent } from './contributor-list.component';
import { ContributorService } from './contributor.service';
import { LocationService } from 'app/shared/location.service';
// Testing the component class behaviors, independent of its template
// Let e2e tests verify how it displays.
describe('ContributorListComponent', () => {
let component: ContributorListComponent;
let injector: Injector;
let contributorService: TestContributorService;
let locationService: TestLocationService;
let contributorGroups: ContributorGroup[];
beforeEach(() => {
injector = Injector.create({
providers: [
{provide: ContributorListComponent, deps: [ContributorService, LocationService] },
{provide: ContributorService, useClass: TestContributorService, deps: [] },
{provide: LocationService, useClass: TestLocationService, deps: [] }
]
});
locationService = injector.get(LocationService) as unknown as TestLocationService;
contributorService = injector.get(ContributorService) as unknown as TestContributorService;
contributorGroups = contributorService.testContributors;
});
it('should select the first group when no query string', () => {
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[0]);
});
it('should select the first group when query string w/o "group" property', () => {
locationService.searchResult = { foo: 'GDE' };
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[0]);
});
it('should select the first group when query group not found', () => {
locationService.searchResult = { group: 'foo' };
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[0]);
});
it('should select the GDE group when query group is "GDE"', () => {
locationService.searchResult = { group: 'GDE' };
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[1]);
});
it('should select the GDE group when query group is "gde" (case insensitive)', () => {
locationService.searchResult = { group: 'gde' };
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[1]);
});
it('should set the query to the "GDE" group when user selects "GDE"', () => {
component = getComponent();
component.selectGroup('GDE');
expect(locationService.searchResult.group).toBe('GDE');
});
it('should set the query to the first group when user selects unknown name', () => {
component = getComponent();
component.selectGroup('GDE'); // a legit group that isn't the first
component.selectGroup('foo'); // not a legit group name
expect(locationService.searchResult.group).toBe('Angular');
});
//// Test Helpers ////
function getComponent(): ContributorListComponent {
const comp = injector.get(ContributorListComponent);
comp.ngOnInit();
return comp;
}
interface SearchResult {
[index: string]: string;
}
class TestLocationService {
searchResult: SearchResult = {};
search = jasmine.createSpy('search').and.callFake(() => this.searchResult);
setSearch = jasmine.createSpy('setSearch')
.and.callFake((_label: string, result: SearchResult) => {
this.searchResult = result;
});
}
class TestContributorService {
testContributors = getTestData();
contributors = of(this.testContributors);
}
function getTestData(): ContributorGroup[] {
return [
// Not interested in the contributors data in these tests
{ name: 'Angular', order: 0, contributors: [] },
{ name: 'GDE', order: 1, contributors: [] },
];
}
});
@@ -1,48 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ContributorGroup } from './contributors.model';
import { ContributorService } from './contributor.service';
import { LocationService } from 'app/shared/location.service';
@Component({
selector: 'aio-contributor-list',
template: `
<div class="flex-center group-buttons">
<button *ngFor="let name of groupNames"
class="button mat-button filter-button"
[class.selected]="name === selectedGroup.name"
(click)="selectGroup(name)">{{name}}</button>
</div>
<section *ngIf="selectedGroup" class="grid-fluid">
<div class="contributor-group">
<aio-contributor *ngFor="let person of selectedGroup.contributors"
[person]="person"></aio-contributor>
</div>
</section>
`,
})
export class ContributorListComponent implements OnInit {
private groups: ContributorGroup[];
groupNames: string[];
selectedGroup: ContributorGroup;
constructor(
private contributorService: ContributorService,
private locationService: LocationService) { }
ngOnInit() {
const groupName = this.locationService.search().group || '';
// no need to unsubscribe because `contributors` completes
this.contributorService.contributors
.subscribe(grps => {
this.groups = grps;
this.groupNames = grps.map(g => g.name);
this.selectGroup(groupName);
});
}
selectGroup(name: string) {
name = name.toLowerCase();
this.selectedGroup = this.groups.find(g => g.name.toLowerCase() === name) || this.groups[0];
this.locationService.setSearch('', {group: this.selectedGroup.name});
}
}
@@ -1,16 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { ContributorListComponent } from './contributor-list.component';
import { ContributorService } from './contributor.service';
import { ContributorComponent } from './contributor.component';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, MatIconModule ],
declarations: [ ContributorListComponent, ContributorComponent ],
providers: [ ContributorService ]
})
export class ContributorListModule implements WithCustomElementComponent {
customElementComponent: Type<any> = ContributorListComponent;
}

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