Compare commits

...

68 Commits

Author SHA1 Message Date
73c7882629 release: cut the v10.1.3 release 2020-09-23 14:54:47 -04:00
32e32e5bdc refactor(dev-infra): simplify runBenchmark (#38941)
* Make url and params optional in runBenchmark
* Make url optional in openBrowser
* Remove unused code from runBenchmark

PR Close #38941
2020-09-22 15:05:34 -07:00
0dda8bf265 refactor(zone.js): remove usages of blacklist related to UNPATCHED_EVENTS (#38930)
Remove usages of blacklist around UNPATCHED_EVENTS configuration

PR Close #38930
2020-09-22 15:05:01 -07:00
8db865d9b0 docs: Update titles to getting started topics (#38887)
The topics for our getting started tutorial are inconsistent.
This change makes the titles consistent and easier to read.

PR Close #38887
2020-09-21 16:33:29 -07:00
667c10a0f9 ci: Update payload size on 10.1.x to be correct (#38923)
Update payload size on 10.1.x to be correct after changing in
04d0aa6781.

PR Close #38923
2020-09-21 16:26:24 -07:00
02862338fa docs: change wrong default app module by ng new (#38549)
In bootstrapping.md the default AppModule has some extra imports which are not generated
by default in ng new removed those extra imports and add them at appropriate place.

PR Close #38549
2020-09-21 12:25:29 -07:00
8f523c1658 refactor(compiler): simplify visitor logic for attributes (#38899)
The logic for computing identifiers, specifically for bound attributes
can be simplified by using the value span of the binding rather than the
source span.

PR Close #38899
2020-09-21 12:23:59 -07:00
790e483982 fix(docs-infra): remove scrollbar styles for accessibility (#38852)
This commit removes the scrollbar styles so that the default
styles in the browser render. This widens the webkit scroll bar.
This makes it easier to grab the scrollbar using assistive
technology and devices, and provides a wider target for
those who have dexterity issues. By removing these styles,
We will no longer have to maintain custom scrollbars specific to WebKit
and they will be accessible by default.

PR Close #38852
2020-09-21 12:22:08 -07:00
a7650b0f76 test(docs-infra): replace deprecated ReflectiveInjector with Injector (#38897)
This commit replaces the old and slow ReflectiveInjector that was
deprecated in v5 with the new Injector.

PR Close #38897
2020-09-21 12:21:27 -07:00
04d0aa6781 revert: feat(router): better warning message when a router outlet has not been instantiated (#38920)
This reverts commit [1609815].
The warning that was added created more confusion than it solved and also warned for valid use-cases.

PR Close #38920
2020-09-21 12:18:45 -07:00
7d5b5153cf docs: Grammar fixes (#38900)
Changed several period into colons to be consistent throughout the doc.

Changed "If don't add the interface..." to "If _you_ don't add the interface..."

PR Close #38900
2020-09-18 16:52:52 -07:00
27a6e5a31c ci: do not require g3 checks for the changes in packages/zone.js/dist folder (#38901)
This commit updates ngbot config to avoid requesting google3 presubmit for the changes in
the `packages/zone.js/dist` folder (which is not synced into google3).

PR Close #38901
2020-09-18 16:48:17 -07:00
2700d88912 build: update to latest version of yarn (#38869)
Update the vendored version of yarn to the latest version.

PR Close #38869
2020-09-18 16:47:34 -07:00
3902ec0dbe fix(http): Fix error message when we call jsonp without importing HttpClientJsonpModule (#38756)
Currently, when we call jsonp method without importing HttpClientJsonpModule, an error message appears saying
'Attempted to construct Jsonp request without JsonpClientModule installed.' instance of 'Attempted to
construct Jsonp request without HttpClientJsonpModule installed.'

PR Close #38756
2020-09-18 11:20:37 -07:00
6f579b20f8 feat(dev-infra): output the number of new and fixed cycles (#38805)
This commit adds a logic to ouput the number of new and fixed cycles after running circular
dependency checker. This information is useful to better understand an impact of changes in case
the number of new/fixed cycles is relatively big.

PR Close #38805
2020-09-18 11:20:08 -07:00
c4a7516747 refactor(core): reduce the number of circular deps (#38805)
This commit updates several import statements in the core package to decrease the number of
cycles detected by the dependency checker tool.

PR Close #38805
2020-09-18 11:20:08 -07:00
33055da5c7 fix(zone.js): should invoke xhr send task when no response error occurs (#38836)
Close #38795

in the XMLHttpRequest patch, when get `readystatechange` event, zone.js try to
invoke `load` event listener first, then call `invokeTask` to finish the
`XMLHttpRequest::send` macroTask, but if the request failed because the
server can not be reached, the `load` event listener will not be invoked,
so the `invokeTask` of the `XMLHttpRequest::send` will not be triggered either,
so we will have a non finished macroTask there which will make the Zone
not stable, also memory leak.

So in this PR, if the `XMLHttpRequest.status = 0` when we get the `readystatechange`
event, that means something wents wrong before we reached the server, we need to
invoke the task to finish the macroTask.

PR Close #38836
2020-09-18 11:19:37 -07:00
fb163df6dc fix(dev-infra): skip husky git commit hooks while merging pull requests (#38888)
During the merge process, all validations have already been completed so git commit
hooks can be safely skipped.  This additionally, prevents errors from occuring which
would be caused the commit hooks executing, such as when yarn updates and then yarn
commands are unable to run within the same process.

PR Close #38888
2020-09-18 11:14:58 -07:00
6b05dc432b refactor(core): remove unused imports (#38818)
This commit removes some unused imports from the spec files.

PR Close #38818
2020-09-18 08:03:51 -07:00
093c3a10f6 fix(ngcc): fix compilation of ChangeDetectorRef in pipe constructors (#38892)
In #38666 we changed how ngcc deals with type expressions, where it
would now always emit the original type expression into the generated
code as a "local" type value reference instead of synthesizing new
imports using an "imported" type value reference. This was done as a fix
to properly deal with renamed symbols, however it turns out that the
compiler has special handling for certain imported symbols, e.g.
`ChangeDetectorRef` from `@angular/core`. The "local" type value
reference prevented this special logic from being hit, resulting in
incorrect compilation of pipe factories.

This commit fixes the issue by manually inspecting the import of the
type expression, in order to return an "imported" type value reference.
By manually inspecting the import we continue to handle renamed symbols.

Fixes #38883

PR Close #38892
2020-09-18 08:02:50 -07:00
a0756e9fa4 refactor(compiler): replace Comment nodes with leadingComments property (#38811)
Common AST formats such as TS and Babel do not use a separate
node for comments, but instead attach comments to other AST nodes.
Previously this was worked around in TS by creating a `NotEmittedStatement`
AST node to attach the comment to. But Babel does not have this facility,
so it will not be a viable approach for the linker.

This commit refactors the output AST, to remove the `CommentStmt` and
`JSDocCommentStmt` nodes. Instead statements have a collection of
`leadingComments` that are rendered/attached to the final AST nodes
when being translated or printed.

PR Close #38811
2020-09-18 08:01:29 -07:00
d8657ddb5c docs: updating the text of user input page to reflect real location of the forms page (#38802)
PR Close #38802
2020-09-18 08:00:44 -07:00
2f71995ef7 Update aio/content/marketing/events.json (#38874)
Co-authored-by: George Kalpakas <kalpakas.g@gmail.com>
PR Close #38874
2020-09-18 08:00:02 -07:00
32a8713620 docs: add upcoming angular conferences (#38874)
PR Close #38874
2020-09-18 07:59:58 -07:00
0958a8da61 docs: drop newEvent() compatibility function (#37251)
Because PhantomJS has been deprecated since March 2018, and `newEvent`
is very confusing for newcomers that read the testing documentation,
we remove it entirely, and instead assume most, if not all, newcomers
will run tests in Chrome as it is the default.

Fixes #23370

PR Close #37251
2020-09-17 09:31:23 -07:00
e64c0c3730 fix(dev-infra): handle no caretaker config being defined (#38862)
Properly handle cases where no caretaker config is provided to ng-dev commands

PR Close #38862
2020-09-16 15:22:13 -07:00
730277806b docs(forms): fix grammar in first sentence of reset function docs (#38872)
PR Close #38872
2020-09-16 15:20:33 -07:00
674620a5ed docs: fix typo in architecture.md guide (#38853)
PR Close #38853
2020-09-16 15:19:47 -07:00
997713e2bb docs(bazel): fix typo in BUILD.bazel comments (#38054)
Update BUILD.bazel

PR Close #38054
2020-09-16 15:15:23 -07:00
234e5af636 test(docs-infra): simplify EventsComponent tests and add more test cases (#36517)
This commit simplifies the tests of `EventsComponent` (by introducing a
`createMockEvent()` helper and getting rid of the irrelevant `Event`
fields) and adds tests for some more usecases. It also makes the tests
more robust by using Jasmine's `Clock` to mock the current date.

PR Close #36517
2020-09-16 15:14:32 -07:00
e43c701388 refactor(docs-infra): remove tooltip from links same as name (#36517)
In the events.json file most of tooltips are same as name so there
were of no use, as they were providing no extra information. So,
removed them from the events.json file

PR Close #36517
2020-09-16 15:14:30 -07:00
2e1264fb5d feat(docs-infra): created new widget for events page (#36517)
Data in events page was hardcoded and it is manually moved in the table.

Created a new events widget which will automatically move past and upcoming
events from events.json (`aio/content/marketing/events.json`) file to the
relevant table in the events tab

PR Close #36517
2020-09-16 15:14:27 -07:00
a89dcba0d6 test(docs-infra): improve typeahead example and add unit test (#34190)
This commit improves the typeahead example, by using the emitted input
value. It also adds a unit test to ensure that the example is working
as intended.

PR Close #34190
2020-09-16 15:13:11 -07:00
bd9f441370 test(docs-infra): add unit tests for rxjs examples (#34190)
This commit adds missing unit tests for all rxjs examples from the docs.

Closes #28017

PR Close #34190
2020-09-16 15:13:08 -07:00
fbcb66e70c release: cut the v10.1.2 release 2020-09-16 14:38:02 -07:00
3d94919800 refactor(dev-infra): refactor commit-message files (#38845)
Refactor the commit-message files to be consistent with how other ng-dev tooling
is structured.

PR Close #38845
2020-09-15 16:05:46 -07:00
af3b401e15 docs: add ngc to glossary (#36781)
ngc angular compiler was not mentioned in the glossary.
Glossary should contain the relevant terms in angular
which are hard to get. So, added a small defination of
ngc to the glossary

PR Close #36781
2020-09-15 11:29:56 -07:00
e4c12c8f9e test(ngcc): load standard files only once (#38840)
In the integration test suite of ngcc, we load a set of files from
`node_modules` into memory. This includes the `typescript` package and
`@angular` scoped packages, which account for a large number of large
files that needs to be loaded from disk. This commit moves this work
to the top-level, such that it doesn't have to be repeated in all tests.

PR Close #38840
2020-09-15 11:23:28 -07:00
ea36466060 perf(ngcc): reduce maximum worker count (#38840)
Recent optimizations to ngcc have significantly reduced the total time
it takes to process `node_modules`, to such extend that sharding across
multiple processes has become less effective. Previously, running
ngcc asynchronously would allow for up to 8 workers to be allocated,
however these workers have to repeat work that could otherwise be shared.
Because ngcc is now able to reuse more shared computations, the overhead
of multiple workers is increased and therefore becomes less effective.
As an additional benefit, having fewer workers requires less memory and
less startup time.

To give an idea, using the following test setup:

```bash
npx @angular/cli new perf-test
cd perf-test
yarn ng add @angular/material
./node_modules/.bin/ngcc --properties es2015 module main \
  --first-only --create-ivy-entry-points
```

We observe the following figures on CI:

|                   | 10.1.1    | PR #38840 |
| ----------------- | --------- | --------- |
| Sync              | 85s       | 25s       |
| Async (8 workers) | 22s       | 16s       |
| Async (4 workers) | -         | 11s       |

In addition to changing the default number of workers, ngcc will now
use the environment variable `NGCC_MAX_WORKERS` that may be configured
to either reduce or increase the number of workers.

PR Close #38840
2020-09-15 11:23:24 -07:00
58411e7ad9 perf(ngcc): introduce cache for sharing data across entry-points (#38840)
ngcc creates typically two `ts.Program` instances for each entry-point,
one for processing sources and another one for processing the typings.
The creation of these programs is somewhat expensive, as it concerns
module resolution and parsing of source files.

This commit implements several layers of caching to optimize the
creation of programs:

1. A shared module resolution cache across all entry-points within a
   single invocation of ngcc. Both the sources and typings program
   benefit from this cache.
2. Sharing the parsed `ts.SourceFile` for a single entry-point between
   the sources and typings program.
3. Sharing parsed `ts.SourceFile`s of TypeScript's default libraries
   across all entry-points within a single invocation. Some of these
   default library typings are large and therefore expensive to parse,
   so sharing the parsed source files across all entry-points offers
   a significant performance improvement.

Using a bare CLI app created using `ng new` + `ng add @angular/material`,
the above changes offer a 3-4x improvement in ngcc's processing time
when running synchronously and ~2x improvement for asynchronous runs.

PR Close #38840
2020-09-15 11:23:18 -07:00
84bd1a233d feat(dev-infra): include CI status check in the caretaker check (#38779)
Add a CI status check in the ng-dev caretaker check command.

PR Close #38779
2020-09-15 08:45:06 -07:00
ef13d8f33a fix(dev-infra): remove ANSI escape codes from log file outputs (#38792)
Remove the ANSI codes from the log file outputs to make the ng-dev log files
more readable.

PR Close #38792
2020-09-15 08:44:04 -07:00
dc4f85888e build: create temporary script for symbol extractor tests (#38819)
Creates a temporary script to running all symbol extractor tests.

PR Close #38819
2020-09-14 16:54:45 -07:00
2bdfb14be0 build: add tag to symbol-extractor tests (#38819)
Add a tag to symbol-extractor tests to allow for bazel querying.

PR Close #38819
2020-09-14 16:54:42 -07:00
26deef2d3e feat(dev-infra): allow local ng-dev configuration to error on invalid commit messages (#38784)
As part of the commit message conformance check, local commit message checks are
made to be warnings rather than failures. An additional local option is also in
place to allow for the commit message validation failures to be considered errors
instead.

PR Close #38784
2020-09-14 14:34:55 -07:00
f52a494248 docs(elements): convert the ng add command to code-example (#38834)
the command was hardly visible currently
changing it to shell for visibility

PR Close #38834
2020-09-14 14:31:10 -07:00
ebede67433 perf(compiler-cli): optimize computation of type-check scope information (#38843)
When type-checking a component, the declaring NgModule scope is used
to create a directive matcher that contains flattened directive metadata,
i.e. the metadata of a directive and its base classes. This computation
is done for all components, whereas the type-check scope is constant per
NgModule. Additionally, the flattening of metadata is constant per
directive instance so doesn't necessarily have to be recomputed for
each component.

This commit introduces a `TypeCheckScopes` class that is responsible
for flattening directives and computing the scope per NgModule. It
caches the computed results as appropriate to avoid repeated computation.

PR Close #38843
2020-09-14 13:07:08 -07:00
565840515c perf(compiler-cli): only emit directive/pipe references that are used (#38843)
For the compilation of a component, the compiler has to prepare some
information about the directives and pipes that are used in the template.
This information includes an expression for directives/pipes, for usage
within the compilation output. For large NgModule compilation scopes
this has shown to introduce a performance hotspot, as the generation of
expressions is quite expensive. This commit reduces the performance
overhead by only generating expressions for the directives/pipes that
are actually used within the template, significantly cutting down on
the compiler's resolve phase.

PR Close #38843
2020-09-14 13:07:03 -07:00
c62d1cb80a refactor(dev-infra): do not validate config file multiple times (#38808)
Currently we validate the configuration file on each `getConfig`
invocation. We can only validate once since the configuration
is cached.

Also while being at it, renames the cache variables to lower-case as those
do not represent constants (which are convention-wise upper case).

PR Close #38808
2020-09-14 08:34:19 -07:00
aa43cbf8c5 fix(dev-infra): correct build setup for dev-infra (#38815)
Correct's the missing dependencies and incorrect build dependencies for the
dev-infra bazel targets.

PR Close #38815
2020-09-11 13:59:35 -07:00
b05d79d14a build(router): update symbols for routing app (#38817)
This commit updates the golden symbol files for the routing app.

PR Close #38817
2020-09-11 13:20:39 -07:00
04c2bb9580 build: create sample router app (#38714)
This commit creates a sample router test application to introduce the
symbol tests. It serves as a guard to ensure that any future work on the
router package does not unintentionally increase the payload size.

PR Close #38714
2020-09-11 12:10:49 -07:00
ec2dbe7fb4 fix(compiler): detect pipes in ICUs in template binder (#38810)
Recent work on compiler internals in #38539 led to an unexpected failure,
where a pipe used exclusively inside of an ICU would no longer be
emitted into the compilation output. This caused runtime errors due to
missing pipes.

The issue occurred because the change in #38539 would determine the set
of used pipes up-front, independent from the template compilation using
the `R3TargetBinder`. However, `R3TargetBinder` did not consider
expressions within ICUs, so any pipe usages within those expressions
would not be detected. This fix unblocks #38539 and also concerns
upcoming linker work, given that prelink compilations would not go
through full template compilation but only `R3TargetBinder`.

PR Close #38810
2020-09-11 12:07:43 -07:00
8096c63c66 docs: clarify what fixes are merged to LTS versions (#38788)
PR Close #38788
2020-09-11 08:45:46 -07:00
6ff9e6e2bd refactor(dev-infra): add default params to runBenchmark (#38748)
* Use '' as the default for 'url'
* Use [] as the default for 'params'
* Use true as the default for 'ignoreBrowserSynchronization'

PR Close #38748
2020-09-11 08:44:38 -07:00
31d0ee4cbf docs: Move Displaying data topic and ToH tutorial (#38774)
Move the "Displaying data topic" into the Tutorials section.
Move the ToH tutorial to the top of the tutorials section.

PR Close #38774
2020-09-10 14:32:28 -07:00
a47383d1e8 fix(localize): ensure that formatOptions is optional (#38787)
Some lower level APIs are used by CLI, and requiring
the `formatOpions` argument at that level is a
breaking change. This commit makes it optional
at every level to avoid the breaking change.

PR Close #38787
2020-09-10 10:55:08 -07:00
9078187378 docs: add note about only one label target being allowed to be applied to a PR (#38793)
To clarify the use of GitHub labels for targetting PRs, a not about only being
allowed to apply one label is added.

PR Close #38793
2020-09-10 09:41:06 -07:00
dcb473db34 docs: update docs to reflect new PR targeting methods for release trains (#38401) (#38793)
As part of the migration to a common strategy/method for branching and releasing across
the main angular repositories, updates need to be made to the documentation. These changes
reflect the updates made and is based on the following document which describes the
merging label expectations: https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU

PR Close #38401

PR Close #38793
2020-09-10 09:41:00 -07:00
edb7f90363 fix(core): clear the RefreshTransplantedView when detached (#38768)
The `RefreshTransplantedView` flag is used to indicate that the view or one of its children
is transplanted and dirty, so it should still be refreshed as part of change detection.
This flag is set on the transplanted view itself as well setting a
counter on as its parents.
When a transplanted view is detached and still has this flag, it means
it got detached before it was refreshed. This can happen for "backwards
references" or transplanted views that are inserted at a location that
was already checked. In this case, we should decrement the parent
counters _and_ clear the flag on the detached view so it's not seen as
"transplanted" anymore (it is detached and has no parent counters to
adjust).

fixes #38619

PR Close #38768
2020-09-10 09:11:41 -07:00
9c51ba321e fix(router): Ensure routes are processed in priority order and only if needed (#38780)
There is a slight difference between `map`...`concatAll` and `concatMap`
in that the latter (`concatMap`) will ensure that the computations are
executed in-order and only if needed while the former may execute the
`map` body of all items if they do not emit immediately. That is, if the stream
is
`from([a, b, c]).pipe(map(v => of(v).pipe(delay(1))), concatAll(), first())`
the `map` body will execute for all of `a`, `b`, and `c`.
However, the following will only execute the `concatMap` body for `a`
`from([a, b, c]).pipe(concatMap(v => of(v).pipe(delay(1))), first())`

See https://stackblitz.com/edit/rxjs-cvwxyx

fixes #38691

PR Close #38780
2020-09-10 08:54:39 -07:00
d8714d045d refactor(core): _reset() remove nextRecord (#38752)
The nextRecord is not neccessary, so remove it and use record._nextMoved to iterate

PR Close #38752
2020-09-10 08:52:54 -07:00
5de2ac3e1b fix(upgrade): add try/catch when downgrading injectables (#38671)
This commit improves the error thrown by the downgrade module with a more
descriptive message on why the downgrade is failing.

Closes #37579

PR Close #38671
2020-09-10 08:50:18 -07:00
7669bd856f feat(dev-infra): Allow local user ng-dev configuration to disable commit message wizard (#38701)
As not all users, particularly contributors consistently contributing with a deep
understanding of our commit message guidelines, will not want to rely on the
commit message wizard, we allow a user to opt out of using this wizard during
commit message creation.

PR Close #38701
2020-09-09 16:31:17 -07:00
18d911d807 feat(dev-infra): Add support for local user ng-dev configuration (#38701)
Create a utility for loading a local user configuration object to describe
local configuration values, such as skipping the commit message wizard.

PR Close #38701
2020-09-09 16:31:17 -07:00
38ff66dc32 docs: put docs style guide back in left nav (#38683)
PR Close #38683
2020-09-09 16:22:58 -07:00
5672aba2f9 refactor(dev-infra): update commit message validation to return validation result (#38703)
Previously, the validateCommitMessage function ran validation and logged the results.
The validateCommitMessage function now returns an object containing the validation
results and the cli action functions are instead responsible for logging the results.

This is being done as a prefactor for a change which allows for commit message
hook validation to be either a blocking error or a warning.

PR Close #38703
2020-09-09 16:22:36 -07:00
5567bdc48e test(language-service): [ivy] remove all markers from test (#38777)
In the test project there are no longer reference markers and location
markers, so there's no need to "pre-process" the source files to remove
them. This will make the Ivy tests cleaner and faster.

PR Close #38777
2020-09-09 16:21:56 -07:00
174 changed files with 6327 additions and 2676 deletions

View File

@ -68,6 +68,7 @@ merge:
- "packages/**/integrationtest/**"
- "packages/**/test/**"
- "packages/zone.js/*"
- "packages/zone.js/dist/**"
- "packages/zone.js/doc/**"
- "packages/zone.js/example/**"
- "packages/zone.js/scripts/**"

3
.gitignore vendored
View File

@ -40,6 +40,9 @@ yarn-error.log
# User specific bazel settings
.bazelrc.user
# User specific ng-dev settings
.ng-dev.user*
.notes.md
baseline.json

View File

@ -44654,7 +44654,7 @@ const FOLDERS_IGNORE = [
const DEFAULT_IGNORE = (0, (_filter || _load_filter()).ignoreLinesToRegex)([...FOLDERS_IGNORE,
// ignore cruft
'yarn.lock', '.lock-wscript', '.wafpickle-{0..9}', '*.swp', '._*', 'npm-debug.log', 'yarn-error.log', '.npmrc', '.yarnrc', '.npmignore', '.gitignore', '.DS_Store']);
'yarn.lock', '.lock-wscript', '.wafpickle-{0..9}', '*.swp', '._*', 'npm-debug.log', 'yarn-error.log', '.npmrc', '.yarnrc', '.yarnrc.yml', '.npmignore', '.gitignore', '.DS_Store']);
const NEVER_IGNORE = (0, (_filter || _load_filter()).ignoreLinesToRegex)([
// never ignore these files
@ -44663,6 +44663,7 @@ const NEVER_IGNORE = (0, (_filter || _load_filter()).ignoreLinesToRegex)([
function packWithIgnoreAndHeaders(cwd, ignoreFunction, { mapHeader } = {}) {
return tar.pack(cwd, {
ignore: ignoreFunction,
sort: true,
map: header => {
const suffix = header.name === '.' ? '' : `/${header.name}`;
header.name = `package${suffix}`;
@ -46678,7 +46679,7 @@ function mkdirfix (name, opts, cb) {
/* 194 */
/***/ (function(module, exports) {
module.exports = {"name":"yarn","installationMethod":"unknown","version":"1.22.4","license":"BSD-2-Clause","preferGlobal":true,"description":"📦🐈 Fast, reliable, and secure dependency management.","dependencies":{"@zkochan/cmd-shim":"^3.1.0","babel-runtime":"^6.26.0","bytes":"^3.0.0","camelcase":"^4.0.0","chalk":"^2.1.0","cli-table3":"^0.4.0","commander":"^2.9.0","death":"^1.0.0","debug":"^3.0.0","deep-equal":"^1.0.1","detect-indent":"^5.0.0","dnscache":"^1.0.1","glob":"^7.1.1","gunzip-maybe":"^1.4.0","hash-for-dep":"^1.2.3","imports-loader":"^0.8.0","ini":"^1.3.4","inquirer":"^6.2.0","invariant":"^2.2.0","is-builtin-module":"^2.0.0","is-ci":"^1.0.10","is-webpack-bundle":"^1.0.0","js-yaml":"^3.13.1","leven":"^2.0.0","loud-rejection":"^1.2.0","micromatch":"^2.3.11","mkdirp":"^0.5.1","node-emoji":"^1.6.1","normalize-url":"^2.0.0","npm-logical-tree":"^1.2.1","object-path":"^0.11.2","proper-lockfile":"^2.0.0","puka":"^1.0.0","read":"^1.0.7","request":"^2.87.0","request-capture-har":"^1.2.2","rimraf":"^2.5.0","semver":"^5.1.0","ssri":"^5.3.0","strip-ansi":"^4.0.0","strip-bom":"^3.0.0","tar-fs":"^1.16.0","tar-stream":"^1.6.1","uuid":"^3.0.1","v8-compile-cache":"^2.0.0","validate-npm-package-license":"^3.0.4","yn":"^2.0.0"},"devDependencies":{"babel-core":"^6.26.0","babel-eslint":"^7.2.3","babel-loader":"^6.2.5","babel-plugin-array-includes":"^2.0.3","babel-plugin-inline-import":"^3.0.0","babel-plugin-transform-builtin-extend":"^1.1.2","babel-plugin-transform-inline-imports-commonjs":"^1.0.0","babel-plugin-transform-runtime":"^6.4.3","babel-preset-env":"^1.6.0","babel-preset-flow":"^6.23.0","babel-preset-stage-0":"^6.0.0","babylon":"^6.5.0","commitizen":"^2.9.6","cz-conventional-changelog":"^2.0.0","eslint":"^4.3.0","eslint-config-fb-strict":"^22.0.0","eslint-plugin-babel":"^5.0.0","eslint-plugin-flowtype":"^2.35.0","eslint-plugin-jasmine":"^2.6.2","eslint-plugin-jest":"^21.0.0","eslint-plugin-jsx-a11y":"^6.0.2","eslint-plugin-prefer-object-spread":"^1.2.1","eslint-plugin-prettier":"^2.1.2","eslint-plugin-react":"^7.1.0","eslint-plugin-relay":"^0.0.28","eslint-plugin-yarn-internal":"file:scripts/eslint-rules","execa":"^0.11.0","fancy-log":"^1.3.2","flow-bin":"^0.66.0","git-release-notes":"^3.0.0","gulp":"^4.0.0","gulp-babel":"^7.0.0","gulp-if":"^2.0.1","gulp-newer":"^1.0.0","gulp-plumber":"^1.0.1","gulp-sourcemaps":"^2.2.0","jest":"^22.4.4","jsinspect":"^0.12.6","minimatch":"^3.0.4","mock-stdin":"^0.3.0","prettier":"^1.5.2","string-replace-loader":"^2.1.1","temp":"^0.8.3","webpack":"^2.1.0-beta.25","yargs":"^6.3.0"},"resolutions":{"sshpk":"^1.14.2"},"engines":{"node":">=4.0.0"},"repository":"yarnpkg/yarn","bin":{"yarn":"./bin/yarn.js","yarnpkg":"./bin/yarn.js"},"scripts":{"build":"gulp build","build-bundle":"node ./scripts/build-webpack.js","build-chocolatey":"powershell ./scripts/build-chocolatey.ps1","build-deb":"./scripts/build-deb.sh","build-dist":"bash ./scripts/build-dist.sh","build-win-installer":"scripts\\build-windows-installer.bat","changelog":"git-release-notes $(git describe --tags --abbrev=0 $(git describe --tags --abbrev=0)^)..$(git describe --tags --abbrev=0) scripts/changelog.md","dupe-check":"yarn jsinspect ./src","lint":"eslint . && flow check","pkg-tests":"yarn --cwd packages/pkg-tests jest yarn.test.js","prettier":"eslint src __tests__ --fix","release-branch":"./scripts/release-branch.sh","test":"yarn lint && yarn test-only","test-only":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --verbose","test-only-debug":"node --inspect-brk --max_old_space_size=4096 node_modules/jest/bin/jest.js --runInBand --verbose","test-coverage":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --coverage --verbose","watch":"gulp watch","commit":"git-cz"},"jest":{"collectCoverageFrom":["src/**/*.js"],"testEnvironment":"node","modulePathIgnorePatterns":["__tests__/fixtures/","packages/pkg-tests/pkg-tests-fixtures","dist/"],"testPathIgnorePatterns":["__tests__/(fixtures|__mocks__)/","updates/","_(temp|mock|install|init|helpers).js$","packages/pkg-tests"]},"config":{"commitizen":{"path":"./node_modules/cz-conventional-changelog"}}}
module.exports = {"name":"yarn","installationMethod":"unknown","version":"1.22.5","license":"BSD-2-Clause","preferGlobal":true,"description":"📦🐈 Fast, reliable, and secure dependency management.","dependencies":{"@zkochan/cmd-shim":"^3.1.0","babel-runtime":"^6.26.0","bytes":"^3.0.0","camelcase":"^4.0.0","chalk":"^2.1.0","cli-table3":"^0.4.0","commander":"^2.9.0","death":"^1.0.0","debug":"^3.0.0","deep-equal":"^1.0.1","detect-indent":"^5.0.0","dnscache":"^1.0.1","glob":"^7.1.1","gunzip-maybe":"^1.4.0","hash-for-dep":"^1.2.3","imports-loader":"^0.8.0","ini":"^1.3.4","inquirer":"^6.2.0","invariant":"^2.2.0","is-builtin-module":"^2.0.0","is-ci":"^1.0.10","is-webpack-bundle":"^1.0.0","js-yaml":"^3.13.1","leven":"^2.0.0","loud-rejection":"^1.2.0","micromatch":"^2.3.11","mkdirp":"^0.5.1","node-emoji":"^1.6.1","normalize-url":"^2.0.0","npm-logical-tree":"^1.2.1","object-path":"^0.11.2","proper-lockfile":"^2.0.0","puka":"^1.0.0","read":"^1.0.7","request":"^2.87.0","request-capture-har":"^1.2.2","rimraf":"^2.5.0","semver":"^5.1.0","ssri":"^5.3.0","strip-ansi":"^4.0.0","strip-bom":"^3.0.0","tar-fs":"^1.16.0","tar-stream":"^1.6.1","uuid":"^3.0.1","v8-compile-cache":"^2.0.0","validate-npm-package-license":"^3.0.4","yn":"^2.0.0"},"devDependencies":{"babel-core":"^6.26.0","babel-eslint":"^7.2.3","babel-loader":"^6.2.5","babel-plugin-array-includes":"^2.0.3","babel-plugin-inline-import":"^3.0.0","babel-plugin-transform-builtin-extend":"^1.1.2","babel-plugin-transform-inline-imports-commonjs":"^1.0.0","babel-plugin-transform-runtime":"^6.4.3","babel-preset-env":"^1.6.0","babel-preset-flow":"^6.23.0","babel-preset-stage-0":"^6.0.0","babylon":"^6.5.0","commitizen":"^2.9.6","cz-conventional-changelog":"^2.0.0","eslint":"^4.3.0","eslint-config-fb-strict":"^22.0.0","eslint-plugin-babel":"^5.0.0","eslint-plugin-flowtype":"^2.35.0","eslint-plugin-jasmine":"^2.6.2","eslint-plugin-jest":"^21.0.0","eslint-plugin-jsx-a11y":"^6.0.2","eslint-plugin-prefer-object-spread":"^1.2.1","eslint-plugin-prettier":"^2.1.2","eslint-plugin-react":"^7.1.0","eslint-plugin-relay":"^0.0.28","eslint-plugin-yarn-internal":"file:scripts/eslint-rules","execa":"^0.11.0","fancy-log":"^1.3.2","flow-bin":"^0.66.0","git-release-notes":"^3.0.0","gulp":"^4.0.0","gulp-babel":"^7.0.0","gulp-if":"^2.0.1","gulp-newer":"^1.0.0","gulp-plumber":"^1.0.1","gulp-sourcemaps":"^2.2.0","jest":"^22.4.4","jsinspect":"^0.12.6","minimatch":"^3.0.4","mock-stdin":"^0.3.0","prettier":"^1.5.2","string-replace-loader":"^2.1.1","temp":"^0.8.3","webpack":"^2.1.0-beta.25","yargs":"^6.3.0"},"resolutions":{"sshpk":"^1.14.2"},"engines":{"node":">=4.0.0"},"repository":"yarnpkg/yarn","bin":{"yarn":"./bin/yarn.js","yarnpkg":"./bin/yarn.js"},"scripts":{"build":"gulp build","build-bundle":"node ./scripts/build-webpack.js","build-chocolatey":"powershell ./scripts/build-chocolatey.ps1","build-deb":"./scripts/build-deb.sh","build-dist":"bash ./scripts/build-dist.sh","build-win-installer":"scripts\\build-windows-installer.bat","changelog":"git-release-notes $(git describe --tags --abbrev=0 $(git describe --tags --abbrev=0)^)..$(git describe --tags --abbrev=0) scripts/changelog.md","dupe-check":"yarn jsinspect ./src","lint":"eslint . && flow check","pkg-tests":"yarn --cwd packages/pkg-tests jest yarn.test.js","prettier":"eslint src __tests__ --fix","release-branch":"./scripts/release-branch.sh","test":"yarn lint && yarn test-only","test-only":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --verbose","test-only-debug":"node --inspect-brk --max_old_space_size=4096 node_modules/jest/bin/jest.js --runInBand --verbose","test-coverage":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --coverage --verbose","watch":"gulp watch","commit":"git-cz"},"jest":{"collectCoverageFrom":["src/**/*.js"],"testEnvironment":"node","modulePathIgnorePatterns":["__tests__/fixtures/","packages/pkg-tests/pkg-tests-fixtures","dist/"],"testPathIgnorePatterns":["__tests__/(fixtures|__mocks__)/","updates/","_(temp|mock|install|init|helpers).js$","packages/pkg-tests"]},"config":{"commitizen":{"path":"./node_modules/cz-conventional-changelog"}}}
/***/ }),
/* 195 */
@ -98338,7 +98339,7 @@ var _buildSubCommands = (0, (_buildSubCommands2 || _load_buildSubCommands()).def
const bundle = yield fetchBundle(config, bundleUrl);
const yarnPath = path.resolve(config.lockfileFolder, `.yarn/releases/yarn-${bundleVersion}.js`);
const yarnPath = path.resolve(config.lockfileFolder, `.yarn/releases/yarn-${bundleVersion}.cjs`);
reporter.log(`Saving it into ${chalk.magenta(yarnPath)}...`);
yield (_fs || _load_fs()).mkdirp(path.dirname(yarnPath));
yield (_fs || _load_fs()).writeFile(yarnPath, bundle);
@ -100190,7 +100191,7 @@ let main = exports.main = (() => {
const config = new (_config || _load_config()).default(reporter);
const outputWrapperEnabled = (0, (_conversion || _load_conversion()).boolifyWithDefault)(process.env.YARN_WRAP_OUTPUT, true);
const shouldWrapOutput = outputWrapperEnabled && !(_commander || _load_commander()).default.json && command.hasWrapper((_commander || _load_commander()).default, (_commander || _load_commander()).default.args);
const shouldWrapOutput = outputWrapperEnabled && !(_commander || _load_commander()).default.json && command.hasWrapper((_commander || _load_commander()).default, (_commander || _load_commander()).default.args) && !(commandName === 'init' && (_commander || _load_commander()).default[`2`]);
if (shouldWrapOutput) {
reporter.header(commandName, { name: 'yarn', version: (_yarnVersion || _load_yarnVersion()).version });
@ -100604,7 +100605,7 @@ let start = (() => {
});
try {
if (yarnPath.endsWith(`.js`)) {
if (/\.[cm]?js$/.test(yarnPath)) {
exitCode = yield (0, (_child || _load_child()).spawnp)(process.execPath, [yarnPath, ...argv], opts);
} else {
exitCode = yield (0, (_child || _load_child()).spawnp)(yarnPath, argv, opts);

View File

@ -2,4 +2,4 @@
# yarn lockfile v1
yarn-path ".yarn/releases/yarn-1.22.4.js"
yarn-path ".yarn/releases/yarn-1.22.5.js"

View File

@ -34,7 +34,7 @@ filegroup(
filegroup(
name = "angularjs_scripts",
srcs = [
# We also declare the unminfied AngularJS files since these can be used for
# We also declare the unminified AngularJS files since these can be used for
# local debugging (e.g. see: packages/upgrade/test/common/test_helpers.ts)
"@npm//:node_modules/angular/angular.js",
"@npm//:node_modules/angular/angular.min.js",

View File

@ -1,3 +1,41 @@
<a name="10.1.3"></a>
## 10.1.3 (2020-09-23)
### Bug Fixes
* **http:** Fix error message when we call jsonp without importing HttpClientJsonpModule ([#38756](https://github.com/angular/angular/issues/38756)) ([3902ec0](https://github.com/angular/angular/commit/3902ec0))
* **ngcc:** fix compilation of `ChangeDetectorRef` in pipe constructors ([#38892](https://github.com/angular/angular/issues/38892)) ([093c3a1](https://github.com/angular/angular/commit/093c3a1)), closes [#38666](https://github.com/angular/angular/issues/38666) [#38883](https://github.com/angular/angular/issues/38883)
### Reverts
* feat(router): better warning message when a router outlet has not been instantiated ([#38920](https://github.com/angular/angular/issues/38920)) ([04d0aa6](https://github.com/angular/angular/commit/04d0aa6))
<a name="10.1.2"></a>
## 10.1.2 (2020-09-16)
### Bug Fixes
* **compiler:** detect pipes in ICUs in template binder ([#38810](https://github.com/angular/angular/issues/38810)) ([ec2dbe7](https://github.com/angular/angular/commit/ec2dbe7)), closes [#38539](https://github.com/angular/angular/issues/38539) [#38539](https://github.com/angular/angular/issues/38539) [#38539](https://github.com/angular/angular/issues/38539)
* **core:** clear the `RefreshTransplantedView` when detached ([#38768](https://github.com/angular/angular/issues/38768)) ([edb7f90](https://github.com/angular/angular/commit/edb7f90)), closes [#38619](https://github.com/angular/angular/issues/38619)
* **localize:** ensure that `formatOptions` is optional ([#38787](https://github.com/angular/angular/issues/38787)) ([a47383d](https://github.com/angular/angular/commit/a47383d))
* **router:** Ensure routes are processed in priority order and only if needed ([#38780](https://github.com/angular/angular/issues/38780)) ([9c51ba3](https://github.com/angular/angular/commit/9c51ba3)), closes [#38691](https://github.com/angular/angular/issues/38691)
* **upgrade:** add try/catch when downgrading injectables ([#38671](https://github.com/angular/angular/issues/38671)) ([5de2ac3](https://github.com/angular/angular/commit/5de2ac3)), closes [#37579](https://github.com/angular/angular/issues/37579)
### Performance Improvements
* **compiler-cli:** only emit directive/pipe references that are used ([#38843](https://github.com/angular/angular/issues/38843)) ([5658405](https://github.com/angular/angular/commit/5658405))
* **compiler-cli:** optimize computation of type-check scope information ([#38843](https://github.com/angular/angular/issues/38843)) ([ebede67](https://github.com/angular/angular/commit/ebede67))
* **ngcc:** introduce cache for sharing data across entry-points ([#38840](https://github.com/angular/angular/issues/38840)) ([58411e7](https://github.com/angular/angular/commit/58411e7))
* **ngcc:** reduce maximum worker count ([#38840](https://github.com/angular/angular/issues/38840)) ([ea36466](https://github.com/angular/angular/commit/ea36466))
<a name="10.1.1"></a>
## 10.1.1 (2020-09-09)

View File

@ -21,11 +21,13 @@ import { ItemDirective } from './item.directive';
ItemDirective
],
// #enddocregion declarations
// #docregion imports
imports: [
BrowserModule,
FormsModule,
HttpClientModule
],
// #enddocregion imports
providers: [],
bootstrap: [AppComponent]
})

View File

@ -1,3 +1,4 @@
// TODO: Add unit tests for this file.
// tslint:disable: no-output-native
// #docregion
import { Component, Output, OnInit, EventEmitter, NgModule } from '@angular/core';

View File

@ -2,7 +2,11 @@
"tests": [
{
"cmd": "yarn",
"args": [ "tsc", "--project", "./tsconfig.app.json" ]
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
},
{
"cmd": "yarn",
"args": ["jasmine", "out-tsc/**/*.spec.js"]
}
]
}

View File

@ -0,0 +1,55 @@
import { docRegionFromEvent, docRegionSubscriber } from './creating';
describe('observables', () => {
it('should create an observable using the constructor', () => {
const console = {log: jasmine.createSpy('log')};
docRegionSubscriber(console);
expect(console.log).toHaveBeenCalledTimes(4);
expect(console.log.calls.allArgs()).toEqual([
[1],
[2],
[3],
['Finished sequence'],
]);
});
it('should listen to input changes', () => {
let triggerInputChange;
const input = {
value: 'Test',
addEventListener: jasmine
.createSpy('addEvent')
.and.callFake((eventName: string, cb: (e) => void) => {
if (eventName === 'keydown') {
triggerInputChange = cb;
}
}),
removeEventListener: jasmine.createSpy('removeEventListener'),
};
const document = { getElementById: () => input };
docRegionFromEvent(document);
triggerInputChange({keyCode: 65});
expect(input.value).toBe('Test');
triggerInputChange({keyCode: 27});
expect(input.value).toBe('');
});
it('should call removeEventListener when unsubscribing', (doneFn: DoneFn) => {
const input = {
addEventListener: jasmine.createSpy('addEvent'),
removeEventListener: jasmine
.createSpy('removeEvent')
.and.callFake((eventName: string, cb: (e) => void) => {
if (eventName === 'keydown') {
doneFn();
}
})
};
const document = { getElementById: () => input };
const subscription = docRegionFromEvent(document);
subscription.unsubscribe();
});
});

View File

@ -1,38 +1,39 @@
// #docplaster
import { Observable } from 'rxjs';
// #docregion subscriber
export function docRegionSubscriber(console) {
// #docregion subscriber
// This function runs when subscribe() is called
function sequenceSubscriber(observer) {
// synchronously deliver 1, 2, and 3, then complete
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
// This function runs when subscribe() is called
function sequenceSubscriber(observer) {
// synchronously deliver 1, 2, and 3, then complete
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
// unsubscribe function doesn't need to do anything in this
// because values are delivered synchronously
return {unsubscribe() {}};
}
// unsubscribe function doesn't need to do anything in this
// because values are delivered synchronously
return {unsubscribe() {}};
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);
// execute the Observable and print the result of each notification
sequence.subscribe({
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
// Logs:
// 1
// 2
// 3
// Finished sequence
// #enddocregion subscriber
}
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);
// execute the Observable and print the result of each notification
sequence.subscribe({
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
// Logs:
// 1
// 2
// 3
// Finished sequence
// #enddocregion subscriber
// #docregion fromevent
function fromEvent(target, eventName) {
@ -51,16 +52,18 @@ function fromEvent(target, eventName) {
// #enddocregion fromevent
// #docregion fromevent_use
export function docRegionFromEvent(document) {
// #docregion fromevent_use
const ESC_KEY = 27;
const nameInput = document.getElementById('name') as HTMLInputElement;
const ESC_KEY = 27;
const nameInput = document.getElementById('name') as HTMLInputElement;
const subscription = fromEvent(nameInput, 'keydown')
.subscribe((e: KeyboardEvent) => {
const subscription = fromEvent(nameInput, 'keydown').subscribe((e: KeyboardEvent) => {
if (e.keyCode === ESC_KEY) {
nameInput.value = '';
}
});
// #enddocregion fromevent_use
return subscription;
}
// #enddocregion fromevent_use

View File

@ -1,5 +1,5 @@
// TODO: Add unit tests for this file.
import { Observable } from 'rxjs';
// #docregion
// Create an Observable that will start listening to geolocation updates

View File

@ -0,0 +1,48 @@
import { docRegionDelaySequence, docRegionMulticastSequence } from './multicasting';
describe('multicasting', () => {
let console;
beforeEach(() => {
jasmine.clock().install();
console = {log: jasmine.createSpy('log')};
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should create an observable and emit in sequence', () => {
docRegionDelaySequence(console);
jasmine.clock().tick(10000);
expect(console.log).toHaveBeenCalledTimes(12);
expect(console.log.calls.allArgs()).toEqual([
[1],
['1st subscribe: 1'],
['2nd subscribe: 1'],
[2],
['1st subscribe: 2'],
['2nd subscribe: 2'],
[3],
['Finished sequence'],
['1st subscribe: 3'],
['1st sequence finished.'],
['2nd subscribe: 3'],
['2nd sequence finished.']
]);
});
it('should create an observable and multicast the emissions', () => {
docRegionMulticastSequence(console);
jasmine.clock().tick(10000);
expect(console.log).toHaveBeenCalledTimes(7);
expect(console.log.calls.allArgs()).toEqual([
['1st subscribe: 1'],
['1st subscribe: 2'],
['2nd subscribe: 2'],
['1st subscribe: 3'],
['2nd subscribe: 3'],
['1st sequence finished.'],
['2nd sequence finished.']
]);
});
});

View File

@ -1,155 +1,160 @@
// #docplaster
import { Observable } from 'rxjs';
// #docregion delay_sequence
export function docRegionDelaySequence(console) {
// #docregion delay_sequence
function sequenceSubscriber(observer) {
const seq = [1, 2, 3];
let timeoutId;
function sequenceSubscriber(observer) {
const seq = [1, 2, 3];
let timeoutId;
// Will run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doInSequence(arr, idx) {
timeoutId = setTimeout(() => {
observer.next(arr[idx]);
if (idx === arr.length - 1) {
observer.complete();
} else {
doInSequence(arr, ++idx);
}
}, 1000);
}
// Will run through an array of numbers, emitting one value
doInSequence(seq, 0);
// Unsubscribe should clear the timeout to stop execution
return {
unsubscribe() {
clearTimeout(timeoutId);
}
};
}
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);
sequence.subscribe({
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
// Logs:
// (at 1 second): 1
// (at 2 seconds): 2
// (at 3 seconds): 3
// (at 3 seconds): Finished sequence
// #enddocregion delay_sequence
// #docregion subscribe_twice
// Subscribe starts the clock, and will emit after 1 second
sequence.subscribe({
next(num) { console.log('1st subscribe: ' + num); },
complete() { console.log('1st sequence finished.'); }
});
// After 1/2 second, subscribe again.
setTimeout(() => {
sequence.subscribe({
next(num) { console.log('2nd subscribe: ' + num); },
complete() { console.log('2nd sequence finished.'); }
});
}, 500);
// Logs:
// (at 1 second): 1st subscribe: 1
// (at 1.5 seconds): 2nd subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2.5 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3.5 seconds): 2nd subscribe: 3
// (at 3.5 seconds): 2nd sequence finished
// #enddocregion subscribe_twice
}
export function docRegionMulticastSequence(console) {
// #docregion multicast_sequence
function multicastSequenceSubscriber() {
const seq = [1, 2, 3];
// Keep track of each observer (one for every active subscription)
const observers = [];
// Still a single timeoutId because there will only ever be one
// set of values being generated, multicasted to each subscriber
let timeoutId;
// Return the subscriber function (runs when subscribe()
// function is invoked)
return observer => {
observers.push(observer);
// When this is the first subscription, start the sequence
if (observers.length === 1) {
timeoutId = doSequence({
next(val) {
// Iterate through observers and notify all subscriptions
observers.forEach(obs => obs.next(val));
},
complete() {
// Notify all complete callbacks
observers.slice(0).forEach(obs => obs.complete());
}
}, seq, 0);
}
return {
unsubscribe() {
// Remove from the observers array so it's no longer notified
observers.splice(observers.indexOf(observer), 1);
// If there's no more listeners, do cleanup
if (observers.length === 0) {
clearTimeout(timeoutId);
}
}
};
};
}
// Run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doInSequence(arr, idx) {
timeoutId = setTimeout(() => {
function doSequence(observer, arr, idx) {
return setTimeout(() => {
observer.next(arr[idx]);
if (idx === arr.length - 1) {
observer.complete();
} else {
doInSequence(arr, ++idx);
doSequence(observer, arr, ++idx);
}
}, 1000);
}
doInSequence(seq, 0);
// Create a new Observable that will deliver the above sequence
const multicastSequence = new Observable(multicastSequenceSubscriber());
// Unsubscribe should clear the timeout to stop execution
return {unsubscribe() {
clearTimeout(timeoutId);
}};
}
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);
sequence.subscribe({
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
// Logs:
// (at 1 second): 1
// (at 2 seconds): 2
// (at 3 seconds): 3
// (at 3 seconds): Finished sequence
// #enddocregion delay_sequence
// #docregion subscribe_twice
// Subscribe starts the clock, and will emit after 1 second
sequence.subscribe({
next(num) { console.log('1st subscribe: ' + num); },
complete() { console.log('1st sequence finished.'); }
});
// After 1/2 second, subscribe again.
setTimeout(() => {
sequence.subscribe({
next(num) { console.log('2nd subscribe: ' + num); },
complete() { console.log('2nd sequence finished.'); }
});
}, 500);
// Logs:
// (at 1 second): 1st subscribe: 1
// (at 1.5 seconds): 2nd subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2.5 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3.5 seconds): 2nd subscribe: 3
// (at 3.5 seconds): 2nd sequence finished
// #enddocregion subscribe_twice
// #docregion multicast_sequence
function multicastSequenceSubscriber() {
const seq = [1, 2, 3];
// Keep track of each observer (one for every active subscription)
const observers = [];
// Still a single timeoutId because there will only ever be one
// set of values being generated, multicasted to each subscriber
let timeoutId;
// Return the subscriber function (runs when subscribe()
// function is invoked)
return (observer) => {
observers.push(observer);
// When this is the first subscription, start the sequence
if (observers.length === 1) {
timeoutId = doSequence({
next(val) {
// Iterate through observers and notify all subscriptions
observers.forEach(obs => obs.next(val));
},
complete() {
// Notify all complete callbacks
observers.slice(0).forEach(obs => obs.complete());
}
}, seq, 0);
}
return {
unsubscribe() {
// Remove from the observers array so it's no longer notified
observers.splice(observers.indexOf(observer), 1);
// If there's no more listeners, do cleanup
if (observers.length === 0) {
clearTimeout(timeoutId);
}
}
};
};
}
// Run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doSequence(observer, arr, idx) {
return setTimeout(() => {
observer.next(arr[idx]);
if (idx === arr.length - 1) {
observer.complete();
} else {
doSequence(observer, arr, ++idx);
}
}, 1000);
}
// Create a new Observable that will deliver the above sequence
const multicastSequence = new Observable(multicastSequenceSubscriber());
// Subscribe starts the clock, and begins to emit after 1 second
multicastSequence.subscribe({
next(num) { console.log('1st subscribe: ' + num); },
complete() { console.log('1st sequence finished.'); }
});
// After 1 1/2 seconds, subscribe again (should "miss" the first value).
setTimeout(() => {
// Subscribe starts the clock, and begins to emit after 1 second
multicastSequence.subscribe({
next(num) { console.log('2nd subscribe: ' + num); },
complete() { console.log('2nd sequence finished.'); }
next(num) { console.log('1st subscribe: ' + num); },
complete() { console.log('1st sequence finished.'); }
});
}, 1500);
// Logs:
// (at 1 second): 1st subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3 seconds): 2nd subscribe: 3
// (at 3 seconds): 2nd sequence finished
// After 1 1/2 seconds, subscribe again (should "miss" the first value).
setTimeout(() => {
multicastSequence.subscribe({
next(num) { console.log('2nd subscribe: ' + num); },
complete() { console.log('2nd sequence finished.'); }
});
}, 1500);
// #enddocregion multicast_sequence
// Logs:
// (at 1 second): 1st subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3 seconds): 2nd subscribe: 3
// (at 3 seconds): 2nd sequence finished
// #enddocregion multicast_sequence
}

View File

@ -0,0 +1,19 @@
import { docRegionObserver } from './subscribing';
describe('subscribing', () => {
it('should subscribe and emit', () => {
const console = {log: jasmine.createSpy('log')};
docRegionObserver(console);
expect(console.log).toHaveBeenCalledTimes(8);
expect(console.log.calls.allArgs()).toEqual([
['Observer got a next value: 1'],
['Observer got a next value: 2'],
['Observer got a next value: 3'],
['Observer got a complete notification'],
['Observer got a next value: 1'],
['Observer got a next value: 2'],
['Observer got a next value: 3'],
['Observer got a complete notification'],
]);
});
});

View File

@ -1,32 +1,35 @@
// #docplaster
import { of } from 'rxjs';
import { Observable, of } from 'rxjs';
export function docRegionObserver(console) {
// #docregion observer
// #docregion observer
// Create simple observable that emits three values
const myObservable = of(1, 2, 3);
// Create simple observable that emits three values
const myObservable = of(1, 2, 3);
// Create observer object
const myObserver = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};
// Create observer object
const myObserver = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};
// Execute with the observer object
myObservable.subscribe(myObserver);
// Execute with the observer object
myObservable.subscribe(myObserver);
// Logs:
// Observer got a next value: 1
// Observer got a next value: 2
// Observer got a next value: 3
// Observer got a complete notification
// Logs:
// Observer got a next value: 1
// Observer got a next value: 2
// Observer got a next value: 3
// Observer got a complete notification
// #enddocregion observer
// #enddocregion observer
// #docregion sub_fn
myObservable.subscribe(
x => console.log('Observer got a next value: ' + x),
err => console.error('Observer got an error: ' + err),
() => console.log('Observer got a complete notification')
);
// #enddocregion sub_fn
// #docregion sub_fn
myObservable.subscribe(
x => console.log('Observer got a next value: ' + x),
err => console.error('Observer got an error: ' + err),
() => console.log('Observer got a complete notification')
);
// #enddocregion sub_fn
}

View File

@ -2,7 +2,11 @@
"tests": [
{
"cmd": "yarn",
"args": [ "tsc", "--project", "./tsconfig.app.json" ]
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
},
{
"cmd": "yarn",
"args": ["jasmine", "out-tsc/**/*.spec.js"]
}
]
}

View File

@ -1,4 +1,4 @@
// TODO: Add unit tests for this file.
import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';

View File

@ -0,0 +1,72 @@
import { of } from 'rxjs';
import { docRegionTypeahead } from './typeahead';
describe('typeahead', () => {
let document;
let ajax;
let triggertInputChange;
beforeEach(() => {
jasmine.clock().install();
const input = {
addEventListener: jasmine
.createSpy('addEvent')
.and.callFake((eventName: string, cb: (e) => void) => {
if (eventName === 'input') {
triggertInputChange = cb;
}
}),
removeEventListener: jasmine.createSpy('removeEvent'),
};
document = { getElementById: (id: string) => input };
ajax = jasmine.createSpy('ajax').and.callFake((url: string) => of('foo bar'));
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should make an ajax call to the corrent endpoint', () => {
docRegionTypeahead(document, ajax);
triggertInputChange({ target: { value: 'foo' } });
jasmine.clock().tick(11);
expect(ajax).toHaveBeenCalledWith('/api/endpoint?search=foo');
});
it('should not make an ajax call, when the input length < 3', () => {
docRegionTypeahead(document, ajax);
triggertInputChange({ target: { value: '' } });
jasmine.clock().tick(11);
expect(ajax).not.toHaveBeenCalled();
triggertInputChange({ target: { value: 'fo' } });
jasmine.clock().tick(11);
expect(ajax).not.toHaveBeenCalled();
});
it('should not make an ajax call for intermediate values when debouncing', () => {
docRegionTypeahead(document, ajax);
triggertInputChange({ target: { value: 'foo' } });
jasmine.clock().tick(9);
triggertInputChange({ target: { value: 'bar' } });
jasmine.clock().tick(9);
triggertInputChange({ target: { value: 'baz' } });
jasmine.clock().tick(9);
triggertInputChange({ target: { value: 'qux' } });
expect(ajax).not.toHaveBeenCalled();
jasmine.clock().tick(10);
expect(ajax).toHaveBeenCalledTimes(1);
expect(ajax).toHaveBeenCalledWith('/api/endpoint?search=qux');
});
it('should not make an ajax call, when the input value has not changed', () => {
docRegionTypeahead(document, ajax);
triggertInputChange({ target: { value: 'foo' } });
jasmine.clock().tick(11);
expect(ajax).toHaveBeenCalled();
ajax.calls.reset();
triggertInputChange({ target: { value: 'foo' } });
jasmine.clock().tick(11);
expect(ajax).not.toHaveBeenCalled();
});
});

View File

@ -1,18 +1,32 @@
import { fromEvent } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
/*
Because of how the code is merged together using the doc regions,
we need to indent the imports with the function below.
*/
// #docplaster
// #docregion
import { fromEvent } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
// #enddocregion
/* tslint:disable:no-shadowed-variable */
/* tslint:disable:align */
export function docRegionTypeahead(document, ajax) {
// #docregion
const searchBox = document.getElementById('search-box');
const searchBox = document.getElementById('search-box');
const typeahead = fromEvent(searchBox, 'input').pipe(
map((e: KeyboardEvent) => (e.target as HTMLInputElement).value),
filter(text => text.length > 2),
debounceTime(10),
distinctUntilChanged(),
switchMap(searchTerm => ajax(`/api/endpoint?search=${searchTerm}`))
);
const typeahead = fromEvent(searchBox, 'input').pipe(
map((e: KeyboardEvent) => (e.target as HTMLInputElement).value),
filter(text => text.length > 2),
debounceTime(10),
distinctUntilChanged(),
switchMap(() => ajax('/api/endpoint'))
);
typeahead.subscribe(data => {
// Handle the data from the API
});
typeahead.subscribe(data => {
// Handle the data from the API
});
// #enddocregion
return typeahead;
}

View File

@ -28,7 +28,7 @@ import {
ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync
} from '@angular/core/testing';
import { addMatchers, newEvent, click } from '../../testing';
import { addMatchers, click } from '../../testing';
export class NotProvided extends ValueService { /* example below */ }
beforeEach(addMatchers);
@ -274,9 +274,11 @@ describe('demo (with TestBed):', () => {
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// Dispatch a DOM event so that Angular learns of input value change.
// then wait while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
input.dispatchEvent(new Event('input'));
return fixture.whenStable();
})
.then(() => {
@ -312,9 +314,11 @@ describe('demo (with TestBed):', () => {
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// Dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
input.dispatchEvent(new Event('input'));
tick();
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
@ -335,10 +339,12 @@ describe('demo (with TestBed):', () => {
// simulate user entering new name in input
input.value = inputText;
// dispatch a DOM event so that Angular learns of input value change.
// Dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.text
// and Angular updates the output span
input.dispatchEvent(newEvent('input'));
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
input.dispatchEvent(new Event('input'));
tick();
fixture.detectChanges();
expect(span.textContent).toBe(expectedText, 'output span');

View File

@ -3,7 +3,7 @@ import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from
import { Router } from '@angular/router';
import {
ActivatedRoute, ActivatedRouteStub, asyncData, click, newEvent
ActivatedRoute, ActivatedRouteStub, asyncData, click
} from '../../testing';
import { Hero } from '../model/hero';
@ -99,7 +99,10 @@ function overrideSetup() {
const newName = 'New Name';
page.nameInput.value = newName;
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
page.nameInput.dispatchEvent(new Event('input')); // tell Angular
expect(component.hero.name).toBe(newName, 'component hero has new name');
expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
@ -197,9 +200,10 @@ function heroModuleSetup() {
// simulate user entering a new name into the input box
nameInput.value = 'quick BROWN fOx';
// dispatch a DOM event so that Angular learns of input value change.
// use newEvent utility function (not provided by Angular) for better browser compatibility
nameInput.dispatchEvent(newEvent('input'));
// Dispatch a DOM event so that Angular learns of input value change.
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
nameInput.dispatchEvent(new Event('input'));
// Tell Angular to update the display binding through the title pipe
fixture.detectChanges();

View File

@ -6,7 +6,7 @@ import { DebugElement } from '@angular/core';
import { Router } from '@angular/router';
import { addMatchers, newEvent } from '../../testing';
import { addMatchers } from '../../testing';
import { HeroService } from '../model/hero.service';
import { getTestHeroes, TestHeroService } from '../model/testing/test-hero.service';
@ -53,7 +53,10 @@ describe('HeroListComponent', () => {
it('should select hero on click', fakeAsync(() => {
const expectedHero = HEROES[1];
const li = page.heroRows[1];
li.dispatchEvent(newEvent('click'));
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
li.dispatchEvent(new Event('click'));
tick();
// `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService
expect(comp.selectedHero).toEqual(expectedHero);
@ -62,7 +65,10 @@ describe('HeroListComponent', () => {
it('should navigate to selected hero detail on click', fakeAsync(() => {
const expectedHero = HEROES[1];
const li = page.heroRows[1];
li.dispatchEvent(newEvent('click'));
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
li.dispatchEvent(new Event('click'));
tick();
// should have navigated

View File

@ -3,7 +3,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive';
import { newEvent } from '../../testing';
// #docregion test-component
@Component({
@ -59,9 +58,12 @@ describe('HighlightDirective', () => {
const input = des[2].nativeElement as HTMLInputElement;
expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');
// dispatch a DOM event so that Angular responds to the input value change.
input.value = 'green';
input.dispatchEvent(newEvent('input'));
// Dispatch a DOM event so that Angular responds to the input value change.
// In older browsers, such as IE, you might need a CustomEvent instead. See
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');

View File

@ -14,18 +14,6 @@ export function advance(f: ComponentFixture<any>): void {
f.detectChanges();
}
/**
* Create custom DOM event the old fashioned way
*
* https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent
* Although officially deprecated, some browsers (phantom) don't accept the preferred "new Event(eventName)"
*/
export function newEvent(eventName: string, bubbles = false, cancelable = false) {
const evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent'
evt.initCustomEvent(eventName, bubbles, cancelable, null);
return evt;
}
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
// #docregion click-event
/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */

View File

@ -18,8 +18,6 @@ When you use the [Angular CLI](cli) command `ng new` to generate an app, the def
/* JavaScript imports */
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
@ -29,9 +27,7 @@ import { AppComponent } from './app.component';
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
@ -120,9 +116,6 @@ Now you could use your `ItemDirective` in a component. This example uses `AppMod
Remember, components, directives, and pipes belong to one module only. You only need to declare them once in your app because you share them by importing the necessary modules. This saves you time and helps keep your app lean.
{@a imports}
## The `imports` array
@ -130,6 +123,12 @@ Remember, components, directives, and pipes belong to one module only. You only
The module's `imports` array appears exclusively in the `@NgModule` metadata object.
It tells Angular about other NgModules that this particular module needs to function properly.
<code-example
path="bootstrapping/src/app/app.module.ts"
region="imports"
header="src/app/app.module.ts (excerpt)">
</code-example>
This list of modules are those that export components, directives, or pipes
that component templates in this module reference. In this case, the component is
`AppComponent`, which references components, directives, or pipes in `BrowserModule`,
@ -138,6 +137,8 @@ A component template can reference another component, directive,
or pipe when the referenced class is declared in this module or
the class was imported from another module.
{@a bootstrap-array}
## The `providers` array

View File

@ -119,7 +119,14 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
In browsers that support Custom Elements natively, the specification requires developers use ES2015 classes to define Custom Elements - developers can opt-in to this by setting the `target: "es2015"` property in their project's [TypeScript configuration file](/guide/typescript-configuration). As Custom Element and ES2015 support may not be available in all browsers, developers can instead choose to use a polyfill to support older browsers and ES5 code.
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill: `ng add @angular/elements --project=*your_project_name*`.
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill:
<code-example language="sh">
ng add @angular/elements --project=*your_project_name*
</code-example>
- For more information about polyfills, see [polyfill documentation](https://www.webcomponents.org/polyfills).
- For more information about Angular browser support, see [Browser Support](guide/browser-support).

View File

@ -627,6 +627,11 @@ The [npm package manager](https://docs.npmjs.com/getting-started/what-is-npm) is
Learn more about how Angular uses [Npm Packages](guide/npm-packages).
{@ ngc}
## ngc
`ngc` is a Typescript-to-Javascript transpiler that processes Angular decorators, metadata, and templates, and emits JavaScript code.
The most recent implementation is internally refered to as `ngtsc` because it's a minimalistic wrapper around the TypeScript compiler `tsc` that adds a transform for processing Angular code.
{@a O}
{@a observable}

View File

@ -94,7 +94,7 @@ All of our major releases are supported for 18 months.
* 6 months of *active support*, during which regularly-scheduled updates and patches are released.
* 12 months of *long-term support (LTS)*, during which only critical fixes and security patches are released.
* 12 months of *long-term support (LTS)*, during which only [critical fixes and security patches](#lts-fixes) are released.
The following table provides the status for Angular versions under support.
@ -107,6 +107,13 @@ Version | Status | Released | Active Ends | LTS Ends
Angular versions ^4.0.0, ^5.0.0, ^6.0.0 and ^7.0.0 are no longer under support.
### LTS fixes
As a general rule, a fix is considered for an LTS version if it resolves one of:
* a newly identified security vulnerability,
* a regression, since the start of LTS, caused by a 3rd party change, such as a new browser version.
{@a deprecation}
## Deprecation practices

View File

@ -324,5 +324,5 @@ These techniques are useful for small-scale demonstrations, but they
quickly become verbose and clumsy when handling large amounts of user input.
Two-way data binding is a more elegant and compact way to move
values between data entry fields and model properties.
The next page, `Forms`, explains how to write
The [`Forms`](guide/forms-overview) page explains how to write
two-way bindings with `NgModel`.

View File

@ -32,7 +32,7 @@ To do this:
1. Create a `typings.d.ts` file in your `src/` folder. This file is automatically included as global type definition.
2. Add the following code in `src/typings.d.ts`.
2. Add the following code in `src/typings.d.ts`:
```
declare module 'host' {
@ -45,7 +45,7 @@ declare module 'host' {
}
```
3. In the component or file that uses the library, add the following code.
3. In the component or file that uses the library, add the following code:
```
import * as host from 'host';
@ -129,7 +129,7 @@ interface JQuery {
}
```
If don't add the interface for the script-defined extension, your IDE shows an error:
If you don't add the interface for the script-defined extension, your IDE shows an error:
```
[TS][Error] Property 'myPlugin' does not exist on type 'JQuery'

View File

@ -3,162 +3,5 @@
</header>
<article class="events-container">
<p>Where we'll be presenting:</p>
<table class="is-full-width">
<thead>
<tr>
<th>Event</th>
<th>Location</th>
<th>Date</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p>Where we already presented:</p>
<table class="is-full-width">
<thead>
<tr>
<th>Event</th>
<th>Location</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<!-- ng-vikings 2020 -->
<tr>
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
<td>Oslo, Norway</td>
<td>May 25-26 conference, 27 workshops, 2020</td>
</tr>
<!-- ng-conf 2020 -->
<tr>
<th><a href="https://ng-conf.org/" title="ng-conf">ng-conf</a></th>
<td>Salt Lake City, Utah</td>
<td>April 1-3, 2020</td>
</tr>
<!-- ngIndia 2020 -->
<tr>
<th><a href="https://www.ng-ind.com/" title="ngIndia">ngIndia</a></th>
<td>Delhi, India</td>
<td>Feb 29, 2020</td>
</tr>
<!-- ReactiveConf 2019 -->
<tr>
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
<td>Prague, Czech Republic</td>
<td>October 30 - November 1, 2019</td>
</tr>
<!-- NG Rome 2019-->
<tr>
<th>
<a href="https://ngrome.io" title="NG Rome MMXIX - The Italian Angular Conference">NG Rome MMXIX</a>
</th>
<td>Rome, Italy</td>
<td>Oct 6th workshops, 7th conference, 2019</td>
</tr>
<!-- AngularConnect 2019-->
<tr>
<th><a href="https://www.angularconnect.com/?utm_source=angular.io&utm_medium=referral"
title="AngularConnect">AngularConnect</a></th>
<td>London, UK</td>
<td>September 19-20, 2019</td>
</tr>
<!-- NG-DE 2019-->
<tr>
<th><a href="https://ng-de.org/" title="NG-DE">NG-DE</a></th>
<td>Berlin, Germany</td>
<td>August 29th workshops, 30-31 conference, 2019</td>
</tr>
<!-- ngJapan-->
<tr>
<th><a href="https://ngjapan.org" title="ng-japan">ng-japan</a></th>
<td>Tokyo, Japan</td>
<td>July 13, 2019</td>
</tr>
<!-- ngVikings 2019-->
<tr>
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
<td>Copenhagen, Denmark</td>
<td>May 26 (workshops), 27-28 (conference), 2019</td>
</tr>
<!-- ng-conf 2019-->
<tr>
<th><a href="https://ng-conf.org/" title="ng-conf">ng-conf</a></th>
<td>Salt Lake City, Utah</td>
<td>May 1-3, 2019</td>
</tr>
<!-- ng-India 2019-->
<tr>
<th><a href="https://www.ng-ind.com/" title="ng-India">ng-India</a></th>
<td>Gurgaon, India</td>
<td>February 23, 2019</td>
</tr>
<!-- ngAtlanta 2019 -->
<tr>
<th><a href="https://ng-atl.org/" title="ngAtlanta">ngAtlanta</a></th>
<td>Atlanta, Georgia</td>
<td>January 9-12, 2019</td>
</tr>
<!-- AngularConnect-->
<tr>
<th>
<a href="https://past.angularconnect.com/2018" title="AngularConnect">AngularConnect</a>
</th>
<td>London, United Kingdom</td>
<td>November 5-7, 2018</td>
</tr>
<!-- ReactiveConf -->
<tr>
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
<td>Prague, Czech Republic</td>
<td>October 29-31, 2018</td>
</tr>
<!-- AngularMix -->
<tr>
<th><a href="https://angularmix.com/" title="AngularMix">AngularMix</a></th>
<td>Orlando, Florida</td>
<td>October 10-12, 2018</td>
</tr>
<!-- Angular Conf Australia-->
<tr>
<th>
<a href="https://www.angularconf.com.au/" title="Angular Conf Australia">Angular Conf Australia</a>
</th>
<td>Melbourne, Australia</td>
<td>Jun 22, 2018</td>
</tr>
<!-- ngJapan-->
<tr>
<th><a href="https://ngjapan.org/en.html" title="ng-japan">ng-japan</a></th>
<td>Tokyo, Japan</td>
<td>Jun 16, 2018</td>
</tr>
<!-- WeRDevs-->
<tr>
<th><a href="https://www.wearedevelopers.com/" title="WeAreDevs">WeAreDevelopers</a></th>
<td>Vienna, Austria</td>
<td>May 16-18, 2018</td>
</tr>
<!-- ngconf 2018-->
<tr>
<th><a href="https://www.ng-conf.org/" title="ng-conf">ng-conf</a></th>
<td>Salt Lake City, Utah</td>
<td>April 18-20, 2018</td>
</tr>
<!-- ngVikings-->
<tr>
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
<td>Helsinki, Finland</td>
<td>March 1-2, 2018</td>
</tr>
<!-- ngAtlanta-->
<tr>
<th><a href="http://ng-atl.org/" title="ngAtlanta">ngAtlanta</a></th>
<td>Atlanta, Georgia</td>
<td>January 30, 2018</td>
</tr>
</tbody>
</table>
<aio-events></aio-events>
</article>

View File

@ -0,0 +1,236 @@
[
{
"name": "ng-china",
"location": "Online",
"linkUrl": "https://ng-china.org/",
"date": {
"start": "2020-11-21",
"end": "2020-11-22"
}
},
{
"name": "EnterpriseNG",
"location": "Online",
"linkUrl": "https://www.ng-conf.org/",
"date": {
"start": "2020-11-19",
"end": "2020-11-20"
}
},
{
"name": "ngrome",
"location": "Online",
"linkUrl": "https://ngrome.io/",
"date": {
"start": "2020-10-20",
"end": "2020-10-20"
}
},
{
"name": "ngVikings",
"location": "Oslo, Norway",
"linkUrl": "https://ngvikings.org/",
"date": {
"start": "2020-05-25",
"end": "2020-05-26"
},
"workshopsDate": {
"start": "2020-05-27",
"end": "2020-05-27"
}
},
{
"name": "ng-conf",
"location": "Salt Lake City, Utah",
"linkUrl": "https://ng-conf.org/",
"date": {
"start": "2020-04-01",
"end": "2020-04-03"
}
},
{
"name": "ngIndia",
"location": "Delhi, India",
"linkUrl": "https://www.ng-ind.com/",
"date": {
"start": "2020-02-29",
"end": "2020-02-29"
}
},
{
"name": "ReactiveConf",
"location": "Prague, Czech Republic",
"linkUrl": "https://reactiveconf.com/",
"date": {
"start": "2019-10-30",
"end": "2019-11-01"
}
},
{
"name": "NG Rome MMXIX",
"location": "Rome, Italy",
"linkUrl": "https://ngrome.io",
"tooltip": "NG Rome MMXIX - The Italian Angular Conference",
"date": {
"start": "2019-10-07",
"end": "2019-10-07"
},
"workshopsDate": {
"start": "2019-10-06",
"end": "2019-10-06"
}
},
{
"name": "AngularConnect",
"location": "London, UK",
"linkUrl": "https://www.angularconnect.com/?utm_source=angular.io&utm_medium=referral",
"date": {
"start": "2019-09-19",
"end": "2019-09-20"
}
},
{
"name": "NG-DE",
"location": "Berlin, Germany",
"linkUrl": "https://ng-de.org/",
"date": {
"start": "2019-08-30",
"end": "2019-08-31"
},
"workshopsDate": {
"start": "2019-08-29",
"end": "2019-08-29"
}
},
{
"name": "ng-japan",
"location": "Tokyo, Japan",
"linkUrl": "https://ngjapan.org/",
"date": {
"start": "2019-07-13",
"end": "2019-07-13"
}
},
{
"name": "ngVikings",
"location": "Copenhagen, Denmark",
"linkUrl": "https://ngvikings.org/",
"date": {
"start": "2019-05-27",
"end": "2019-05-28"
},
"workshopsDate": {
"start": "2019-05-26",
"end": "2019-05-26"
}
},
{
"name": "ng-conf",
"location": "Salt Lake City, Utah",
"linkUrl": "https://ng-conf.org/",
"date": {
"start": "2019-05-01",
"end": "2019-05-03"
}
},
{
"name": "ng-India",
"location": "Gurgaon, India",
"linkUrl": "https://www.ng-ind.com/",
"date": {
"start": "2019-02-23",
"end": "2019-02-23"
}
},
{
"name": "ngAtlanta",
"location": "Atlanta, Georgia",
"linkUrl": "https://ng-atl.org/",
"date": {
"start": "2019-01-09",
"end": "2019-01-12"
}
},
{
"name": "AngularConnect",
"location": "London, United Kingdom",
"linkUrl": "https://past.angularconnect.com/2018",
"date": {
"start": "2018-11-05",
"end": "2018-11-07"
}
},
{
"name": "ReactiveConf",
"location": "Prague, Czech Republic",
"linkUrl": "https://reactiveconf.com/",
"date": {
"start": "2018-10-29",
"end": "2018-10-31"
}
},
{
"name": "AngularMix",
"location": "Orlando, Florida",
"linkUrl": "https://angularmix.com/",
"date": {
"start": "2018-10-10",
"end": "2018-10-12"
}
},
{
"name": "Angular Conf Australia",
"location": "Melbourne, Australia",
"linkUrl": "https://www.angularconf.com.au/",
"date": {
"start": "2018-06-22",
"end": "2018-06-22"
}
},
{
"name": "ng-japan",
"location": "Tokyo, Japan",
"linkUrl": "https://ngjapan.org/en.html",
"date": {
"start": "2018-06-16",
"end": "2018-06-16"
}
},
{
"name": "WeAreDevelopers",
"location": "Vienna, Austria",
"linkUrl": "https://www.wearedevelopers.com/",
"tooltip": "WeAreDevs",
"date": {
"start": "2018-05-16",
"end": "2018-05-18"
}
},
{
"name": "ng-conf",
"location": "Salt Lake City, Utah",
"linkUrl": "https://ng-conf.org/",
"date": {
"start": "2018-04-18",
"end": "2018-04-20"
}
},
{
"name": "ngVikings",
"location": "Helsinki, Finland",
"linkUrl": "https://ngvikings.org/",
"date": {
"start": "2018-03-01",
"end": "2018-03-02"
}
},
{
"name": "ngAtlanta",
"location": "Atlanta, Georgia",
"linkUrl": "https://ng-atl.org/",
"date": {
"start": "2018-01-30",
"end": "2018-01-30"
}
}
]

View File

@ -61,27 +61,27 @@
"children": [
{
"url": "start",
"title": "A Sample App",
"title": "Getting started",
"tooltip": "Take a look at Angular's component model, template syntax, and component communication."
},
{
"url": "start/start-routing",
"title": "In-app Navigation",
"title": "Adding navigation",
"tooltip": "Navigate among different page views using the browser's URL."
},
{
"url": "start/start-data",
"title": "Manage Data",
"title": "Managing Data",
"tooltip": "Use services and access external data via HTTP."
},
{
"url": "start/start-forms",
"title": "Forms for User Input",
"title": "Using Forms for User Input",
"tooltip": "Learn about fetching and managing data from users with forms."
},
{
"url": "start/start-deployment",
"title": "Deployment",
"title": "Deploying an application",
"tooltip": "Move to local development, or deploy your application to Firebase or your own server."
}
]
@ -101,11 +101,6 @@
"title": "Components",
"tooltip": "Building dynamic views with data binding",
"children": [
{
"url": "guide/displaying-data",
"title": "Data binding",
"tooltip": "Property binding helps show app data in the UI."
},
{
"url": "guide/user-input",
"title": "User Input",
@ -542,27 +537,6 @@
"title": "Tutorials",
"tooltip": "End-to-end tutorials for learning Angular concepts and patterns.",
"children": [
{
"title": "Routing",
"tooltip": "End-to-end tutorials for learning about Angular's router.",
"children": [
{
"url": "guide/router-tutorial",
"title": "Using Angular Routes in a Single-page Application",
"tooltip": "A tutorial that covers many patterns associated with Angular routing."
},
{
"url": "guide/router-tutorial-toh",
"title": "Router tutorial: tour of heroes",
"tooltip": "Explore how to use Angular's router. Based on the Tour of Heroes example."
}
]
},
{
"url": "guide/forms",
"title": "Building a Template-driven Form",
"tooltip": "Create a template-driven form using directives and Angular template syntax."
},
{
"title": "Tutorial: Tour of Heroes",
"tooltip": "The Tour of Heroes app is used as a reference point in many Angular examples.",
@ -609,6 +583,32 @@
}
]
},
{
"title": "Routing",
"tooltip": "End-to-end tutorials for learning about Angular's router.",
"children": [
{
"url": "guide/router-tutorial",
"title": "Using Angular Routes in a Single-page Application",
"tooltip": "A tutorial that covers many patterns associated with Angular routing."
},
{
"url": "guide/router-tutorial-toh",
"title": "Router tutorial: tour of heroes",
"tooltip": "Explore how to use Angular's router. Based on the Tour of Heroes example."
}
]
},
{
"url": "guide/forms",
"title": "Building a Template-driven Form",
"tooltip": "Create a template-driven form using directives and Angular template syntax."
},
{
"url": "guide/displaying-data",
"title": "Data binding",
"tooltip": "Property binding helps show app data in the UI."
},
{
"url": "guide/web-worker",
"title": "Web Workers",
@ -954,6 +954,11 @@
"url": "guide/styleguide",
"title": "Coding Style Guide",
"tooltip": "Guidelines for writing Angular code."
},
{
"url": "guide/docs-style-guide",
"title": "Documentation Style Guide",
"tooltip": "Style guide for documentation authors."
}
]
}

View File

@ -1,4 +1,4 @@
# Part 1: Getting started with a basic Angular app
# Getting started with a basic Angular app
Welcome to Angular!

View File

@ -1,4 +1,4 @@
# Try it: Manage data
# Managing data
At the end of [In-app Navigation](start/start-routing "Try it: In-app Navigation"), the online store application has a product catalog with two views: a product list and product details.
Users can click on a product name from the list to see details in a new view, with a distinct URL, or route.

View File

@ -1,4 +1,4 @@
# Try it: Deployment
# Deploying an application
To deploy your application, you have to compile it, and then host the JavaScript, CSS, and HTML on a web server. Built Angular applications are very portable and can live in any environment or served by any technology, such as Node, Java, .NET, PHP, and many others.

View File

@ -1,4 +1,4 @@
# Try it: Use forms for user input
# Using forms for user input
At the end of [Managing Data](start/start-data "Try it: Managing Data"), the online store application has a product catalog and a shopping cart.

View File

@ -1,4 +1,4 @@
# In-app navigation
# Adding navigation
At the end of [part 1](start "Get started with a basic Angular app"), the online store application has a basic product catalog.
The app doesn't have any variable states or navigation.

View File

@ -40,6 +40,10 @@ export const ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES = [
{
selector: 'live-example',
loadChildren: () => import('./live-example/live-example.module').then(m => m.LiveExampleModule)
},
{
selector: 'aio-events',
loadChildren: () => import('./events/events.module').then(m => m.EventsModule)
}
];

View File

@ -0,0 +1,43 @@
<p>Where we'll be presenting:</p>
<table class="is-full-width">
<thead>
<tr>
<th>Event</th>
<th>Location</th>
<th>Date</th>
</tr>
</thead>
<tbody >
<tr *ngFor="let event of upcomingEvents">
<th><a href="{{event.linkUrl}}" title="{{event.tooltip}}">{{event.name}}</a></th>
<td>{{event.location}}</td>
<td>
<div>
{{getEventDates(event)}}
</div>
</td>
</tr>
</tbody>
</table>
<p>Where we already presented:</p>
<table class="is-full-width">
<thead>
<tr>
<th>Event</th>
<th>Location</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let event of pastEvents">
<th><a href="{{event.linkUrl}}" title="{{event.tooltip}}">{{event.name}}</a></th>
<td>{{event.location}}</td>
<td>
<div>
{{getEventDates(event)}}
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,231 @@
import { Injector } from '@angular/core';
import { Subject } from 'rxjs';
import { Duration, Event, EventsComponent } from './events.component';
import { EventsService } from './events.service';
describe('EventsComponent', () => {
let component: EventsComponent;
let injector: Injector;
let eventsService: TestEventsService;
beforeEach(() => {
injector = Injector.create({
providers: [
{ provide: EventsComponent, deps: [EventsService] } ,
{ provide: EventsService, useClass: TestEventsService, deps: [] },
]
});
eventsService = injector.get(EventsService) as unknown as TestEventsService;
component = injector.get(EventsComponent) as unknown as EventsComponent;
});
it('should have no pastEvents when first created', () => {
expect(component.pastEvents).toBeUndefined();
});
it('should have no upcoming when first created', () => {
expect(component.upcomingEvents).toBeUndefined();
});
describe('ngOnInit()', () => {
beforeEach(() => {
jasmine.clock().install();
jasmine.clock().mockDate(new Date(2020, 5, 15, 23, 59, 59));
component.ngOnInit();
});
afterEach(() => jasmine.clock().uninstall());
it('should separate past and upcoming events', () => {
eventsService.events.next([
createMockEvent(
'Upcoming event 1',
{start: '2020-06-16', end: '2020-06-17'},
{start: '2020-06-18', end: '2020-06-18'}),
createMockEvent(
'Upcoming event 3',
{start: '2222-01-01', end: '2222-01-02'}),
createMockEvent(
'Past event 2',
{start: '2020-06-13', end: '2020-06-14'}),
createMockEvent(
'Upcoming event 2',
{start: '2020-06-17', end: '2020-06-18'},
{start: '2020-06-16', end: '2020-06-16'}),
createMockEvent(
'Past event 1',
{start: '2020-05-30', end: '2020-05-31'}),
createMockEvent(
'Past event 3',
{start: '2020-06-14', end: '2020-06-14'},
{start: '2020-06-16', end: '2020-06-17'}),
]);
expect(component.pastEvents.map(evt => evt.name)).toEqual(jasmine.arrayWithExactContents(
['Past event 1', 'Past event 2', 'Past event 3']));
expect(component.upcomingEvents.map(evt => evt.name)).toEqual(jasmine.arrayWithExactContents(
['Upcoming event 1', 'Upcoming event 2', 'Upcoming event 3']));
});
it('should order past events in reverse chronological order (ignoring workshops dates)', () => {
eventsService.events.next([
createMockEvent(
'Past event 2',
{start: '1999-12-13', end: '1999-12-14'},
{start: '1999-12-11', end: '1999-12-11'}),
createMockEvent(
'Past event 4',
{start: '2020-01-16', end: '2020-01-17'},
{start: '2020-01-14', end: '2020-01-15'}),
createMockEvent(
'Past event 3',
{start: '2020-01-15', end: '2020-01-16'},
{start: '2020-01-17', end: '2020-01-18'}),
createMockEvent(
'Past event 1',
{start: '1999-12-12', end: '1999-12-15'}),
]);
expect(component.pastEvents.map(evt => evt.name)).toEqual(
['Past event 4', 'Past event 3', 'Past event 2', 'Past event 1']);
});
it('should order upcoming events in chronological order (ignoring workshops dates)', () => {
eventsService.events.next([
createMockEvent(
'Upcoming event 2',
{start: '2020-12-13', end: '2020-12-14'},
{start: '2020-12-11', end: '2020-12-11'}),
createMockEvent(
'Upcoming event 4',
{start: '2021-01-16', end: '2021-01-17'},
{start: '2021-01-14', end: '2021-01-15'}),
createMockEvent(
'Upcoming event 3',
{start: '2021-01-15', end: '2021-01-16'},
{start: '2021-01-17', end: '2021-01-18'}),
createMockEvent(
'Upcoming event 1',
{start: '2020-12-12', end: '2020-12-15'}),
]);
expect(component.upcomingEvents.map(evt => evt.name)).toEqual(
['Upcoming event 1', 'Upcoming event 2', 'Upcoming event 3', 'Upcoming event 4']);
});
it('should treat ongoing events as upcoming', () => {
eventsService.events.next([
createMockEvent(
'Ongoing event 1',
{start: '2020-06-14', end: '2020-06-16'}),
createMockEvent(
'Ongoing event 2',
{start: '2020-06-14', end: '2020-06-15'},
{start: '2020-06-13', end: '2020-06-13'}),
]);
expect(component.pastEvents).toEqual([]);
expect(component.upcomingEvents.map(evt => evt.name)).toEqual(jasmine.arrayWithExactContents(
['Ongoing event 1', 'Ongoing event 2']));
});
});
describe('getEventDates()', () => {
describe('(without workshops)', () => {
it('should correctly format the main event date', () => {
const testEvent = createMockEvent('Test', {start: '2020-06-20', end: '2020-06-20'});
expect(component.getEventDates(testEvent)).toBe('June 20, 2020');
});
it('should correctly format the main event date spanning mupliple days', () => {
const testEvent = createMockEvent('Test', {start: '2019-09-19', end: '2019-09-21'});
expect(component.getEventDates(testEvent)).toBe('September 19-21, 2019');
});
it('should correctly format the main event date spanning mupliple months', () => {
const testEvent = createMockEvent('Test', {start: '2019-10-30', end: '2019-11-01'});
expect(component.getEventDates(testEvent)).toBe('October 30 - November 1, 2019');
});
});
describe('(with workshops)', () => {
it('should correctly format event dates with workshops after main event', () => {
const testEvent = createMockEvent(
'Test',
{start: '2020-07-25', end: '2020-07-26'},
{start: '2020-07-27', end: '2020-07-27'});
expect(component.getEventDates(testEvent))
.toBe('July 25-26 (conference), July 27 (workshops), 2020');
});
it('should correctly format event dates with workshops before main event', () => {
const testEvent = createMockEvent(
'Test',
{start: '2019-10-07', end: '2019-10-07'},
{start: '2019-10-06', end: '2019-10-06'});
expect(component.getEventDates(testEvent))
.toBe('October 6 (workshops), October 7 (conference), 2019');
});
it('should correctly format event dates spanning multiple days', () => {
const testEvent = createMockEvent(
'Test',
{start: '2019-08-30', end: '2019-08-31'},
{start: '2019-08-28', end: '2019-08-29'});
expect(component.getEventDates(testEvent))
.toBe('August 28-29 (workshops), August 30-31 (conference), 2019');
});
it('should correctly format event dates with workshops on different month before the main event',
() => {
const testEvent = createMockEvent(
'Test',
{start: '2020-08-01', end: '2020-08-02'},
{start: '2020-07-30', end: '2020-07-31'});
expect(component.getEventDates(testEvent))
.toBe('July 30-31 (workshops), August 1-2 (conference), 2020');
});
it('should correctly format event dates with workshops on different month after the main event',
() => {
const testEvent = createMockEvent(
'Test',
{start: '2020-07-30', end: '2020-07-31'},
{start: '2020-08-01', end: '2020-08-02'});
expect(component.getEventDates(testEvent))
.toBe('July 30-31 (conference), August 1-2 (workshops), 2020');
});
it('should correctly format event dates spanning multiple months', () => {
const testEvent = createMockEvent(
'Test',
{start: '2020-07-31', end: '2020-08-01'},
{start: '2020-07-30', end: '2020-08-01'});
expect(component.getEventDates(testEvent))
.toBe('July 30 - August 1 (workshops), July 31 - August 1 (conference), 2020');
});
});
});
// Helpers
class TestEventsService {
events = new Subject<Event[]>();
}
function createMockEvent(name: string, date: Duration, workshopsDate?: Duration): Event {
return {
name,
location: '',
linkUrl: '',
date,
workshopsDate,
};
}
});

View File

@ -0,0 +1,102 @@
import { Component, OnInit } from '@angular/core';
import { EventsService } from './events.service';
const DAY = 24 * 60 * 60 * 1000;
const MONTHS = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
export type date = string; // of the format `YYYY-MM-DD`.
export interface Duration {
start: date;
end: date;
}
export interface Event {
name: string;
location: string;
linkUrl: string;
tooltip?: string;
date: Duration;
workshopsDate?: Duration;
}
@Component({
selector: 'aio-events',
templateUrl: 'events.component.html'
})
export class EventsComponent implements OnInit {
pastEvents: Event[];
upcomingEvents: Event[];
constructor(private eventsService: EventsService) { }
ngOnInit() {
this.eventsService.events.subscribe(events => {
this.pastEvents = events
.filter(event => new Date(event.date.end).getTime() < Date.now() - DAY)
.sort((l: Event, r: Event) => isBefore(l.date, r.date) ? 1 : -1);
this.upcomingEvents = events
.filter(event => new Date(event.date.end).getTime() >= Date.now() - DAY)
.sort((l: Event, r: Event) => isBefore(l.date, r.date) ? -1 : 1);
});
}
getEventDates(event: Event) {
let dateString;
// Check if there is a workshop
if (event.workshopsDate) {
const mainEventDateString = `${processDate(event.date)} (conference)`;
const workshopsDateString = `${processDate(event.workshopsDate)} (workshops)`;
const areWorkshopsBeforeEvent = isBefore(event.workshopsDate, event.date);
dateString = areWorkshopsBeforeEvent ?
`${workshopsDateString}, ${mainEventDateString}` :
`${mainEventDateString}, ${workshopsDateString}`;
} else {
// If no work shop date create conference date string
dateString = processDate(event.date);
}
dateString = `${dateString}, ${new Date(event.date.end).getFullYear()}`;
return dateString;
}
}
function processDate(dates: Duration) {
// Covert Date sting to date object for comparisons
const startDate = new Date(dates.start);
const endDate = new Date(dates.end);
// Create a date string in the start like January 31
let processedDate = `${MONTHS[startDate.getMonth()]} ${startDate.getDate()}`;
// If they are in different months add the string '- February 2' Making the final string January 31 - February 2
if (startDate.getMonth() !== endDate.getMonth()) {
processedDate = `${processedDate} - ${MONTHS[endDate.getMonth()]} ${endDate.getDate()}`;
} else if (startDate.getDate() !== endDate.getDate()) {
// If not add - date eg it will make // January 30-31
processedDate = `${processedDate}-${endDate.getDate()}`;
}
return processedDate;
}
function isBefore(duration1: Duration, duration2: Duration): boolean {
return (duration1.start < duration2.start) ||
(duration1.start === duration2.start && duration1.end < duration2.end);
}

View File

@ -0,0 +1,15 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EventsComponent } from './events.component';
import { EventsService } from './events.service';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule ],
declarations: [ EventsComponent ],
entryComponents: [ EventsComponent ],
providers: [ EventsService]
})
export class EventsModule implements WithCustomElementComponent {
customElementComponent: Type<any> = EventsComponent;
}

View File

@ -0,0 +1,56 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { EventsService } from './events.service';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
describe('EventsService', () => {
let injector: Injector;
let eventsService: EventsService;
let httpMock: HttpTestingController;
let mockLogger: MockLogger;
beforeEach(() => {
injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
EventsService,
{ provide: Logger, useClass: MockLogger }
]
});
eventsService = injector.get<EventsService>(EventsService);
mockLogger = injector.get(Logger) as any;
httpMock = injector.get(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('should make a single connection to the server', () => {
eventsService.events.subscribe();
eventsService.events.subscribe();
httpMock.expectOne('generated/events.json');
expect().nothing(); // Prevent jasmine from complaining about no expectations.
});
it('should handle a failed request for `events.json`', () => {
const request = httpMock.expectOne('generated/events.json');
request.error(new ErrorEvent('404'));
expect(mockLogger.output.error).toEqual([
[jasmine.any(Error)]
]);
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/events\.json request failed:/);
});
it('should return an empty array on a failed request for `events.json`', done => {
const request = httpMock.expectOne('generated/events.json');
request.error(new ErrorEvent('404'));
eventsService.events.subscribe(results => {
expect(results).toEqual([]);
done();
});
});
});

View File

@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ConnectableObservable, Observable, of } from 'rxjs';
import { catchError, publishLast } from 'rxjs/operators';
import { Event } from './events.component';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
import { Logger } from 'app/shared/logger.service';
const eventsPath = CONTENT_URL_PREFIX + 'events.json';
@Injectable()
export class EventsService {
events: Observable<Event[]>;
constructor(private http: HttpClient, private logger: Logger) {
this.events = this.getEvents();
}
private getEvents() {
const events = this.http.get<any>(eventsPath).pipe(
catchError(error => {
this.logger.error(new Error(`${eventsPath} request failed: ${error.message}`));
return of([]);
}),
publishLast()
);
(events as ConnectableObservable<Event[]>).connect();
return events;
}
}

View File

@ -25,7 +25,6 @@
@import 'progress-bar';
@import 'presskit';
@import 'resources';
@import 'scrollbar';
@import 'search-results';
@import 'select-menu';
@import 'table';

View File

@ -1,27 +0,0 @@
body::-webkit-scrollbar, mat-sidenav.sidenav::-webkit-scrollbar, .mat-sidenav-content::-webkit-scrollbar {
height: 6px;
width: 6px;
}
body::-webkit-scrollbar-track, mat-sidenav.sidenav::-webkit-scrollbar-track, .mat-sidenav-content::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
}
body::-webkit-scrollbar-thumb, mat-sidenav.sidenav::-webkit-scrollbar-thumb, .mat-sidenav-content::-webkit-scrollbar-thumb {
background-color: $mediumgray;
outline: 1px solid $darkgray;
}
.search-results::-webkit-scrollbar, .toc-container::-webkit-scrollbar {
height: 4px;
width: 4px;
}
.search-results::-webkit-scrollbar-track, .toc-container::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
}
.search-results::-webkit-scrollbar-thumb, .toc-container::-webkit-scrollbar-thumb {
background-color: $mediumgray;
outline: 1px solid slategrey;
}

View File

@ -5,6 +5,7 @@ module.exports = function createSitemap() {
'contributors-json',
'navigation-json',
'resources-json',
'events-json'
],
ignoredPaths: [
'file-not-found',

View File

@ -82,6 +82,11 @@ module.exports = new Package('angular-content', [basePackage, contentPackage])
include: CONTENTS_PATH + '/marketing/resources.json',
fileReader: 'jsonFileReader'
},
{
basePath: CONTENTS_PATH,
include: CONTENTS_PATH + '/marketing/events.json',
fileReader: 'jsonFileReader'
},
]);
collectExamples.exampleFolders.push('examples');
@ -110,7 +115,8 @@ module.exports = new Package('angular-content', [basePackage, contentPackage])
{docTypes: ['navigation-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
{docTypes: ['contributors-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
{docTypes: ['announcements-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
{docTypes: ['resources-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'}
{docTypes: ['resources-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
{docTypes: ['events-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'}
]);
})

View File

@ -13,7 +13,7 @@ import * as webdriver from 'selenium-webdriver';
declare var expect: any;
export function openBrowser(config: {
url: string,
url?: string,
params?: {name: string, value: any}[],
ignoreBrowserSynchronization?: boolean
}) {

View File

@ -23,27 +23,34 @@ const globalOptions = {
const runner = createBenchpressRunner();
export async function runBenchmark(config: {
export async function runBenchmark({
id,
url = '',
params = [],
ignoreBrowserSynchronization = true,
microMetrics,
work,
prepare,
setup,
}: {
id: string,
url: string,
params: {name: string, value: any}[],
url?: string,
params?: {name: string, value: any}[],
ignoreBrowserSynchronization?: boolean,
microMetrics?: {[key: string]: string},
work?: () => void,
prepare?: () => void,
setup?: () => void
work?: (() => void)|(() => Promise<unknown>),
prepare?: (() => void)|(() => Promise<unknown>),
setup?: (() => void)|(() => Promise<unknown>),
}): Promise<any> {
openBrowser(config);
if (config.setup) {
await config.setup();
openBrowser({url, params, ignoreBrowserSynchronization});
if (setup) {
await setup();
}
const description: {[key: string]: any} = {};
config.params.forEach((param) => description[param.name] = param.value);
return runner.sample({
id: config.id,
execute: config.work,
prepare: config.prepare,
microMetrics: config.microMetrics,
id,
execute: work,
prepare,
microMetrics,
providers: [{provide: Options.SAMPLE_DESCRIPTION, useValue: {}}]
});
}

View File

@ -2,25 +2,20 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "caretaker",
srcs = [
"cli.ts",
],
srcs = glob([
"**/*.ts",
]),
module_name = "@angular/dev-infra-private/caretaker",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/caretaker/check",
"//dev-infra/utils",
"@npm//@types/node",
"@npm//@types/node-fetch",
"@npm//@types/yargs",
"@npm//multimatch",
"@npm//node-fetch",
"@npm//typed-graphqlify",
"@npm//yaml",
"@npm//yargs",
],
)
ts_library(
name = "config",
srcs = [
"config.ts",
],
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
],
)

View File

@ -1,21 +0,0 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "check",
srcs = glob(["*.ts"]),
module_name = "@angular/dev-infra-private/caretaker/service-statuses",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/caretaker:config",
"//dev-infra/utils",
"@npm//@types/fs-extra",
"@npm//@types/node",
"@npm//@types/node-fetch",
"@npm//@types/yargs",
"@npm//multimatch",
"@npm//node-fetch",
"@npm//typed-graphqlify",
"@npm//yaml",
"@npm//yargs",
],
)

View File

@ -9,6 +9,7 @@
import {GitClient} from '../../utils/git';
import {getCaretakerConfig} from '../config';
import {printCiStatus} from './ci';
import {printG3Comparison} from './g3';
import {printGithubTasks} from './github';
import {printServiceStatuses} from './services';
@ -21,7 +22,9 @@ export async function checkServiceStatuses(githubToken: string) {
/** The GitClient for interacting with git and Github. */
const git = new GitClient(githubToken, config);
// TODO(josephperrott): Allow these checks to be loaded in parallel.
await printServiceStatuses();
await printGithubTasks(git, config.caretaker);
await printG3Comparison(git);
await printCiStatus(git);
}

View File

@ -0,0 +1,59 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import fetch from 'node-fetch';
import {bold, green, info, red} from '../../utils/console';
import {GitClient} from '../../utils/git';
/** The results of checking the status of CI. */
interface StatusCheckResult {
status: 'success'|'failed'|'canceled'|'infrastructure_fail'|'timedout'|'failed'|'no_tests';
timestamp: Date;
buildUrl: string;
}
/** Retrieve and log status of CI for the project. */
export async function printCiStatus(git: GitClient) {
info.group(bold(`CI`));
// TODO(josephperrott): Expand list of branches checked to all active branches.
await printStatus(git, 'master');
info.groupEnd();
info();
}
/** Log the status of CI for a given branch to the console. */
async function printStatus(git: GitClient, branch: string) {
const result = await getStatusOfBranch(git, branch);
const branchName = branch.padEnd(10);
if (result === null) {
info(`${branchName} was not found on CircleCI`);
} else if (result.status === 'success') {
info(`${branchName}`);
} else {
info(`${branchName} ❌ (Ran at: ${result.timestamp.toLocaleString()})`);
}
}
/** Get the CI status of a given branch from CircleCI. */
async function getStatusOfBranch(git: GitClient, branch: string): Promise<StatusCheckResult|null> {
const {owner, name} = git.remoteConfig;
const url = `https://circleci.com/api/v1.1/project/gh/${owner}/${name}/tree/${
branch}?limit=1&filter=completed&shallow=true`;
const result = (await fetch(url).then(result => result.json()))?.[0];
if (result) {
return {
status: result.outcome,
timestamp: new Date(result.stop_time),
buildUrl: result.build_url
};
}
return null;
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {existsSync, readFileSync} from 'fs-extra';
import {existsSync, readFileSync} from 'fs';
import * as multimatch from 'multimatch';
import {join} from 'path';
import {parse as parseYaml} from 'yaml';

View File

@ -13,15 +13,9 @@ import {GitClient} from '../../utils/git';
import {CaretakerConfig} from '../config';
interface GithubInfoQuery {
[key: string]: {
issueCount: number,
};
}
/** Retrieve the number of matching issues for each github query. */
export async function printGithubTasks(git: GitClient, config: CaretakerConfig) {
if (!config.githubQueries?.length) {
export async function printGithubTasks(git: GitClient, config?: CaretakerConfig) {
if (!config?.githubQueries?.length) {
debug('No github queries defined in the configuration, skipping.');
return;
}

View File

@ -3,18 +3,10 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "commit-message",
srcs = [
"builder.ts",
"cli.ts",
"commit-message-draft.ts",
"config.ts",
"parse.ts",
"restore-commit-message.ts",
"validate.ts",
"validate-file.ts",
"validate-range.ts",
"wizard.ts",
],
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/dev-infra-private/commit-message",
visibility = ["//dev-infra:__subpackages__"],
deps = [
@ -32,11 +24,7 @@ ts_library(
ts_library(
name = "test_lib",
testonly = True,
srcs = [
"builder.spec.ts",
"parse.spec.ts",
"validate.spec.ts",
],
srcs = glob(["**/*.spec.ts"]),
deps = [
":commit-message",
"//dev-infra/utils",

View File

@ -7,104 +7,19 @@
*/
import * as yargs from 'yargs';
import {info} from '../utils/console';
import {restoreCommitMessage} from './restore-commit-message';
import {validateFile} from './validate-file';
import {validateCommitRange} from './validate-range';
import {runWizard} from './wizard';
import {RestoreCommitMessageModule} from './restore-commit-message/cli';
import {ValidateFileModule} from './validate-file/cli';
import {ValidateRangeModule} from './validate-range/cli';
import {WizardModule} from './wizard/cli';
/** Build the parser for the commit-message commands. */
export function buildCommitMessageParser(localYargs: yargs.Argv) {
return localYargs.help()
.strict()
.command(
'restore-commit-message-draft', false,
args => {
return args.option('file-env-variable', {
type: 'string',
array: true,
conflicts: ['file'],
required: true,
description:
'The key for the environment variable which holds the arguments for the\n' +
'prepare-commit-msg hook as described here:\n' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
coerce: arg => {
const [file, source] = (process.env[arg] || '').split(' ');
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return [file, source];
},
});
},
args => {
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
})
.command(
'wizard <filePath> [source] [commitSha]', '', ((args: any) => {
return args
.positional(
'filePath',
{description: 'The file path to write the generated commit message into'})
.positional('source', {
choices: ['message', 'template', 'merge', 'squash', 'commit'],
description: 'The source of the commit message as described here: ' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
})
.positional(
'commitSha', {description: 'The commit sha if source is set to `commit`'});
}),
async (args: any) => {
await runWizard(args);
})
.command(
'pre-commit-validate', 'Validate the most recent commit message', {
'file': {
type: 'string',
conflicts: ['file-env-variable'],
description: 'The path of the commit message file.',
},
'file-env-variable': {
type: 'string',
conflicts: ['file'],
description:
'The key of the environment variable for the path of the commit message file.',
coerce: arg => {
const file = process.env[arg];
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return file;
},
}
},
args => {
const file = args.file || args['file-env-variable'] || '.git/COMMIT_EDITMSG';
validateFile(file);
})
.command(
'validate-range', 'Validate a range of commit messages', {
'range': {
description: 'The range of commits to check, e.g. --range abc123..xyz456',
demandOption: ' A range must be provided, e.g. --range abc123..xyz456',
type: 'string',
requiresArg: true,
},
},
argv => {
// If on CI, and not pull request number is provided, assume the branch
// being run on is an upstream branch.
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
info(`need to validate commit messages on CI runs on upstream branches.`);
info();
info(`Skipping check of provided commit range`);
return;
}
validateCommitRange(argv.range);
});
.command(RestoreCommitMessageModule)
.command(WizardModule)
.command(ValidateFileModule)
.command(ValidateRangeModule);
}
if (require.main == module) {

View File

@ -0,0 +1,13 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* The source triggering the git commit message creation.
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export type CommitMsgSource = 'message'|'template'|'merge'|'squash'|'commit';

View File

@ -8,6 +8,7 @@
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
/** Configuration for commit-message comands. */
export interface CommitMessageConfig {
maxLineLength: number;
minBodyLength: number;

View File

@ -0,0 +1,51 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Arguments, Argv, CommandModule} from 'yargs';
import {CommitMsgSource} from '../commit-message-source';
import {restoreCommitMessage} from './restore-commit-message';
export interface RestoreCommitMessageOptions {
fileEnvVariable: string[];
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs.option('file-env-variable' as 'fileEnvVariable', {
type: 'string',
array: true,
demandOption: true,
description: 'The key for the environment variable which holds the arguments for the\n' +
'prepare-commit-msg hook as described here:\n' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
coerce: arg => {
const [file, source] = (process.env[arg] || '').split(' ');
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return [file, source];
},
});
}
/** Handles the command. */
async function handler({fileEnvVariable}: Arguments<RestoreCommitMessageOptions>) {
restoreCommitMessage(fileEnvVariable[0], fileEnvVariable[1] as CommitMsgSource);
}
/** yargs command module describing the command. */
export const RestoreCommitMessageModule: CommandModule<{}, RestoreCommitMessageOptions> = {
handler,
builder,
command: 'restore-commit-message-draft',
// Description: Restore a commit message draft if one has been saved from a failed commit attempt.
// No describe is defiend to hide the command from the --help.
describe: false,
};

View File

@ -8,9 +8,10 @@
import {writeFileSync} from 'fs';
import {debug, log} from '../utils/console';
import {debug, log} from '../../utils/console';
import {loadCommitMessageDraft} from './commit-message-draft';
import {loadCommitMessageDraft} from '../commit-message-draft';
import {CommitMsgSource} from '../commit-message-source';
/**
* Restore the commit message draft to the git to be used as the default commit message.
@ -18,8 +19,7 @@ import {loadCommitMessageDraft} from './commit-message-draft';
* The source provided may be one of the sources described in
* https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export function restoreCommitMessage(
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
export function restoreCommitMessage(filePath: string, source?: CommitMsgSource) {
if (!!source) {
log('Skipping commit message restoration attempt');
if (source === 'message') {

View File

@ -1,30 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {readFileSync} from 'fs';
import {resolve} from 'path';
import {getRepoBaseDir} from '../utils/config';
import {info} from '../utils/console';
import {deleteCommitMessageDraft, saveCommitMessageDraft} from './commit-message-draft';
import {validateCommitMessage} from './validate';
/** Validate commit message at the provided file path. */
export function validateFile(filePath: string) {
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
if (validateCommitMessage(commitMessage)) {
info('√ Valid commit message');
deleteCommitMessageDraft(filePath);
return;
}
// On all invalid commit messages, the commit message should be saved as a draft to be
// restored on the next commit attempt.
saveCommitMessageDraft(filePath, commitMessage);
// If the validation did not return true, exit as a failure.
process.exit(1);
}

View File

@ -0,0 +1,62 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Arguments, Argv, CommandModule} from 'yargs';
import {getUserConfig} from '../../utils/config';
import {validateFile} from './validate-file';
export interface ValidateFileOptions {
file?: string;
fileEnvVariable?: string;
error: boolean;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs
.option('file', {
type: 'string',
conflicts: ['file-env-variable'],
description: 'The path of the commit message file.',
})
.option('file-env-variable' as 'fileEnvVariable', {
type: 'string',
conflicts: ['file'],
description: 'The key of the environment variable for the path of the commit message file.',
coerce: (arg: string) => {
const file = process.env[arg];
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return file;
},
})
.option('error', {
type: 'boolean',
description:
'Whether invalid commit messages should be treated as failures rather than a warning',
default: !!getUserConfig().commitMessage?.errorOnInvalidMessage || !!process.env['CI']
});
}
/** Handles the command. */
async function handler({error, file, fileEnvVariable}: Arguments<ValidateFileOptions>) {
const filePath = file || fileEnvVariable || '.git/COMMIT_EDITMSG';
validateFile(filePath, error);
}
/** yargs command module describing the command. */
export const ValidateFileModule: CommandModule<{}, ValidateFileOptions> = {
handler,
builder,
command: 'pre-commit-validate',
describe: 'Validate the most recent commit message',
};

View File

@ -0,0 +1,47 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {readFileSync} from 'fs';
import {resolve} from 'path';
import {getRepoBaseDir} from '../../utils/config';
import {error, green, info, log, red, yellow} from '../../utils/console';
import {deleteCommitMessageDraft, saveCommitMessageDraft} from '../commit-message-draft';
import {printValidationErrors, validateCommitMessage} from '../validate';
/** Validate commit message at the provided file path. */
export function validateFile(filePath: string, isErrorMode: boolean) {
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
const {valid, errors} = validateCommitMessage(commitMessage);
if (valid) {
info(`${green('√')} Valid commit message`);
deleteCommitMessageDraft(filePath);
process.exitCode = 0;
return;
}
/** Function used to print to the console log. */
let printFn = isErrorMode ? error : log;
printFn(`${isErrorMode ? red('✘') : yellow('!')} Invalid commit message`);
printValidationErrors(errors, printFn);
if (isErrorMode) {
printFn(red('Aborting commit attempt due to invalid commit message.'));
printFn(
red('Commit message aborted as failure rather than warning due to local configuration.'));
} else {
printFn(yellow('Before this commit can be merged into the upstream repository, it must be'));
printFn(yellow('amended to follow commit message guidelines.'));
}
// On all invalid commit messages, the commit message should be saved as a draft to be
// restored on the next commit attempt.
saveCommitMessageDraft(filePath, commitMessage);
// Set the correct exit code based on if invalid commit message is an error.
process.exitCode = isErrorMode ? 1 : 0;
}

View File

@ -0,0 +1,50 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Arguments, Argv, CommandModule} from 'yargs';
import {info} from '../../utils/console';
import {validateCommitRange} from './validate-range';
export interface ValidateRangeOptions {
range: string;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs.option('range', {
description: 'The range of commits to check, e.g. --range abc123..xyz456',
demandOption: ' A range must be provided, e.g. --range abc123..xyz456',
type: 'string',
requiresArg: true,
});
}
/** Handles the command. */
async function handler({range}: Arguments<ValidateRangeOptions>) {
// If on CI, and no pull request number is provided, assume the branch
// being run on is an upstream branch.
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
info(`need to validate commit messages on CI runs on upstream branches.`);
info();
info(`Skipping check of provided commit range`);
return;
}
validateCommitRange(range);
}
/** yargs command module describing the command. */
export const ValidateRangeModule: CommandModule<{}, ValidateRangeOptions> = {
handler,
builder,
command: 'validate-range',
describe: 'Validate a range of commit messages',
};

View File

@ -5,11 +5,11 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {info} from '../utils/console';
import {exec} from '../utils/shelljs';
import {error, info} from '../../utils/console';
import {exec} from '../../utils/shelljs';
import {parseCommitMessage} from './parse';
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
import {parseCommitMessage} from '../parse';
import {printValidationErrors, validateCommitMessage, ValidateCommitMessageOptions} from '../validate';
// Whether the provided commit is a fixup commit.
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
@ -19,11 +19,20 @@ const extractCommitHeader = (m: string) => parseCommitMessage(m).header;
/** Validate all commits in a provided git commit range. */
export function validateCommitRange(range: string) {
// A random value is used as a string to allow for a definite split point in the git log result.
/**
* A random value is used as a string to allow for a definite split point in the git log result.
*/
const randomValueSeparator = `${Math.random()}`;
// Custom git log format that provides the commit header and body, separated as expected with
// the custom separator as the trailing value.
/**
* Custom git log format that provides the commit header and body, separated as expected with the
* custom separator as the trailing value.
*/
const gitLogFormat = `%s%n%n%b${randomValueSeparator}`;
/**
* A list of tuples containing a commit header string and the list of error messages for the
* commit.
*/
const errors: [commitHeader: string, errors: string[]][] = [];
// Retrieve the commits in the provided range.
const result = exec(`git log --reverse --format=${gitLogFormat} ${range}`);
@ -45,12 +54,22 @@ export function validateCommitRange(range: string) {
undefined :
commits.slice(0, i).filter(isNonFixup).map(extractCommitHeader)
};
return validateCommitMessage(m, options);
const {valid, errors: localErrors, commit} = validateCommitMessage(m, options);
if (localErrors.length) {
errors.push([commit.header, localErrors]);
}
return valid;
});
if (allCommitsInRangeValid) {
info('√ All commit messages in range valid.');
} else {
error('✘ Invalid commit message');
errors.forEach(([header, validationErrors]) => {
error.group(header);
printValidationErrors(validationErrors);
error.groupEnd();
});
// Exit with a non-zero exit code if invalid commit messages have
// been discovered.
process.exit(1);

View File

@ -8,7 +8,7 @@
// Imports
import * as validateConfig from './config';
import {validateCommitMessage} from './validate';
import {validateCommitMessage, ValidateCommitMessageResult} from './validate';
type CommitMessageConfig = validateConfig.CommitMessageConfig;
@ -31,44 +31,35 @@ const SCOPES = config.commitMessage.scopes.join(', ');
const INVALID = false;
const VALID = true;
function expectValidationResult(
validationResult: ValidateCommitMessageResult, valid: boolean, errors: string[] = []) {
expect(validationResult).toEqual(jasmine.objectContaining({valid, errors}));
}
// TODO(josephperrott): Clean up tests to test script rather than for
// specific commit messages we want to use.
describe('validate-commit-message.js', () => {
let lastError: string = '';
beforeEach(() => {
lastError = '';
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
spyOn(validateConfig, 'getCommitMessageConfig')
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
});
describe('validateMessage()', () => {
it('should be valid', () => {
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('fixup! fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('squash! fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('Revert: "fix(packaging): something"')).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage('feat(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('fixup! fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('squash! fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('Revert: "fix(packaging): something"'), VALID);
});
it('should validate max length', () => {
const msg =
'fix(compiler): something super mega extra giga tera long, maybe even longer and longer and longer and longer and longer and longer...';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`The commit message header is longer than ${
config.commitMessage.maxLineLength} characters`);
expectValidationResult(validateCommitMessage(msg), INVALID, [
`The commit message header is longer than ${config.commitMessage.maxLineLength} characters`
]);
});
it('should skip max length limit for URLs', () => {
@ -77,49 +68,56 @@ describe('validate-commit-message.js', () => {
'limit. For more details see the following super long URL:\n\n' +
'https://github.com/angular/components/commit/e2ace018ddfad10608e0e32932c43dcfef4095d7#diff-9879d6db96fd29134fc802214163b95a';
expect(validateCommitMessage(msg)).toBe(VALID);
expectValidationResult(validateCommitMessage(msg), VALID);
});
it('should validate "<type>(<scope>): <subject>" format', () => {
const msg = 'not correct format';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`The commit message header does not match the expected format.`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`The commit message header does not match the expected format.`]);
});
it('should fail when type is invalid', () => {
const msg = 'weird(core): something';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`'weird' is not an allowed type.\n => TYPES: ${TYPES}`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'weird' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
it('should fail when scope is invalid', () => {
const errorMessageFor = (scope: string, header: string) =>
`'${scope}' is not an allowed scope.\n => SCOPES: ${SCOPES}`;
expect(validateCommitMessage('fix(Compiler): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('Compiler', 'fix(Compiler): something'));
expectValidationResult(
validateCommitMessage('fix(Compiler): something'), INVALID,
[errorMessageFor('Compiler', 'fix(Compiler): something')]);
expect(validateCommitMessage('feat(bah): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('bah', 'feat(bah): something'));
expectValidationResult(
validateCommitMessage('feat(bah): something'), INVALID,
[errorMessageFor('bah', 'feat(bah): something')]);
expect(validateCommitMessage('fix(webworker): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
expectValidationResult(
validateCommitMessage('fix(webworker): something'), INVALID,
[errorMessageFor('webworker', 'fix(webworker): something')]);
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
expectValidationResult(
validateCommitMessage('refactor(security): something'), INVALID,
[errorMessageFor('security', 'refactor(security): something')]);
expect(validateCommitMessage('refactor(docs): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('docs', 'refactor(docs): something'));
expectValidationResult(
validateCommitMessage('refactor(docs): something'), INVALID,
[errorMessageFor('docs', 'refactor(docs): something')]);
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
expectValidationResult(
validateCommitMessage('feat(angular): something'), INVALID,
[errorMessageFor('angular', 'feat(angular): something')]);
});
it('should allow empty scope', () => {
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage('build: blablabla'), VALID);
});
// We do not want to allow WIP. It is OK to fail the PR build in this case to show that there is
@ -127,30 +125,25 @@ describe('validate-commit-message.js', () => {
it('should not allow "WIP: ..." syntax', () => {
const msg = 'WIP: fix: something';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
describe('(revert)', () => {
it('should allow valid "revert: ..." syntaxes', () => {
expect(validateCommitMessage('revert: anything')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('Revert: "anything"')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('revert anything')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('rEvErT anything')).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage('revert: anything'), VALID);
expectValidationResult(validateCommitMessage('Revert: "anything"'), VALID);
expectValidationResult(validateCommitMessage('revert anything'), VALID);
expectValidationResult(validateCommitMessage('rEvErT anything'), VALID);
});
it('should not allow "revert(scope): ..." syntax', () => {
const msg = 'revert(compiler): reduce generated code payload size by 65%';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`'revert' is not an allowed type.\n => TYPES: ${TYPES}`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'revert' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
// https://github.com/angular/angular/issues/23479
@ -158,28 +151,26 @@ describe('validate-commit-message.js', () => {
const msg =
'Revert "fix(compiler): Pretty print object instead of [Object object] (#22689)" (#23442)';
expect(validateCommitMessage(msg)).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage(msg), VALID);
});
});
describe('(squash)', () => {
describe('without `disallowSquash`', () => {
it('should return commits as valid', () => {
expect(validateCommitMessage('squash! feat(core): add feature')).toBe(VALID);
expect(validateCommitMessage('squash! fix: a bug')).toBe(VALID);
expect(validateCommitMessage('squash! fix a typo')).toBe(VALID);
expectValidationResult(validateCommitMessage('squash! feat(core): add feature'), VALID);
expectValidationResult(validateCommitMessage('squash! fix: a bug'), VALID);
expectValidationResult(validateCommitMessage('squash! fix a typo'), VALID);
});
});
describe('with `disallowSquash`', () => {
it('should fail', () => {
expect(validateCommitMessage('fix(core): something', {disallowSquash: true})).toBe(VALID);
expect(validateCommitMessage('squash! fix(core): something', {
disallowSquash: true
})).toBe(INVALID);
expect(lastError).toContain(
'The commit must be manually squashed into the target commit');
expectValidationResult(
validateCommitMessage('fix(core): something', {disallowSquash: true}), VALID);
expectValidationResult(
validateCommitMessage('squash! fix(core): something', {disallowSquash: true}),
INVALID, ['The commit must be manually squashed into the target commit']);
});
});
});
@ -187,9 +178,9 @@ describe('validate-commit-message.js', () => {
describe('(fixup)', () => {
describe('without `nonFixupCommitHeaders`', () => {
it('should return commits as valid', () => {
expect(validateCommitMessage('fixup! feat(core): add feature')).toBe(VALID);
expect(validateCommitMessage('fixup! fix: a bug')).toBe(VALID);
expect(validateCommitMessage('fixup! fixup! fix: a bug')).toBe(VALID);
expectValidationResult(validateCommitMessage('fixup! feat(core): add feature'), VALID);
expectValidationResult(validateCommitMessage('fixup! fix: a bug'), VALID);
expectValidationResult(validateCommitMessage('fixup! fixup! fix: a bug'), VALID);
});
});
@ -197,36 +188,39 @@ describe('validate-commit-message.js', () => {
it('should check that the fixup commit matches a non-fixup one', () => {
const msg = 'fixup! foo';
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}))
.toBe(VALID);
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}))
.toBe(VALID);
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}))
.toBe(VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}),
VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}),
VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}),
VALID);
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}))
.toBe(INVALID);
expect(lastError).toContain(
'Unable to find match for fixup commit among prior commits: \n' +
' qux\n' +
' quux\n' +
' quuux');
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}),
INVALID,
['Unable to find match for fixup commit among prior commits: \n' +
' qux\n' +
' quux\n' +
' quuux']);
});
it('should fail if `nonFixupCommitHeaders` is empty', () => {
expect(validateCommitMessage('refactor(core): make reactive', {
disallowSquash: false,
nonFixupCommitHeaders: []
})).toBe(VALID);
expect(validateCommitMessage(
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}))
.toBe(INVALID);
expect(lastError).toContain(
`Unable to find match for fixup commit among prior commits: -`);
expectValidationResult(
validateCommitMessage(
'refactor(core): make reactive',
{disallowSquash: false, nonFixupCommitHeaders: []}),
VALID);
expectValidationResult(
validateCommitMessage(
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}),
INVALID, [`Unable to find match for fixup commit among prior commits: -`]);
});
});
});
@ -246,24 +240,27 @@ describe('validate-commit-message.js', () => {
});
it('should fail validation if the body is shorter than `minBodyLength`', () => {
expect(validateCommitMessage(
'fix(core): something\n\n Explanation of the motivation behind this change'))
.toBe(VALID);
expect(validateCommitMessage('fix(core): something\n\n too short')).toBe(INVALID);
expect(lastError).toContain(
'The commit message body does not meet the minimum length of 30 characters');
expect(validateCommitMessage('fix(core): something')).toBe(INVALID);
expect(lastError).toContain(
'The commit message body does not meet the minimum length of 30 characters');
expectValidationResult(
validateCommitMessage(
'fix(core): something\n\n Explanation of the motivation behind this change'),
VALID);
expectValidationResult(
validateCommitMessage('fix(core): something\n\n too short'), INVALID,
['The commit message body does not meet the minimum length of 30 characters']);
expectValidationResult(validateCommitMessage('fix(core): something'), INVALID, [
'The commit message body does not meet the minimum length of 30 characters'
]);
});
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
() => {
expect(validateCommitMessage('docs: just fixing a typo')).toBe(VALID);
expect(validateCommitMessage('docs(core): just fixing a typo')).toBe(VALID);
expect(validateCommitMessage(
'docs(core): just fixing a typo\n\nThis was just a silly typo.'))
.toBe(VALID);
expectValidationResult(validateCommitMessage('docs: just fixing a typo'), VALID);
expectValidationResult(validateCommitMessage('docs(core): just fixing a typo'), VALID);
expectValidationResult(
validateCommitMessage(
'docs(core): just fixing a typo\n\nThis was just a silly typo.'),
VALID);
});
});
});

View File

@ -8,7 +8,7 @@
import {error} from '../utils/console';
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
import {parseCommitMessage} from './parse';
import {parseCommitMessage, ParsedCommitMessage} from './parse';
/** Options for commit message validation. */
export interface ValidateCommitMessageOptions {
@ -16,133 +16,147 @@ export interface ValidateCommitMessageOptions {
nonFixupCommitHeaders?: string[];
}
/** The result of a commit message validation check. */
export interface ValidateCommitMessageResult {
valid: boolean;
errors: string[];
commit: ParsedCommitMessage;
}
/** Regex matching a URL for an entire commit body line. */
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
/** Validate a commit message against using the local repo's config. */
export function validateCommitMessage(
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
function printError(errorMessage: string) {
error(
`INVALID COMMIT MSG: \n` +
`${'─'.repeat(40)}\n` +
`${commitMsg}\n` +
`${'─'.repeat(40)}\n` +
`ERROR: \n` +
` ${errorMessage}` +
`\n\n` +
`The expected format for a commit is: \n` +
`<type>(<scope>): <subject>\n\n<body>`);
}
commitMsg: string, options: ValidateCommitMessageOptions = {}): ValidateCommitMessageResult {
const config = getCommitMessageConfig().commitMessage;
const commit = parseCommitMessage(commitMsg);
const errors: string[] = [];
////////////////////////////////////
// Checking revert, squash, fixup //
////////////////////////////////////
/** Perform the validation checks against the parsed commit. */
function validateCommitAndCollectErrors() {
// TODO(josephperrott): Remove early return calls when commit message errors are found
// All revert commits are considered valid.
if (commit.isRevert) {
return true;
}
////////////////////////////////////
// Checking revert, squash, fixup //
////////////////////////////////////
// All squashes are considered valid, as the commit will be squashed into another in
// the git history anyway, unless the options provided to not allow squash commits.
if (commit.isSquash) {
if (options.disallowSquash) {
printError('The commit must be manually squashed into the target commit');
// All revert commits are considered valid.
if (commit.isRevert) {
return true;
}
// All squashes are considered valid, as the commit will be squashed into another in
// the git history anyway, unless the options provided to not allow squash commits.
if (commit.isSquash) {
if (options.disallowSquash) {
errors.push('The commit must be manually squashed into the target commit');
return false;
}
return true;
}
// Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
// against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
// non-fixup commit (i.e. a commit whose header is identical to this commit's header after
// stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
// check.
if (commit.isFixup) {
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
errors.push(
'Unable to find match for fixup commit among prior commits: ' +
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
return false;
}
return true;
}
////////////////////////////
// Checking commit header //
////////////////////////////
if (commit.header.length > config.maxLineLength) {
errors.push(`The commit message header is longer than ${config.maxLineLength} characters`);
return false;
}
return true;
}
// Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
// against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
// non-fixup commit (i.e. a commit whose header is identical to this commit's header after
// stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
// check.
if (commit.isFixup) {
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
printError(
'Unable to find match for fixup commit among prior commits: ' +
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
if (!commit.type) {
errors.push(`The commit message header does not match the expected format.`);
return false;
}
if (COMMIT_TYPES[commit.type] === undefined) {
errors.push(`'${commit.type}' is not an allowed type.\n => TYPES: ${
Object.keys(COMMIT_TYPES).join(', ')}`);
return false;
}
/** The scope requirement level for the provided type of the commit message. */
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
errors.push(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
commit.scope}' was provided.`);
return false;
}
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
errors.push(
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
return false;
}
if (commit.scope && !config.scopes.includes(commit.scope)) {
errors.push(
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
return false;
}
// Commits with the type of `release` do not require a commit body.
if (commit.type === 'release') {
return true;
}
//////////////////////////
// Checking commit body //
//////////////////////////
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
errors.push(`The commit message body does not meet the minimum length of ${
config.minBodyLength} characters`);
return false;
}
const bodyByLine = commit.body.split('\n');
const lineExceedsMaxLength = bodyByLine.some(line => {
// Check if any line exceeds the max line length limit. The limit is ignored for
// lines that just contain an URL (as these usually cannot be wrapped or shortened).
return line.length > config.maxLineLength && !COMMIT_BODY_URL_LINE_RE.test(line);
});
if (lineExceedsMaxLength) {
errors.push(
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
return false;
}
return true;
}
////////////////////////////
// Checking commit header //
////////////////////////////
if (commit.header.length > config.maxLineLength) {
printError(`The commit message header is longer than ${config.maxLineLength} characters`);
return false;
}
if (!commit.type) {
printError(`The commit message header does not match the expected format.`);
return false;
}
if (COMMIT_TYPES[commit.type] === undefined) {
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
Object.keys(COMMIT_TYPES).join(', ')}`);
return false;
}
/** The scope requirement level for the provided type of the commit message. */
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
commit.scope}' was provided.`);
return false;
}
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
printError(
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
return false;
}
if (commit.scope && !config.scopes.includes(commit.scope)) {
printError(
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
return false;
}
// Commits with the type of `release` do not require a commit body.
if (commit.type === 'release') {
return true;
}
//////////////////////////
// Checking commit body //
//////////////////////////
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
printError(`The commit message body does not meet the minimum length of ${
config.minBodyLength} characters`);
return false;
}
const bodyByLine = commit.body.split('\n');
const lineExceedsMaxLength = bodyByLine.some(line => {
// Check if any line exceeds the max line length limit. The limit is ignored for
// lines that just contain an URL (as these usually cannot be wrapped or shortened).
return line.length > config.maxLineLength && !COMMIT_BODY_URL_LINE_RE.test(line);
});
if (lineExceedsMaxLength) {
printError(
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
return false;
}
return true;
return {valid: validateCommitAndCollectErrors(), errors, commit};
}
/** Print the error messages from the commit message validation to the console. */
export function printValidationErrors(errors: string[], print = error) {
print.group(`Error${errors.length === 1 ? '' : 's'}:`);
errors.forEach(line => print(line));
print.groupEnd();
print();
print('The expected format for a commit is: ');
print('<type>(<scope>): <summary>');
print();
print('<body>');
print();
}

View File

@ -0,0 +1,54 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Arguments, Argv, CommandModule} from 'yargs';
import {CommitMsgSource} from '../commit-message-source';
import {runWizard} from './wizard';
export interface WizardOptions {
filePath: string;
commitSha: string|undefined;
source: CommitMsgSource|undefined;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs
.positional('filePath', {
description: 'The file path to write the generated commit message into',
type: 'string',
demandOption: true,
})
.positional('source', {
choices: ['message', 'template', 'merge', 'squash', 'commit'] as const,
description: 'The source of the commit message as described here: ' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
})
.positional('commitSha', {
description: 'The commit sha if source is set to `commit`',
type: 'string',
});
}
/** Handles the command. */
async function handler(args: Arguments<WizardOptions>) {
await runWizard(args);
}
/** yargs command module describing the command. */
export const WizardModule: CommandModule<{}, WizardOptions> = {
handler,
builder,
command: 'wizard <filePath> [source] [commitSha]',
// Description: Run the wizard to build a base commit message before opening to complete.
// No describe is defiend to hide the command from the --help.
describe: false,
};

View File

@ -7,15 +7,12 @@
*/
import {writeFileSync} from 'fs';
import {info} from '../utils/console';
import {getUserConfig} from '../../utils/config';
import {debug, info} from '../../utils/console';
import {buildCommitMessage} from './builder';
import {buildCommitMessage} from '../builder';
import {CommitMsgSource} from '../commit-message-source';
/**
* The source triggering the git commit message creation.
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export type PrepareCommitMsgHookSource = 'message'|'template'|'merge'|'squash'|'commit';
/** The default commit message used if the wizard does not procude a commit message. */
const defaultCommitMessage = `<type>(<scope>): <summary>
@ -24,11 +21,16 @@ const defaultCommitMessage = `<type>(<scope>): <summary>
# lines at 100 characters.>\n\n`;
export async function runWizard(
args: {filePath: string, source?: PrepareCommitMsgHookSource, commitSha?: string}) {
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
args: {filePath: string, source?: CommitMsgSource, commitSha?: string}) {
if (getUserConfig().commitMessage?.disableWizard) {
debug('Skipping commit message wizard due to enabled `commitMessage.disableWizard` option in');
debug('user config.');
process.exitCode = 0;
return;
}
if (args.source !== undefined) {
info(`Skipping commit message wizard due because the commit was created via '${
info(`Skipping commit message wizard because the commit was created via '${
args.source}' source`);
process.exitCode = 0;
return;

View File

@ -3,6 +3,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "common",
srcs = glob(["*.ts"]),
module_name = "@angular/dev-infra-private/pr/common",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",

View File

@ -33,6 +33,10 @@ import {MergeResult, MergeStatus, PullRequestMergeTask} from './task';
export async function mergePullRequest(
prNumber: number, githubToken: string, projectRoot: string = getRepoBaseDir(),
config?: MergeConfigWithRemote) {
// Set the environment variable to skip all git commit hooks triggered by husky. We are unable to
// rely on `---no-verify` as some hooks still run, notably the `prepare-commit-msg` hook.
process.env['HUSKY_SKIP_HOOKS'] = '1';
const api = await createPullRequestMergeTask(githubToken, projectRoot, config);
// Perform the merge. Force mode can be activated through a command line flag.

View File

@ -12,11 +12,13 @@
"@angular/benchpress": "0.2.1",
"@octokit/graphql": "<from-root>",
"@octokit/types": "<from-root>",
"@octokit/rest": "<from-root>",
"brotli": "<from-root>",
"chalk": "<from-root>",
"cli-progress": "<from-root>",
"glob": "<from-root>",
"inquirer": "<from-root>",
"inquirer-autocomplete-prompt": "<from-root>",
"minimatch": "<from-root>",
"multimatch": "<from-root>",
"node-fetch": "<from-root>",
@ -26,9 +28,7 @@
"tslib": "<from-root>",
"typed-graphqlify": "<from-root>",
"yaml": "<from-root>",
"yargs": "<from-root>"
},
"peerDependencies": {
"yargs": "<from-root>",
"@bazel/buildifier": "<from-root>",
"clang-format": "<from-root>",
"protractor": "<from-root>",

View File

@ -112,7 +112,8 @@ export function main(
if (fixedCircularDeps.length !== 0) {
error(yellow(` Fixed circular dependencies that need to be removed from the golden:`));
fixedCircularDeps.forEach(c => error(`${convertReferenceChainToString(c)}`));
error();
info(yellow(`\n Total: ${newCircularDeps.length} new cycle(s), ${
fixedCircularDeps.length} fixed cycle(s). \n`));
if (approveCommand) {
info(yellow(` Please approve the new golden with: ${approveCommand}`));
} else {

View File

@ -12,13 +12,11 @@ ts_library(
"@npm//@octokit/graphql",
"@npm//@octokit/rest",
"@npm//@octokit/types",
"@npm//@types/fs-extra",
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/shelljs",
"@npm//@types/yargs",
"@npm//chalk",
"@npm//fs-extra",
"@npm//inquirer",
"@npm//inquirer-autocomplete-prompt",
"@npm//shelljs",

View File

@ -9,7 +9,7 @@
import {existsSync} from 'fs';
import {dirname, join} from 'path';
import {error} from './console';
import {debug, error} from './console';
import {exec} from './shelljs';
import {isTsNodeAvailable} from './ts-node';
@ -49,7 +49,16 @@ export type NgDevConfig<T = {}> = CommonConfig&T;
const CONFIG_FILE_PATH = '.ng-dev/config';
/** The configuration for ng-dev. */
let CONFIG: {}|null = null;
let cachedConfig: NgDevConfig|null = null;
/**
* The filename expected for local user config, without the file extension to allow a typescript,
* javascript or json file to be used.
*/
const USER_CONFIG_FILE_PATH = '.ng-dev.user';
/** The local user configuration for ng-dev. */
let userConfig: {[key: string]: any}|null = null;
/**
* Get the configuration from the file system, returning the already loaded
@ -57,15 +66,15 @@ let CONFIG: {}|null = null;
*/
export function getConfig(): NgDevConfig {
// If the global config is not defined, load it from the file system.
if (CONFIG === null) {
if (cachedConfig === null) {
// The full path to the configuration file.
const configPath = join(getRepoBaseDir(), CONFIG_FILE_PATH);
// Set the global config object.
CONFIG = readConfigFile(configPath);
// Read the configuration and validate it before caching it for the future.
cachedConfig = validateCommonConfig(readConfigFile(configPath));
}
// Return a clone of the global config to ensure that a new instance of the config is returned
// each time, preventing unexpected effects of modifications to the config object.
return validateCommonConfig({...CONFIG});
// Return a clone of the cached global config to ensure that a new instance of the config
// is returned each time, preventing unexpected effects of modifications to the config object.
return {...cachedConfig};
}
/** Validate the common configuration has been met for the ng-dev command. */
@ -86,8 +95,11 @@ function validateCommonConfig(config: Partial<NgDevConfig>) {
return config as NgDevConfig;
}
/** Resolves and reads the specified configuration file. */
function readConfigFile(configPath: string): object {
/**
* Resolves and reads the specified configuration file, optionally returning an empty object if the
* configuration file cannot be read.
*/
function readConfigFile(configPath: string, returnEmptyObjectOnError = false): object {
// If the the `.ts` extension has not been set up already, and a TypeScript based
// version of the given configuration seems to exist, set up `ts-node` if available.
if (require.extensions['.ts'] === undefined && existsSync(`${configPath}.ts`) &&
@ -103,7 +115,12 @@ function readConfigFile(configPath: string): object {
try {
return require(configPath);
} catch (e) {
error('Could not read configuration file.');
if (returnEmptyObjectOnError) {
debug(`Could not read configuration file at ${configPath}, returning empty object instead.`);
debug(e);
return {};
}
error(`Could not read configuration file at ${configPath}.`);
error(e);
process.exit(1);
}
@ -135,3 +152,23 @@ export function getRepoBaseDir() {
}
return baseRepoDir.trim();
}
/**
* Get the local user configuration from the file system, returning the already loaded copy if it is
* defined.
*
* @returns The user configuration object, or an empty object if no user configuration file is
* present. The object is an untyped object as there are no required user configurations.
*/
export function getUserConfig() {
// If the global config is not defined, load it from the file system.
if (userConfig === null) {
// The full path to the configuration file.
const configPath = join(getRepoBaseDir(), USER_CONFIG_FILE_PATH);
// Set the global config object.
userConfig = readConfigFile(configPath, true);
}
// Return a clone of the user config to ensure that a new instance of the config is returned
// each time, preventing unexpected effects of modifications to the config object.
return {...userConfig};
}

View File

@ -7,7 +7,7 @@
*/
import chalk from 'chalk';
import {writeFileSync} from 'fs-extra';
import {writeFileSync} from 'fs';
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
import {join} from 'path';
@ -196,6 +196,9 @@ export function captureLogOutputForCommand(argv: Arguments) {
/** Path to the log file location. */
const logFilePath = join(getRepoBaseDir(), '.ng-dev.log');
// Strip ANSI escape codes from log outputs.
LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, '');
writeFileSync(logFilePath, LOGGED_TEXT);
// For failure codes greater than 1, the new logged lines should be written to a specific log

View File

@ -4,71 +4,23 @@ Caretaker is responsible for merging PRs into the individual branches and intern
## Responsibilities
- Draining the queue of PRs ready to be merged. (PRs with [`PR action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22PR+action%3A+merge%22) label)
- Draining the queue of PRs ready to be merged. (PRs with [`action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22action%3A+merge%22) label)
- Assigning [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
## Merging the PR
A PR needs to have `PR action: merge` and `PR target: *` labels to be considered
ready to merge. Merging is performed by running `merge-pr` with a PR number to merge.
A PR needs to have `action: merge` and `target: *` labels to be considered
ready to merge. Merging is performed by running `ng-dev pr merge` with a PR number to merge.
The tooling automatically verifies the given PR is ready for merge. If the PR passes the tests, the
tool will automatically merge it based on the applied target label.
To merge a PR run:
```
$ ./scripts/github/merge-pr 1234
$ yarn ng-dev pr merge <pr number>
```
The `merge-pr` script will:
- Ensure that all appropriate labels are on the PR.
- Fetches the latest PR code from the `angular/angular` repo.
- It will `cherry-pick` all of the SHAs from the PR into the current corresponding branches `master` and or `?.?.x` (patch).
- It will rewrite commit history by automatically adding `Close #1234` and `(#1234)` into the commit message.
NOTE: The `merge-pr` will land the PR on `master` and or `?.?.x` (patch) as described by `PR target: *` label.
### Recovering from failed `merge-pr` due to conflicts
When running `merge-pr` the script will output the commands which it is about to run.
```
$ ./scripts/github/merge-pr 1234
======================
GitHub Merge PR Steps
======================
git cherry-pick angular/pr/1234~1..angular/pr/1234
git filter-branch -f --msg-filter "/home/misko/angular/scripts/github/utils/github.closes 1234" HEAD~1..HEAD
```
If the `cherry-pick` command fails than resolve conflicts and use `git cherry-pick --continue` once ready. After the `cherry-pick` is done cut&paste and run the `filter-branch` command to properly rewrite the messages
## Cherry-picking PRs into patch branch
In addition to merging PRs into the master branch, many PRs need to be also merged into a patch branch.
Follow these steps to get patch branch up to date.
1. Check out the most recent patch branch: `git checkout 4.3.x`
2. Get a list of PRs merged into master: `git log master --oneline -n10`
3. For each PR number in the commit message run: `./scripts/github/merge-pr 1234`
- The PR will only merge if the `PR target:` matches the branch.
Once all of the PRs are in patch branch, push the all branches and tags to github using `push-upstream` script.
## Pushing merged PRs into github
Use `push-upstream` script to push all of the branch and tags to github.
```
$ ./scripts/github/push-upstream
git push git@github.com:angular/angular.git master:master 4.3.x:4.3.x
Counting objects: 25, done.
Delta compression using up to 6 threads.
Compressing objects: 100% (17/17), done.
Writing objects: 100% (25/25), 2.22 KiB | 284.00 KiB/s, done.
Total 25 (delta 22), reused 8 (delta 7)
remote: Resolving deltas: 100% (22/22), completed with 18 local objects.
To github.com:angular/angular.git
079d884b6..d1c4a94bb master -> master
git push --tags -f git@github.com:angular/angular.git patch_sync:patch_sync
Everything up-to-date
```
The `ng-dev pr merge` tool will automatically restore to the previous git state when a merge fails.

View File

@ -12,7 +12,7 @@ Change approvals in our monorepo are managed via [PullApprove](https://docs.pull
# Merging
Once a change has all of the required approvals, either the last approver or the PR author (if PR author has the project collaborator status)
should mark the PR with the `PR action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
should mark the PR with the `action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
This signals to the caretaker that the PR should be merged. See [merge instructions](CARETAKER.md).
# Who is the Caretaker?

View File

@ -1,6 +1,6 @@
# Triage Process and GitHub Labels for Angular
This document describes how the Angular team uses labels and milestones to triage issues on github.
This document describes how the Angular team uses labels and milestones to triage issues on GitHub.
The basic idea of the process is that caretaker only assigns a component (`comp: *`) label.
The owner of the component is then responsible for the secondary / component-level triage.
@ -125,32 +125,32 @@ Triaging PRs is the same as triaging issues, except that the labels `frequency:
PRs also have additional label categories that should be used to signal their state.
Every triaged PR must have a `PR action` label assigned to it:
Every triaged PR must have a `action: *` label assigned to it:
* `PR action: discuss`: Discussion is needed, to be led by the author.
* `action: discuss`: Discussion is needed, to be led by the author.
* _**Who adds it:** Typically the PR author._
* _**Who removes it:** Whoever added it._
* `PR action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
* `action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
* _**Who adds it:** Any team member. The caretaker can use it to differentiate PRs pending review from merge-ready PRs._
* _**Who removes it:** Whoever added it or the reviewer adding the last missing review._
* `PR action: cleanup`: More work is needed from the author.
* `action: cleanup`: More work is needed from the author.
* _**Who adds it:** The reviewer requesting changes to the PR._
* _**Who removes it:** Either the author (after implementing the requested changes) or the reviewer (after confirming the requested changes have been implemented)._
* `PR action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
* `action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
* _**Who adds it:** Typically the PR author._
* _**Who removes it:** Whoever added it._
In addition, PRs can have the following states:
* `PR state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
* `state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
* _**Who adds it:** The PR author._
* _**Who removes it:** Whoever added it._
* `PR state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
* `state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
* _**Who adds it:** Any team member._
* _**Who removes it:** Any team member._
When a PR is ready for review, a review should be requested using the Reviewers interface in Github.
When a PR is ready for review, a review should be requested using the Reviewers interface in GitHub.
## PR Target
@ -160,15 +160,29 @@ In our git workflow, we merge changes either to the `master` branch, the active
The decision about the target must be done by the PR author and/or reviewer.
This decision is then honored when the PR is being merged by the caretaker.
To communicate the target we use the following labels:
To communicate the target we use GitHub labels and only one target label may be applied to a PR.
* `PR target: master & patch`: the PR should me merged into the master branch and cherry-picked into the most recent patch branch. All PRs with fixes, docs and refactorings should use this target.
* `PR target: master-only`: the PR should be merged only into the `master` branch. All PRs with new features, API changes or high-risk changes should use this target.
* `PR target: patch-only`: the PR should be merged only into the most recent patch branch (e.g. 5.0.x). This target is useful if a `master & patch` PR can't be cleanly cherry-picked into the stable branch and a new PR is needed.
* `PR target: LTS-only`: the PR should be merged only into the active LTS branch(es). Only security and critical fixes are allowed in these branches. Always send a new PR targeting just the LTS branch and request review approval from @IgorMinar.
* `PR target: TBD`: the target is yet to be determined.
Targeting an active release train:
If a PR is missing the `PR target: *` label, or if the label is set to "TBD" when the PR is sent to the caretaker, the caretaker should reject the PR and request the appropriate target label to be applied before the PR is merged.
* `target: major`: Any breaking change
* `target: minor`: Any new feature
* `target: patch`: Bug fixes, refactorings, documentation changes, etc. that pose no or very low risk of adversely
affecting existing applications.
Special Cases:
* `target: rc`: A critical fix for an active release-train while it is in a feature freeze or RC phase
* `target: lts`: A criticial fix for a specific release-train that is still within the long term support phase
Notes:
- To land a change only in a patch/RC branch, without landing it in any other active release-train branch (such
as `master`), the patch/RC branch can be targeted in the GitHub UI with the appropriate
`target: patch`/`target: rc` label.
- `target: lts` PRs must target the specific LTS branch they would need to merge into in the GitHub UI, in
cases which a change is desired in multiple LTS branches, individual PRs for each LTS branch must be created
If a PR is missing the `target:*` label, it will be marked as pending by the angular robot status checks.
## PR Approvals
@ -182,7 +196,7 @@ In any case, the reviewer should actually look through the code and provide feed
Note that approved state does not mean a PR is ready to be merged.
For example, a reviewer might approve the PR but request a minor tweak that doesn't need further review, e.g., a rebase or small uncontroversial change.
Only the `PR action: merge` label means that the PR is ready for merging.
Only the `action: merge` label means that the PR is ready for merging.
## Special Labels
@ -201,7 +215,7 @@ Only issues with `cla:yes` should be merged into master.
Applying this label to a PR makes the angular.io preview available regardless of the author. [More info](../aio/aio-builds-setup/docs/overview--security-model.md)
### `PR action: merge-assistance`
### `action: merge-assistance`
* _**Who adds it:** Any team member._
* _**Who removes it:** Any team member._
@ -211,7 +225,7 @@ The comment should be formatted like this: `merge-assistance: <explain what kind
For example, the PR owner might not be a Googler and needs help to run g3sync; or one of the checks is failing due to external causes and the PR should still be merged.
### `PR action: rerun CI at HEAD`
### `action: rerun CI at HEAD`
* _**Who adds it:** Any team member._
* _**Who removes it:** The Angular Bot, once it triggers the CI rerun._

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,8 @@
"aio": {
"master": {
"uncompressed": {
"runtime-es2015": 2987,
"main-es2015": 450880,
"runtime-es2015": 3037,
"main-es2015": 450952,
"polyfills-es2015": 52685
}
}
@ -11,18 +11,18 @@
"aio-local": {
"master": {
"uncompressed": {
"runtime-es2015": 2987,
"main-es2015": 448419,
"polyfills-es2015": 52630
"runtime-es2015": 3037,
"main-es2015": 448493,
"polyfills-es2015": 52415
}
}
},
"aio-local-viewengine": {
"master": {
"uncompressed": {
"runtime-es2015": 3097,
"main-es2015": 430239,
"polyfills-es2015": 52195
"runtime-es2015": 3157,
"main-es2015": 430008,
"polyfills-es2015": 52415
}
}
}

View File

@ -49,7 +49,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 221939,
"main-es2015": 221400,
"polyfills-es2015": 36723,
"5-es2015": 781
}

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "10.1.1",
"version": "10.1.3",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@ -35,6 +35,8 @@
"tslint": "tsc -p tools/tsconfig.json && tslint -c tslint.json \"+(dev-infra|packages|modules|scripts|tools)/**/*.+(js|ts)\"",
"public-api:check": "node goldens/public-api/manage.js test",
"public-api:update": "node goldens/public-api/manage.js accept",
"symbol-extractor:check": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js test",
"symbol-extractor:update": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js accept",
"ts-circular-deps": "ts-node --transpile-only -- dev-infra/ts-circular-dependencies/index.ts --config ./packages/circular-deps-test.conf.js",
"ts-circular-deps:check": "yarn -s ts-circular-deps check",
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve",

View File

@ -79,9 +79,10 @@ export class HttpXhrBackend implements HttpBackend {
*/
handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
// Quick check to give a better error message when a user attempts to use
// HttpClient.jsonp() without installing the JsonpClientModule
// HttpClient.jsonp() without installing the HttpClientJsonpModule
if (req.method === 'JSONP') {
throw new Error(`Attempted to construct Jsonp request without JsonpClientModule installed.`);
throw new Error(
`Attempted to construct Jsonp request without HttpClientJsonpModule installed.`);
}
// Everything happens on Observable subscription.

View File

@ -32,6 +32,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck",
"@npm//@bazel/typescript",
"@npm//@types/node",

View File

@ -14,6 +14,7 @@ import {Logger} from '../../../src/ngtsc/logging';
import {ParsedConfiguration} from '../../../src/perform_compile';
import {getEntryPointFormat} from '../packages/entry_point';
import {makeEntryPointBundle} from '../packages/entry_point_bundle';
import {createModuleResolutionCache, SharedFileCache} from '../packages/source_file_cache';
import {PathMappings} from '../path_mappings';
import {FileWriter} from '../writing/file_writer';
@ -30,6 +31,8 @@ export function getCreateCompileFn(
return (beforeWritingFiles, onTaskCompleted) => {
const {Transformer} = require('../packages/transformer');
const transformer = new Transformer(fileSystem, logger, tsConfig);
const sharedFileCache = new SharedFileCache(fileSystem);
const moduleResolutionCache = createModuleResolutionCache(fileSystem);
return (task: Task) => {
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
@ -54,8 +57,8 @@ export function getCreateCompileFn(
logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`);
const bundle = makeEntryPointBundle(
fileSystem, entryPoint, formatPath, isCore, format, processDts, pathMappings, true,
enableI18nLegacyMessageIdFormat);
fileSystem, entryPoint, sharedFileCache, moduleResolutionCache, formatPath, isCore,
format, processDts, pathMappings, true, enableI18nLegacyMessageIdFormat);
const result = transformer.transform(bundle);
if (result.success) {

View File

@ -10,7 +10,7 @@ import * as ts from 'typescript';
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference, TypeValueReferenceKind, ValueUnavailableKind} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, Import, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference, TypeValueReferenceKind, ValueUnavailableKind} from '../../../src/ngtsc/reflection';
import {isWithinPackage} from '../analysis/util';
import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils';
@ -1608,10 +1608,11 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
/**
* Compute the `TypeValueReference` for the given `typeExpression`.
*
* In ngcc, all the `typeExpression` are guaranteed to be "values" because it is working in JS and
* not TS. This means that the TS compiler is not going to remove the "type" import and so we can
* always use a LOCAL `TypeValueReference` kind, rather than trying to force an additional import
* for non-local expressions.
* Although `typeExpression` is a valid `ts.Expression` that could be emitted directly into the
* generated code, ngcc still needs to resolve the declaration and create an `IMPORTED` type
* value reference as the compiler has specialized handling for some symbols, for example
* `ChangeDetectorRef` from `@angular/core`. Such an `IMPORTED` type value reference will result
* in a newly generated namespace import, instead of emitting the original `typeExpression` as is.
*/
private typeToValue(typeExpression: ts.Expression|null): TypeValueReference {
if (typeExpression === null) {
@ -1621,13 +1622,42 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
};
}
const imp = this.getImportOfExpression(typeExpression);
const decl = this.getDeclarationOfExpression(typeExpression);
if (imp === null || decl === null || decl.node === null) {
return {
kind: TypeValueReferenceKind.LOCAL,
expression: typeExpression,
defaultImportStatement: null,
};
}
return {
kind: TypeValueReferenceKind.LOCAL,
expression: typeExpression,
defaultImportStatement: null,
kind: TypeValueReferenceKind.IMPORTED,
valueDeclaration: decl.node,
moduleName: imp.from,
importedName: imp.name,
nestedPath: null,
};
}
/**
* Determines where the `expression` is imported from.
*
* @param expression the expression to determine the import details for.
* @returns the `Import` for the expression, or `null` if the expression is not imported or the
* expression syntax is not supported.
*/
private getImportOfExpression(expression: ts.Expression): Import|null {
if (ts.isIdentifier(expression)) {
return this.getImportOfIdentifier(expression);
} else if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name)) {
return this.getImportOfIdentifier(expression.name);
} else {
return null;
}
}
/**
* Get the parameter type and decorators for the constructor of a class,
* where the information is stored on a static property of the class.

View File

@ -8,8 +8,6 @@
/// <reference types="node" />
import * as os from 'os';
import {AbsoluteFsPath, FileSystem, resolve} from '../../src/ngtsc/file_system';
import {Logger} from '../../src/ngtsc/logging';
import {ParsedConfiguration} from '../../src/perform_compile';
@ -35,7 +33,7 @@ import {composeTaskCompletedCallbacks, createLogErrorHandler, createMarkAsProces
import {AsyncLocker} from './locking/async_locker';
import {LockFileWithChildProcess} from './locking/lock_file_with_child_process';
import {SyncLocker} from './locking/sync_locker';
import {AsyncNgccOptions, getSharedSetup, SyncNgccOptions} from './ngcc_options';
import {AsyncNgccOptions, getMaxNumberOfWorkers, getSharedSetup, SyncNgccOptions} from './ngcc_options';
import {NgccConfiguration} from './packages/configuration';
import {EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
import {EntryPointManifest, InvalidatingEntryPointManifest} from './packages/entry_point_manifest';
@ -92,10 +90,9 @@ export function mainNgcc(options: AsyncNgccOptions|SyncNgccOptions): void|Promis
return;
}
// Execute in parallel, if async execution is acceptable and there are more than 2 CPU cores.
// (One CPU core is always reserved for the master process and we need at least 2 worker processes
// in order to run tasks in parallel.)
const inParallel = async && (os.cpus().length > 2);
// Determine the number of workers to use and whether ngcc should run in parallel.
const workerCount = async ? getMaxNumberOfWorkers() : 1;
const inParallel = workerCount > 1;
const analyzeEntryPoints = getAnalyzeEntryPointsFn(
logger, finder, fileSystem, supportedPropertiesToConsider, compileAllFormats,
@ -113,7 +110,7 @@ export function mainNgcc(options: AsyncNgccOptions|SyncNgccOptions): void|Promis
const createTaskCompletedCallback =
getCreateTaskCompletedCallback(pkgJsonUpdater, errorOnFailedEntryPoint, logger, fileSystem);
const executor = getExecutor(
async, inParallel, logger, fileWriter, pkgJsonUpdater, fileSystem, config,
async, workerCount, logger, fileWriter, pkgJsonUpdater, fileSystem, config,
createTaskCompletedCallback);
return executor.execute(analyzeEntryPoints, createCompileFn);
@ -153,7 +150,7 @@ function getCreateTaskCompletedCallback(
}
function getExecutor(
async: boolean, inParallel: boolean, logger: Logger, fileWriter: FileWriter,
async: boolean, workerCount: number, logger: Logger, fileWriter: FileWriter,
pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem, config: NgccConfiguration,
createTaskCompletedCallback: CreateTaskCompletedCallback): Executor {
const lockFile = new LockFileWithChildProcess(fileSystem, logger);
@ -161,9 +158,8 @@ function getExecutor(
// Execute asynchronously (either serially or in parallel)
const {retryAttempts, retryDelay} = config.getLockingConfig();
const locker = new AsyncLocker(lockFile, logger, retryDelay, retryAttempts);
if (inParallel) {
// Execute in parallel. Use up to 8 CPU cores for workers, always reserving one for master.
const workerCount = Math.min(8, os.cpus().length - 1);
if (workerCount > 1) {
// Execute in parallel.
return new ClusterExecutor(
workerCount, fileSystem, logger, fileWriter, pkgJsonUpdater, locker,
createTaskCompletedCallback);

View File

@ -5,6 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as os from 'os';
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../src/ngtsc/file_system';
import {ConsoleLogger, Logger, LogLevel} from '../../src/ngtsc/logging';
import {ParsedConfiguration, readConfiguration} from '../../src/perform_compile';
@ -254,3 +256,26 @@ function checkForSolutionStyleTsConfig(
` ngcc ... --tsconfig "${fileSystem.relative(projectPath, tsConfig.project)}"`);
}
}
/**
* Determines the maximum number of workers to use for parallel execution. This can be set using the
* NGCC_MAX_WORKERS environment variable, or is computed based on the number of available CPUs. One
* CPU core is always reserved for the master process, so we take the number of CPUs minus one, with
* a maximum of 4 workers. We don't scale the number of workers beyond 4 by default, as it takes
* considerably more memory and CPU cycles while not offering a substantial improvement in time.
*/
export function getMaxNumberOfWorkers(): number {
const maxWorkers = process.env.NGCC_MAX_WORKERS;
if (maxWorkers === undefined) {
// Use up to 4 CPU cores for workers, always reserving one for master.
return Math.max(1, Math.min(4, os.cpus().length - 1));
}
const numericMaxWorkers = +maxWorkers.trim();
if (!Number.isInteger(numericMaxWorkers)) {
throw new Error('NGCC_MAX_WORKERS should be an integer.');
} else if (numericMaxWorkers < 1) {
throw new Error('NGCC_MAX_WORKERS should be at least 1.');
}
return numericMaxWorkers;
}

View File

@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
import {PathMappings} from '../path_mappings';
import {BundleProgram, makeBundleProgram} from './bundle_program';
import {EntryPoint, EntryPointFormat} from './entry_point';
import {NgccSourcesCompilerHost} from './ngcc_compiler_host';
import {NgccDtsCompilerHost, NgccSourcesCompilerHost} from './ngcc_compiler_host';
import {EntryPointFileCache, SharedFileCache} from './source_file_cache';
/**
* A bundle of files and paths (and TS programs) that correspond to a particular
@ -31,6 +32,8 @@ export interface EntryPointBundle {
* Get an object that describes a formatted bundle for an entry-point.
* @param fs The current file-system being used.
* @param entryPoint The entry-point that contains the bundle.
* @param sharedFileCache The cache to use for source files that are shared across all entry-points.
* @param moduleResolutionCache The module resolution cache to use.
* @param formatPath The path to the source files for this bundle.
* @param isCore This entry point is the Angular core package.
* @param format The underlying format of the bundle.
@ -42,7 +45,8 @@ export interface EntryPointBundle {
* component templates.
*/
export function makeEntryPointBundle(
fs: FileSystem, entryPoint: EntryPoint, formatPath: string, isCore: boolean,
fs: FileSystem, entryPoint: EntryPoint, sharedFileCache: SharedFileCache,
moduleResolutionCache: ts.ModuleResolutionCache, formatPath: string, isCore: boolean,
format: EntryPointFormat, transformDts: boolean, pathMappings?: PathMappings,
mirrorDtsFromSrc: boolean = false,
enableI18nLegacyMessageIdFormat: boolean = true): EntryPointBundle {
@ -50,8 +54,10 @@ export function makeEntryPointBundle(
const rootDir = entryPoint.packagePath;
const options: ts
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.packagePath);
const dtsHost = new NgtscCompilerHost(fs, options);
const entryPointCache = new EntryPointFileCache(fs, sharedFileCache);
const dtsHost = new NgccDtsCompilerHost(fs, options, entryPointCache, moduleResolutionCache);
const srcHost = new NgccSourcesCompilerHost(
fs, options, entryPointCache, moduleResolutionCache, entryPoint.packagePath);
// Create the bundle programs, as necessary.
const absFormatPath = fs.resolve(entryPoint.path, formatPath);

View File

@ -10,6 +10,7 @@ import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
import {isWithinPackage} from '../analysis/util';
import {isRelativePath} from '../utils';
import {EntryPointFileCache} from './source_file_cache';
/**
* Represents a compiler host that resolves a module import as a JavaScript source file if
@ -18,19 +19,24 @@ import {isRelativePath} from '../utils';
* would otherwise let TypeScript prefer the .d.ts file instead of the JavaScript source file.
*/
export class NgccSourcesCompilerHost extends NgtscCompilerHost {
private cache = ts.createModuleResolutionCache(
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));
constructor(fs: FileSystem, options: ts.CompilerOptions, protected packagePath: AbsoluteFsPath) {
constructor(
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache,
private moduleResolutionCache: ts.ModuleResolutionCache,
protected packagePath: AbsoluteFsPath) {
super(fs, options);
}
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
return this.cache.getCachedSourceFile(fileName, languageVersion);
}
resolveModuleNames(
moduleNames: string[], containingFile: string, reusedNames?: string[],
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
return moduleNames.map(moduleName => {
const {resolvedModule} = ts.resolveModuleName(
moduleName, containingFile, this.options, this, this.cache, redirectedReference);
moduleName, containingFile, this.options, this, this.moduleResolutionCache,
redirectedReference);
// If the module request originated from a relative import in a JavaScript source file,
// TypeScript may have resolved the module to its .d.ts declaration file if the .js source
@ -59,3 +65,31 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
});
}
}
/**
* A compiler host implementation that is used for the typings program. It leverages the entry-point
* cache for source files and module resolution, as these results can be reused across the sources
* program.
*/
export class NgccDtsCompilerHost extends NgtscCompilerHost {
constructor(
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache,
private moduleResolutionCache: ts.ModuleResolutionCache) {
super(fs, options);
}
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
return this.cache.getCachedSourceFile(fileName, languageVersion);
}
resolveModuleNames(
moduleNames: string[], containingFile: string, reusedNames?: string[],
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
return moduleNames.map(moduleName => {
const {resolvedModule} = ts.resolveModuleName(
moduleName, containingFile, this.options, this, this.moduleResolutionCache,
redirectedReference);
return resolvedModule;
});
}
}

View File

@ -0,0 +1,197 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
/**
* A cache that holds on to source files that can be shared for processing all entry-points in a
* single invocation of ngcc. In particular, the following files are shared across all entry-points
* through this cache:
*
* 1. Default library files such as `lib.dom.d.ts` and `lib.es5.d.ts`. These files don't change
* and some are very large, so parsing is expensive. Therefore, the parsed `ts.SourceFile`s for
* the default library files are cached.
* 2. The typings of @angular scoped packages. The typing files for @angular packages are typically
* used in the entry-points that ngcc processes, so benefit from a single source file cache.
* Especially `@angular/core/core.d.ts` is large and expensive to parse repeatedly. In contrast
* to default library files, we have to account for these files to be invalidated during a single
* invocation of ngcc, as ngcc will overwrite the .d.ts files during its processing.
*
* The lifecycle of this cache corresponds with a single invocation of ngcc. Separate invocations,
* e.g. the CLI's synchronous module resolution fallback will therefore all have their own cache.
* This allows for the source file cache to be garbage collected once ngcc processing has completed.
*/
export class SharedFileCache {
private sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();
constructor(private fs: FileSystem) {}
/**
* Loads a `ts.SourceFile` if the provided `fileName` is deemed appropriate to be cached. To
* optimize for memory usage, only files that are generally used in all entry-points are cached.
* If `fileName` is not considered to benefit from caching or the requested file does not exist,
* then `undefined` is returned.
*/
getCachedSourceFile(fileName: string): ts.SourceFile|undefined {
const absPath = this.fs.resolve(fileName);
if (isDefaultLibrary(absPath, this.fs)) {
return this.getStableCachedFile(absPath);
} else if (isAngularDts(absPath, this.fs)) {
return this.getVolatileCachedFile(absPath);
} else {
return undefined;
}
}
/**
* Attempts to load the source file from the cache, or parses the file into a `ts.SourceFile` if
* it's not yet cached. This method assumes that the file will not be modified for the duration
* that this cache is valid for. If that assumption does not hold, the `getVolatileCachedFile`
* method is to be used instead.
*/
private getStableCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined {
if (!this.sfCache.has(absPath)) {
const content = readFile(absPath, this.fs);
if (content === undefined) {
return undefined;
}
const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015);
this.sfCache.set(absPath, sf);
}
return this.sfCache.get(absPath)!;
}
/**
* In contrast to `getStableCachedFile`, this method always verifies that the cached source file
* is the same as what's stored on disk. This is done for files that are expected to change during
* ngcc's processing, such as @angular scoped packages for which the .d.ts files are overwritten
* by ngcc. If the contents on disk have changed compared to a previously cached source file, the
* content from disk is re-parsed and the cache entry is replaced.
*/
private getVolatileCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined {
const content = readFile(absPath, this.fs);
if (content === undefined) {
return undefined;
}
if (!this.sfCache.has(absPath) || this.sfCache.get(absPath)!.text !== content) {
const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015);
this.sfCache.set(absPath, sf);
}
return this.sfCache.get(absPath)!;
}
}
const DEFAULT_LIB_PATTERN = ['node_modules', 'typescript', 'lib', /^lib\..+\.d\.ts$/];
/**
* Determines whether the provided path corresponds with a default library file inside of the
* typescript package.
*
* @param absPath The path for which to determine if it corresponds with a default library file.
* @param fs The filesystem to use for inspecting the path.
*/
export function isDefaultLibrary(absPath: AbsoluteFsPath, fs: FileSystem): boolean {
return isFile(absPath, DEFAULT_LIB_PATTERN, fs);
}
const ANGULAR_DTS_PATTERN = ['node_modules', '@angular', /./, /\.d\.ts$/];
/**
* Determines whether the provided path corresponds with a .d.ts file inside of an @angular
* scoped package. This logic only accounts for the .d.ts files in the root, which is sufficient
* to find the large, flattened entry-point files that benefit from caching.
*
* @param absPath The path for which to determine if it corresponds with an @angular .d.ts file.
* @param fs The filesystem to use for inspecting the path.
*/
export function isAngularDts(absPath: AbsoluteFsPath, fs: FileSystem): boolean {
return isFile(absPath, ANGULAR_DTS_PATTERN, fs);
}
/**
* Helper function to determine whether a file corresponds with a given pattern of segments.
*
* @param path The path for which to determine if it corresponds with the provided segments.
* @param segments Array of segments; the `path` must have ending segments that match the
* patterns in this array.
* @param fs The filesystem to use for inspecting the path.
*/
function isFile(
path: AbsoluteFsPath, segments: ReadonlyArray<string|RegExp>, fs: FileSystem): boolean {
for (let i = segments.length - 1; i >= 0; i--) {
const pattern = segments[i];
const segment = fs.basename(path);
if (typeof pattern === 'string') {
if (pattern !== segment) {
return false;
}
} else {
if (!pattern.test(segment)) {
return false;
}
}
path = fs.dirname(path);
}
return true;
}
/**
* A cache for processing a single entry-point. This exists to share `ts.SourceFile`s between the
* source and typing programs that are created for a single program.
*/
export class EntryPointFileCache {
private readonly sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();
constructor(private fs: FileSystem, private sharedFileCache: SharedFileCache) {}
/**
* Returns and caches a parsed `ts.SourceFile` for the provided `fileName`. If the `fileName` is
* cached in the shared file cache, that result is used. Otherwise, the source file is cached
* internally. This method returns `undefined` if the requested file does not exist.
*
* @param fileName The path of the file to retrieve a source file for.
* @param languageVersion The language version to use for parsing the file.
*/
getCachedSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
const staticSf = this.sharedFileCache.getCachedSourceFile(fileName);
if (staticSf !== undefined) {
return staticSf;
}
const absPath = this.fs.resolve(fileName);
if (this.sfCache.has(absPath)) {
return this.sfCache.get(absPath);
}
const content = readFile(absPath, this.fs);
if (content === undefined) {
return undefined;
}
const sf = ts.createSourceFile(fileName, content, languageVersion);
this.sfCache.set(absPath, sf);
return sf;
}
}
function readFile(absPath: AbsoluteFsPath, fs: FileSystem): string|undefined {
if (!fs.exists(absPath) || !fs.stat(absPath).isFile()) {
return undefined;
}
return fs.readFile(absPath);
}
/**
* Creates a `ts.ModuleResolutionCache` that uses the provided filesystem for path operations.
*
* @param fs The filesystem to use for path operations.
*/
export function createModuleResolutionCache(fs: FileSystem): ts.ModuleResolutionCache {
return ts.createModuleResolutionCache(fs.pwd(), fileName => {
return fs.isCaseSensitive() ? fileName : fileName.toLowerCase();
});
}

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CommentStmt, ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler';
import {ConstantPool, Expression, jsDocComment, LeadingComment, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler';
import MagicString from 'magic-string';
import * as ts from 'typescript';
@ -166,11 +166,11 @@ export class Renderer {
sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: ImportManager,
annotateForClosureCompiler: boolean): string {
const name = this.host.getInternalNameOfClass(compiledClass.declaration);
const statements: Statement[][] = compiledClass.compilation.map(c => {
return createAssignmentStatements(
name, c.name, c.initializer, annotateForClosureCompiler ? '* @nocollapse ' : undefined);
});
return this.renderStatements(sourceFile, Array.prototype.concat.apply([], statements), imports);
const leadingComment =
annotateForClosureCompiler ? jsDocComment([{tagName: 'nocollapse'}]) : undefined;
const statements: Statement[] = compiledClass.compilation.map(
c => createAssignmentStatement(name, c.name, c.initializer, leadingComment));
return this.renderStatements(sourceFile, statements, imports);
}
/**
@ -213,16 +213,16 @@ export function renderConstantPool(
* compiled decorator to be applied to the class.
* @param analyzedClass The info about the class whose statement we want to create.
*/
function createAssignmentStatements(
function createAssignmentStatement(
receiverName: ts.DeclarationName, propName: string, initializer: Expression,
leadingComment?: string): Statement[] {
leadingComment?: LeadingComment): Statement {
const receiver = new WrappedNodeExpr(receiverName);
const statements =
[new WritePropExpr(
receiver, propName, initializer, /* type */ undefined, /* sourceSpan */ undefined)
.toStmt()];
const statement =
new WritePropExpr(
receiver, propName, initializer, /* type */ undefined, /* sourceSpan */ undefined)
.toStmt();
if (leadingComment !== undefined) {
statements.unshift(new CommentStmt(leadingComment, true));
statement.addLeadingComment(leadingComment);
}
return statements;
return statement;
}

View File

@ -14,6 +14,7 @@ import {NgccEntryPointConfig} from '../../src/packages/configuration';
import {EntryPoint, EntryPointFormat} from '../../src/packages/entry_point';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host';
import {createModuleResolutionCache, EntryPointFileCache, SharedFileCache} from '../../src/packages/source_file_cache';
export type TestConfig = Pick<NgccEntryPointConfig, 'generateDeepReexports'>;
@ -68,7 +69,10 @@ export function makeTestBundleProgram(
const rootDir = fs.dirname(entryPointPath);
const options: ts.CompilerOptions =
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
const host = new NgccSourcesCompilerHost(fs, options, rootDir);
const moduleResolutionCache = createModuleResolutionCache(fs);
const entryPointFileCache = new EntryPointFileCache(fs, new SharedFileCache(fs));
const host =
new NgccSourcesCompilerHost(fs, options, entryPointFileCache, moduleResolutionCache, rootDir);
return makeBundleProgram(
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
}

View File

@ -1211,7 +1211,7 @@ exports.MissingClass2 = MissingClass2;
});
describe('getConstructorParameters', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types',
it('should retain imported name for type value references for decorated constructor parameter types',
() => {
const files = [
{
@ -1271,7 +1271,7 @@ exports.MissingClass2 = MissingClass2;
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters(
parameters, ['shared.Baz', 'local.External', 'SameFile']);
parameters, ['Baz', 'External', 'SameFile'], ['shared-lib', './local', null]);
});
it('should find the decorated constructor parameters', () => {

View File

@ -1140,7 +1140,7 @@ runInEachFileSystem(() => {
});
describe('getConstructorParameters()', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types',
it('should retain imported name for type value references for decorated constructor parameter types',
() => {
const files = [
{
@ -1188,7 +1188,8 @@ runInEachFileSystem(() => {
const parameters = host.getConstructorParameters(classNode)!;
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters(parameters, ['Baz', 'External', 'SameFile']);
expectTypeValueReferencesForParameters(
parameters, ['Baz', 'External', 'SameFile'], ['shared-lib', './local', null]);
});
it('should find the decorated constructor parameters', () => {
@ -1205,7 +1206,8 @@ runInEachFileSystem(() => {
'_viewContainer', '_template', 'injected'
]);
expectTypeValueReferencesForParameters(
parameters, ['ViewContainerRef', 'TemplateRef', null]);
parameters, ['ViewContainerRef', 'TemplateRef', null],
['@angular/core', '@angular/core', null]);
});
it('should accept `ctorParameters` as an array', () => {

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