Compare commits

...

232 Commits

Author SHA1 Message Date
652ac79d63 build: idiomatic install of @angular/bazel npm package (#26258) 2018-10-16 13:20:00 -07:00
31632b27c7 build: update ngcontainer to bazel 0.18.0 (#26465)
* build: update ngcontainer to bazel 0.18.0

* build: update skylint to bazel 0.18

use .bazelignore file to ignore node_modules directory
2018-10-15 16:51:26 -07:00
08e4489cf5 build(bazel): update to rules_typescript 0.20.2 (#26279) 2018-10-05 17:57:18 -07:00
0e1ca096da build: update to rules_typescript 0.20.1 and rules_nodejs 0.15.0 (#26260) 2018-10-05 07:16:47 -07:00
2546c66376 build(bazel): use fine-grained npm deps (#26111) 2018-10-04 13:14:14 -07:00
81f5656be0 build: introduce a package.bzl
This lets Angular Bazel users install our transitive deps, rather than have to list them in their WORKSPACE file.
If they want a different version of one of these deps, they just need to install it before calling rules_angular_dependencies.
2018-10-02 06:46:07 -07:00
a2878b0b1d fix(docs-infra): remove unnecessary margin on short descriptions (#25768)
(This was added in 405d97431f but it is
not clear the reasoning. It looks better to remove it now.)

PR Close #25768
2018-10-01 09:36:34 -07:00
5977b72e9c build(docs-infra): fix formatting of entry point export table (#25768)
Now that `list-table` cells are `pre` formatterd we must be careful
of what whitespace appears in text nodes.

PR Close #25768
2018-10-01 09:36:34 -07:00
7373da9b11 build(docs-infra): simplify property syntax rendering (#25768)
PR Close #25768
2018-10-01 09:36:34 -07:00
783a682a7d build(docs-infra): remove unused property table heading (#25768)
PR Close #25768
2018-10-01 09:36:34 -07:00
d22418d417 build(docs-infra): add short description "See more" link (#25768)
If there is additional (non-short) description then add in a
link to the short description to take the reader there.

PR Close #25768
2018-10-01 09:36:34 -07:00
4b1fd98093 build(docs-infra): pluralize NgModule(s) heading as appropriate (#25768)
PR Close #25768
2018-10-01 09:36:34 -07:00
935ef13096 build(docs-infra): improve directive selector rendering (#25768)
`:not(...)` blocks are now rendered as italic, while the
rest of the selector is bold.

PR Close #25768
2018-10-01 09:36:33 -07:00
f4b60588fb build(docs-infra): move directive macros into memberHelpers.html (#25768)
PR Close #25768
2018-10-01 09:36:33 -07:00
15dadb92ef build(docs-infra): include directives etc in class descendants lists (#25768)
PR Close #25768
2018-10-01 09:36:33 -07:00
ce06a75ebf build(docs-infra): display inherited members on directives (#25768)
PR Close #25768
2018-10-01 09:36:33 -07:00
9889276b15 build(docs-infra): directive inputs and outputs (#25768)
PR Close #25768
2018-10-01 09:36:33 -07:00
d0f7eadc09 build(docs-infra): rename example template variable in directive pages (#25768)
PR Close #25768
2018-10-01 09:36:33 -07:00
4b132c9848 build(docs-infra): remove class overview from directive pages (#25768)
PR Close #25768
2018-10-01 09:36:33 -07:00
46729c76a0 build(docs-infra): improve directive selector rendering (#25768)
If the documentation contains a `@selectors` tag then the content of that
is used to describe the selectors of a directive.

Otherwise the selector string is split and each selector is listed as
a list item in an unordered list.

PR Close #25768
2018-10-01 09:36:33 -07:00
f22deb2e2d build(docs-infra): improve directive API doc templates (#25768)
Closes #22790
Closes #25530

PR Close #25768
2018-10-01 09:36:32 -07:00
57de9fc41a build(docs-infra): upgrade @angular/cli to 6.2.3 (#26145)
PR Close #26145
2018-10-01 09:35:49 -07:00
31034f5146 build(docs-infra): upgrade @angular/* to 7.0.0-beta.7 (#26145)
PR Close #26145
2018-10-01 09:35:48 -07:00
c5899f4cd4 build(docs-infra): update payload size limits to reflect current status (#26145)
PR Close #26145
2018-10-01 09:35:48 -07:00
ab379ab72a refactor(ivy): always use styling helper methods when comparing values (#26149)
PR Close #26149
2018-10-01 09:35:22 -07:00
32e479ffec refactor(ivy): reorganize styling and player files (#26149)
PR Close #26149
2018-10-01 09:35:22 -07:00
391c708d7e fix(ivy): ensure [style]/[class] bindings identity check the previous value (#26149)
PR Close #26149
2018-10-01 09:35:22 -07:00
c51331689f refactor(ivy): rename stylingProp => styleProp (#26149)
PR Close #26149
2018-10-01 09:35:22 -07:00
68fadd9b97 refactor(ivy): replace LNode.nodeInjector with TNode.injectorIndex (#26177)
PR Close #26177
2018-10-01 09:34:52 -07:00
2ad1bb4eb9 release: cut the v7.0.0-rc.0 release 2018-09-28 15:14:20 -07:00
794c3595d4 docs: fix a typo in the Universal guide (#25853)
line 39: `highly-interactive` is the pre-qualifier of `Angular application`, which is the subject so the comma is not necessary (I think). I think this will make it easier for non-native speakers.

PR Close #25853
2018-09-28 09:36:09 -07:00
b807106f54 build: use separate tags for ivy builds in publish-build-artifacts.sh (#26159)
PR Close #26159
2018-09-28 09:35:32 -07:00
86e6a2099a test: remove typescript 2.9 and 3.0 typings tests (#26151)
We no longer support these versions and the tests actually break with
the output from 3.1 (at least in the case of tsc 2.9)

PR Close #26151
2018-09-28 09:34:51 -07:00
9993c72335 feat: add support for TypeScript 3.1 (#26151)
PR Close #26151
2018-09-28 09:34:51 -07:00
f455518d80 docs: integrate cli doc from wiki into main doc (#25776)
PR Close #25776
2018-09-27 15:33:47 -07:00
7cf5807100 fix(ivy): ensure [style] and [class] bindings are placed in the same instruction (#26126)
PR Close #26126
2018-09-27 15:32:40 -07:00
9523991a9b refactor(router): cleanup to navigation stream for readability and documentation (#25740)
* Pull out `activateRoutes` into new operator
* Add `asyncTap` operator
* Use `asyncTap` operator for router hooks and remove corresponding abstracted operators
* Clean up formatting
* Minor performance improvements

PR Close #25740
2018-09-27 14:02:58 -07:00
9acd04c192 refactor(router): update test based on router quick-cancelling ongoing navigations (#25740)
PR Close #25740
2018-09-27 14:02:58 -07:00
c091d40fb0 refactor(router): make sure redirect within NavigationStart event works (#25740)
PR Close #25740
2018-09-27 14:02:58 -07:00
b7baf632c0 refactor(router): move routing into a single Observable stream (#25740)
This is a major refactor of how the router previously worked. There are a couple major advantages of this refactor, and future work will be built on top of it.

First, we will no longer have multiple navigations running at the same time. Previously, a new navigation wouldn't cause the old navigation to be cancelled and cleaned up. Instead, multiple navigations could be going at once, and we imperatively checked that we were operating on the most current `router.navigationId` as we progressed through the Observable streams. This had some major faults, the biggest of which was async races where an ongoing async action could result in a redirect once the async action completed, but there was no way to guarantee there weren't also other redirects that would be queued up by other async actions. After this refactor, there's a single Observable stream that will get cleaned up each time a new navigation is requested.

Additionally, the individual pieces of routing have been pulled out into their own operators. While this was needed in order to create one continuous stream, it also will allow future improvements to the testing APIs as things such as Guards or Resolvers should now be able to be tested in much more isolation.

* Add the new `router.transitions` observable of the new `NavigationTransition` type to contain the transition information
* Update `router.navigations` to pipe off of `router.transitions`
* Re-write navigation Observable flow to a single configured stream
* Refactor `switchMap` instead of the previous `mergeMap` to ensure new navigations cause a cancellation and cleanup of already running navigations
* Wire in existing error and cancellation logic so cancellation matches previous behavior

PR Close #25740
2018-09-27 14:02:57 -07:00
4c0d4fc649 refactor(router): create pipeable afterPreactivation function (#25740)
PR Close #25740
2018-09-27 14:02:57 -07:00
5b3c08b237 refactor(router): create pipeable resolveData function (#25740)
PR Close #25740
2018-09-27 14:02:57 -07:00
68f2e0c391 refactor(router): create pipeable checkGuards function (#25740)
PR Close #25740
2018-09-27 14:02:57 -07:00
9c1c945489 refactor(router): create pipeable setupPreactivation function (#25740)
PR Close #25740
2018-09-27 14:02:57 -07:00
ef5338663d refactor(router): create pipeable beforePreactivation function (#25740)
PR Close #25740
2018-09-27 14:02:57 -07:00
380b3d7653 refactor(router): create pipeable applyRedirects function (#25740)
PR Close #25740
2018-09-27 14:02:57 -07:00
4decc8521d refactor(router): create pipeable recognize function (#25740)
PR Close #25740
2018-09-27 14:02:57 -07:00
4d544bcb46 style: update gulp task to format untracked and diff files separately (#24969)
PR Close #24969
2018-09-27 12:09:08 -07:00
4c819f79b2 style: add combined task to format from git diff and status commands (#24969)
PR Close #24969
2018-09-27 12:09:08 -07:00
ac3252a73b style: add gulp task to only format changed files (#24969)
Closes #24904

PR Close #24969
2018-09-27 12:09:08 -07:00
a08af77b70 refactor: fix return type of tryCall (#25481)
PR Close #25481
2018-09-27 12:07:38 -07:00
aac08e0438 build: pass stripExportPattern as an array of RegExp (#26012)
This is a workaround for https://github.com/bazelbuild/rules_nodejs/issues/317

PR Close #26012
2018-09-27 12:07:03 -07:00
63b795ae4a refactor(ivy): make sure that test bed symbols are imported from ivy_switch (#26121)
PR Close #26121
2018-09-27 12:06:34 -07:00
5f6900ecc0 feat(ivy): add ability to inspect local refs through context discovery (#26117)
PR Close #26117
2018-09-27 12:00:53 -07:00
325e8010e9 fixup! feat(ivy): adding support for ngNonBindable attribute 2018-09-27 11:52:07 -07:00
632b19d5c2 fixup! feat(ivy): adding support for ngNonBindable attribute 2018-09-27 11:52:07 -07:00
add1198b88 fixup! feat(ivy): adding support for ngNonBindable attribute 2018-09-27 11:52:07 -07:00
0ed2df2a36 fixup! feat(ivy): adding support for ngNonBindable attribute 2018-09-27 11:52:07 -07:00
bc1f2d6411 fixup! feat(ivy): adding support for ngNonBindable attribute 2018-09-27 11:52:07 -07:00
d7326d81ba fixup! feat(ivy): adding support for ngNonBindable attribute 2018-09-27 11:52:07 -07:00
c683f74225 feat(ivy): fixed typo in test case description 2018-09-27 11:52:07 -07:00
b286abeabe feat(ivy): adding support for ngNonBindable attribute 2018-09-27 11:52:07 -07:00
eeebe28c0f ci(docs-infra): run the script in the correct folder 2018-09-27 09:04:53 -07:00
ffc6e199bf build: RxJS updated to 6.3 (#26087)
PR Close #26087
2018-09-26 17:01:15 -07:00
a01acec7fe fix(docs-infra): use correct parameters for paginated requests to GitHub (#25671)
As it turns out, in GitHub API paginated requests, page numbering is
1-based. (https://developer.github.com/v3/#pagination)

Starting at page 0 (which returns the first page), results in making the
same request twice and logging incorrect numbers (since the first 100
items are listed twice).

PR Close #25671
2018-09-26 15:26:19 -07:00
021f4344b1 fix(docs-infra): fix preview server periodic clean-up (#25671)
Includes the following fixes:

- Fix cron entry format for clean-up script.
  Crontabs in `/etc` should not have a user field. No idea why it used
  to work before, but it started giving errors recently:
  `/bin/sh: root: not found`.

- Set required env variable in clean-up script. (Broken in cc6f36a9d.)
  This was producing the following error:
  `ERROR: Missing required environment variable 'AIO_CIRCLE_CI_TOKEN'!`

- Use the correct path for downloads to be removed. (Broken in cc6f36a9d.)

PR Close #25671
2018-09-26 15:26:19 -07:00
f113b49909 test(docs-infra): remove unnecessary test helpers (#25671)
`supertest.Request` extends `Promise` and can be used directly without
"promisifying".

PR Close #25671
2018-09-26 15:26:19 -07:00
d8d276c245 docs(docs-infra): update preview server docs to account for recent changes (#25671)
Mostly (but not exclusively) a follow-up to #23576.

PR Close #25671
2018-09-26 15:26:19 -07:00
e42bd012f9 ci(docs-infra): test PR previews on CI (#25671)
The deployment of PR previews is triggered by the notification webhook
of the `aio_preview` CircleCI job (which creates and stores the build
artifacts).

This commit adds a new job (`test_aio_preview`), which waits for the
preview to be deployed (for PRs that do have a preview) and then runs
some tests against it (currently only PWA tests).

Fixes #23818

PR Close #25671
2018-09-26 15:26:19 -07:00
6d6b0ff1ad feat(docs-infra): add API endpoint for checking if PR can have preview (#25671)
There several reasons why PRs cannot have (public) previews:
- The PR did not affect any relevant files (e.g. non-spec files in
  `aio/` or `packages/`).
- The PR cannot be automatically verified as "trusted" (based on its
  author or labels).

Note:
The endpoint does not check whether there currently is a (public)
preview for the specified PR; only whether there can be one.

PR Close #25671
2018-09-26 15:26:19 -07:00
f378454c92 fix(docs-infra): correctly check PR files on preview server (#25671)
According to the docs, the response of GitHub's [PR files API][1]
_"includes a maximum of 300 files"_. This means that if a PR contains
more files, it is possible that not all files are retrieved (which
could, for example, give a false negative for the "significant files
touched" check - not likely but possible).

This commit fixes it by using paginated requests to retrieve all changed
files.

[1]: https://developer.github.com/v3/pulls/#list-pull-requests-files

PR Close #25671
2018-09-26 15:26:19 -07:00
c8c8436e58 test(docs-infra): fix test for preview server's GithubPullRequests (#25671)
PR Close #25671
2018-09-26 15:26:19 -07:00
b31c8b6063 test(docs-infra): fix test for preview server's BuildCleaner completing prematurely (#25671)
PR Close #25671
2018-09-26 15:26:19 -07:00
897261efdc test(docs-infra): fix preview server unit tests on Windows (#25671)
Some tests where comparing actual with expected paths, without taking
into account that paths will be different on Windows.

This commit uses `path.resolve()` to convert expected paths to their
OS-specific form.

PR Close #25671
2018-09-26 15:26:19 -07:00
35d70ff265 test(docs-infra): add support for source-maps in preview server tests (#25671)
PR Close #25671
2018-09-26 15:26:19 -07:00
fc0a7959a4 refactor(docs-infra): use mockable logger (#25671)
Related discussion:
https://github.com/angular/angular/pull/23576#discussion_r187925949.

PR Close #25671
2018-09-26 15:26:19 -07:00
182c08bee1 refactor(docs-infra): fix method name (getPrfromBranch --> getPrFromBranch) (#25671)
PR Close #25671
2018-09-26 15:26:19 -07:00
e2bc0ad6c2 build(docs-infra): upgrade preview server dependencies (#25671)
PR Close #25671
2018-09-26 15:26:19 -07:00
73333ee3e5 build(docs-infra): replace concurrently with npm-run-all for preview server dev (#25671)
`npm-run-all` works just as well, but is better at handling termination on Windows.

PR Close #25671
2018-09-26 15:26:19 -07:00
c819859598 build(docs-infra): do not exit preview server dev script when build fails (#25671)
PR Close #25671
2018-09-26 15:26:19 -07:00
79aefa7659 build(docs-infra): avoid race condition in aio-builds-setup/ npm scripts (#25671)
Previously, due to multiple scripts re-building during `yarn dev`
initialization, there could be race conditions that led to errors.

This commit fixes it by ensuring `yarn build` is run once (before
the main `yarn dev` script).

PR Close #25671
2018-09-26 15:26:19 -07:00
e1990a5a80 docs: firefox web components info (#26118)
PR Close #26118
2018-09-26 15:25:44 -07:00
4cff5b2964 release: cut the v7.0.0-beta.7 release 2018-09-26 15:07:11 -07:00
459758231b docs: release notes for the v6.1.9 release 2018-09-26 13:12:20 -07:00
f29b218060 feat(docs-infra): generate Angular CLI command reference (#25363)
PR Close #25363
2018-09-26 11:24:02 -07:00
39a67548ac build(docs-infra): add option to run only the doc-gen (#25363)
This can save time when iterating by not
regenerating the zips and embedded examples.

PR Close #25363
2018-09-26 11:24:02 -07:00
bc88f318f6 docs: update routing integration section based on feedback (#20023)
PR Close #20023
2018-09-26 10:14:49 -07:00
a5b7008c8e docs: add section on router integration (#20023)
PR Close #20023
2018-09-26 10:14:49 -07:00
0aafbac99b docs: clean up providedIn: 'root' syntax for router examples (#20023)
PR Close #20023
2018-09-26 10:14:49 -07:00
ac5aa8f46d docs: router guide review feedback changes (#20023)
PR Close #20023
2018-09-26 10:14:49 -07:00
1fb3c4ffee docs: Update router guide to use Angular CLI (#20023)
PR Close #20023
2018-09-26 10:14:49 -07:00
3c8aa0b301 docs: Refresh content on routable animations for router guide (#20023)
PR Close #20023
2018-09-26 10:14:49 -07:00
15a2b8f622 fix(ivy): wrapper fns arent necessary anymore (#26108)
PR Close #26108
2018-09-26 00:03:16 -07:00
d19108531c docs: cleanup minor changes for forms overview (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
354d1944bb docs: remove unused properties from forms overview example (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
eaccd03ed7 docs: fix typos from review feedback (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
343df337f4 docs: update with forms overview review feedback (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
9b14483824 docs: more overview feedback changes (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
bd42caf1c7 docs: update nav descriptions based on feedback (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
7db8111973 docs: add updated reactive forms data flow image (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
74fef157e6 docs: updates from review feedback (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
9661bed3ba docs: add updated forms overview images (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
95168e4de0 docs: integrate forms diagrams into overview (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
13c3e241c8 docs: add final thoughts to forms overview (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
8d098d389a docs: incorporated forms overview review feedback (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
79a2567aa3 docs: forms overview review changes (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
5649acd03f docs: add forms overview example for snippets (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
ebd01e8e79 docs: more form overview edits (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
e08955b557 docs: incorporated forms overview feedback (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
04dfca41f4 docs(forms): add package overview for forms (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
129f69c3bc docs: add forms overview guide (#25663)
PR Close #25663
2018-09-25 18:48:15 -07:00
c7e2930f25 docs: fix issues related to tutorial. (#24445)
PR Close #24445
2018-09-25 18:45:19 -07:00
6a62ed2245 fix(ivy): objects like ElementRef should not use a special injection fn (#26064)
PR Close #26064
2018-09-25 12:51:29 -07:00
482e12c940 build: remove obsolete comment in env.sh (#25819)
The comment is no longer true since #25602.

PR Close #25819
2018-09-25 11:04:33 -07:00
0c344715e5 feat(ivy): expose a series of helpful application inspection tools (#25919)
PR Close #25919
2018-09-25 09:46:12 -07:00
cf095d982d docs: fix a typo (#26074)
PR Close #26074
2018-09-24 13:48:24 -07:00
23ec88ef23 refactor(ivy): remove unreferenced utils file (#26076)
PR Close #26076
2018-09-24 11:39:52 -07:00
2bd767c4a6 fix(service-worker): do not blow up when caches are unwritable (#26042)
In some cases, example when the user clears the caches in DevTools but
the SW remains active on another tab and keeps references to the deleted
caches, trying to write to the cache throws errors (e.g.
`Entry was not found`).

When this happens, the SW can no longer work correctly and should enter
a degraded mode allowing requests to be served from the network.

Possibly related:
- https://github.com/GoogleChrome/workbox/issues/792
- https://bugs.chromium.org/p/chromium/issues/detail?id=639034

This commits remedies this situation, by ensuring the SW can enter the
degraded `EXISTING_CLIENTS_ONLY` mode and forward requests to the
network.

PR Close #26042
2018-09-24 09:53:39 -07:00
1e02cd9961 docs: Fixes typo in FormArray (#26031)
PR Close #26031
2018-09-24 09:14:07 -07:00
bc02e19831 docs: correct path reference in upgrade guide (#26072)
The incorrect path is referenced, this is confusing to users following the "Upgrading from AngularJS" guide.

PR Close #26072
2018-09-24 09:13:24 -07:00
47eb2122c0 docs: fix Sajee info in contributors (#26063)
PR Close #26063
2018-09-24 09:12:45 -07:00
b8422b41bb build(docs-infra): fail doc-gen if a content rule fails (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
8ac4dd6447 build(docs-infra): allow usage notes on decorator option properties (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
206ae7a233 docs(core): remove usage notes from ReflexiveInjector.parent property (#26039)
Properties are not allowed usage notes, and in this case the example
is so simple it didn't warrant moving it to the overall class documentation.

PR Close #26039
2018-09-24 09:11:02 -07:00
79b6256789 docs(core): move headings to @usageNotes (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
72dce34f42 docs(common): move KeyValuePipe example to @usageNotes (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
7d39bc68fb docs(forms): move extended text to @usageNotes (#26039)
Headings are not allowed in the basic description block.

PR Close #26039
2018-09-24 09:11:02 -07:00
32ad2438ca docs(http): move examples to @usageNotes (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
fc4b993d98 docs(platform-browser): move examples to @usageNotes (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
ff028f0b39 docs(router): move examples to @usageNotes (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
fef9cebed0 docs(upgrade): move examples etc into @usageNotes (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
c08549ae38 docs(common): move KeyValuePipe example to @usageNotes (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
3808416479 build(docs-infra): remove legacy jsdoc tag processing (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
cf8ad24dcf docs(common): remove legacy @whatItDoes tag (#26039)
PR Close #26039
2018-09-24 09:11:02 -07:00
cee7448efc build(docs-infra): add @nocollapse tag-def to prevent warning (#26039)
See https://github.com/angular/angular/blob/master/packages/compiler-cli/src/transformers/nocollapse_hack.ts

PR Close #26039
2018-09-24 09:11:02 -07:00
7f1cace2a2 build(docs-infra): sort NgModule exports by id (#26051)
PR Close #26051
2018-09-21 17:00:03 -07:00
56c86c7e79 build(docs-infra): sort package exports by id (#26051)
Closes #26046

PR Close #26051
2018-09-21 17:00:03 -07:00
82a14dc107 feat(ivy): provide groundwork for animations in core (#25234)
PR Close #25234
2018-09-21 14:51:24 -07:00
a880686081 fix(docs-infra): ensure that only search is removed from URL on click (#26056)
When we have navigated to the site via a URL that contains a search
query param, the site shows the search results.

We want to remove that query param from the URL when the search
results are closed, but the current implementation is also removing
other query params unnecessarily.

Now only the search param is removed when the search results are
closed.

See https://github.com/angular/angular/pull/25479/files#r219497804
for more context.

PR Close #26056
2018-09-21 10:29:24 -07:00
026b60cd70 build(docs-infra): expose deprecated status on items more clearly (#25750)
PR Close #25750
2018-09-21 10:26:48 -07:00
cea2e0477c fix(docs-infra): render security risk labels (#25750)
PR Close #25750
2018-09-21 10:26:48 -07:00
96f9f03d25 build(docs-infra): improve search quality (#25750)
PR Close #25750
2018-09-21 10:26:48 -07:00
9931bd7576 build(docs-infra): do not include license comment in first API doc (#26050)
The default dgeni config is to concatenate leading comments in front of API items.

In the case that you have an API item that starts a file with no import statements,
the license comment at the top of the file was being added to the front of the
API item's comment. SInce the license comment includes the `@license` tag
and the API item's comment did not start with `@description` the content of
the API item's comment was being put inside the `@license` tag, and no
description was being extracted from the API item's comment.

This commit updates to a version of dgeni-packages that has a switch to turn off
this concatenation, and then also configures this switch.

Closes #26045

PR Close #26050
2018-09-21 10:25:41 -07:00
48094835bf docs: add Sajee to contributors (#26028)
PR Close #26028
2018-09-21 10:24:15 -07:00
e42c1b0da8 ci: correct github robot size check configuration (#26057)
PR Close #26057
2018-09-21 10:21:28 -07:00
e7ade38731 Revert "refactor(router): cleanup to navigation stream for readability" and associated changes (#26060)
PR Close #26060
2018-09-21 10:15:43 -07:00
d5f47d6b71 refactor(ivy): special injection tokens should not be cached (#26048)
PR Close #26048
2018-09-20 18:02:08 -07:00
64aa6701f6 refactor(router): cleanup to navigation stream for readability and documentation (#25740)
* Pull out `activateRoutes` into new operator
* Add `asyncTap` operator
* Use `asyncTap` operator for router hooks and remove corresponding abstracted operators
* Clean up formatting
* Minor performance improvements

PR Close #25740
2018-09-20 17:42:58 -07:00
12ccf57340 refactor(router): update test based on router quick-cancelling ongoing navigations (#25740)
PR Close #25740
2018-09-20 17:42:58 -07:00
c634176035 refactor(router): make sure redirect within NavigationStart event works (#25740)
PR Close #25740
2018-09-20 17:42:58 -07:00
4bb10d224c refactor(router): move routing into a single Observable stream (#25740)
This is a major refactor of how the router previously worked. There are a couple major advantages of this refactor, and future work will be built on top of it.

First, we will no longer have multiple navigations running at the same time. Previously, a new navigation wouldn't cause the old navigation to be cancelled and cleaned up. Instead, multiple navigations could be going at once, and we imperatively checked that we were operating on the most current `router.navigationId` as we progressed through the Observable streams. This had some major faults, the biggest of which was async races where an ongoing async action could result in a redirect once the async action completed, but there was no way to guarantee there weren't also other redirects that would be queued up by other async actions. After this refactor, there's a single Observable stream that will get cleaned up each time a new navigation is requested.

Additionally, the individual pieces of routing have been pulled out into their own operators. While this was needed in order to create one continuous stream, it also will allow future improvements to the testing APIs as things such as Guards or Resolvers should now be able to be tested in much more isolation.

* Add the new `router.transitions` observable of the new `NavigationTransition` type to contain the transition information
* Update `router.navigations` to pipe off of `router.transitions`
* Re-write navigation Observable flow to a single configured stream
* Refactor `switchMap` instead of the previous `mergeMap` to ensure new navigations cause a cancellation and cleanup of already running navigations
* Wire in existing error and cancellation logic so cancellation matches previous behavior

PR Close #25740
2018-09-20 17:42:58 -07:00
5d689469f6 refactor(router): create pipeable afterPreactivation function (#25740)
PR Close #25740
2018-09-20 17:42:58 -07:00
855ad8804e refactor(router): create pipeable resolveData function (#25740)
PR Close #25740
2018-09-20 17:42:58 -07:00
29d3f3f6dd refactor(router): create pipeable checkGuards function (#25740)
PR Close #25740
2018-09-20 17:42:58 -07:00
33101359c6 refactor(router): create pipeable setupPreactivation function (#25740)
PR Close #25740
2018-09-20 17:42:58 -07:00
44eef5c343 refactor(router): create pipeable beforePreactivation function (#25740)
PR Close #25740
2018-09-20 17:42:58 -07:00
68b7847b4c refactor(router): create pipeable applyRedirects function (#25740)
PR Close #25740
2018-09-20 17:42:58 -07:00
2b2e841e5b refactor(router): create pipeable recognize function (#25740)
PR Close #25740
2018-09-20 17:42:58 -07:00
549de1e21a fix(core): add missing peerDependency to @angular/compiler (#26033)
In 919f42fea1 (diff-58563046c4439699f2e6a89187099a54) a dependency to the compiler was added. However the peerDependency was not added.
PR Close #26033
2018-09-20 10:53:25 -07:00
48e73c1558 ci: only run aio_preview job on PR builds (#26030)
There can be no preview on non-PR builds, so there is no point in
running the job.

PR Close #26030
2018-09-20 09:32:44 -07:00
41ac58ab7d docs: copy-edit (#25732)
PR Close #25732
2018-09-19 18:22:45 -07:00
f37cf52b4c docs: integrate material from cli wiki (#25732)
PR Close #25732
2018-09-19 18:22:45 -07:00
927323f24e docs: add missing @ngModule tags (#25734)
PR Close #25734
2018-09-19 16:18:24 -07:00
b94436d86c build(docs-infra): process and render ngmodule exports (#25734)
All directives and pipes must now be tagged with one ore more
public NgModule, from which they are exported.

If an item is exported transitively via a re-exported internal NgModule
then it may be that the item appears to be exported from more than
one public NgModule. For example, there are shared directives that
are exported in this way from `FormsModule` and `ReactiveFormsModule`.

The doc-gen will error and fail if a directive or pipe is not tagged correctly.

NgModule pages now list all the directives and pipes that are exported from it.
Directive and Pipe pages now list any NgModule from which they are exported.
Packages also now list any NgModules that are contained - previously they were
missed.

PR Close #25734
2018-09-19 16:18:24 -07:00
bc5cb8153e build(docs-infra): separate NgModules from Classes in API docs (#25734)
PR Close #25734
2018-09-19 16:18:24 -07:00
34b848ad51 build(docs-infra): remove unused info-bar API template (#25734)
PR Close #25734
2018-09-19 16:18:24 -07:00
d7e5bbf2d0 feat(compiler-cli): add support to extend angularCompilerOptions (#22717)
`TypeScript` only supports merging and extending of `compilerOptions`. This is an implementation to support extending and inheriting of `angularCompilerOptions` from multiple files.

Closes: #22684

PR Close #22717
2018-09-19 16:17:28 -07:00
a9a81f91cf docs(forms): update form apis based on review feedback (#25724)
PR Close #25724
2018-09-19 16:09:00 -07:00
07c10e2844 docs(forms): update API reference for forms interfaces and abstract classes (#25724)
PR Close #25724
2018-09-19 16:09:00 -07:00
df5999a739 fix(docs-infra): configure Firebase to strip off the .html extension (#25999)
Firebase used to do it automatically (with `cleanUrls: true`), but it
stopped doing it unless the resulting URL corresponds to an existing
file (which is not always the case in angular.io; e.g. the resulting URL
might be matched by a new redirect rule).
This change in Firebase hosting behavior resulted in some URLs not being
correctly redirected (e.g. URLs to the archived v2 site, or `.html`
suffixed URLs from 3rd-party sites).

This commit fixes it, by configuring Firebase hosting to strip off the
`.html` extension and redirect (if no other redirect rule matched).

PR Close #25999
2018-09-19 16:08:06 -07:00
3fb0da2de5 feat(platform-server): update domino to v2.1.0 (#25564)
PR Close #25564
2018-09-19 16:07:36 -07:00
8f0fcc3f71 feat(docs-infra): Add opensearch description (#25479)
Enables Chrome users to search angular.io and its subdomains from the browsers navigation bar.
Not sure if compatible with Firefox yet.
The queried term in the URL is removed after closing the search-results.

PR Close #25479
2018-09-19 15:31:49 -07:00
ca1e56dc8b release: cut the v7.0.0-beta.6 release 2018-09-19 14:30:43 -07:00
d0e710d472 docs: copy edit (#25582)
PR Close #25582
2018-09-19 10:43:06 -07:00
bc7f962039 docs: clean up formats, add detail (#25582)
PR Close #25582
2018-09-19 10:43:06 -07:00
78d42a9503 docs: update view-related api doc (#25582)
PR Close #25582
2018-09-19 10:43:06 -07:00
dd5e35ee67 docs: add ngmodule api doc (#25618)
PR Close #25618
2018-09-19 10:40:58 -07:00
f91b0455c0 docs(animations): updated animation docs (#24206)
PR Close #24206
2018-09-19 10:37:31 -07:00
e8bab1349f docs: delete extra sentence (#25984)
PR Close #25984
2018-09-19 09:42:12 -07:00
e952c65759 docs: correct changelog for 6.1.7 2018-09-18 13:34:31 -07:00
5241ea086d test(bazel): Run Angular test on RBE (#25370)
PR Close #25370
2018-09-18 13:29:54 -07:00
cbbad1b791 refactor(ivy): pre-factor: set explicit type parameters for ModuleWithProviders (#25970)
Ivy depends on having the generic type token later when reading the ModuleWithProviders from a .d.ts file.

PR Close #25970
2018-09-18 13:28:44 -07:00
96ee898cee build: ignore aio/docs-infra commits in changelog gulp task (#25838)
PR Close #25838
2018-09-18 13:21:26 -07:00
2c40a86b61 build: update conventional-changelog and simplify changelog gulp task (#25838)
The version prefix issue has been fixed with
conventional-changelog/conventional-changelog#179.

PR Close #25838
2018-09-18 13:21:26 -07:00
a53a559f5a ci: use locally built skylint and buildifier (#25917)
PR Close #25917
2018-09-18 13:14:35 -07:00
6de393b2b8 build: bump the com_github_bazelbuild_buildtools version to 0.12.0 (#25917)
This is a preliminary fix to make buildifier work with Bazel 0.16.

See alexeagle/angular-bazel-example/issues/173

PR Close #25917
2018-09-18 13:14:35 -07:00
56283ed594 build: update .nvmrc file to correct node version (#25992)
The version was updated in 34ec9244a6
but this file got missed.
PR Close #25992
2018-09-18 13:11:58 -07:00
ddd3bf83c7 docs(forms): change documentation of the FormGroup patchValue method (#25901)
Improve the grammar of the description to make it more readable.
PR Close #25901
2018-09-18 13:08:05 -07:00
9b1bb370a3 fix(ivy): ngcc should compile entry-points in the correct order (#25862)
The compiler should process all an entry-points dependencies
before processing that entry-point.

PR Close #25862
2018-09-18 13:06:28 -07:00
976389836e build: update node type version (#25862)
PR Close #25862
2018-09-18 13:06:28 -07:00
f76a9ad156 style(ivy): remove unused import (#25862)
PR Close #25862
2018-09-18 13:06:28 -07:00
6f1100a7e9 refactor(ivy): use canonical-path in ngcc (#25862)
It turns out that `path.posix` does not always reliably
return forward slash paths on Windows.

PR Close #25862
2018-09-18 13:06:28 -07:00
b99d7ed5bf build(bazel): update to rules_typescript 0.17.0 & rules_nodejs 0.13.4 (#25920)
PR Close #25920
2018-09-18 13:05:38 -07:00
f47f2628e1 refactor(ivy): remove LNode.view (#25988)
PR Close #25988
2018-09-18 13:04:23 -07:00
5653874683 fix(ivy): events should not mark views dirty by default (#25969)
PR Close #25969
2018-09-17 13:02:39 -07:00
21e566d9bc build: use nodejs public api (#25940)
`nodejs_binary` and `nodejs_test` from `@build_bazel_rules_nodejs//:defs.bzl` and `@build_bazel_rules_nodejs//internal/node:node.bzl` are different as the first one uses a macro https://github.com/bazelbuild/rules_nodejs/blob/master/internal/node/node.bzl#L229 to wrap the `nodejs_binary` and `nodejs_test` as an `.exe` for Windows.

PR Close #25940
2018-09-17 12:52:39 -07:00
bdbb2f9bfa ci: update to bazel 0.17 (#25967)
this includes support for @ character in labels, which we need for fine-grained deps

PR Close #25967
2018-09-17 12:51:52 -07:00
2e32d4ee17 build(bazel): update BAZEL_VERSION to 0.17.1 (#25967)
Bazel version '0.16.1' doesn't seem to be available anymore! Upgrade to 0.17.1 instead.

PR Close #25967
2018-09-17 12:51:52 -07:00
8f81dba367 docs: fix typo in bootstrapping guide (#25939)
Fixes #25938

PR Close #25939
2018-09-14 16:38:18 -07:00
aedebaf025 refactor(ivy): remove LNode.tNode (#25958)
PR Close #25958
2018-09-14 16:16:28 -07:00
47f4412650 refactor(ivy): LContainers should store views not nodes (#25933)
PR Close #25933
2018-09-13 15:56:04 -07:00
a09c3923db docs: delete old comments from example (#25931)
PR Close #25931
2018-09-13 15:33:33 -07:00
10a656fc38 refactor(ivy): ensure hello world doesn't pull in context discovery creation code (#25895)
PR Close #25895
2018-09-12 13:25:12 -04:00
8dc2b119fb fix(router): mount correct component if router outlet was not instantiated and if using a route reuse strategy (#25313) (#25314)
This unsets 'attachRef' on outlet context if no route is to be reused in route activation.

Closes #25313

PR Close #25314
2018-09-11 16:26:42 -07:00
f2ba55f2fb docs(router): change description of parameter usage for navigation functions (#21840)
PR Close #21840
2018-09-11 16:24:23 -07:00
00d3666d95 fix(compiler): Fix look up of entryComponents in AOT Summaries (#24892)
Previously, when you attempted to bootstrap a component that had a
router-outlet using ngsummaries, it would complain that the component
was not provided by any module even if it was. This commit fixes a
mistake (AFAICT) which caused the lookup of the component in the AOT
summaries to fail.

I believe this change is safe. I've run the affected tests within Google
and there were no breakages caused by this change.

PR Close #24892
2018-09-11 16:23:17 -07:00
21009b06a1 fix(ivy): use proper sanitizer names (#25817)
Fixes #25816

PR Close #25817
2018-09-11 16:22:38 -07:00
379e8c5e19 docs: add disableTypeScriptVersionCheck documentation (#25537)
PR Close #25537
2018-09-11 12:25:55 -07:00
f3b552f51f build(bazel): remove outdated "cfg = "data"" references (#25434)
PR Close #25434

PR Close #25434
2018-09-11 08:27:39 -07:00
c5b594e351 docs(bazel): add info on options for nodejs_test rule (#25877)
PR Close #25877
2018-09-11 07:11:51 -07:00
86a3be8610 docs(ivy): add explanation of LViewData (#25779)
PR Close #25779
2018-09-11 07:10:15 -07:00
d5bd86ae5d fix(ivy): don't accidently read the inherited definition (#25736)
Create getter methods `getXXXDef` for each definition which
uses `hasOwnProperty` to verify that we don't accidently read form the
parent class.

Fixes: #24011
Fixes: #25026

PR Close #25736
2018-09-11 07:09:38 -07:00
a9099e8f70 fix(ivy): ensure Ivy *Ref classes derive from view engine equivalents (#25775)
Various user code uses 'instanceof' to check whether a particular instance
is a TemplateRef, ElementRef, etc. Ivy needs to work with these checks.

PR Close #25775
2018-09-11 06:53:22 -07:00
96d6b79ada feat(ivy): resolve references to vars in .d.ts files (#25775)
Previously, if ngtsc encountered a VariableDeclaration without an
initializer, it would assume that the variable was undefined, and
return that result.

However, for symbols exported from external modules that resolve to
.d.ts files, variable declarations are of the form:

export declare let varName: Type;

This form also lacks an initializer, but indicates the presence of an
importable symbol which can be referenced. This commit changes the
static resolver to understand variable declarations with the 'declare'
keyword and to generate references when it encounters them.

PR Close #25775
2018-09-11 06:53:21 -07:00
13ccdfd89d feat(ivy): support bootstrap in ngModuleDef (#25775)
The bootstrap property of @NgModule was not previously compiled by
the compiler in AOT or JIT modes (in Ivy). This commit adds support
for bootstrap.

PR Close #25775
2018-09-11 06:53:21 -07:00
a0c4b2d8f0 fix(ivy): add @nocollapse when writing closure-annotated code (#25775)
Closure requires @nocollapse on Ivy definition static fields in order
to not convert them to standalone constants. However tsickle, the tool
which would ordinarily be responsible for adding @nocollapse, doesn't
properly annotate fields which are added synthetically via transforms.
So this commit adds @nocollapse by applying regular expressions against
code during the final write to disk.

PR Close #25775
2018-09-11 06:53:21 -07:00
7ba0cb7c93 refactor(ivy): remove superfluous Array check (#25894)
related #25755

PR Close #25894
2018-09-10 14:00:58 -07:00
d83f9d432a fix(common): register locale data for all equivalent closure locales (#25867)
This fix is for the issue below when compiling I18N Angular apps using closure.

For certain locales closure converts the input locale id to a different equivalent locale string. For example if the input locale is 'id'(for Indonesia) goog.LOCALE is set to 'in' and the closure locale data is registered only for 'in'. The Angular compiler uses the original input locale string, 'id' to set the LOCALE_ID token and there is a mismatch of locale used to register and locale used when requesting the locale data.

The fix is for the closure-locale.ts code to register the locale data for all equivalent locales names so that it doesn't matter what goog.LOCALE is actually set to.

PR Close #25867
2018-09-10 13:59:56 -07:00
e3633888ed feat(ivy): support animation @triggers in templates (#25849)
PR Close #25849
2018-09-10 13:59:27 -07:00
ed266daf2c docs(aio): add ng-sq-ui to resources (#25874)
PR Close #25874
2018-09-10 10:31:11 -07:00
89af5291de docs: move compiler options to last section of the page (#22353)
PR Close #22353
2018-09-10 10:30:00 -07:00
91d79939be refactor(ivy): traverse tNode tree directly (#25872)
PR Close #25872
2018-09-10 09:59:17 -07:00
96eb79b1c7 build: add support for running builds outside of sandbox on Mac. (#25870)
Add following to your `~/.bazelrc`. This will run the build faster locally
(outside of sandbox), but continue running the builds with sandboxing
on CI.

```
build --spawn_strategy=standalone --strategy=ESM5=sandboxed
```
PR Close #25870
2018-09-07 16:06:55 -07:00
83a1334876 refactor(ivy): migrate previousOrParentNode to use TNodes (#25829)
PR Close #25829
2018-09-07 16:06:17 -07:00
2a21ca09d2 feat(bazel): add additional parameters to ts_api_guardian_test def (#25694)
Added `strip_export_pattern` and `allow_module_identifiers` so that these can be passed from downstream

PR Close #25694
2018-09-07 14:24:31 -07:00
ddc13352e9 fix(bazel): specify the package and lock files using the workspace (#25694)
PR Close #25694
2018-09-07 14:24:31 -07:00
62be8c2e2f feat(ivy): allow combined context discovery for components, directives and elements (#25754)
PR Close #25754
2018-09-07 14:14:56 -07:00
d2dfd48be0 feat(ivy): patch animations into metadata (#25828)
PR Close #25828
2018-09-07 13:46:06 -07:00
d6cd041cbd docs(service-worker): update http-server command (#25845)
PR Close #25845
2018-09-06 14:59:51 -07:00
694b8ae779 build(docs-infra): ensure any stale generated content is deleted (#25841)
Since `aio/src/generated/` is git-ignored, it is easy for stale content
(e.g. removed images, examples, zips, etc.) to remain there on local
clones and then get copied into the `dist/` directory.

This commit ensures `aio/src/generated/` is cleaned up before generating
the new content.

PR Close #25841
2018-09-06 14:59:29 -07:00
152de20774 fix(docs-infra): re-enable SW offline mode (#25826)
This basically reverts #25692, since Firebase has fixed the issue with
`/index.html` redirection on their side.

PR Close #25826
2018-09-06 14:59:08 -07:00
34ec9244a6 build: update to Node 10 (#25822)
PR Close #25822
2018-09-06 14:58:30 -07:00
837 changed files with 39229 additions and 12526 deletions

2
.bazelignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

View File

@ -57,6 +57,6 @@ test --experimental_ui
################################ ################################
# Temporary Settings for Ivy # # Temporary Settings for Ivy #
################################ ################################
# to determine if the compiler used should be Ivy or ViewEngine one can use `--define=compile=local` on # to determine if the compiler used should be Ivy or ViewEngine one can use `--define=compile=local` on
# any bazel target. This is a temporary flag until codebase is permanently switched to Ivy. # any bazel target. This is a temporary flag until codebase is permanently switched to Ivy.
build --define=compile=legacy build --define=compile=legacy

View File

@ -28,3 +28,6 @@ build --local_resources=14336,8.0,1.0
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309 # Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
test --flaky_test_attempts=2 test --flaky_test_attempts=2
# More details on failures
build --verbose_failures=true

View File

@ -12,8 +12,8 @@
## IMPORTANT ## IMPORTANT
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of # If you change the `docker_image` version, also change the `cache_key` suffix and the version of
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file. # `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
var_1: &docker_image angular/ngcontainer:0.4.0 var_1: &docker_image angular/ngcontainer:0.7.0
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-bust1-0.4.0 var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.7.0
# Define common ENV vars # Define common ENV vars
var_3: &define_env_vars var_3: &define_env_vars
@ -26,6 +26,11 @@ var_4: &setup-bazel-remote-cache
command: ~/bazel-remote-proxy -backend circleci:// command: ~/bazel-remote-proxy -backend circleci://
background: true background: true
var_5: &setup_bazel_remote_execution
run:
name: "Setup bazel RBE remote execution"
command: openssl aes-256-cbc -d -in .circleci/gcp_token -k "${CIRCLE_PROJECT_REPONAME}" -out /home/circleci/.gcp_credentials && echo "export GOOGLE_APPLICATION_CREDENTIALS=/home/circleci/.gcp_credentials" >> $BASH_ENV && sudo bash -c "cat .circleci/rbe-bazel.rc >> /etc/bazel.bazelrc"
# Settings common to each job # Settings common to each job
anchor_1: &job_defaults anchor_1: &job_defaults
working_directory: ~/ng working_directory: ~/ng
@ -42,19 +47,18 @@ version: 2
jobs: jobs:
lint: lint:
<<: *job_defaults <<: *job_defaults
resource_class: xlarge
steps: steps:
- checkout: - checkout:
<<: *post_checkout <<: *post_checkout
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
# Check BUILD.bazel formatting before we have a node_modules directory # Check BUILD.bazel formatting before we have a node_modules directory
# Then we don't need any exclude pattern to avoid checking those files # Then we don't need any exclude pattern to avoid checking those files
- run: 'buildifier -mode=check $(find . -type f \( -name "*.bzl" -or -name BUILD.bazel -or -name BUILD \)) || - run: 'yarn buildifier -mode=check ||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)' (echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
# Run the skylark linter to check our Bazel rules # Run the skylark linter to check our Bazel rules
# deprecated-api is disabled because we use actions.new_file(genfiles_dir) - run: 'yarn skylint ||
# which has no replacement, see https://github.com/bazelbuild/bazel/issues/4858
- run: 'find . -type f -name "*.bzl" |
xargs java -jar /usr/local/bin/Skylint_deploy.jar --disable-checks=deprecated-api ||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)' (echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
- restore_cache: - restore_cache:
@ -70,22 +74,20 @@ jobs:
- *define_env_vars - *define_env_vars
- checkout: - checkout:
<<: *post_checkout <<: *post_checkout
# See remote cache documentation in /docs/BAZEL.md
- run: .circleci/setup_cache.sh
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- *setup-bazel-remote-cache
- restore_cache:
key: *cache_key
- run: ls /home/circleci/bazel_repository_cache || true
- run: bazel info release - run: bazel info release
- run: bazel run @nodejs//:yarn - run: bazel run @nodejs//:yarn
# Use bazel query so that we explicitly ask for all buildable targets to be built as well # Use bazel query so that we explicitly ask for all buildable targets to be built as well
# This avoids waiting for the slowest build target to finish before running the first test # This avoids waiting for the slowest build target to finish before running the first test
# See https://github.com/bazelbuild/bazel/issues/4257 # See https://github.com/bazelbuild/bazel/issues/4257
# NOTE: Angular developers should typically just bazel build //packages/... or bazel test //packages/... # NOTE: Angular developers should typically just bazel build //packages/... or bazel test //packages/...
- run: bazel query --output=label //... | xargs bazel test --build_tag_filters=-ivy-only --test_tag_filters=-manual,-ivy-only # Setup remote execution and run RBE-compatible tests.
- *setup_bazel_remote_execution
- run: bazel query --output=label //... | xargs bazel test --build_tag_filters=-ivy-only --test_tag_filters=-manual,-ivy-only,-local
# Now run RBE incompatible tests locally.
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- run: bazel query --output=label //... | xargs bazel test --build_tag_filters=-ivy-only,local --test_tag_filters=-manual,-ivy-only,local
# CircleCI will allow us to go back and view/download these artifacts from past builds. # CircleCI will allow us to go back and view/download these artifacts from past builds.
# Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts. # Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts.
@ -111,6 +113,7 @@ jobs:
paths: paths:
- "node_modules" - "node_modules"
- "~/bazel_repository_cache" - "~/bazel_repository_cache"
# Temporary job to test what will happen when we flip the Ivy flag to true # Temporary job to test what will happen when we flip the Ivy flag to true
test_ivy_jit: test_ivy_jit:
<<: *job_defaults <<: *job_defaults
@ -119,15 +122,10 @@ jobs:
- *define_env_vars - *define_env_vars
- checkout: - checkout:
<<: *post_checkout <<: *post_checkout
# See remote cache documentation in /docs/BAZEL.md
- run: .circleci/setup_cache.sh
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- *setup-bazel-remote-cache
- restore_cache:
key: *cache_key
- run: bazel run @yarn//:yarn - run: bazel run @yarn//:yarn
- *setup_bazel_remote_execution
- run: bazel query --output=label //... | xargs bazel test --define=compile=jit --build_tag_filters=ivy-jit --test_tag_filters=-manual,ivy-jit - run: bazel query --output=label //... | xargs bazel test --define=compile=jit --build_tag_filters=ivy-jit --test_tag_filters=-manual,ivy-jit
test_ivy_aot: test_ivy_aot:
@ -137,17 +135,13 @@ jobs:
- *define_env_vars - *define_env_vars
- checkout: - checkout:
<<: *post_checkout <<: *post_checkout
# See remote cache documentation in /docs/BAZEL.md
- run: .circleci/setup_cache.sh
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- *setup-bazel-remote-cache
- restore_cache:
key: *cache_key
- run: bazel run @yarn//:yarn - run: bazel run @yarn//:yarn
- *setup_bazel_remote_execution
- run: bazel query --output=label //... | xargs bazel test --define=compile=local --build_tag_filters=ivy-local --test_tag_filters=-manual,ivy-local - run: bazel query --output=label //... | xargs bazel test --define=compile=local --build_tag_filters=ivy-local --test_tag_filters=-manual,ivy-local
# This job should only be run on PR builds, where `CIRCLE_PR_NUMBER` is defined.
aio_preview: aio_preview:
<<: *job_defaults <<: *job_defaults
environment: environment:
@ -158,13 +152,28 @@ jobs:
- restore_cache: - restore_cache:
key: *cache_key key: *cache_key
- run: yarn install --frozen-lockfile --non-interactive - run: yarn install --frozen-lockfile --non-interactive
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH - run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH $CIRCLE_PR_NUMBER $CIRCLE_SHA1
- store_artifacts: - store_artifacts:
path: *aio_preview_artifact_path path: *aio_preview_artifact_path
# The `destination` needs to be kept in synch with the value of # The `destination` needs to be kept in synch with the value of
# `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile` # `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile`
destination: aio/dist/aio-snapshot.tgz destination: aio/dist/aio-snapshot.tgz
# This job should only be run on PR builds, where `CIRCLE_PR_NUMBER` is defined.
test_aio_preview:
<<: *job_defaults
steps:
- checkout:
<<: *post_checkout
- restore_cache:
key: *cache_key
- run: yarn install --cwd aio --frozen-lockfile --non-interactive
- run:
name: Wait for preview and run tests
command: |
source "./scripts/ci/env.sh" print
xvfb-run --auto-servernum node aio/scripts/test-preview.js $CIRCLE_PR_NUMBER $CIRCLE_SHA1 $AIO_MIN_PWA_SCORE
# This job exists only for backwards-compatibility with old scripts and tests # This job exists only for backwards-compatibility with old scripts and tests
# that rely on the pre-Bazel dist/packages-dist layout. # that rely on the pre-Bazel dist/packages-dist layout.
# It duplicates some work with the job above: we build the bazel packages # It duplicates some work with the job above: we build the bazel packages
@ -179,21 +188,10 @@ jobs:
- *define_env_vars - *define_env_vars
- checkout: - checkout:
<<: *post_checkout <<: *post_checkout
# See remote cache documentation in /docs/BAZEL.md
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- run: bazel run @nodejs//:yarn - run: bazel run @nodejs//:yarn
- run: - *setup_bazel_remote_execution
# RBE is enabled by appending rbe-bazel.rc. - run: scripts/build-packages-dist.sh
name: Enable RBE
command: 'sudo bash -c "cat .circleci/rbe-bazel.rc >> /etc/bazel.bazelrc"'
- run:
name: "Setup GCP environment"
command: 'openssl aes-256-cbc -d -in .circleci/gcp_token -k "${CIRCLE_PROJECT_REPONAME}" -out /home/circleci/.gcp_credentials'
- run:
name: build-packages-dist
command: scripts/build-packages-dist.sh
environment:
GOOGLE_APPLICATION_CREDENTIALS: /home/circleci/.gcp_credentials
# Save the npm packages from //packages/... for other workflow jobs to read # Save the npm packages from //packages/... for other workflow jobs to read
# https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs # https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs
@ -270,7 +268,14 @@ workflows:
- test_ivy_jit - test_ivy_jit
- test_ivy_aot - test_ivy_aot
- build-packages-dist - build-packages-dist
- aio_preview - aio_preview:
# Only run on PR builds. (There can be no previews for non-PR builds.)
filters:
branches:
only: /pull\/\d+/
- test_aio_preview:
requires:
- aio_preview
- integration_test: - integration_test:
requires: requires:
- build-packages-dist - build-packages-dist
@ -299,6 +304,7 @@ workflows:
branches: branches:
only: only:
- master - master
notify: notify:
webhooks: webhooks:
- url: https://ngbuilds.io/circle-build - url: https://ngbuilds.io/circle-build

View File

@ -34,9 +34,9 @@ build --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
# "exec_compatible_with"). # "exec_compatible_with").
# More about platforms: https://docs.bazel.build/versions/master/platforms.html # More about platforms: https://docs.bazel.build/versions/master/platforms.html
build --extra_toolchains=@bazel_toolchains//configs/ubuntu16_04_clang/1.0/bazel_0.15.0/cpp:cc-toolchain-clang-x86_64-default build --extra_toolchains=@bazel_toolchains//configs/ubuntu16_04_clang/1.0/bazel_0.15.0/cpp:cc-toolchain-clang-x86_64-default
build --extra_execution_platforms=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:rbe_ubuntu1604 build --extra_execution_platforms=//tools:rbe_ubuntu1604-angular
build --host_platform=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:rbe_ubuntu1604 build --host_platform=//tools:rbe_ubuntu1604-angular
build --platforms=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:rbe_ubuntu1604 build --platforms=//tools:rbe_ubuntu1604-angular
# Set various strategies so that all actions execute remotely. Mixing remote # Set various strategies so that all actions execute remotely. Mixing remote
# and local execution will lead to errors unless the toolchain and remote # and local execution will lead to errors unless the toolchain and remote

View File

@ -3,11 +3,8 @@
#options for the size plugin #options for the size plugin
size: size:
disabled: false disabled: false
maxSizeIncrease: 1000 maxSizeIncrease: 2000
circleCiStatusName: "ci/circleci: build-packages-dist" circleCiStatusName: "ci/circleci: test"
status:
disabled: false
context: "ci/angular: size"
# options for the merge plugin # options for the merge plugin
merge: merge:
@ -68,7 +65,7 @@ merge:
# This enables us to request reviews from both eng and tech writers, or multiple eng folks, and prevents accidental merges. # This enables us to request reviews from both eng and tech writers, or multiple eng folks, and prevents accidental merges.
# Rather than merging PRs with pending reviews, if all PullApprove requirements are satisfied and additional reviews are not needed pending reviewers should be removed via GitHub UI (this also leaves an audit trail behind these decisions). # Rather than merging PRs with pending reviews, if all PullApprove requirements are satisfied and additional reviews are not needed pending reviewers should be removed via GitHub UI (this also leaves an audit trail behind these decisions).
requireReviews: true, requireReviews: true,
# whether the PR shouldn't have a conflict with the base branch # whether the PR shouldn't have a conflict with the base branch
noConflict: true noConflict: true
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master") # list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")

1
.gitignore vendored
View File

@ -14,7 +14,6 @@ pubspec.lock
.settings/ .settings/
*.swo *.swo
modules/.settings modules/.settings
.bazelrc
.vscode .vscode
modules/.vscode modules/.vscode

2
.nvmrc
View File

@ -1 +1 @@
8.9 10.9.0

View File

@ -87,10 +87,10 @@ groups:
files: files:
include: include:
- "WORKSPACE" - "WORKSPACE"
- ".bazel*"
- "*.bazel" - "*.bazel"
- "*.bzl" - "*.bzl"
- "packages/bazel/*" - "packages/bazel/*"
- "tools/bazel.rc"
- "/docs/BAZEL.md" - "/docs/BAZEL.md"
users: users:
- alexeagle #primary - alexeagle #primary
@ -108,7 +108,6 @@ groups:
- "*.lock" - "*.lock"
- "tools/*" - "tools/*"
exclude: exclude:
- "tools/bazel.rc"
- "tools/public_api_guard/*" - "tools/public_api_guard/*"
- "aio/*" - "aio/*"
users: users:

View File

@ -2,7 +2,7 @@ language: node_js
sudo: false sudo: false
dist: trusty dist: trusty
node_js: node_js:
- '8.9.1' - '10.9.0'
addons: addons:
# firefox: "38.0" # firefox: "38.0"

View File

@ -15,19 +15,14 @@ alias(
actual = "@nodejs//:yarn", actual = "@nodejs//:yarn",
) )
alias(
name = "node_modules",
actual = "@angular_deps//:node_modules",
)
filegroup( filegroup(
name = "web_test_bootstrap_scripts", name = "web_test_bootstrap_scripts",
# do not sort # do not sort
srcs = [ srcs = [
"@angular_deps//:node_modules/reflect-metadata/Reflect.js", "@ngdeps//node_modules/reflect-metadata:Reflect.js",
"@angular_deps//:node_modules/zone.js/dist/zone.js", "@ngdeps//node_modules/zone.js:dist/zone.js",
"@angular_deps//:node_modules/zone.js/dist/zone-testing.js", "@ngdeps//node_modules/zone.js:dist/zone-testing.js",
"@angular_deps//:node_modules/zone.js/dist/task-tracking.js", "@ngdeps//node_modules/zone.js:dist/task-tracking.js",
"//:test-events.js", "//:test-events.js",
], ],
) )
@ -35,11 +30,28 @@ filegroup(
filegroup( filegroup(
name = "angularjs_scripts", name = "angularjs_scripts",
srcs = [ srcs = [
"@angular_deps//:node_modules/angular-1.5/angular.js", "@ngdeps//node_modules/angular:angular.js",
"@angular_deps//:node_modules/angular-1.6/angular.js", "@ngdeps//node_modules/angular-1.5:angular.js",
"@angular_deps//:node_modules/angular-mocks-1.5/angular-mocks.js", "@ngdeps//node_modules/angular-1.6:angular.js",
"@angular_deps//:node_modules/angular-mocks-1.6/angular-mocks.js", "@ngdeps//node_modules/angular-mocks:angular-mocks.js",
"@angular_deps//:node_modules/angular-mocks/angular-mocks.js", "@ngdeps//node_modules/angular-mocks-1.5:angular-mocks.js",
"@angular_deps//:node_modules/angular/angular.js", "@ngdeps//node_modules/angular-mocks-1.6:angular-mocks.js",
], ],
) )
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
# A nodejs_binary for @angular/bazel/ngc-wrapped to use by default in
# ng_module that depends on @npm//@angular/bazel instead of the
# output of the //packages/bazel/src/ngc-wrapped ts_library rule. This
# default is for downstream users that depend on the @angular/bazel npm
# package. The generated @npm//@angular/bazel/ngc-wrapped target
# does not work because it does not have the node `--expose-gc` flag
# set which is required to support the call to `global.gc()`.
nodejs_binary(
name = "@angular/bazel/ngc-wrapped",
data = ["@npm//@angular/bazel"],
entry_point = "@angular/bazel/src/ngc-wrapped/index.js",
install_source_map_support = False,
templated_args = ["--node_options=--expose-gc"],
)

View File

@ -1,3 +1,69 @@
<a name="7.0.0-rc.0"></a>
# [7.0.0-rc.0](https://github.com/angular/angular/compare/7.0.0-beta.7...7.0.0-rc.0) (2018-09-28)
### Features
* add support for TypeScript 3.1 ([#26151](https://github.com/angular/angular/issues/26151)) ([9993c72](https://github.com/angular/angular/commit/9993c72))
<a name="7.0.0-beta.7"></a>
# [7.0.0-beta.7](https://github.com/angular/angular/compare/7.0.0-beta.6...7.0.0-beta.7) (2018-09-26)
### Bug Fixes
* **core:** add missing `peerDependency ` to `[@angular](https://github.com/angular)/compiler` ([#26033](https://github.com/angular/angular/issues/26033)) ([549de1e](https://github.com/angular/angular/commit/549de1e)), closes [/github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e#diff-58563046c4439699f2e6a89187099a54](https://github.com//github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e/issues/diff-58563046c4439699f2e6a89187099a54)
* **service-worker:** do not blow up when caches are unwritable ([#26042](https://github.com/angular/angular/issues/26042)) ([2bd767c](https://github.com/angular/angular/commit/2bd767c))
### Features
* **compiler-cli:** add support to extend `angularCompilerOptions` ([#22717](https://github.com/angular/angular/issues/22717)) ([d7e5bbf](https://github.com/angular/angular/commit/d7e5bbf)), closes [#22684](https://github.com/angular/angular/issues/22684)
* **platform-server:** update domino to v2.1.0 ([#25564](https://github.com/angular/angular/issues/25564)) ([3fb0da2](https://github.com/angular/angular/commit/3fb0da2))
<a name="6.1.9"></a>
## [6.1.9](https://github.com/angular/angular/compare/6.1.8...6.1.9) (2018-09-26)
### Bug Fixes
* **service-worker:** do not blow up when caches are unwritable ([#26042](https://github.com/angular/angular/issues/26042)) ([a169743](https://github.com/angular/angular/commit/a169743))
<a name="7.0.0-beta.6"></a>
# [7.0.0-beta.6](https://github.com/angular/angular/compare/7.0.0-beta.5...7.0.0-beta.6) (2018-09-19)
### Bug Fixes
* **bazel:** specify the package and lock files using the workspace ([#25694](https://github.com/angular/angular/issues/25694)) ([ddc1335](https://github.com/angular/angular/commit/ddc1335))
* **common:** register locale data for all equivalent closure locales ([#25867](https://github.com/angular/angular/issues/25867)) ([d83f9d4](https://github.com/angular/angular/commit/d83f9d4))
* **compiler:** Fix look up of entryComponents in AOT Summaries ([#24892](https://github.com/angular/angular/issues/24892)) ([00d3666](https://github.com/angular/angular/commit/00d3666))
* **ivy:** add [@nocollapse](https://github.com/nocollapse) when writing closure-annotated code ([#25775](https://github.com/angular/angular/issues/25775)) ([a0c4b2d](https://github.com/angular/angular/commit/a0c4b2d))
* **ivy:** don't accidently read the inherited definition ([#25736](https://github.com/angular/angular/issues/25736)) ([d5bd86a](https://github.com/angular/angular/commit/d5bd86a)), closes [#24011](https://github.com/angular/angular/issues/24011) [#25026](https://github.com/angular/angular/issues/25026)
* **ivy:** ensure Ivy *Ref classes derive from view engine equivalents ([#25775](https://github.com/angular/angular/issues/25775)) ([a9099e8](https://github.com/angular/angular/commit/a9099e8))
* **ivy:** events should not mark views dirty by default ([#25969](https://github.com/angular/angular/issues/25969)) ([5653874](https://github.com/angular/angular/commit/5653874))
* **ivy:** ngcc should compile entry-points in the correct order ([#25862](https://github.com/angular/angular/issues/25862)) ([9b1bb37](https://github.com/angular/angular/commit/9b1bb37))
* **ivy:** use proper sanitizer names ([#25817](https://github.com/angular/angular/issues/25817)) ([21009b0](https://github.com/angular/angular/commit/21009b0)), closes [#25816](https://github.com/angular/angular/issues/25816)
* **router:** mount correct component if router outlet was not instantiated and if using a route reuse strategy ([#25313](https://github.com/angular/angular/issues/25313)) ([#25314](https://github.com/angular/angular/issues/25314)) ([8dc2b11](https://github.com/angular/angular/commit/8dc2b11))
### Features
* **bazel:** add additional parameters to `ts_api_guardian_test` def ([#25694](https://github.com/angular/angular/issues/25694)) ([2a21ca0](https://github.com/angular/angular/commit/2a21ca0))
* **ivy:** allow combined context discovery for components, directives and elements ([#25754](https://github.com/angular/angular/issues/25754)) ([62be8c2](https://github.com/angular/angular/commit/62be8c2))
* **ivy:** patch animations into metadata ([#25828](https://github.com/angular/angular/issues/25828)) ([d2dfd48](https://github.com/angular/angular/commit/d2dfd48))
* **ivy:** resolve references to vars in .d.ts files ([#25775](https://github.com/angular/angular/issues/25775)) ([96d6b79](https://github.com/angular/angular/commit/96d6b79))
* **ivy:** support animation [@triggers](https://github.com/triggers) in templates ([#25849](https://github.com/angular/angular/issues/25849)) ([e363388](https://github.com/angular/angular/commit/e363388))
* **ivy:** support bootstrap in ngModuleDef ([#25775](https://github.com/angular/angular/issues/25775)) ([13ccdfd](https://github.com/angular/angular/commit/13ccdfd))
<a name="7.0.0-beta.5"></a> <a name="7.0.0-beta.5"></a>
# [7.0.0-beta.5](https://github.com/angular/angular/compare/7.0.0-beta.4...7.0.0-beta.5) (2018-09-06) # [7.0.0-beta.5](https://github.com/angular/angular/compare/7.0.0-beta.4...7.0.0-beta.5) (2018-09-06)
@ -33,12 +99,6 @@
* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([ebcf762](https://github.com/angular/angular/commit/ebcf762)) * **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([ebcf762](https://github.com/angular/angular/commit/ebcf762))
* **docs-infra:** show "suggest edits" only for /guide and /tutorial dirs ([#24378](https://github.com/angular/angular/issues/24378)) ([66b7870](https://github.com/angular/angular/commit/66b7870)) * **docs-infra:** show "suggest edits" only for /guide and /tutorial dirs ([#24378](https://github.com/angular/angular/issues/24378)) ([66b7870](https://github.com/angular/angular/commit/66b7870))
* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([82e0676](https://github.com/angular/angular/commit/82e0676)), closes [#25334](https://github.com/angular/angular/issues/25334) * **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([82e0676](https://github.com/angular/angular/commit/82e0676)), closes [#25334](https://github.com/angular/angular/issues/25334)
### Features
* **docs-infra:** add "suggest edits" feature to all docs ([#24378](https://github.com/angular/angular/issues/24378)) ([82088a8](https://github.com/angular/angular/commit/82088a8))
* **docs-infra:** disable "status" selector in API list when displaying only packages ([#25718](https://github.com/angular/angular/issues/25718)) ([6f7df8a](https://github.com/angular/angular/commit/6f7df8a)), closes [#25708](https://github.com/angular/angular/issues/25708)
* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([23a96dc](https://github.com/angular/angular/commit/23a96dc)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728) * **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([23a96dc](https://github.com/angular/angular/commit/23a96dc)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728)

118
WORKSPACE
View File

@ -1,96 +1,24 @@
workspace(name = "angular") workspace(name = "angular")
# load(
# Download Bazel toolchain dependencies as needed by build actions "//packages/bazel:package.bzl",
# "rules_angular_dependencies",
http_archive( "rules_angular_dev_dependencies",
name = "build_bazel_rules_nodejs",
urls = ["https://github.com/bazelbuild/rules_nodejs/archive/0.12.0.zip"],
strip_prefix = "rules_nodejs-0.12.0",
sha256 = "2977cdbc8ae0eed7d4186385af56a50a3321a549e2136a959998bba89d2edb6e",
) )
http_archive( # Angular Bazel users will call this function
name = "build_bazel_rules_typescript", rules_angular_dependencies()
url = "https://github.com/bazelbuild/rules_typescript/archive/0.16.2.zip", # These are the dependencies only for us
strip_prefix = "rules_typescript-0.16.2", rules_angular_dev_dependencies()
sha256 = "31601b777840fbf600dbd1893ade0d1de37166e7ba52b90735b107cfb67e38c7",
)
load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies")
rules_typescript_dependencies()
http_archive(
name = "bazel_toolchains",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz",
"https://github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz",
],
strip_prefix = "bazel-toolchains-5124557861ebf4c0b67f98180bff1f8551e0b421",
sha256 = "c3b08805602cd1d2b67ebe96407c1e8c6ed3d4ce55236ae2efe2f1948f38168d",
)
http_archive(
name = "io_bazel_rules_sass",
url = "https://github.com/bazelbuild/rules_sass/archive/1.11.0.zip",
strip_prefix = "rules_sass-1.11.0",
sha256 = "dbe9fb97d5a7833b2a733eebc78c9c1e3880f676ac8af16e58ccf2139cbcad03",
)
# This commit matches the version of buildifier in angular/ngcontainer
# If you change this, also check if it matches the version in the angular/ngcontainer
# version in /.circleci/config.yml
BAZEL_BUILDTOOLS_VERSION = "82b21607e00913b16fe1c51bec80232d9d6de31c"
http_archive(
name = "com_github_bazelbuild_buildtools",
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
sha256 = "edb24c2f9c55b10a820ec74db0564415c0cf553fa55e9fc709a6332fb6685eff",
)
# Fetching the Bazel source code allows us to compile the Skylark linter
http_archive(
name = "io_bazel",
url = "https://github.com/bazelbuild/bazel/archive/968f87900dce45a7af749a965b72dbac51b176b3.zip",
strip_prefix = "bazel-968f87900dce45a7af749a965b72dbac51b176b3",
sha256 = "e373d2ae24955c1254c495c9c421c009d88966565c35e4e8444c082cb1f0f48f",
)
http_archive(
name = "io_bazel_skydoc",
# TODO: switch to upstream when https://github.com/bazelbuild/skydoc/pull/103 is merged
url = "https://github.com/alexeagle/skydoc/archive/fe2e9f888d28e567fef62ec9d4a93c425526d701.zip",
strip_prefix = "skydoc-fe2e9f888d28e567fef62ec9d4a93c425526d701",
sha256 = "7bfb5545f59792a2745f2523b9eef363f9c3e7274791c030885e7069f8116016",
)
# We have a source dependency on the Devkit repository, because it's built with
# Bazel.
# This allows us to edit sources and have the effect appear immediately without
# re-packaging or "npm link"ing.
# Even better, things like aspects will visit the entire graph including
# ts_library rules in the devkit repository.
http_archive(
name = "angular_cli",
url = "https://github.com/angular/angular-cli/archive/v6.1.0-rc.0.zip",
strip_prefix = "angular-cli-6.1.0-rc.0",
sha256 = "8cf320ea58c321e103f39087376feea502f20eaf79c61a4fdb05c7286c8684fd",
)
http_archive(
name = "org_brotli",
url = "https://github.com/google/brotli/archive/v1.0.5.zip",
strip_prefix = "brotli-1.0.5",
sha256 = "774b893a0700b0692a76e2e5b7e7610dbbe330ffbe3fe864b4b52ca718061d5a",
)
# #
# Point Bazel to WORKSPACEs that live in subdirectories # Point Bazel to WORKSPACEs that live in subdirectories
# #
http_archive(
local_repository(
name = "rxjs", name = "rxjs",
path = "node_modules/rxjs/src", url = "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz",
strip_prefix = "package/src",
sha256 = "72b0b4e517f43358f554c125e40e39f67688cd2738a8998b4a266981ed32f403",
) )
# Point to the integration test workspace just so that Bazel doesn't descend into it # Point to the integration test workspace just so that Bazel doesn't descend into it
@ -103,27 +31,37 @@ local_repository(
# #
# Load and install our dependencies downloaded above. # Load and install our dependencies downloaded above.
# #
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install")
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories") check_bazel_version("0.18.0", """
check_bazel_version("0.16.0", """
If you are on a Mac and using Homebrew, there is a breaking change to the installation in Bazel 0.16 If you are on a Mac and using Homebrew, there is a breaking change to the installation in Bazel 0.16
See https://blog.bazel.build/2018/08/22/bazel-homebrew.html See https://blog.bazel.build/2018/08/22/bazel-homebrew.html
""") """)
node_repositories( node_repositories(
package_json = ["//:package.json"], node_version = "10.9.0",
preserve_symlinks = True, package_json = ["//:package.json"],
preserve_symlinks = True,
yarn_version = "1.9.2",
)
yarn_install(
name = "npm",
package_json = "//tools:npm/package.json",
yarn_lock = "//tools:npm/yarn.lock",
) )
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains") load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
go_rules_dependencies() go_rules_dependencies()
go_register_toolchains() go_register_toolchains()
load("@io_bazel_rules_webtesting//web:repositories.bzl", "browser_repositories", "web_test_repositories") load("@io_bazel_rules_webtesting//web:repositories.bzl", "browser_repositories", "web_test_repositories")
web_test_repositories() web_test_repositories()
browser_repositories( browser_repositories(
chromium = True, chromium = True,
firefox = True, firefox = True,
@ -141,7 +79,9 @@ ng_setup_workspace()
# Skylark documentation generation # Skylark documentation generation
load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories") load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories")
sass_repositories() sass_repositories()
load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories") load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories")
skydoc_repositories() skydoc_repositories()

View File

@ -1,2 +1,2 @@
# Periodically clean up builds that do not correspond to currently open PRs # Periodically clean up builds that do not correspond to currently open PRs
0 12 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1 0 12 * * * /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1

View File

@ -66,6 +66,21 @@ server {
return 200 ''; return 200 '';
} }
# Check PRs previewability
location "~^/can-have-public-preview/\d+/?$" {
if ($request_method != "GET") {
add_header Allow "GET";
return 405;
}
proxy_pass_request_headers on;
proxy_redirect off;
proxy_method GET;
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
resolver 127.0.0.1;
}
# Notify about CircleCI builds # Notify about CircleCI builds
location "~^/circle-build/?$" { location "~^/circle-build/?$" {
if ($request_method != "POST") { if ($request_method != "POST") {

View File

@ -5,12 +5,12 @@ import * as shell from 'shelljs';
import {HIDDEN_DIR_PREFIX} from '../common/constants'; import {HIDDEN_DIR_PREFIX} from '../common/constants';
import {GithubApi} from '../common/github-api'; import {GithubApi} from '../common/github-api';
import {GithubPullRequests} from '../common/github-pull-requests'; import {GithubPullRequests} from '../common/github-pull-requests';
import {assertNotMissingOrEmpty, createLogger, getPrInfoFromDownloadPath} from '../common/utils'; import {assertNotMissingOrEmpty, getPrInfoFromDownloadPath, Logger} from '../common/utils';
// Classes // Classes
export class BuildCleaner { export class BuildCleaner {
private logger = createLogger('BuildCleaner'); private logger = new Logger('BuildCleaner');
// Constructor // Constructor
constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string, constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string,
@ -122,6 +122,6 @@ export class BuildCleaner {
this.logger.log(`Existing downloads: ${existingDownloads.length}`); this.logger.log(`Existing downloads: ${existingDownloads.length}`);
this.logger.log(`Removing ${toRemove.length} download(s): ${toRemove.join(', ')}`); this.logger.log(`Removing ${toRemove.length} download(s): ${toRemove.join(', ')}`);
toRemove.forEach(filePath => shell.rm(filePath)); toRemove.forEach(filePath => shell.rm(path.join(this.downloadsDir, filePath)));
} }
} }

View File

@ -62,7 +62,7 @@ export class CircleCiApi {
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`); throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`);
} }
return response.json<BuildInfo>(); return response.json();
} catch (error) { } catch (error) {
throw new Error(`CircleCI build info request failed (${error.message})`); throw new Error(`CircleCI build info request failed (${error.message})`);
} }
@ -77,7 +77,7 @@ export class CircleCiApi {
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`; const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
try { try {
const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`); const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`);
const artifacts = await response.json<ArtifactResponse>(); const artifacts = await response.json() as ArtifactResponse;
const artifact = artifacts.find(item => item.path === artifactPath); const artifact = artifacts.find(item => item.path === artifactPath);
if (!artifact) { if (!artifact) {
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`); throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);

View File

@ -38,7 +38,8 @@ export class GithubApi {
return this.request<T>('post', path, data); return this.request<T>('post', path, data);
} }
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> { // In GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination)
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 1): Promise<T[]> {
const perPage = 100; const perPage = 100;
const params = { const params = {
...baseParams, ...baseParams,

View File

@ -74,6 +74,6 @@ export class GithubPullRequests {
*/ */
public fetchFiles(pr: number): Promise<FileInfo[]> { public fetchFiles(pr: number): Promise<FileInfo[]> {
assert(pr > 0, `Invalid PR number: ${pr}`); assert(pr > 0, `Invalid PR number: ${pr}`);
return this.api.get<FileInfo[]>(`/repos/${this.repoSlug}/pulls/${pr}/files`); return this.api.getPaginated<FileInfo>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
} }
} }

View File

@ -1,14 +1,14 @@
export const runTests = (specFiles: string[], helpers?: string[]) => { // We can't use `import...from` here, because of the following mess:
// We can't use `import` here, because of the following mess: // - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`. // - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings. //
// // Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the // `jasmine-core` module and the `jasmine` module).
// `jasmine-core` module and the `jasmine` module). import Jasmine = require('jasmine');
// tslint:disable-next-line: no-var-requires variable-name import 'source-map-support/register';
const Jasmine = require('jasmine');
export const runTests = (specFiles: string[]) => {
const config = { const config = {
helpers,
random: true, random: true,
spec_files: specFiles, spec_files: specFiles,
stopSpecOnExpectationFailure: true, stopSpecOnExpectationFailure: true,
@ -16,7 +16,7 @@ export const runTests = (specFiles: string[], helpers?: string[]) => {
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason)); process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
const runner = new Jasmine(); const runner = new Jasmine({});
runner.loadConfig(config); runner.loadConfig(config);
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1)); runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
runner.execute(); runner.execute();

View File

@ -74,12 +74,25 @@ export const getEnvVar = (name: string, isOptional = false): string => {
return value || ''; return value || '';
}; };
export function createLogger(scope: string) { /**
const padding = ' '.repeat(20 - scope.length); * A basic logger implementation.
return { * Delegates to `console`, but prepends each message with the current date and specified scope (i.e caller).
error: (...args: any[]) => console.error(`[${new Date()}]`, `${scope}:${padding}`, ...args), */
info: (...args: any[]) => console.info(`[${new Date()}]`, `${scope}:${padding}`, ...args), export class Logger {
log: (...args: any[]) => console.log(`[${new Date()}]`, `${scope}:${padding}`, ...args), private padding = ' '.repeat(20 - this.scope.length);
warn: (...args: any[]) => console.warn(`[${new Date()}]`, `${scope}:${padding}`, ...args),
}; /**
* Create a new `Logger` instance for the specified `scope`.
* @param scope The logger's scope (added to all messages).
*/
constructor(private scope: string) {}
public error(...args: any[]) { this.callMethod('error', args); }
public info(...args: any[]) { this.callMethod('info', args); }
public log(...args: any[]) { this.callMethod('log', args); }
public warn(...args: any[]) { this.callMethod('warn', args); }
private callMethod(method: 'error' | 'info' | 'log' | 'warn', args: any[]) {
console[method](`[${new Date()}]`, `${this.scope}:${this.padding}`, ...args);
}
} }

View File

@ -5,14 +5,14 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as shell from 'shelljs'; import * as shell from 'shelljs';
import {HIDDEN_DIR_PREFIX} from '../common/constants'; import {HIDDEN_DIR_PREFIX} from '../common/constants';
import {assertNotMissingOrEmpty, computeShortSha, createLogger} from '../common/utils'; import {assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events'; import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
import {PreviewServerError} from './preview-error'; import {PreviewServerError} from './preview-error';
// Classes // Classes
export class BuildCreator extends EventEmitter { export class BuildCreator extends EventEmitter {
private logger = createLogger('BuildCreator'); private logger = new Logger('BuildCreator');
// Constructor // Constructor
constructor(protected buildsDir: string) { constructor(protected buildsDir: string) {

View File

@ -4,7 +4,7 @@ import {dirname} from 'path';
import {mkdir} from 'shelljs'; import {mkdir} from 'shelljs';
import {promisify} from 'util'; import {promisify} from 'util';
import {CircleCiApi} from '../common/circle-ci-api'; import {CircleCiApi} from '../common/circle-ci-api';
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, createLogger} from '../common/utils'; import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, Logger} from '../common/utils';
import {PreviewServerError} from './preview-error'; import {PreviewServerError} from './preview-error';
export interface GithubInfo { export interface GithubInfo {
@ -19,7 +19,7 @@ export interface GithubInfo {
* A helper that can get information about builds and download build artifacts. * A helper that can get information about builds and download build artifacts.
*/ */
export class BuildRetriever { export class BuildRetriever {
private logger = createLogger('BuildRetriever'); private logger = new Logger('BuildRetriever');
constructor(private api: CircleCiApi, private downloadSizeLimit: number, private downloadDir: string) { constructor(private api: CircleCiApi, private downloadSizeLimit: number, private downloadDir: string) {
assert(downloadSizeLimit > 0, 'Invalid parameter "downloadSizeLimit" should be a number greater than 0.'); assert(downloadSizeLimit > 0, 'Invalid parameter "downloadSizeLimit" should be a number greater than 0.');
assertNotMissingOrEmpty('downloadDir', downloadDir); assertNotMissingOrEmpty('downloadDir', downloadDir);
@ -34,7 +34,7 @@ export class BuildRetriever {
const buildInfo = await this.api.getBuildInfo(buildNum); const buildInfo = await this.api.getBuildInfo(buildNum);
const githubInfo: GithubInfo = { const githubInfo: GithubInfo = {
org: buildInfo.username, org: buildInfo.username,
pr: getPrfromBranch(buildInfo.branch), pr: getPrFromBranch(buildInfo.branch),
repo: buildInfo.reponame, repo: buildInfo.reponame,
sha: buildInfo.vcs_revision, sha: buildInfo.vcs_revision,
success: !buildInfo.failed, success: !buildInfo.failed,
@ -73,7 +73,7 @@ export class BuildRetriever {
} }
} }
function getPrfromBranch(branch: string): number { function getPrFromBranch(branch: string): number {
// CircleCI only exposes PR numbers via the `branch` field :-( // CircleCI only exposes PR numbers via the `branch` field :-(
const match = /^pull\/(\d+)$/.exec(branch); const match = /^pull\/(\d+)$/.exec(branch);
if (!match) { if (!match) {

View File

@ -2,11 +2,12 @@
import * as bodyParser from 'body-parser'; import * as bodyParser from 'body-parser';
import * as express from 'express'; import * as express from 'express';
import * as http from 'http'; import * as http from 'http';
import {AddressInfo} from 'net';
import {CircleCiApi} from '../common/circle-ci-api'; import {CircleCiApi} from '../common/circle-ci-api';
import {GithubApi} from '../common/github-api'; import {GithubApi} from '../common/github-api';
import {GithubPullRequests} from '../common/github-pull-requests'; import {GithubPullRequests} from '../common/github-pull-requests';
import {GithubTeams} from '../common/github-teams'; import {GithubTeams} from '../common/github-teams';
import {assert, assertNotMissingOrEmpty, createLogger} from '../common/utils'; import {assert, assertNotMissingOrEmpty, Logger} from '../common/utils';
import {BuildCreator} from './build-creator'; import {BuildCreator} from './build-creator';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events'; import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
import {BuildRetriever} from './build-retriever'; import {BuildRetriever} from './build-retriever';
@ -31,7 +32,7 @@ export interface PreviewServerConfig {
trustedPrLabel: string; trustedPrLabel: string;
} }
const logger = createLogger('PreviewServer'); const logger = new Logger('PreviewServer');
// Classes // Classes
export class PreviewServerFactory { export class PreviewServerFactory {
@ -52,7 +53,7 @@ export class PreviewServerFactory {
const httpServer = http.createServer(middleware as any); const httpServer = http.createServer(middleware as any);
httpServer.on('listening', () => { httpServer.on('listening', () => {
const info = httpServer.address(); const info = httpServer.address() as AddressInfo;
logger.info(`Up and running (and listening on ${info.address}:${info.port})...`); logger.info(`Up and running (and listening on ${info.address}:${info.port})...`);
}); });
@ -63,10 +64,36 @@ export class PreviewServerFactory {
buildCreator: BuildCreator, cfg: PreviewServerConfig): express.Express { buildCreator: BuildCreator, cfg: PreviewServerConfig): express.Express {
const middleware = express(); const middleware = express();
const jsonParser = bodyParser.json(); const jsonParser = bodyParser.json();
const significantFilesRe = new RegExp(cfg.significantFilesPattern);
// RESPOND TO IS-ALIVE PING // RESPOND TO IS-ALIVE PING
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200)); middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
// RESPOND TO CAN-HAVE-PUBLIC-PREVIEW CHECK
const canHavePublicPreviewRe = /^\/can-have-public-preview\/(\d+)\/?$/;
middleware.get(canHavePublicPreviewRe, async (req, res) => {
try {
const pr = +canHavePublicPreviewRe.exec(req.url)![1];
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
// Cannot have preview: PR did not touch relevant files: `aio/` or `packages/` (except for spec files).
res.send({canHavePublicPreview: false, reason: 'No significant files touched.'});
logger.log(`PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`);
} else if (!await buildVerifier.getPrIsTrusted(pr)) {
// Cannot have preview: PR not automatically verifiable as "trusted".
res.send({canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'});
logger.log(`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`);
} else {
// Can have preview.
res.send({canHavePublicPreview: true, reason: null});
logger.log(`PR:${pr} - Can have a public preview.`);
}
} catch (err) {
logger.error('Previewability check error', err);
respondWithError(res, err);
}
});
// CIRCLE_CI BUILD COMPLETE WEBHOOK // CIRCLE_CI BUILD COMPLETE WEBHOOK
middleware.post(/^\/circle-build\/?$/, jsonParser, async (req, res) => { middleware.post(/^\/circle-build\/?$/, jsonParser, async (req, res) => {
try { try {
@ -107,7 +134,7 @@ export class PreviewServerFactory {
`Invalid webhook: expected "githubRepo" property to equal "${cfg.githubRepo}" but got "${repo}".`); `Invalid webhook: expected "githubRepo" property to equal "${cfg.githubRepo}" but got "${repo}".`);
// Do not deploy unless this PR has touched relevant files: `aio/` or `packages/` (except for spec files) // Do not deploy unless this PR has touched relevant files: `aio/` or `packages/` (except for spec files)
if (!await buildVerifier.getSignificantFilesChanged(pr, new RegExp(cfg.significantFilesPattern))) { if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
res.sendStatus(204); res.sendStatus(204);
logger.log(`PR:${pr}, Build:${buildNum} - ` + logger.log(`PR:${pr}, Build:${buildNum} - ` +
`Skipping preview processing because this PR did not touch any significant files.`); `Skipping preview processing because this PR did not touch any significant files.`);

View File

@ -11,7 +11,7 @@ import {
AIO_NGINX_PORT_HTTPS, AIO_NGINX_PORT_HTTPS,
AIO_WWW_USER, AIO_WWW_USER,
} from '../common/env-variables'; } from '../common/env-variables';
import {computeShortSha, createLogger} from '../common/utils'; import {computeShortSha, Logger} from '../common/utils';
// Interfaces - Types // Interfaces - Types
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; } export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
@ -31,7 +31,7 @@ class Helper {
https: AIO_NGINX_PORT_HTTPS, https: AIO_NGINX_PORT_HTTPS,
}; };
private logger = createLogger('TestHelper'); private logger = new Logger('TestHelper');
// Constructor // Constructor
constructor() { constructor() {
@ -105,7 +105,7 @@ class Helper {
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme])); Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
} }
public verifyResponse(status: number | [number, string], regex = /^/): VerifyCmdResultFn { public verifyResponse(status: number | [number, string], regex: string | RegExp = /^/): VerifyCmdResultFn {
let statusCode: number; let statusCode: number;
let statusText: string; let statusText: string;
@ -180,26 +180,42 @@ class Helper {
} }
} }
interface DefaultCurlOptions {
defaultMethod?: CurlOptions['method'];
defaultOptions?: CurlOptions['options'];
defaultHeaders?: CurlOptions['headers'];
defaultData?: CurlOptions['data'];
defaultExtraPath?: CurlOptions['extraPath'];
}
interface CurlOptions { interface CurlOptions {
method?: string; method?: string;
options?: string; options?: string;
headers?: string[];
data?: any; data?: any;
url?: string; url?: string;
extraPath?: string; extraPath?: string;
} }
export function makeCurl(baseUrl: string) { export function makeCurl(baseUrl: string, {
defaultMethod = 'POST',
defaultOptions = '',
defaultHeaders = ['Content-Type: application/json'],
defaultData = {},
defaultExtraPath = '',
}: DefaultCurlOptions = {}) {
return function curl({ return function curl({
method = 'POST', method = defaultMethod,
options = '', options = defaultOptions,
data = {}, headers = defaultHeaders,
data = defaultData,
url = baseUrl, url = baseUrl,
extraPath = '', extraPath = defaultExtraPath,
}: CurlOptions) { }: CurlOptions) {
const dataString = data ? JSON.stringify(data) : ''; const dataString = data ? JSON.stringify(data) : '';
const cmd = `curl -iLX ${method} ` + const cmd = `curl -iLX ${method} ` +
`${options} ` + `${options} ` +
`--header "Content-Type: application/json" ` + headers.map(header => `--header "${header}" `).join('') +
`--data '${dataString}' ` + `--data '${dataString}' ` +
`${url}${extraPath}`; `${url}${extraPath}`;
return helper.runCmd(cmd); return helper.runCmd(cmd);

View File

@ -2,7 +2,7 @@
import * as nock from 'nock'; import * as nock from 'nock';
import * as tar from 'tar-stream'; import * as tar from 'tar-stream';
import {gzipSync} from 'zlib'; import {gzipSync} from 'zlib';
import {createLogger, getEnvVar} from '../common/utils'; import {getEnvVar, Logger} from '../common/utils';
import {BuildNums, PrNums, SHA} from './constants'; import {BuildNums, PrNums, SHA} from './constants';
// We are using the `nock` library to fake responses from REST requests, when testing. // We are using the `nock` library to fake responses from REST requests, when testing.
@ -14,7 +14,7 @@ import {BuildNums, PrNums, SHA} from './constants';
// below and return a suitable response. This is quite complicated to setup since the // below and return a suitable response. This is quite complicated to setup since the
// response from, say, CircleCI will affect what request is made to, say, Github. // response from, say, CircleCI will affect what request is made to, say, Github.
const logger = createLogger('NOCK'); const logger = new Logger('mock-external-apis');
const log = (...args: any[]) => { const log = (...args: any[]) => {
// Filter out non-matching URL checks // Filter out non-matching URL checks
@ -76,7 +76,7 @@ const GITHUB_PULLS_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/p
const GITHUB_TEAMS_URL = `/orgs/${AIO_GITHUB_ORGANIZATION}/teams`; const GITHUB_TEAMS_URL = `/orgs/${AIO_GITHUB_ORGANIZATION}/teams`;
const getIssueUrl = (prNum: number) => `${GITHUB_ISSUES_URL}/${prNum}`; const getIssueUrl = (prNum: number) => `${GITHUB_ISSUES_URL}/${prNum}`;
const getFilesUrl = (prNum: number) => `${GITHUB_PULLS_URL}/${prNum}/files`; const getFilesUrl = (prNum: number, pageNum = 1) => `${GITHUB_PULLS_URL}/${prNum}/files?page=${pageNum}&per_page=100`;
const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`; const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`;
const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`; const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`;
@ -97,7 +97,7 @@ const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authoriz
////////////////////////////// //////////////////////////////
// GENERAL responses // GENERAL responses
githubApi.get(GITHUB_TEAMS_URL + '?page=0&per_page=100').reply(200, TEST_TEAM_INFO); githubApi.get(GITHUB_TEAMS_URL + '?page=1&per_page=100').reply(200, TEST_TEAM_INFO);
githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200); githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200);
// BUILD_INFO errors // BUILD_INFO errors

View File

@ -3,6 +3,7 @@ import * as path from 'path';
import {rm} from 'shelljs'; import {rm} from 'shelljs';
import {AIO_BUILDS_DIR, AIO_NGINX_HOSTNAME, AIO_NGINX_PORT_HTTP, AIO_NGINX_PORT_HTTPS} from '../common/env-variables'; import {AIO_BUILDS_DIR, AIO_NGINX_HOSTNAME, AIO_NGINX_PORT_HTTP, AIO_NGINX_PORT_HTTPS} from '../common/env-variables';
import {computeShortSha} from '../common/utils'; import {computeShortSha} from '../common/utils';
import {PrNums} from './constants';
import {helper as h} from './helper'; import {helper as h} from './helper';
import {customMatchers} from './jasmine-custom-matchers'; import {customMatchers} from './jasmine-custom-matchers';
@ -252,6 +253,42 @@ describe(`nginx`, () => {
}); });
describe(`${host}/can-have-public-preview`, () => {
const baseUrl = `${scheme}://${host}/can-have-public-preview`;
it('should disallow non-GET requests', async () => {
await Promise.all([
h.runCmd(`curl -iLX POST ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
h.runCmd(`curl -iLX PUT ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
h.runCmd(`curl -iLX PATCH ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
h.runCmd(`curl -iLX DELETE ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
]);
});
it('should pass requests through to the preview server', async () => {
await h.runCmd(`curl -iLX GET ${baseUrl}/${PrNums.CHANGED_FILES_ERROR}`).
then(h.verifyResponse(500, /CHANGED_FILES_ERROR/));
});
it('should respond with 404 for unknown paths', async () => {
const cmdPrefix = `curl -iLX GET ${baseUrl}`;
await Promise.all([
h.runCmd(`${cmdPrefix}/foo/42`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}-foo/42`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}nfoo/42`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/42/foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/f00`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/`).then(h.verifyResponse(404)),
]);
});
});
describe(`${host}/circle-build`, () => { describe(`${host}/circle-build`, () => {
it('should disallow non-POST requests', done => { it('should disallow non-POST requests', done => {
@ -287,6 +324,7 @@ describe(`nginx`, () => {
h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)), h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
]).then(done); ]).then(done);
}); });
}); });

View File

@ -18,6 +18,92 @@ describe('preview-server', () => {
afterEach(() => h.cleanUp()); afterEach(() => h.cleanUp());
describe(`${host}/can-have-public-preview`, () => {
const curl = makeCurl(`${host}/can-have-public-preview`, {
defaultData: null,
defaultExtraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`,
defaultHeaders: [],
defaultMethod: 'GET',
});
it('should disallow non-GET requests', async () => {
const bodyRegex = /^Unknown resource in request/;
await Promise.all([
curl({method: 'POST'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
]);
});
it('should respond with 404 for unknown paths', async () => {
const bodyRegex = /^Unknown resource in request/;
await Promise.all([
curl({extraPath: `/foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: `-foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: `nfoo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}/foo`}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: '/f00'}).then(h.verifyResponse(404, bodyRegex)),
curl({extraPath: '/'}).then(h.verifyResponse(404, bodyRegex)),
]);
});
it('should respond with 500 if checking for significant file changes fails', async () => {
await Promise.all([
curl({extraPath: `/${PrNums.CHANGED_FILES_404}`}).then(h.verifyResponse(500, /CHANGED_FILES_404/)),
curl({extraPath: `/${PrNums.CHANGED_FILES_ERROR}`}).then(h.verifyResponse(500, /CHANGED_FILES_ERROR/)),
]);
});
it('should respond with 200 (false) if no significant files were touched', async () => {
const expectedResponse = JSON.stringify({
canHavePublicPreview: false,
reason: 'No significant files touched.',
});
await curl({extraPath: `/${PrNums.CHANGED_FILES_NONE}`}).then(h.verifyResponse(200, expectedResponse));
});
it('should respond with 500 if checking "trusted" status fails', async () => {
await curl({extraPath: `/${PrNums.TRUST_CHECK_ERROR}`}).then(h.verifyResponse(500, 'TRUST_CHECK_ERROR'));
});
it('should respond with 200 (false) if the PR is not automatically verifiable as "trusted"', async () => {
const expectedResponse = JSON.stringify({
canHavePublicPreview: false,
reason: 'Not automatically verifiable as \\"trusted\\".',
});
await Promise.all([
curl({extraPath: `/${PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
curl({extraPath: `/${PrNums.TRUST_CHECK_UNTRUSTED}`}).then(h.verifyResponse(200, expectedResponse)),
]);
});
it('should respond with 200 (true) if the PR can have a public preview', async () => {
const expectedResponse = JSON.stringify({
canHavePublicPreview: true,
reason: null,
});
await Promise.all([
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
curl({extraPath: `/${PrNums.TRUST_CHECK_TRUSTED_LABEL}`}).then(h.verifyResponse(200, expectedResponse)),
]);
});
});
describe(`${host}/circle-build`, () => { describe(`${host}/circle-build`, () => {
const curl = makeCurl(`${host}/circle-build`); const curl = makeCurl(`${host}/circle-build`);

View File

@ -7,43 +7,49 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"prebuild": "yarn clean-dist", "prebuild": "yarn clean-dist",
"build": "tsc", "build": "yarn ~~build",
"build-watch": "yarn build --watch", "prebuild-watch": "yarn prebuild",
"build-watch": "yarn ~~build-watch",
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"", "clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
"dev": "concurrently --kill-others --raw --success first \"yarn build-watch\" \"yarn test-watch\"", "predev": "yarn build || true",
"dev": "run-p ~~build-watch ~~test-watch",
"lint": "tslint --project tsconfig.json", "lint": "tslint --project tsconfig.json",
"pre~~test-only": "yarn lint",
"~~test-only": "node dist/test",
"pretest": "yarn build", "pretest": "yarn build",
"test": "yarn ~~test-only", "test": "yarn ~~test-only",
"pretest-watch": "yarn build", "pretest-watch": "yarn pretest",
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist" "test-watch": "yarn ~~test-watch",
"~~build": "tsc",
"~~build-watch": "yarn ~~build --watch",
"pre~~test-only": "yarn lint",
"~~test-only": "node dist/test",
"~~test-watch": "nodemon --delay 1 --exec \"yarn ~~test-only\" --watch dist"
}, },
"dependencies": { "dependencies": {
"body-parser": "^1.18.2", "body-parser": "^1.18.3",
"delete-empty": "^2.0.0", "delete-empty": "^2.0.0",
"express": "^4.15.4", "express": "^4.16.3",
"jasmine": "^2.8.0", "jasmine": "^3.2.0",
"nock": "^9.2.5", "nock": "^9.6.1",
"node-fetch": "^2.1.2", "node-fetch": "^2.2.0",
"shelljs": "^0.8.1", "shelljs": "^0.8.2",
"tar-stream": "^1.6.0", "source-map-support": "^0.5.9",
"tslib": "^1.7.1" "tar-stream": "^1.6.1",
"tslib": "^1.9.3"
}, },
"devDependencies": { "devDependencies": {
"@types/body-parser": "^1.16.5", "@types/body-parser": "^1.17.0",
"@types/express": "^4.0.37", "@types/express": "^4.16.0",
"@types/jasmine": "^2.6.0", "@types/jasmine": "^2.8.8",
"@types/nock": "^9.1.3", "@types/nock": "^9.3.0",
"@types/node": "^8.0.30", "@types/node": "^10.9.2",
"@types/node-fetch": "^1.6.8", "@types/node-fetch": "^2.1.2",
"@types/shelljs": "^0.8.0", "@types/shelljs": "^0.8.0",
"@types/supertest": "^2.0.3", "@types/supertest": "^2.0.5",
"concurrently": "^3.5.0", "nodemon": "^1.18.3",
"nodemon": "^1.12.1", "npm-run-all": "^4.1.3",
"supertest": "^3.0.0", "supertest": "^3.1.0",
"tslint": "^5.7.0", "tslint": "^5.11.0",
"tslint-jasmine-noSkipOrFocus": "^1.0.8", "tslint-jasmine-noSkipOrFocus": "^1.0.9",
"typescript": "^2.5.2" "typescript": "^3.0.1"
} }
} }

View File

@ -5,25 +5,28 @@ import * as shell from 'shelljs';
import {BuildCleaner} from '../../lib/clean-up/build-cleaner'; import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants'; import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
import {GithubPullRequests} from '../../lib/common/github-pull-requests'; import {GithubPullRequests} from '../../lib/common/github-pull-requests';
import {Logger} from '../../lib/common/utils';
const EXISTING_BUILDS = [10, 20, 30, 40]; const EXISTING_BUILDS = [10, 20, 30, 40];
const EXISTING_DOWNLOADS = [ const EXISTING_DOWNLOADS = [
'downloads/10-ABCDEF0-build.zip', '10-ABCDEF0-build.zip',
'downloads/10-1234567-build.zip', '10-1234567-build.zip',
'downloads/20-ABCDEF0-build.zip', '20-ABCDEF0-build.zip',
'downloads/20-1234567-build.zip', '20-1234567-build.zip',
]; ];
const OPEN_PRS = [10, 40]; const OPEN_PRS = [10, 40];
const ANY_DATE = jasmine.any(String); const ANY_DATE = jasmine.any(String);
// Tests // Tests
describe('BuildCleaner', () => { describe('BuildCleaner', () => {
let loggerErrorSpy: jasmine.Spy;
let loggerLogSpy: jasmine.Spy;
let cleaner: BuildCleaner; let cleaner: BuildCleaner;
beforeEach(() => { beforeEach(() => {
spyOn(console, 'error'); loggerErrorSpy = spyOn(Logger.prototype, 'error');
spyOn(console, 'log'); loggerLogSpy = spyOn(Logger.prototype, 'log');
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', 'build.zip'); cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '/downloads', 'build.zip');
}); });
describe('constructor()', () => { describe('constructor()', () => {
@ -51,11 +54,13 @@ describe('BuildCleaner', () => {
toThrowError('Missing or empty required parameter \'githubToken\'!'); toThrowError('Missing or empty required parameter \'githubToken\'!');
}); });
it('should throw if \'downloadsDir\' is empty', () => { it('should throw if \'downloadsDir\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '', 'build.zip')). expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '', 'build.zip')).
toThrowError('Missing or empty required parameter \'downloadsDir\'!'); toThrowError('Missing or empty required parameter \'downloadsDir\'!');
}); });
it('should throw if \'artifactPath\' is empty', () => { it('should throw if \'artifactPath\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', '')). expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', '')).
toThrowError('Missing or empty required parameter \'artifactPath\'!'); toThrowError('Missing or empty required parameter \'artifactPath\'!');
@ -85,9 +90,12 @@ describe('BuildCleaner', () => {
}); });
it('should return a promise', () => { it('should return a promise', async () => {
const promise = cleaner.cleanUp(); const promise = cleaner.cleanUp();
expect(promise).toEqual(jasmine.any(Promise)); expect(promise).toEqual(jasmine.any(Promise));
// Do not complete the test and release the spies synchronously, to avoid running the actual implementations.
await promise;
}); });
@ -160,6 +168,7 @@ describe('BuildCleaner', () => {
} }
}); });
it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => { it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
try { try {
cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test')); cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test'));
@ -168,6 +177,7 @@ describe('BuildCleaner', () => {
expect(err).toBe('Test'); expect(err).toBe('Test');
} }
}); });
}); });
@ -277,11 +287,14 @@ describe('BuildCleaner', () => {
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]); prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
}); });
it('should log the number of open PRs', () => { it('should log the number of open PRs', () => {
promise.then(prNumbers => { promise.then(prNumbers => {
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`); expect(loggerLogSpy).toHaveBeenCalledWith(
ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
}); });
}); });
}); });
@ -301,9 +314,9 @@ describe('BuildCleaner', () => {
}); });
it('should get the contents of the builds directory', () => { it('should get the contents of the downloads directory', () => {
expect(fsReaddirSpy).toHaveBeenCalled(); expect(fsReaddirSpy).toHaveBeenCalled();
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('downloads'); expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/downloads');
}); });
@ -317,7 +330,7 @@ describe('BuildCleaner', () => {
}); });
it('should resolve with the returned files (as numbers)', done => { it('should resolve with the returned file names', done => {
promise.then(result => { promise.then(result => {
expect(result).toEqual(EXISTING_DOWNLOADS); expect(result).toEqual(EXISTING_DOWNLOADS);
done(); done();
@ -383,8 +396,7 @@ describe('BuildCleaner', () => {
cleaner.removeDir('/foo/bar'); cleaner.removeDir('/foo/bar');
expect(console.error).toHaveBeenCalledWith( expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
jasmine.any(String), 'BuildCleaner: ', 'ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
}); });
}); });
@ -401,8 +413,8 @@ describe('BuildCleaner', () => {
it('should log the number of existing builds and builds to be removed', () => { it('should log the number of existing builds and builds to be removed', () => {
cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]); cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing builds: 3'); expect(loggerLogSpy).toHaveBeenCalledWith('Existing builds: 3');
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Removing 2 build(s): 1, 2'); expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
}); });
@ -454,25 +466,36 @@ describe('BuildCleaner', () => {
describe('removeUnnecessaryDownloads()', () => { describe('removeUnnecessaryDownloads()', () => {
let shellRmSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
spyOn(shell, 'rm'); shellRmSpy = spyOn(shell, 'rm');
});
it('should log the number of existing downloads and downloads to be removed', () => {
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
expect(loggerLogSpy).toHaveBeenCalledWith('Existing downloads: 4');
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 download(s): 20-ABCDEF0-build.zip, 20-1234567-build.zip');
});
it('should construct full paths to directories (by prepending \'downloadsDir\')', () => {
cleaner.removeUnnecessaryDownloads(['dl-1', 'dl-2', 'dl-3'], []);
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-1'));
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-2'));
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-3'));
}); });
it('should remove the downloads that do not correspond to open PRs', () => { it('should remove the downloads that do not correspond to open PRs', () => {
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS); cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
expect(shell.rm).toHaveBeenCalledTimes(2); expect(shellRmSpy).toHaveBeenCalledTimes(2);
expect(shell.rm).toHaveBeenCalledWith('downloads/20-ABCDEF0-build.zip'); expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-ABCDEF0-build.zip'));
expect(shell.rm).toHaveBeenCalledWith('downloads/20-1234567-build.zip'); expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-1234567-build.zip'));
}); });
it('should log the number of existing builds and builds to be removed', () => {
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing downloads: 4');
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ',
'Removing 2 download(s): downloads/20-ABCDEF0-build.zip, downloads/20-1234567-build.zip');
});
}); });
}); });

View File

@ -126,8 +126,8 @@ describe('GithubApi', () => {
(api as any).getPaginated('/foo/bar'); (api as any).getPaginated('/foo/bar');
(api as any).getPaginated('/foo/bar', {baz: 'qux'}); (api as any).getPaginated('/foo/bar', {baz: 'qux'});
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 0, per_page: 100}); expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 1, per_page: 100});
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 0, per_page: 100}); expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 1, per_page: 100});
}); });
@ -162,9 +162,9 @@ describe('GithubApi', () => {
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100}); const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
expect(apiGetSpy).toHaveBeenCalledTimes(3); expect(apiGetSpy).toHaveBeenCalledTimes(3);
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(0)]); expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(1)]); expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(2)]); expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
expect(data).toEqual(allItems); expect(data).toEqual(allItems);

View File

@ -4,13 +4,13 @@ import {GithubPullRequests} from '../../lib/common/github-pull-requests';
// Tests // Tests
describe('GithubPullRequests', () => { describe('GithubPullRequests', () => {
let githubApi: jasmine.SpyObj<GithubApi>; let githubApi: jasmine.SpyObj<GithubApi>;
beforeEach(() => { beforeEach(() => {
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']); githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
}); });
describe('constructor()', () => { describe('constructor()', () => {
it('should throw if \'githubOrg\' is missing or empty', () => { it('should throw if \'githubOrg\' is missing or empty', () => {
@ -95,16 +95,14 @@ describe('GithubPullRequests', () => {
done(); done();
}); });
}); });
}); });
describe('fetchAll()', () => { describe('fetchAll()', () => {
let prs: GithubPullRequests; let prs: GithubPullRequests;
beforeEach(() => { beforeEach(() => prs = new GithubPullRequests(githubApi, 'foo', 'bar'));
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
spyOn(console, 'log');
});
it('should call \'getPaginated()\' with the correct pathname and params', () => { it('should call \'getPaginated()\' with the correct pathname and params', () => {
@ -131,8 +129,10 @@ describe('GithubPullRequests', () => {
githubApi.getPaginated.and.returnValue('Test'); githubApi.getPaginated.and.returnValue('Test');
expect(prs.fetchAll() as any).toBe('Test'); expect(prs.fetchAll() as any).toBe('Test');
}); });
}); });
describe('fetchFiles()', () => { describe('fetchFiles()', () => {
let prs: GithubPullRequests; let prs: GithubPullRequests;
@ -141,21 +141,21 @@ describe('GithubPullRequests', () => {
}); });
it('should make a GET request to GitHub with the correct pathname', () => { it('should make a paginated GET request to GitHub with the correct pathname', () => {
prs.fetchFiles(42); prs.fetchFiles(42);
expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files'); expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
}); });
it('should resolve with the data returned from GitHub', done => { it('should resolve with the data returned from GitHub', done => {
const expected: any = [{ sha: 'ABCDE', filename: 'a/b/c'}, { sha: '12345', filename: 'x/y/z' }]; const expected: any = [{sha: 'ABCDE', filename: 'a/b/c'}, {sha: '12345', filename: 'x/y/z'}];
githubApi.get.and.callFake(() => Promise.resolve(expected)); githubApi.getPaginated.and.callFake(() => Promise.resolve(expected));
prs.fetch(42).then(data => { prs.fetchFiles(42).then(data => {
expect(data).toEqual(expected); expect(data).toEqual(expected);
done(); done();
}); });
}); });
}); });
}); });

View File

@ -1,4 +1,5 @@
// Imports // Imports
import {resolve as resolvePath} from 'path';
import { import {
assert, assert,
assertNotMissingOrEmpty, assertNotMissingOrEmpty,
@ -6,6 +7,7 @@ import {
computeShortSha, computeShortSha,
getEnvVar, getEnvVar,
getPrInfoFromDownloadPath, getPrInfoFromDownloadPath,
Logger,
} from '../../lib/common/utils'; } from '../../lib/common/utils';
// Tests // Tests
@ -19,6 +21,7 @@ describe('utils', () => {
}); });
}); });
describe('assert', () => { describe('assert', () => {
it('should throw if passed a false value', () => { it('should throw if passed a false value', () => {
expect(() => assert(false, 'error message')).toThrowError('error message'); expect(() => assert(false, 'error message')).toThrowError('error message');
@ -29,6 +32,7 @@ describe('utils', () => {
}); });
}); });
describe('computeArtifactDownloadPath', () => { describe('computeArtifactDownloadPath', () => {
it('should compute an absolute path based on the artifact info provided', () => { it('should compute an absolute path based on the artifact info provided', () => {
const downloadDir = '/a/b/c'; const downloadDir = '/a/b/c';
@ -36,10 +40,11 @@ describe('utils', () => {
const sha = 'ABCDEF1234567'; const sha = 'ABCDEF1234567';
const artifactPath = 'a/path/to/file.zip'; const artifactPath = 'a/path/to/file.zip';
const path = computeArtifactDownloadPath(downloadDir, pr, sha, artifactPath); const path = computeArtifactDownloadPath(downloadDir, pr, sha, artifactPath);
expect(path).toEqual('/a/b/c/123-ABCDEF1-file.zip'); expect(path).toBe(resolvePath('/a/b/c/123-ABCDEF1-file.zip'));
}); });
}); });
describe('getPrInfoFromDownloadPath', () => { describe('getPrInfoFromDownloadPath', () => {
it('should extract the PR and SHA from the file path', () => { it('should extract the PR and SHA from the file path', () => {
const {pr, sha} = getPrInfoFromDownloadPath('a/b/c/12345-ABCDE-artifact.zip'); const {pr, sha} = getPrInfoFromDownloadPath('a/b/c/12345-ABCDE-artifact.zip');
@ -48,6 +53,7 @@ describe('utils', () => {
}); });
}); });
describe('assertNotMissingOrEmpty()', () => { describe('assertNotMissingOrEmpty()', () => {
it('should throw if passed an empty value', () => { it('should throw if passed an empty value', () => {
@ -122,4 +128,79 @@ describe('utils', () => {
}); });
describe('Logger', () => {
let consoleErrorSpy: jasmine.Spy;
let consoleInfoSpy: jasmine.Spy;
let consoleLogSpy: jasmine.Spy;
let consoleWarnSpy: jasmine.Spy;
let logger: Logger;
beforeEach(() => {
consoleErrorSpy = spyOn(console, 'error');
consoleInfoSpy = spyOn(console, 'info');
consoleLogSpy = spyOn(console, 'log');
consoleWarnSpy = spyOn(console, 'warn');
logger = new Logger('TestScope');
});
it('should delegate to `console`', () => {
logger.error('foo');
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy.calls.argsFor(0)).toContain('foo');
logger.info('bar');
expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
expect(consoleInfoSpy.calls.argsFor(0)).toContain('bar');
logger.log('baz');
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
expect(consoleLogSpy.calls.argsFor(0)).toContain('baz');
logger.warn('qux');
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy.calls.argsFor(0)).toContain('qux');
});
it('should prepend messages with the current date and logger\'s scope', () => {
const mockDate = new Date(1337);
const expectedDateStr = `[${mockDate}]`;
const expectedScopeStr = 'TestScope: ';
jasmine.clock().mockDate(mockDate);
jasmine.clock().withMock(() => {
logger.error();
logger.info();
logger.log();
logger.warn();
});
expect(consoleErrorSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
expect(consoleInfoSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
expect(consoleLogSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
expect(consoleWarnSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
});
it('should pass all arguments to `console`', () => {
const someString = jasmine.any(String);
logger.error('foo1', 'foo2');
expect(consoleErrorSpy).toHaveBeenCalledWith(someString, someString, 'foo1', 'foo2');
logger.info('bar1', 'bar2');
expect(consoleInfoSpy).toHaveBeenCalledWith(someString, someString, 'bar1', 'bar2');
logger.log('baz1', 'baz2');
expect(consoleLogSpy).toHaveBeenCalledWith(someString, someString, 'baz1', 'baz2');
logger.warn('qux1', 'qux2');
expect(consoleWarnSpy).toHaveBeenCalledWith(someString, someString, 'qux1', 'qux2');
});
});
}); });

View File

@ -1,6 +0,0 @@
declare namespace jasmine {
export interface DoneFn extends Function {
(): void;
fail: (message: Error | string) => void;
}
}

View File

@ -3,5 +3,4 @@ import {runTests} from '../lib/common/run-tests';
// Run // Run
const specFiles = [`${__dirname}/**/*.spec.js`]; const specFiles = [`${__dirname}/**/*.spec.js`];
const helpers = [`${__dirname}/helpers.js`]; runTests(specFiles);
runTests(specFiles, helpers);

View File

@ -5,6 +5,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as shell from 'shelljs'; import * as shell from 'shelljs';
import {SHORT_SHA_LEN} from '../../lib/common/constants'; import {SHORT_SHA_LEN} from '../../lib/common/constants';
import {Logger} from '../../lib/common/utils';
import {BuildCreator} from '../../lib/preview-server/build-creator'; import {BuildCreator} from '../../lib/preview-server/build-creator';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events'; import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
import {PreviewServerError} from '../../lib/preview-server/preview-error'; import {PreviewServerError} from '../../lib/preview-server/preview-error';
@ -491,7 +492,7 @@ describe('BuildCreator', () => {
beforeEach(() => { beforeEach(() => {
cpExecCbs = []; cpExecCbs = [];
consoleWarnSpy = spyOn(console, 'warn'); consoleWarnSpy = spyOn(Logger.prototype, 'warn');
shellChmodSpy = spyOn(shell, 'chmod'); shellChmodSpy = spyOn(shell, 'chmod');
shellRmSpy = spyOn(shell, 'rm'); shellRmSpy = spyOn(shell, 'rm');
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb)); cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
@ -513,8 +514,7 @@ describe('BuildCreator', () => {
it('should log (as a warning) any stderr output if extracting succeeded', done => { it('should log (as a warning) any stderr output if extracting succeeded', done => {
(bc as any).extractArchive('foo', 'bar'). (bc as any).extractArchive('foo', 'bar').
then(() => expect(consoleWarnSpy) then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')).
.toHaveBeenCalledWith(jasmine.any(String), 'BuildCreator: ', 'This is stderr')).
then(done); then(done);
cpExecCbs[0](null, 'This is stdout', 'This is stderr'); cpExecCbs[0](null, 'This is stdout', 'This is stderr');

View File

@ -1,11 +1,13 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as nock from 'nock'; import * as nock from 'nock';
import {resolve as resolvePath} from 'path';
import {BuildInfo, CircleCiApi} from '../../lib/common/circle-ci-api'; import {BuildInfo, CircleCiApi} from '../../lib/common/circle-ci-api';
import {Logger} from '../../lib/common/utils';
import {BuildRetriever} from '../../lib/preview-server/build-retriever'; import {BuildRetriever} from '../../lib/preview-server/build-retriever';
describe('BuildRetriever', () => { describe('BuildRetriever', () => {
const MAX_DOWNLOAD_SIZE = 10000; const MAX_DOWNLOAD_SIZE = 10000;
const DOWNLOAD_DIR = '/DOWNLOAD/DIR'; const DOWNLOAD_DIR = resolvePath('/DOWNLOAD/DIR');
const BASE_URL = 'http://test.com'; const BASE_URL = 'http://test.com';
const ARTIFACT_PATH = '/some/path/build.zip'; const ARTIFACT_PATH = '/some/path/build.zip';
@ -29,10 +31,6 @@ describe('BuildRetriever', () => {
vcs_revision: 'COMMIT', vcs_revision: 'COMMIT',
}; };
spyOn(console, 'log');
spyOn(console, 'warn');
spyOn(console, 'error');
api = new CircleCiApi('ORG', 'REPO', 'TOKEN'); api = new CircleCiApi('ORG', 'REPO', 'TOKEN');
spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO)); spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO));
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl') getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl')
@ -91,6 +89,7 @@ describe('BuildRetriever', () => {
let retriever: BuildRetriever; let retriever: BuildRetriever;
beforeEach(() => { beforeEach(() => {
spyOn(Logger.prototype, 'warn');
retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR); retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
}); });
@ -133,11 +132,14 @@ describe('BuildRetriever', () => {
it('should write the artifact file to disk', async () => { it('should write the artifact file to disk', async () => {
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS); const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
const downloadPath = resolvePath(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`);
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH); await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
expect(writeFileSpy) expect(writeFileSpy).toHaveBeenCalledWith(downloadPath, jasmine.any(Buffer), jasmine.any(Function));
.toHaveBeenCalledWith(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`, jasmine.any(Buffer), jasmine.any(Function));
const buffer: Buffer = writeFileSpy.calls.mostRecent().args[1]; const buffer: Buffer = writeFileSpy.calls.mostRecent().args[1];
expect(buffer.toString()).toEqual(ARTIFACT_CONTENTS); expect(buffer.toString()).toEqual(ARTIFACT_CONTENTS);
artifactRequest.done(); artifactRequest.done();
}); });

View File

@ -2,11 +2,11 @@
import * as express from 'express'; import * as express from 'express';
import * as http from 'http'; import * as http from 'http';
import * as supertest from 'supertest'; import * as supertest from 'supertest';
import {promisify} from 'util';
import {CircleCiApi} from '../../lib/common/circle-ci-api'; import {CircleCiApi} from '../../lib/common/circle-ci-api';
import {GithubApi} from '../../lib/common/github-api'; import {GithubApi} from '../../lib/common/github-api';
import {GithubPullRequests} from '../../lib/common/github-pull-requests'; import {GithubPullRequests} from '../../lib/common/github-pull-requests';
import {GithubTeams} from '../../lib/common/github-teams'; import {GithubTeams} from '../../lib/common/github-teams';
import {Logger} from '../../lib/common/utils';
import {BuildCreator} from '../../lib/preview-server/build-creator'; import {BuildCreator} from '../../lib/preview-server/build-creator';
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events'; import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
import {BuildRetriever, GithubInfo} from '../../lib/preview-server/build-retriever'; import {BuildRetriever, GithubInfo} from '../../lib/preview-server/build-retriever';
@ -38,15 +38,18 @@ describe('PreviewServerFactory', () => {
significantFilesPattern: '^(?:aio|packages)\\/(?!.*[._]spec\\.[jt]s$)', significantFilesPattern: '^(?:aio|packages)\\/(?!.*[._]spec\\.[jt]s$)',
trustedPrLabel: 'trusted: pr-label', trustedPrLabel: 'trusted: pr-label',
}; };
let loggerErrorSpy: jasmine.Spy;
let loggerInfoSpy: jasmine.Spy;
let loggerLogSpy: jasmine.Spy;
// Helpers // Helpers
const createPreviewServer = (partialConfig: Partial<PreviewServerConfig> = {}) => const createPreviewServer = (partialConfig: Partial<PreviewServerConfig> = {}) =>
PreviewServerFactory.create({...defaultConfig, ...partialConfig}); PreviewServerFactory.create({...defaultConfig, ...partialConfig});
beforeEach(() => { beforeEach(() => {
spyOn(console, 'error'); loggerErrorSpy = spyOn(Logger.prototype, 'error');
spyOn(console, 'info'); loggerInfoSpy = spyOn(Logger.prototype, 'info');
spyOn(console, 'log'); loggerLogSpy = spyOn(Logger.prototype, 'log');
}); });
describe('create()', () => { describe('create()', () => {
@ -140,11 +143,10 @@ describe('PreviewServerFactory', () => {
const server = createPreviewServer(); const server = createPreviewServer();
server.address = () => ({address: 'foo', family: '', port: 1337}); server.address = () => ({address: 'foo', family: '', port: 1337});
expect(console.info).not.toHaveBeenCalled(); expect(loggerInfoSpy).not.toHaveBeenCalled();
server.emit('listening'); server.emit('listening');
expect(console.info).toHaveBeenCalledWith( expect(loggerInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
jasmine.any(String), 'PreviewServer: ', 'Up and running (and listening on foo:1337)...');
}); });
}); });
@ -241,10 +243,6 @@ describe('PreviewServerFactory', () => {
let buildCreator: BuildCreator; let buildCreator: BuildCreator;
let agent: supertest.SuperTest<supertest.Test>; let agent: supertest.SuperTest<supertest.Test>;
// Helpers
const promisifyRequest = async (req: supertest.Request) => await promisify(req.end.bind(req))();
const verifyRequests = async (reqs: supertest.Request[]) => await Promise.all(reqs.map(promisifyRequest));
beforeEach(() => { beforeEach(() => {
const circleCiApi = new CircleCiApi(defaultConfig.githubOrg, defaultConfig.githubRepo, const circleCiApi = new CircleCiApi(defaultConfig.githubOrg, defaultConfig.githubRepo,
defaultConfig.circleCiToken); defaultConfig.circleCiToken);
@ -257,14 +255,15 @@ describe('PreviewServerFactory', () => {
buildCreator = new BuildCreator(defaultConfig.buildsDir); buildCreator = new BuildCreator(defaultConfig.buildsDir);
const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator, const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator,
defaultConfig); defaultConfig);
agent = supertest.agent(middleware); agent = supertest.agent(middleware);
}); });
describe('GET /health-check', () => { describe('GET /health-check', () => {
it('should respond with 200', async () => { it('should respond with 200', async () => {
await verifyRequests([ await Promise.all([
agent.get('/health-check').expect(200), agent.get('/health-check').expect(200),
agent.get('/health-check/').expect(200), agent.get('/health-check/').expect(200),
]); ]);
@ -272,7 +271,7 @@ describe('PreviewServerFactory', () => {
it('should respond with 404 for non-GET requests', async () => { it('should respond with 404 for non-GET requests', async () => {
await verifyRequests([ await Promise.all([
agent.put('/health-check').expect(404), agent.put('/health-check').expect(404),
agent.post('/health-check').expect(404), agent.post('/health-check').expect(404),
agent.patch('/health-check').expect(404), agent.patch('/health-check').expect(404),
@ -282,7 +281,7 @@ describe('PreviewServerFactory', () => {
it('should respond with 404 if the path does not match exactly', async () => { it('should respond with 404 if the path does not match exactly', async () => {
await verifyRequests([ await Promise.all([
agent.get('/health-check/foo').expect(404), agent.get('/health-check/foo').expect(404),
agent.get('/health-check-foo').expect(404), agent.get('/health-check-foo').expect(404),
agent.get('/health-checknfoo').expect(404), agent.get('/health-checknfoo').expect(404),
@ -294,7 +293,104 @@ describe('PreviewServerFactory', () => {
}); });
describe('/circle-build', () => {
describe('GET /can-have-public-preview/<pr>', () => {
const baseUrl = '/can-have-public-preview';
const pr = 777;
const url = `${baseUrl}/${pr}`;
let bvGetPrIsTrustedSpy: jasmine.Spy;
let bvGetSignificantFilesChangedSpy: jasmine.Spy;
beforeEach(() => {
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
bvGetSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged').
and.returnValue(Promise.resolve(true));
});
it('should respond with 404 for non-GET requests', async () => {
await Promise.all([
agent.put(url).expect(404),
agent.post(url).expect(404),
agent.patch(url).expect(404),
agent.delete(url).expect(404),
]);
});
it('should respond with 404 if the path does not match exactly', async () => {
await Promise.all([
agent.get('/can-have-public-preview/42/foo').expect(404),
agent.get('/can-have-public-preview-foo/42').expect(404),
agent.get('/can-have-public-previewnfoo/42').expect(404),
agent.get('/foo/can-have-public-preview/42').expect(404),
agent.get('/foo-can-have-public-preview/42').expect(404),
agent.get('/fooncan-have-public-preview/42').expect(404),
]);
});
it('should respond appropriately if the PR did not touch any significant files', async () => {
bvGetSignificantFilesChangedSpy.and.returnValue(Promise.resolve(false));
const expectedResponse = {canHavePublicPreview: false, reason: 'No significant files touched.'};
const expectedLog = `PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`;
await agent.get(url).expect(200, expectedResponse);
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
});
it('should respond appropriately if the PR is not automatically verifiable as "trusted"', async () => {
bvGetPrIsTrustedSpy.and.returnValue(Promise.resolve(false));
const expectedResponse = {canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'};
const expectedLog =
`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`;
await agent.get(url).expect(200, expectedResponse);
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
});
it('should respond appropriately if the PR can have a preview', async () => {
const expectedResponse = {canHavePublicPreview: true, reason: null};
const expectedLog = `PR:${pr} - Can have a public preview.`;
await agent.get(url).expect(200, expectedResponse);
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
});
it('should respond with error if `getSignificantFilesChanged()` fails', async () => {
bvGetSignificantFilesChangedSpy.and.callFake(() => Promise.reject('getSignificantFilesChanged error'));
await agent.get(url).expect(500, 'getSignificantFilesChanged error');
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', 'getSignificantFilesChanged error');
});
it('should respond with error if `getPrIsTrusted()` fails', async () => {
const error = new Error('getPrIsTrusted error');
bvGetPrIsTrustedSpy.and.callFake(() => { throw error; });
await agent.get(url).expect(500, 'getPrIsTrusted error');
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', error);
});
});
describe('POST /circle-build', () => {
let getGithubInfoSpy: jasmine.Spy; let getGithubInfoSpy: jasmine.Spy;
let getSignificantFilesChangedSpy: jasmine.Spy; let getSignificantFilesChangedSpy: jasmine.Spy;
let downloadBuildArtifactSpy: jasmine.Spy; let downloadBuildArtifactSpy: jasmine.Spy;
@ -359,8 +455,8 @@ describe('PreviewServerFactory', () => {
await agent.post(URL).send(BASIC_PAYLOAD).expect(204); await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
expect(getGithubInfoSpy).not.toHaveBeenCalled(); expect(getGithubInfoSpy).not.toHaveBeenCalled();
expect(getSignificantFilesChangedSpy).not.toHaveBeenCalled(); expect(getSignificantFilesChangedSpy).not.toHaveBeenCalled();
expect(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ', expect(loggerLogSpy).toHaveBeenCalledWith(
'Build:12345, Job:lint -', 'Skipping preview processing because this is not the "aio_preview" job.'); 'Build:12345, Job:lint -', 'Skipping preview processing because this is not the "aio_preview" job.');
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled(); expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
expect(getPrIsTrustedSpy).not.toHaveBeenCalled(); expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
expect(createBuildSpy).not.toHaveBeenCalled(); expect(createBuildSpy).not.toHaveBeenCalled();
@ -371,7 +467,7 @@ describe('PreviewServerFactory', () => {
await agent.post(URL).send(BASIC_PAYLOAD).expect(204); await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM); expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp)); expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp));
expect(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ', expect(loggerLogSpy).toHaveBeenCalledWith(
'PR:777, Build:12345 - Skipping preview processing because this PR did not touch any significant files.'); 'PR:777, Build:12345 - Skipping preview processing because this PR did not touch any significant files.');
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled(); expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
expect(getPrIsTrustedSpy).not.toHaveBeenCalled(); expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
@ -467,7 +563,7 @@ describe('PreviewServerFactory', () => {
it('should respond with 404 for non-POST requests', async () => { it('should respond with 404 for non-POST requests', async () => {
await verifyRequests([ await Promise.all([
agent.get(url).expect(404), agent.get(url).expect(404),
agent.put(url).expect(404), agent.put(url).expect(404),
agent.patch(url).expect(404), agent.patch(url).expect(404),
@ -482,7 +578,7 @@ describe('PreviewServerFactory', () => {
const request1 = agent.post(url); const request1 = agent.post(url);
const request2 = agent.post(url).send(); const request2 = agent.post(url).send();
await verifyRequests([ await Promise.all([
request1.expect(400, responseBody), request1.expect(400, responseBody),
request2.expect(400, responseBody), request2.expect(400, responseBody),
]); ]);
@ -495,7 +591,7 @@ describe('PreviewServerFactory', () => {
const request1 = agent.post(url).send({}); const request1 = agent.post(url).send({});
const request2 = agent.post(url).send({number: null}); const request2 = agent.post(url).send({number: null});
await verifyRequests([ await Promise.all([
request1.expect(400, `${responseBodyPrefix} {}`), request1.expect(400, `${responseBodyPrefix} {}`),
request2.expect(400, `${responseBodyPrefix} {"number":null}`), request2.expect(400, `${responseBodyPrefix} {"number":null}`),
]); ]);
@ -503,7 +599,7 @@ describe('PreviewServerFactory', () => {
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => { it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => {
await promisifyRequest(createRequest(+pr)); await createRequest(+pr);
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9); expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
}); });
@ -511,9 +607,8 @@ describe('PreviewServerFactory', () => {
it('should propagate errors from BuildVerifier', async () => { it('should propagate errors from BuildVerifier', async () => {
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test')); bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
const req = createRequest(+pr).expect(500, 'Test'); await createRequest(+pr).expect(500, 'Test');
await promisifyRequest(req);
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9); expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled(); expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
}); });
@ -522,19 +617,17 @@ describe('PreviewServerFactory', () => {
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => { it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => {
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42)); bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
await promisifyRequest(createRequest(24)); await createRequest(24);
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false); expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false);
await promisifyRequest(createRequest(42)); await createRequest(42);
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true); expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true);
}); });
it('should propagate errors from BuildCreator', async () => { it('should propagate errors from BuildCreator', async () => {
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test')); bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
await createRequest(+pr).expect(500, 'Test');
const req = createRequest(+pr).expect(500, 'Test');
await verifyRequests([req]);
}); });
@ -544,7 +637,7 @@ describe('PreviewServerFactory', () => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false)); bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200])); const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
await verifyRequests(reqs); await Promise.all(reqs);
}); });
@ -552,7 +645,7 @@ describe('PreviewServerFactory', () => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false)); bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200])); const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
await verifyRequests(reqs); await Promise.all(reqs);
}); });
@ -560,14 +653,13 @@ describe('PreviewServerFactory', () => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false)); bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200])); const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
await verifyRequests(reqs); await Promise.all(reqs);
}); });
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', async () => { it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', async () => {
const promises = ['foo', 'notlabeled']. const promises = ['foo', 'notlabeled'].
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])). map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200]));
map(promisifyRequest);
await Promise.all(promises); await Promise.all(promises);
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled(); expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
@ -584,7 +676,7 @@ describe('PreviewServerFactory', () => {
it('should respond with 404', async () => { it('should respond with 404', async () => {
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`; const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
await verifyRequests([ await Promise.all([
agent.get('/some/url').expect(404, responseFor('get')), agent.get('/some/url').expect(404, responseFor('get')),
agent.put('/some/url').expect(404, responseFor('put')), agent.put('/some/url').expect(404, responseFor('put')),
agent.post('/some/url').expect(404, responseFor('post')), agent.post('/some/url').expect(404, responseFor('post')),

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
set -eu -o pipefail set -eu -o pipefail
# Set up env variables # Set up env variables
export AIO_CIRCLE_CI_TOKEN=UNUSED_CIRCLE_CI_TOKEN
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null) export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
# Run the clean-up # Run the clean-up

View File

@ -5,4 +5,5 @@ TODO (gkalpak): Add docs. Mention:
- Testing on CI. - Testing on CI.
Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh` Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
- Deploying from CI. - Deploying from CI.
Relevant files: `scripts/ci/deploy.sh`, `aio/scripts/deploy-to-firebase.sh` Relevant files: `.circleci/config.yml`, `scripts/ci/deploy.sh`, `aio/scripts/build-artifacts.sh`,
`aio/scripts/deploy-to-firebase.sh`

View File

@ -34,34 +34,31 @@ container:
### On CI (CircleCI) ### On CI (CircleCI)
- Build job completes successfully. - The CI script builds the angular.io project.
- The CI script checks whether the build job was initiated by a PR against the angular/angular
master branch.
- The CI script checks whether the PR has touched any files that might affect the angular.io app
(currently the `aio/` or `packages/` directories, ignoring spec files).
- The CI script gzips and stores the build artifacts in the CI infrastructure. - The CI script gzips and stores the build artifacts in the CI infrastructure.
- When the build completes CircleCI triggers a webhook on the preview-server. - When the build completes, CircleCI triggers a webhook on the preview-server.
More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md). More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md).
### Hosting build artifacts ### Hosting build artifacts
- nginx receives the webhook trigger and passes it through to the preview server. - nginx receives the webhook trigger and passes it through to the preview server.
- The preview-server runs several preliminary checks to determine whether the request is valid and
whether the corresponding PR can have a (public or non-public) preview (more details can be found
[here](overview--security-model.md)).
- The preview-server makes a request to CircleCI for the URL of the AIO build artifacts. - The preview-server makes a request to CircleCI for the URL of the AIO build artifacts.
- The preview-server makes a request to this URL to receive the artifact - failing if the size - The preview-server makes a request to this URL to receive the artifact - failing if the size
exceeds the specified max file size - and stores it in a temporary location. exceeds the specified max file size - and stores it in a temporary location.
- The preview-server runs several checks to determine whether the request should be accepted and - The preview-server runs more checks to determine whether the preview should be publicly accessible
whether it should be publicly accessible or stored for later verification (more details can be or stored for later verification (more details can be found [here](overview--security-model.md)).
found [here](overview--security-model.md)).
- The preview-server changes the "visibility" of the associated PR, if necessary. For example, if - The preview-server changes the "visibility" of the associated PR, if necessary. For example, if
builds for the same PR had been previously deployed as non-public and the current build has been builds for the same PR had been previously deployed as non-public and the current build has been
automatically verified, all previous builds are made public as well. automatically verified, all previous builds are made public as well.
If the PR transitions from "non-public" to "public", the preview-server posts a comment on the If the PR transitions from "non-public" to "public", the preview-server posts a comment on the
corresponding PR on GitHub mentioning the SHAs and the links where the previews can be found. corresponding PR on GitHub mentioning the SHAs and the links where the previews can be found.
- The preview-server verifies that it is not trying to overwrite an existing build. - The preview-server verifies that it is not trying to overwrite an existing build.
- The preview-server deploys the artifacts to a sub-directory named after the PR number and the first - The preview-server deploys the artifacts to a sub-directory named after the PR number and the
few characters of the SHA: `<PR>/<SHA>/` first few characters of the SHA: `<PR>/<SHA>/`
(Non-publicly accessible PRs will be stored in a different location, but again derived from the PR (Non-publicly accessible PRs will be stored in a different location, but again derived from the PR
number and SHA.) number and SHA.)
- If the PR is publicly accessible, the preview-server posts a comment on the corresponding PR on - If the PR is publicly accessible, the preview-server posts a comment on the corresponding PR on
@ -101,8 +98,8 @@ More info on the possible HTTP status codes and their meaning can be found
### Removing obsolete artifacts ### Removing obsolete artifacts
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
clean-up tasks once a day. The task retrieves all open PRs from GitHub and removes all directories clean-up task once a day. The task retrieves all open PRs from GitHub and removes all directories
that do not correspond with an open PR. that do not correspond to an open PR.
### Health-check ### Health-check

View File

@ -1,8 +1,8 @@
# Overview - HTTP Status Codes # Overview - HTTP Status Codes
This is a list of all the possible HTTP status codes returned by the nginx and preview servers, along This is a list of all the possible HTTP status codes returned by the nginx and preview servers,
with a brief explanation of what they mean: along with a brief explanation of what they mean:
## `http://*.ngbuilds.io/*` ## `http://*.ngbuilds.io/*`
@ -25,6 +25,23 @@ with a brief explanation of what they mean:
File not found. File not found.
## `https://ngbuilds.io/can-have-public-preview/<pr>`
- **200 (OK)**:
Whether the PR can have a public preview (based on its author, label, changed files).
_Response type:_ JSON
_Response format:_
```ts
{
canHavePublicPreview: boolean,
reason: string | null,
}
```
- **405 (Method Not Allowed)**:
Request method other than GET.
## `https://ngbuilds.io/circle-build` ## `https://ngbuilds.io/circle-build`
- **201 (Created)**: - **201 (Created)**:

View File

@ -11,8 +11,8 @@ part of the CI process and serving them publicly.
## Security objectives ## Security objectives
- **Prevent hosting arbitrary content to on servers.** - **Prevent hosting arbitrary content on our servers.**
Since there is no restriction on who can submit a PR, we cannot allow arbitrary untrusted PRs' Since there is no restriction on who can submit a PR, we cannot allow arbitrary, untrusted PRs'
build artifacts to be hosted. build artifacts to be hosted.
- **Prevent overwriting other people's hosted build artifacts.** - **Prevent overwriting other people's hosted build artifacts.**
@ -40,40 +40,49 @@ part of the CI process and serving them publicly.
### In a nutshell ### In a nutshell
The implemented approach can be broken up to the following sub-tasks: The implemented approach can be broken up to the following sub-tasks:
0. Receive notification from CircleCI of a completed build. 1. Receive notification from CircleCI of a completed build.
1. Verify that the build is valid and download the artifact. 2. Verify that the build is valid and can have a preview.
2. Fetch the PR's metadata, including author and labels. 3. Download the build artifact.
3. Check whether the PR can be automatically verified as "trusted" (based on its author or labels). 4. Fetch the PR's metadata, including author and labels.
4. If necessary, update the corresponding PR's verification status. 5. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
5. Deploy the artifacts to the corresponding PR's directory. 6. If necessary, update the corresponding PR's verification status.
6. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established 7. Deploy the artifacts to the corresponding PR's directory.
8. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
during deployment will remain valid until the artifacts are removed). during deployment will remain valid until the artifacts are removed).
7. Prevent hosted preview files from accessing anything outside their directory. 9. Prevent hosted preview files from accessing anything outside their directory.
### Implementation details ### Implementation details
This section describes how each of the aforementioned sub-tasks is accomplished: This section describes how each of the aforementioned sub-tasks is accomplished:
0. **Receive notification from CircleCI of a completed build** 1. **Receive notification from CircleCI of a completed build**
CircleCI is configured to trigger a webhook on our preview-server whenever a build completes. CircleCI is configured to trigger a webhook on our preview-server whenever a build completes.
The payload contains the number of the build that completed. The payload contains the number of the build that completed.
1. **Verify that the build is valid and download the artifact.** 2. **Verify that the build is valid and can have a preview.**
We cannot trust that the data in the webhook trigger is authentic, so we only extract the build We cannot trust that the data in the webhook trigger is authentic, so we only extract the build
number and then run a direct query against the CircleCI API to get hold of the real data for number and then run a direct query against the CircleCI API to get hold of the real data for
the given build number. the given build number.
If the build was not successful then we ignore this trigger. Otherwise we check that the We perform a number of preliminary checks:
associated github organisation and repository are what we expect (e.g. angular/angular). - Was the webhook triggered by the designated CircleCI job (currently `aio_preview`)?
- Was the build successful?
- Are the associated GitHub organisation and repository what we expect (e.g. `angular/angular`)?
- Has the PR touched any files that might affect the angular.io app (currently the `aio/` or
`packages/` directories, ignoring spec files)?
Next we make another call to the CircleCI API to get a list of the URLS for artifacts of that If any of the preliminary checks fails, the process is aborted and not preview is generated.
3. **Download the build artifact.**
Next we make another call to the CircleCI API to get a list of the URLs for artifacts of that
build. If there is one that matches the configured artifact path, we download the contents of the build. If there is one that matches the configured artifact path, we download the contents of the
build artifact and store it in a local folder. This download has a maximum size limit to prevent build artifact and store it in a local folder. This download has a maximum size limit to prevent
PRs from producing artifacts that are so large they would cause the preview server to crash. PRs from producing artifacts that are so large they would cause the preview server to crash.
2. **Fetch the PR's metadata, including author and labels**. 4. **Fetch the PR's metadata, including author and labels**.
Once we have securely downloaded the artifact for a build, we retrieve the PR's metadata - Once we have securely downloaded the artifact for a build, we retrieve the PR's metadata -
including the author's username and the labels - using the including the author's username and the labels - using the
@ -81,7 +90,7 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
To avoid rate-limit restrictions, we use a Personal Access Token (issued by To avoid rate-limit restrictions, we use a Personal Access Token (issued by
[@mary-poppins](https://github.com/mary-poppins)). [@mary-poppins](https://github.com/mary-poppins)).
3. **Check whether the PR can be automatically verified as "trusted"**. 5. **Check whether the PR can be automatically verified as "trusted"**.
"Trusted" means that we are confident that the build artifacts are suitable for being deployed "Trusted" means that we are confident that the build artifacts are suitable for being deployed
and publicly accessible on the preview server. There are two ways to check that: and publicly accessible on the preview server. There are two ways to check that:
@ -93,31 +102,32 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
`read:org` scope issued by a user that can "see" the specified GitHub organization. `read:org` scope issued by a user that can "see" the specified GitHub organization.
Here too, we use the token by @mary-poppins. Here too, we use the token by @mary-poppins.
4. **If necessary update the corresponding PR's verification status**. 6. **If necessary update the corresponding PR's verification status**.
Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e. Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e.
whether it is publicly accessible or not), based on the new verification status. For example, if whether it is publicly accessible or not), based on the new verification status. For example, if
a PR was initially considered "not trusted" but the check triggered by a new build determined a PR was initially considered "not trusted" but the check triggered by a new build determined
otherwise, the PR (and all the previously hosted previews) are made public. It works the same otherwise, the PR (and all the previously downloaded previews) are made public. It works the same
way if a PR has gone from "trusted" to "not trusted". way if a PR has gone from "trusted" to "not trusted".
5. **Deploy the artifacts to the corresponding PR's directory.** 7. **Deploy the artifacts to the corresponding PR's directory.**
With the preceding steps, we have verified that the build artifacts are valid. With the preceding steps, we have verified that the build artifacts are valid. Additionally, we
Additionally, we have determined whether the PR can be trusted to have its previews have determined whether the PR can be trusted to have its previews publicly accessible or whether
publicly accessible or whether further verification is necessary. The artifacts will be stored to further verification is necessary.
the PR's directory, but will not be publicly accessible unless the PR has been verified.
Essentially, as long as sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to
"project" the trust we have in a team's members through the PR to the build artifacts.
6. **Prevent overwriting previously deployed artifacts**. The artifacts will be stored to the PR's directory, but will not be publicly accessible unless
the PR has been verified. Essentially, as long as sub-tasks 2, 3, 4 and 5 can be securely
accomplished, it is possible to "project" the trust we have in a team's members through the PR to
the build artifacts.
8. **Prevent overwriting previously deployed artifacts**.
In order to enforce this restriction (and ensure that the deployed artifacts' validity is In order to enforce this restriction (and ensure that the deployed artifacts' validity is
preserved throughout their "lifetime"), the server that handles the artifacts (currently a Node.js preserved throughout their "lifetime"), the server that handles the artifacts (currently a Node.js Express server) rejects builds that have already been handled.
Express server) rejects builds that have already been handled.
_Note: A PR can contain multiple builds; one for each SHA that was built on CircleCI._ _Note: A PR can contain multiple builds; one for each SHA that was built on CircleCI._
7. **Prevent hosted preview files from accessing anything outside their directory.** 9. **Prevent hosted preview files from accessing anything outside their directory.**
Nginx (which is used to serve the hosted preview) has been configured to not follow symlinks Nginx (which is used to serve the hosted preview) has been configured to not follow symlinks
outside of the directory where the preview files are stored. outside of the directory where the preview files are stored.
@ -130,10 +140,10 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
This means that any secret access keys need only be stored on the preview-server and not on any of This means that any secret access keys need only be stored on the preview-server and not on any of
the CI build infrastructure (e.g. CircleCI). the CI build infrastructure (e.g. CircleCI).
- Each trusted PR author has full control over the content that is hosted as a preview for their PRs. - Each trusted PR author has full control over the content that is hosted as a preview for their
Part of the security model relies on the trustworthiness of these authors. PRs. Part of the security model relies on the trustworthiness of these authors.
- Adding the specified label on a PR to mark it as trusted, gives the author full control over - Adding the specified label on a PR to mark it as trusted, gives the author full control over the
the content that is hosted for the specific PR preview (e.g. by pushing more commits to it). content that is hosted for the specific PR preview (e.g. by pushing more commits to it). The user
The user adding the label is responsible for ensuring that this control is not abused and that adding the label is responsible for ensuring that this control is not abused and that the PR is
the PR is either closed (one way of another) or the access is revoked. either closed (one way of another) or the access is revoked.

View File

@ -8,7 +8,7 @@ Necessary secrets:
1. `GITHUB_TOKEN` 1. `GITHUB_TOKEN`
- Used for: - Used for:
- Retrieving open PRs without rate-limiting. - Retrieving open PRs without rate-limiting.
- Retrieving PR author. - Retrieving PR info, such as author, labels, changed files.
- Retrieving members of the trusted GitHub teams. - Retrieving members of the trusted GitHub teams.
- Posting comments with preview links on PRs. - Posting comments with preview links on PRs.
@ -25,8 +25,9 @@ Necessary secrets:
- Generate new token with the `public_repo` scope. - Generate new token with the `public_repo` scope.
2. `CIRCLE_CI_TOKEN` 2. `CIRCLE_CI_TOKEN`
- Visit https://circleci.com/gh/angular/angular/edit#api - Visit https://circleci.com/gh/angular/angular/edit#api.
- Create an API token with `Build Artifacts` scope - Create an API token with `Build Artifacts` scope.
## Save secrets on the VM ## Save secrets on the VM

1
aio/content/cli-src/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
yarn.lock

View File

@ -0,0 +1,5 @@
{
"dependencies": {
"@angular/cli": "https://github.com/angular/cli-builds#master"
}
}

83
aio/content/cli/index.md Normal file
View File

@ -0,0 +1,83 @@
<h1 class="no-toc">CLI Command Reference</h1>
The Angular CLI is a command-line tool that you use to initialize, develop, scaffold, and maintain Angular applications.
## Getting Started
### Installing Angular CLI
The current version of Angular CLI is 6.x.
* Both the CLI and the projects that you generate with the tool have dependencies that require Node 8.9 or higher, together with NPM 5.5.1 or higher.
* Install the CLI using npm:
`npm install -g @angular/cli`
* The CLI is an open-source tool:
https://github.com/angular/angular-cli/tree/master/packages/angular/cli
For details about changes between versions, and information about updating from previous releases, see the Releases tab on GitHub.
### Basic workflow
Invoke the tool on the command line through the ng executable. Online help is available on the command line:
```
> ng help Lists commands with short descriptions
> ng <command> --help Lists options for a command.
```
To create, build, and serve a new, basic Angular project on a development server, use the following commands:
```
cd <parent of new workspace>
ng new my-project
cd my-project
ng serve
```
In your browser, open http://localhost:4200/ to see the new app run.
### Workspaces and project files
Angular 6 introduced the workspace directory structure for Angular apps. A workspace defines a project. A project can contain multiple apps, as well as libraries that can be used in any of the apps.
Some commands (such as build) must be executed from within a workspace folder, and others (such as new) must be executed from outside any workspace. This requirement is called out in the description of each command where it applies.The `new` command creates a [workspace](guide/glossary#workspace) to contain [projects](guide/glossary#project). A project can be an app or a library, and a workspace can contain multiple apps and libraries.
A newly generated app project contains the source files for a root module, with a root component and template, which you can edit directly, or add to and modify using CLI commands. Use the generate command to add new files for additional components and services, and code for new pipes, directives, and so on.
* Commands such as `add` and `generate`, that create or operate on apps and libraries, must be executed from within a workspace folder.
* Apps in a workspace can use libraries in the same workspace.
* Each project has a `src` folder that contains the logic, data, and assets.
See an example of the [file structure](guide/quickstart#project-file-review) in [Getting Started](guide/quickstart).
When you use the `serve` command to build an app, the server automatically rebuilds the app and reloads the page when you change any of the source files.
### Configuring the CLI
Configuration files let you customize your project. The CLI configuration file, angular.json, is created at the top level of the project folder. This is where you can set CLI defaults for your project, and specify which files to include when the CLI builds the project.
The CLI config command lets you set and retrieve configuration values from the command line, or you can edit the angular.json file directly.
* See the complete schema for angular.json.
* Learn more about configuration options for Angular (link to new guide?)
### Command options and arguments
All commands and some options have aliases, as listed in the descriptions. Option names are prefixed with a double dash (--), but arguments and option aliases are not.
Typically, the name of a generated artifact can be given as an argument to the command or specified with the --name option. Most commands have additional options.
Command syntax is shown as follows:
```
ng commandNameOrAlias <arg> [options]
```
Options take either string or Boolean arguments. Defaults are shown in bold for Boolean or enumerated values, and are given with the description. For example:
```
--optionNameOrAlias=<filename>
--optionNameOrAlias=true|false
--optionNameOrAlias=allowedValue1|allowedValue2|allowedValue3
```
Boolean options can also be expressed with a prefix `no-` to indicate a value of false. For example, `--no-prod` is equivalent to `--prod=false`.

View File

@ -1,351 +1,259 @@
'use strict'; // necessary for es6 output in node 'use strict'; // necessary for es6 output in node
import { browser, element, by, ElementFinder } from 'protractor'; import { browser } from 'protractor';
import { logging, promise } from 'selenium-webdriver'; import { logging } from 'selenium-webdriver';
import * as openClose from './open-close.po';
import * as statusSlider from './status-slider.po';
import * as toggle from './toggle.po';
import * as enterLeave from './enter-leave.po';
import * as auto from './auto.po';
import * as filterStagger from './filter-stagger.po';
import * as heroGroups from './hero-groups';
import { getLinkById, sleepFor } from './util';
/**
* The tests here basically just checking that the end styles
* of each animation are in effect.
*
* Relies on the Angular testability only becoming stable once
* animation(s) have finished.
*
* Ideally we'd use https://developer.mozilla.org/en-US/docs/Web/API/Document/getAnimations
* but they're not supported in Chrome at the moment. The upcoming nganimate polyfill
* may also add some introspection support.
*/
describe('Animation Tests', () => { describe('Animation Tests', () => {
const openCloseHref = getLinkById('open-close');
const statusSliderHref = getLinkById('status');
const toggleHref = getLinkById('toggle');
const enterLeaveHref = getLinkById('enter-leave');
const autoHref = getLinkById('auto');
const filterHref = getLinkById('heroes');
const heroGroupsHref = getLinkById('hero-groups');
const INACTIVE_COLOR = 'rgba(238, 238, 238, 1)'; beforeAll(() => {
const ACTIVE_COLOR = 'rgba(207, 216, 220, 1)';
const NO_TRANSFORM_MATRIX_REGEX = /matrix\(1,\s*0,\s*0,\s*1,\s*0,\s*0\)/;
beforeEach(() => {
browser.get(''); browser.get('');
}); });
describe('basic states', () => { describe('Open/Close Component', () => {
let host: ElementFinder; beforeAll(async () => {
await openCloseHref.click();
beforeEach(() => { sleepFor();
host = element(by.css('app-hero-list-basic'));
}); });
it('animates between active and inactive', () => { it('should be open', async () => {
addInactiveHero(); let text = await openClose.getComponentText();
const toggleButton = openClose.getToggleButton();
const container = openClose.getComponentContainer();
let li = host.element(by.css('li')); if (text.includes('Closed')) {
await toggleButton.click();
sleepFor();
}
expect(getScaleX(li)).toBe(1.0); text = await openClose.getComponentText();
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR); const containerHeight = await container.getCssValue('height');
li.click(); expect(text).toContain('The box is now Open!');
browser.driver.sleep(300); expect(containerHeight).toBe('200px');
expect(getScaleX(li)).toBe(1.1);
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
li.click();
browser.driver.sleep(300);
expect(getScaleX(li)).toBe(1.0);
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
}); });
}); it('should be closed', async () => {
let text = await openClose.getComponentText();
const toggleButton = openClose.getToggleButton();
const container = openClose.getComponentContainer();
describe('styles inline in transitions', () => { if (text.includes('Open')) {
await toggleButton.click();
sleepFor();
}
let host: ElementFinder; text = await openClose.getComponentText();
const containerHeight = await container.getCssValue('height');
beforeEach(function() { expect(text).toContain('The box is now Closed!');
host = element(by.css('app-hero-list-inline-styles')); expect(containerHeight).toBe('100px');
}); });
it('are not kept after animation', () => { it('should log animation events', async () => {
addInactiveHero(); const toggleButton = openClose.getToggleButton();
const loggingCheckbox = openClose.getLoggingCheckbox();
await loggingCheckbox.click();
await toggleButton.click();
let li = host.element(by.css('li')); const logs = await browser.manage().logs().get(logging.Type.BROWSER);
li.click(); const animationMessages = logs.filter(({ message }) => message.indexOf('Animation') !== -1 ? true : false);
browser.driver.sleep(300);
expect(getScaleX(li)).toBe(1.0);
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
});
}); expect(animationMessages.length).toBeGreaterThan(0);
describe('combined transition syntax', () => {
let host: ElementFinder;
beforeEach(() => {
host = element(by.css('app-hero-list-combined-transitions'));
});
it('animates between active and inactive', () => {
addInactiveHero();
let li = host.element(by.css('li'));
expect(getScaleX(li)).toBe(1.0);
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
li.click();
browser.driver.sleep(300);
expect(getScaleX(li)).toBe(1.1);
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
li.click();
browser.driver.sleep(300);
expect(getScaleX(li)).toBe(1.0);
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
});
});
describe('two-way transition syntax', () => {
let host: ElementFinder;
beforeEach(() => {
host = element(by.css('app-hero-list-twoway'));
});
it('animates between active and inactive', () => {
addInactiveHero();
let li = host.element(by.css('li'));
expect(getScaleX(li)).toBe(1.0);
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
li.click();
browser.driver.sleep(300);
expect(getScaleX(li)).toBe(1.1);
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
li.click();
browser.driver.sleep(300);
expect(getScaleX(li)).toBe(1.0);
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
});
});
describe('enter & leave', () => {
let host: ElementFinder;
beforeEach(() => {
host = element(by.css('app-hero-list-enter-leave'));
});
it('adds and removes element', () => {
addInactiveHero();
let li = host.element(by.css('li'));
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
removeHero();
expect(li.isPresent()).toBe(false);
});
});
describe('enter & leave & states', () => {
let host: ElementFinder;
beforeEach(function() {
host = element(by.css('app-hero-list-enter-leave-states'));
});
it('adds and removes and animates between active and inactive', () => {
addInactiveHero();
let li = host.element(by.css('li'));
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
li.click();
browser.driver.sleep(300);
expect(getScaleX(li)).toBe(1.1);
li.click();
browser.driver.sleep(300);
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
removeHero();
expect(li.isPresent()).toBe(false);
});
});
describe('auto style calc', () => {
let host: ElementFinder;
beforeEach(function() {
host = element(by.css('app-hero-list-auto'));
});
it('adds and removes element', () => {
addInactiveHero();
let li = host.element(by.css('li'));
expect(li.getCssValue('height')).toBe('50px');
removeHero();
expect(li.isPresent()).toBe(false);
});
});
describe('different timings', () => {
let host: ElementFinder;
beforeEach(() => {
host = element(by.css('app-hero-list-timings'));
});
it('adds and removes element', () => {
addInactiveHero();
let li = host.element(by.css('li'));
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
expect(li.getCssValue('opacity')).toMatch('1');
removeHero();
expect(li.isPresent()).toBe(false);
});
});
describe('multiple keyframes', () => {
let host: ElementFinder;
beforeEach(() => {
host = element(by.css('app-hero-list-multistep'));
});
it('adds and removes element', () => {
addInactiveHero();
let li = host.element(by.css('li'));
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
expect(li.getCssValue('opacity')).toMatch('1');
removeHero();
expect(li.isPresent()).toBe(false);
});
});
describe('parallel groups', () => {
let host: ElementFinder;
beforeEach(() => {
host = element(by.css('app-hero-list-groups'));
});
it('adds and removes element', () => {
addInactiveHero();
let li = host.element(by.css('li'));
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
expect(li.getCssValue('opacity')).toMatch('1');
removeHero(700);
expect(li.isPresent()).toBe(false);
});
});
describe('adding active heroes', () => {
let host: ElementFinder;
beforeEach(() => {
host = element(by.css('app-hero-list-basic'));
});
it('animates between active and inactive', () => {
addActiveHero();
let li = host.element(by.css('li'));
expect(getScaleX(li)).toBe(1.1);
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
li.click();
browser.driver.sleep(300);
expect(getScaleX(li)).toBe(1.0);
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
li.click();
browser.driver.sleep(300);
expect(getScaleX(li)).toBe(1.1);
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
}); });
}); });
describe('callbacks', () => { describe('Status Slider Component', () => {
it('fires a callback on start and done', () => { const activeColor = 'rgba(255, 165, 0, 1)';
addActiveHero(); const inactiveColor = 'rgba(0, 0, 255, 1)';
browser.manage().logs().get(logging.Type.BROWSER)
.then((logs: logging.Entry[]) => {
const animationMessages = logs.filter((log) => {
return log.message.indexOf('Animation') !== -1 ? true : false;
});
expect(animationMessages.length).toBeGreaterThan(0); beforeAll(async () => {
}); await statusSliderHref.click();
sleepFor(2000);
});
it('should be inactive with an orange background', async () => {
let text = await statusSlider.getComponentText();
const toggleButton = statusSlider.getToggleButton();
const container = statusSlider.getComponentContainer();
if (text === 'Active') {
await toggleButton.click();
sleepFor(2000);
}
text = await statusSlider.getComponentText();
const bgColor = await container.getCssValue('backgroundColor');
expect(text).toBe('Inactive');
expect(bgColor).toBe(inactiveColor);
});
it('should be active with a blue background', async () => {
let text = await statusSlider.getComponentText();
const toggleButton = statusSlider.getToggleButton();
const container = statusSlider.getComponentContainer();
if (text === 'Inactive') {
await toggleButton.click();
sleepFor(2000);
}
text = await statusSlider.getComponentText();
const bgColor = await container.getCssValue('backgroundColor');
expect(text).toBe('Active');
expect(bgColor).toBe(activeColor);
}); });
}); });
function addActiveHero(sleep?: number) { describe('Toggle Animations Component', () => {
sleep = sleep || 500; beforeAll(async () => {
element(by.buttonText('Add active hero')).click(); await toggleHref.click();
browser.driver.sleep(sleep); sleepFor();
}
function addInactiveHero(sleep?: number) {
sleep = sleep || 500;
element(by.buttonText('Add inactive hero')).click();
browser.driver.sleep(sleep);
}
function removeHero(sleep?: number) {
sleep = sleep || 500;
element(by.buttonText('Remove hero')).click();
browser.driver.sleep(sleep);
}
function getScaleX(el: ElementFinder) {
return Promise.all([
getBoundingClientWidth(el),
getOffsetWidth(el)
]).then(function(promiseResolutions) {
let clientWidth = promiseResolutions[0];
let offsetWidth = promiseResolutions[1];
return clientWidth / offsetWidth;
}); });
}
function getBoundingClientWidth(el: ElementFinder) { it('should disabled animations on the child element', async () => {
return browser.executeScript( const toggleButton = toggle.getToggleAnimationsButton();
'return arguments[0].getBoundingClientRect().width',
el.getWebElement()
) as PromiseLike<number>;
}
function getOffsetWidth(el: ElementFinder) { await toggleButton.click();
return browser.executeScript(
'return arguments[0].offsetWidth', const container = toggle.getComponentContainer();
el.getWebElement() const cssClasses = await container.getAttribute('class');
) as PromiseLike<number>;
} expect(cssClasses).toContain('ng-animate-disabled');
});
});
describe('Enter/Leave Component', () => {
beforeAll(async () => {
await enterLeaveHref.click();
sleepFor(100);
});
it('should attach a flyInOut trigger to the list of items', async () => {
const heroesList = enterLeave.getHeroesList();
const hero = heroesList.get(0);
const cssClasses = await hero.getAttribute('class');
const transform = await hero.getCssValue('transform');
expect(cssClasses).toContain('ng-trigger-flyInOut');
expect(transform).toBe('matrix(1, 0, 0, 1, 0, 0)');
});
it('should remove the hero from the list when clicked', async () => {
const heroesList = enterLeave.getHeroesList();
const total = await heroesList.count();
const hero = heroesList.get(0);
await hero.click();
await sleepFor(100);
const newTotal = await heroesList.count();
expect(newTotal).toBeLessThan(total);
});
});
describe('Auto Calculation Component', () => {
beforeAll(async () => {
await autoHref.click();
sleepFor(0);
});
it('should attach a shrinkOut trigger to the list of items', async () => {
const heroesList = auto.getHeroesList();
const hero = heroesList.get(0);
const cssClasses = await hero.getAttribute('class');
expect(cssClasses).toContain('ng-trigger-shrinkOut');
});
it('should remove the hero from the list when clicked', async () => {
const heroesList = auto.getHeroesList();
const total = await heroesList.count();
const hero = heroesList.get(0);
await hero.click();
await sleepFor(250);
const newTotal = await heroesList.count();
expect(newTotal).toBeLessThan(total);
});
});
describe('Filter/Stagger Component', () => {
beforeAll(async () => {
await filterHref.click();
sleepFor();
});
it('should attach a filterAnimations trigger to the list container', async () => {
const heroesList = filterStagger.getComponentContainer();
const cssClasses = await heroesList.getAttribute('class');
expect(cssClasses).toContain('ng-trigger-filterAnimation');
});
it('should filter down the list when a search is performed', async () => {
const heroesList = filterStagger.getHeroesList();
const total = await heroesList.count();
const formInput = filterStagger.getFormInput();
await formInput.sendKeys('Mag');
await sleepFor(500);
const newTotal = await heroesList.count();
expect(newTotal).toBeLessThan(total);
expect(newTotal).toBe(2);
});
});
describe('Hero Groups Component', () => {
beforeAll(async () => {
await heroGroupsHref.click();
sleepFor(300);
});
it('should attach a flyInOut trigger to the list of items', async () => {
const heroesList = heroGroups.getHeroesList();
const hero = heroesList.get(0);
const cssClasses = await hero.getAttribute('class');
const transform = await hero.getCssValue('transform');
const opacity = await hero.getCssValue('opacity');
expect(cssClasses).toContain('ng-trigger-flyInOut');
expect(transform).toBe('matrix(1, 0, 0, 1, 0, 0)');
expect(opacity).toBe('1');
});
it('should remove the hero from the list when clicked', async () => {
const heroesList = heroGroups.getHeroesList();
const total = await heroesList.count();
const hero = heroesList.get(0);
await hero.click();
await sleepFor(300);
const newTotal = await heroesList.count();
expect(newTotal).toBeLessThan(total);
});
});
}); });

View File

@ -0,0 +1,19 @@
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-hero-list-auto-page');
}
export function getComponent() {
return by.css('app-hero-list-auto');
}
export function getComponentContainer() {
const findContainer = () => by.css('ul');
return locate(getComponent(), findContainer());
}
export function getHeroesList() {
return getComponentContainer().all(by.css('li'));
}

View File

@ -0,0 +1,19 @@
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-hero-list-enter-leave-page');
}
export function getComponent() {
return by.css('app-hero-list-enter-leave');
}
export function getComponentContainer() {
const findContainer = () => by.css('ul');
return locate(getComponent(), findContainer());
}
export function getHeroesList() {
return getComponentContainer().all(by.css('li'));
}

View File

@ -0,0 +1,20 @@
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-hero-list-page');
}
export function getComponentContainer() {
const findContainer = () => by.css('ul');
return locate(getPage(), findContainer());
}
export function getHeroesList() {
return getComponentContainer().all(by.css('li'));
}
export function getFormInput() {
const formInput = () => by.css('form > input');
return locate(getPage(), formInput());
}

View File

@ -0,0 +1,19 @@
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-hero-list-groups-page');
}
export function getComponent() {
return by.css('app-hero-list-groups');
}
export function getComponentContainer() {
const findContainer = () => by.css('ul');
return locate(getComponent(), findContainer());
}
export function getHeroesList() {
return getComponentContainer().all(by.css('li'));
}

View File

@ -0,0 +1,33 @@
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-open-close-page');
}
export function getComponent() {
return by.css('app-open-close');
}
export function getToggleButton() {
const toggleButton = () => by.buttonText('Toggle Open/Close');
return locate(getComponent(), toggleButton());
}
export function getLoggingCheckbox() {
const loggingCheckbox = () => by.css('section > input[type="checkbox"]');
return locate(getPage(), loggingCheckbox());
}
export function getComponentContainer() {
const findContainer = () => by.css('div');
return locate(getComponent(), findContainer());
}
export async function getComponentText() {
const findContainerText = () => by.css('div');
const contents = locate(getComponent(), findContainerText());
const componentText = await contents.getText();
return componentText;
}

View File

@ -0,0 +1,28 @@
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-status-slider-page');
}
export function getComponent() {
return by.css('app-status-slider');
}
export function getToggleButton() {
const toggleButton = () => by.buttonText('Toggle Status');
return locate(getComponent(), toggleButton());
}
export function getComponentContainer() {
const findContainer = () => by.css('div');
return locate(getComponent(), findContainer());
}
export async function getComponentText() {
const findContainerText = () => by.css('div');
const contents = locate(getComponent(), findContainerText());
const componentText = await contents.getText();
return componentText;
}

View File

@ -0,0 +1,25 @@
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-toggle-animations-child-page');
}
export function getComponent() {
return by.css('app-open-close-toggle');
}
export function getToggleButton() {
const toggleButton = () => by.buttonText('Toggle Open/Closed');
return locate(getComponent(), toggleButton());
}
export function getToggleAnimationsButton() {
const toggleAnimationsButton = () => by.buttonText('Toggle Animations');
return locate(getComponent(), toggleAnimationsButton());
}
export function getComponentContainer() {
const findContainer = () => by.css('div');
return locate(getComponent()).all(findContainer()).get(0);
}

View File

@ -0,0 +1,19 @@
import { Locator, ElementFinder, browser, by, element } from 'protractor';
/**
*
* locate(finder1, finder2) => element(finder1).element(finder2).element(finderN);
*/
export function locate(locator: Locator, ...locators: Locator[]) {
return locators.reduce((current: ElementFinder, next: Locator) => {
return current.element(next);
}, element(locator)) as ElementFinder;
}
export async function sleepFor(time = 1000) {
return await browser.sleep(time);
}
export function getLinkById(id: string) {
return element(by.css(`a[id=${id}]`));
}

View File

@ -0,0 +1,3 @@
<p>
Angular's animations library makes it easy to define and apply animation effects such as page and list transitions.
</p>

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-about',
templateUrl: './about.component.html',
styleUrls: ['./about.component.css']
})
export class AboutComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,11 @@
// #docregion
import { animation, style, animate } from '@angular/animations';
export const transAnimation = animation([
style({
height: '{{ height }}',
opacity: '{{ opacity }}',
backgroundColor: '{{ backgroundColor }}'
}),
animate('{{ time }}')
]);

View File

@ -0,0 +1,74 @@
// #docregion reusable
import {
animation, trigger, animateChild, group,
transition, animate, style, query
} from '@angular/animations';
export const transAnimation = animation([
style({
height: '{{ height }}',
opacity: '{{ opacity }}',
backgroundColor: '{{ backgroundColor }}'
}),
animate('{{ time }}')
]);
// #enddocregion reusable
// Routable animations
// #docregion route-animations
export const slideInAnimation =
// #docregion style-view
trigger('routeAnimations', [
transition('HomePage <=> AboutPage', [
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
]),
// #enddocregion style-view
// #docregion query
query(':enter', [
style({ left: '-100%'})
]),
query(':leave', animateChild()),
group([
query(':leave', [
animate('300ms ease-out', style({ left: '100%'}))
]),
query(':enter', [
animate('300ms ease-out', style({ left: '0%'}))
])
]),
query(':enter', animateChild()),
]),
transition('* <=> FilterPage', [
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
]),
query(':enter', [
style({ left: '-100%'})
]),
query(':leave', animateChild()),
group([
query(':leave', [
animate('200ms ease-out', style({ left: '100%'}))
]),
query(':enter', [
animate('300ms ease-out', style({ left: '0%'}))
])
]),
query(':enter', animateChild()),
])
// #enddocregion query
]);
// #enddocregion route-animations

View File

@ -0,0 +1,35 @@
// #docplaster
// #docregion imports
import { Component, HostBinding } from '@angular/core';
import {
trigger,
state,
style,
animate,
transition,
// ...
} from '@angular/animations';
// #enddocregion imports
// #docregion decorator, toggle-app-animations
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.css'],
animations: [
// animation triggers go here
]
})
// #enddocregion decorator
export class AppComponent {
@HostBinding('@.disabled')
public animationsDisabled = false;
// #enddocregion toggle-app-animations
toggleAnimations() {
this.animationsDisabled = !this.animationsDisabled;
}
// #docregion toggle-app-animations
}
// #enddocregion toggle-app-animations

View File

@ -0,0 +1,7 @@
:host {
display: block;
}
section {
margin-top: 100px;
}

View File

@ -0,0 +1,21 @@
<h1>Animations</h1>
Toggle All Animations <input type="checkbox" [checked]="!animationsDisabled" (click)="toggleAnimations()"/>
<nav>
<a id="home" routerLink="/home" routerLinkActive="active">Home</a>
<a id="about" routerLink="/about" routerLinkActive="active">About</a>
<a id="open-close" routerLink="/open-close" routerLinkActive="active">Open/Close</a>
<a id="status" routerLink="/status" routerLinkActive="active">Status Slider</a>
<a id="toggle" routerLink="/toggle" routerLinkActive="active">Toggle Animations</a>
<a id="enter-leave" routerLink="/enter-leave" routerLinkActive="active">Enter/Leave</a>
<a id="auto" routerLink="/auto" routerLinkActive="active">Auto Calculation</a>
<a id="heroes" routerLink="/heroes" routerLinkActive="active">Filter/Stagger</a>
<a id="hero-groups" routerLink="/hero-groups" routerLinkActive="active">Hero Groups</a>
</nav>
<!-- #docregion route-animations-outlet -->
<div [@routeAnimations]="prepareRoute(outlet)" >
<router-outlet #outlet="outlet"></router-outlet>
</div>
<!-- #enddocregion route-animations-outlet -->

View File

@ -0,0 +1,47 @@
// #docplaster
// #docregion imports
import { Component, HostBinding } from '@angular/core';
import {
trigger,
state,
style,
animate,
transition,
// ...
} from '@angular/animations';
// #enddocregion imports
import { RouterOutlet } from '@angular/router';
import { slideInAnimation } from './animations';
// #docregion decorator, toggle-app-animations, define
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.css'],
animations: [
// #enddocregion decorator
slideInAnimation
// #docregion decorator
// animation triggers go here
]
})
// #enddocregion decorator, define
export class AppComponent {
@HostBinding('@.disabled')
public animationsDisabled = false;
// #enddocregion toggle-app-animations
// #docregion prepare-router-outlet
prepareRoute(outlet: RouterOutlet) {
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
}
// #enddocregion prepare-router-outlet
toggleAnimations() {
this.animationsDisabled = !this.animationsDisabled;
}
// #docregion toggle-app-animations
}
// #enddocregion toggle-app-animations

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule
],
declarations: [ ],
bootstrap: [ ]
})
export class AppModule { }

View File

@ -1,43 +1,63 @@
// #docplaster // #docregion route-animation-data
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
// #docregion animations-module
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
// #enddocregion animations-module import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { HeroTeamBuilderComponent } from './hero-team-builder.component'; import { OpenCloseComponent } from './open-close.component';
import { HeroListBasicComponent } from './hero-list-basic.component'; import { OpenClosePageComponent } from './open-close-page.component';
import { HeroListInlineStylesComponent } from './hero-list-inline-styles.component'; import { OpenCloseChildComponent } from './open-close.component.4';
import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component'; import { ToggleAnimationsPageComponent } from './toggle-animations-page.component';
import { HeroListEnterLeaveStatesComponent } from './hero-list-enter-leave-states.component'; import { StatusSliderComponent } from './status-slider.component';
import { HeroListCombinedTransitionsComponent } from './hero-list-combined-transitions.component'; import { StatusSliderPageComponent } from './status-slider-page.component';
import { HeroListTwowayComponent } from './hero-list-twoway.component'; import { HeroListPageComponent } from './hero-list-page.component';
import { HeroListAutoComponent } from './hero-list-auto.component'; import { HeroListGroupPageComponent } from './hero-list-group-page.component';
import { HeroListGroupsComponent } from './hero-list-groups.component'; import { HeroListGroupsComponent } from './hero-list-groups.component';
import { HeroListMultistepComponent } from './hero-list-multistep.component'; import { HeroListEnterLeavePageComponent } from './hero-list-enter-leave-page.component';
import { HeroListTimingsComponent } from './hero-list-timings.component'; import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component';
// #docregion animations-module import { HeroListAutoCalcPageComponent } from './hero-list-auto-page.component';
import { HeroListAutoComponent } from './hero-list-auto.component';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
@NgModule({ @NgModule({
imports: [ BrowserModule, BrowserAnimationsModule ], imports: [
// ... more stuff ... BrowserModule,
// #enddocregion animations-module BrowserAnimationsModule,
declarations: [ RouterModule.forRoot([
HeroTeamBuilderComponent, { path: '', pathMatch: 'full', redirectTo: '/enter-leave' },
HeroListBasicComponent, { path: 'open-close', component: OpenClosePageComponent },
HeroListInlineStylesComponent, { path: 'status', component: StatusSliderPageComponent },
HeroListCombinedTransitionsComponent, { path: 'toggle', component: ToggleAnimationsPageComponent },
HeroListTwowayComponent, { path: 'heroes', component: HeroListPageComponent, data: {animation: 'FilterPage'} },
HeroListEnterLeaveComponent, { path: 'hero-groups', component: HeroListGroupPageComponent },
HeroListEnterLeaveStatesComponent, { path: 'enter-leave', component: HeroListEnterLeavePageComponent },
HeroListAutoComponent, { path: 'auto', component: HeroListAutoCalcPageComponent },
HeroListTimingsComponent, { path: 'home', component: HomeComponent, data: {animation: 'HomePage'} },
HeroListMultistepComponent, { path: 'about', component: AboutComponent, data: {animation: 'AboutPage'} },
HeroListGroupsComponent
])
], ],
bootstrap: [ HeroTeamBuilderComponent ] // #enddocregion route-animation-data
// #docregion animations-module declarations: [
AppComponent,
StatusSliderComponent,
OpenCloseComponent,
OpenCloseChildComponent,
OpenClosePageComponent,
StatusSliderPageComponent,
ToggleAnimationsPageComponent,
HeroListPageComponent,
HeroListGroupsComponent,
HeroListGroupPageComponent,
HeroListEnterLeavePageComponent,
HeroListEnterLeaveComponent,
HeroListAutoCalcPageComponent,
HeroListAutoComponent,
HomeComponent,
AboutComponent
],
bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule { }
// #enddocregion animations-module

View File

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { HEROES } from './mock-heroes';
@Component({
selector: 'app-hero-list-auto-page',
template: `
<section>
<h2>Automatic Calculation</h2>
<app-hero-list-auto [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-auto>
</section>
`
})
export class HeroListAutoCalcPageComponent {
heroes = HEROES.slice();
onRemove(id: number) {
this.heroes = this.heroes.filter(hero => hero.id !== id);
}
}

View File

@ -0,0 +1,9 @@
<ul class="heroes">
<li *ngFor="let hero of heroes"
[@shrinkOut]="'in'" (click)="removeHero(hero.id)">
<div class="inner">
<span class="badge">{{ hero.id }}</span>
<span>{{ hero.name }}</span>
</div>
</li>
</ul>

View File

@ -1,6 +1,8 @@
import { import {
Component, Component,
Input Input,
Output,
EventEmitter
} from '@angular/core'; } from '@angular/core';
import { import {
trigger, trigger,
@ -10,38 +12,30 @@ import {
transition transition
} from '@angular/animations'; } from '@angular/animations';
import { Hero } from './hero.service'; import { Hero } from './hero';
@Component({ @Component({
selector: 'app-hero-list-auto', selector: 'app-hero-list-auto',
// #docregion template templateUrl: 'hero-list-auto.component.html',
template: ` styleUrls: ['./hero-list-page.component.css'],
<ul> // #docregion auto-calc
<li *ngFor="let hero of heroes"
[@shrinkOut]="'in'">
{{hero.name}}
</li>
</ul>
`,
// #enddocregion template
styleUrls: ['./hero-list.component.css'],
/* When the element leaves (transition "in => void" occurs),
* get the element's current computed height and animate
* it down to 0.
*/
// #docregion animationdef
animations: [ animations: [
trigger('shrinkOut', [ trigger('shrinkOut', [
state('in', style({height: '*'})), state('in', style({ height: '*' })),
transition('* => void', [ transition('* => void', [
style({height: '*'}), style({ height: '*' }),
animate(250, style({height: 0})) animate(250, style({ height: 0 }))
]) ])
]) ])
] ]
// #enddocregion animationdef // #enddocregion auto-calc
}) })
export class HeroListAutoComponent { export class HeroListAutoComponent {
@Input() heroes: Hero[]; @Input() heroes: Hero[];
@Output() remove = new EventEmitter<number>();
removeHero(id: number) {
this.remove.emit(id);
}
} }

View File

@ -1,70 +0,0 @@
// #docplaster
// #docregion
// #docregion imports
import {
Component,
Input
} from '@angular/core';
import {
trigger,
state,
style,
animate,
transition
} from '@angular/animations';
// #enddocregion imports
import { Hero } from './hero.service';
@Component({
selector: 'app-hero-list-basic',
// #enddocregion
/* The click event calls hero.toggleState(), which
* causes the state of that hero to switch from
* active to inactive or vice versa.
*/
// #docregion
// #docregion template
template: `
<ul>
<li *ngFor="let hero of heroes"
[@heroState]="hero.state"
(click)="hero.toggleState()">
{{hero.name}}
</li>
</ul>
`,
// #enddocregion template
styleUrls: ['./hero-list.component.css'],
// #enddocregion
/**
* Define two states, "inactive" and "active", and the end
* styles that apply whenever the element is in those states.
* Then define animations for transitioning between the states,
* one in each direction
*/
// #docregion
// #docregion animationdef
animations: [
trigger('heroState', [
// #docregion states
state('inactive', style({
backgroundColor: '#eee',
transform: 'scale(1)'
})),
state('active', style({
backgroundColor: '#cfd8dc',
transform: 'scale(1.1)'
})),
// #enddocregion states
// #docregion transitions
transition('inactive => active', animate('100ms ease-in')),
transition('active => inactive', animate('100ms ease-out'))
// #enddocregion transitions
])
]
// #enddocregion animationdef
})
export class HeroListBasicComponent {
@Input() heroes: Hero[];
}

View File

@ -1,59 +0,0 @@
// #docregion
// #docregion imports
import {
Component,
Input
} from '@angular/core';
import {
trigger,
state,
style,
animate,
transition
} from '@angular/animations';
// #enddocregion imports
import { Hero } from './hero.service';
@Component({
selector: 'app-hero-list-combined-transitions',
// #docregion template
template: `
<ul>
<li *ngFor="let hero of heroes"
[@heroState]="hero.state"
(click)="hero.toggleState()">
{{hero.name}}
</li>
</ul>
`,
// #enddocregion template
styleUrls: ['./hero-list.component.css'],
/*
* Define two states, "inactive" and "active", and the end
* styles that apply whenever the element is in those states.
* Then define an animated transition between these two
* states, in *both* directions.
*/
// #docregion animationdef
animations: [
trigger('heroState', [
state('inactive', style({
backgroundColor: '#eee',
transform: 'scale(1)'
})),
state('active', style({
backgroundColor: '#cfd8dc',
transform: 'scale(1.1)'
})),
// #docregion transitions
transition('inactive => active, active => inactive',
animate('100ms ease-out'))
// #enddocregion transitions
])
]
// #enddocregion animationdef
})
export class HeroListCombinedTransitionsComponent {
@Input() heroes: Hero[];
}

View File

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { HEROES } from './mock-heroes';
@Component({
selector: 'app-hero-list-enter-leave-page',
template: `
<section>
<h2>Enter/Leave</h2>
<app-hero-list-enter-leave [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-enter-leave>
</section>
`
})
export class HeroListEnterLeavePageComponent {
heroes = HEROES.slice();
onRemove(id: number) {
this.heroes = this.heroes.filter(hero => hero.id !== id);
}
}

View File

@ -1,63 +0,0 @@
import {
Component,
Input
} from '@angular/core';
import {
trigger,
state,
style,
animate,
transition
} from '@angular/animations';
import { Hero } from './hero.service';
@Component({
selector: 'app-hero-list-enter-leave-states',
// #docregion template
template: `
<ul>
<li *ngFor="let hero of heroes"
(click)="hero.toggleState()"
[@heroState]="hero.state">
{{hero.name}}
</li>
</ul>
`,
// #enddocregion template
styleUrls: ['./hero-list.component.css'],
/* The elements here have two possible states based
* on the hero state, "active", or "inactive". We animate
* six transitions: Between the two states in both directions,
* and between each state and void. With this we can animate
* the enter and leave of elements differently based on which
* state they are in when they are added and removed.
*/
// #docregion animationdef
animations: [
trigger('heroState', [
state('inactive', style({transform: 'translateX(0) scale(1)'})),
state('active', style({transform: 'translateX(0) scale(1.1)'})),
transition('inactive => active', animate('100ms ease-in')),
transition('active => inactive', animate('100ms ease-out')),
transition('void => inactive', [
style({transform: 'translateX(-100%) scale(1)'}),
animate(100)
]),
transition('inactive => void', [
animate(100, style({transform: 'translateX(100%) scale(1)'}))
]),
transition('void => active', [
style({transform: 'translateX(0) scale(0)'}),
animate(200)
]),
transition('active => void', [
animate(200, style({transform: 'translateX(0) scale(0)'}))
])
])
]
// #enddocregion animationdef
})
export class HeroListEnterLeaveStatesComponent {
@Input() heroes: Hero[];
}

View File

@ -1,6 +1,8 @@
import { import {
Component, Component,
Input Input,
Output,
EventEmitter
} from '@angular/core'; } from '@angular/core';
import { import {
trigger, trigger,
@ -10,42 +12,45 @@ import {
transition transition
} from '@angular/animations'; } from '@angular/animations';
import { Hero } from './hero.service'; import { Hero } from './hero';
@Component({ @Component({
selector: 'app-hero-list-enter-leave', selector: 'app-hero-list-enter-leave',
// #docregion template // #docregion template
template: ` template: `
<ul> <ul class="heroes">
<li *ngFor="let hero of heroes" <li *ngFor="let hero of heroes"
[@flyInOut]="'in'"> [@flyInOut]="'in'" (click)="removeHero(hero.id)">
{{hero.name}} <div class="inner">
<span class="badge">{{ hero.id }}</span>
<span>{{ hero.name }}</span>
</div>
</li> </li>
</ul> </ul>
`, `,
// #enddocregion template // #enddocregion template
styleUrls: ['./hero-list.component.css'], styleUrls: ['./hero-list-page.component.css'],
/* The element here always has the state "in" when it
* is present. We animate two transitions: From void
* to in and from in to void, to achieve an animated
* enter and leave transition. The element enters from
* the left and leaves to the right using translateX.
*/
// #docregion animationdef // #docregion animationdef
animations: [ animations: [
trigger('flyInOut', [ trigger('flyInOut', [
state('in', style({transform: 'translateX(0)'})), state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [ transition('void => *', [
style({transform: 'translateX(-100%)'}), style({ transform: 'translateX(-100%)' }),
animate(100) animate(100)
]), ]),
transition('* => void', [ transition('* => void', [
animate(100, style({transform: 'translateX(100%)'})) animate(100, style({ transform: 'translateX(100%)' }))
]) ])
]) ])
] ]
// #enddocregion animationdef // #enddocregion animationdef
}) })
export class HeroListEnterLeaveComponent { export class HeroListEnterLeaveComponent {
@Input() heroes: Hero[]; @Input() heroes: Hero[];
@Output() remove = new EventEmitter<number>();
removeHero(id: number) {
this.remove.emit(id);
}
} }

View File

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { HEROES } from './mock-heroes';
@Component({
selector: 'app-hero-list-groups-page',
template: `
<section>
<h2>Hero List Group</h2>
<app-hero-list-groups [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-groups>
</section>
`
})
export class HeroListGroupPageComponent {
heroes = HEROES.slice();
onRemove(id: number) {
this.heroes = this.heroes.filter(hero => hero.id !== id);
}
}

View File

@ -1,6 +1,8 @@
import { import {
Component, Component,
Input Input,
Output,
EventEmitter
} from '@angular/core'; } from '@angular/core';
import { import {
trigger, trigger,
@ -11,45 +13,31 @@ import {
group group
} from '@angular/animations'; } from '@angular/animations';
import { Hero } from './hero.service'; import { Hero } from './hero';
@Component({ @Component({
selector: 'app-hero-list-groups', selector: 'app-hero-list-groups',
template: ` template: `
<ul> <ul class="heroes">
<li *ngFor="let hero of heroes" <li *ngFor="let hero of heroes"
[@flyInOut]="'in'"> [@flyInOut]="'in'" (click)="removeHero(hero.id)">
{{hero.name}} <div class="inner">
<span class="badge">{{ hero.id }}</span>
<span>{{ hero.name }}</span>
</div>
</li> </li>
</ul> </ul>
`, `,
styleUrls: ['./hero-list.component.css'], styleUrls: ['./hero-list-page.component.css'],
styles: [`
li {
padding: 0 !important;
text-align: center;
}
`],
/* The element here always has the state "in" when it
* is present. We animate two transitions: From void
* to in and from in to void, to achieve an animated
* enter and leave transition.
*
* The transitions have *parallel group* that allow
* animating several properties at the same time but
* with different timing configurations. On enter
* (void => *) we start the opacity animation 0.1s
* earlier than the translation/width animation.
* On leave (* => void) we do the opposite -
* the translation/width animation begins immediately
* and the opacity animation 0.1s later.
*/
// #docregion animationdef // #docregion animationdef
animations: [ animations: [
trigger('flyInOut', [ trigger('flyInOut', [
state('in', style({width: 120, transform: 'translateX(0)', opacity: 1})), state('in', style({
width: 120,
transform: 'translateX(0)', opacity: 1
})),
transition('void => *', [ transition('void => *', [
style({width: 10, transform: 'translateX(50px)', opacity: 0}), style({ width: 10, transform: 'translateX(50px)', opacity: 0 }),
group([ group([
animate('0.3s 0.1s ease', style({ animate('0.3s 0.1s ease', style({
transform: 'translateX(0)', transform: 'translateX(0)',
@ -77,4 +65,10 @@ import { Hero } from './hero.service';
}) })
export class HeroListGroupsComponent { export class HeroListGroupsComponent {
@Input() heroes: Hero[]; @Input() heroes: Hero[];
@Output() remove = new EventEmitter<number>();
removeHero(id: number) {
this.remove.emit(id);
}
} }

View File

@ -1,60 +0,0 @@
// #docregion
// #docregion imports
import {
Component,
Input,
} from '@angular/core';
import {
trigger,
style,
animate,
transition
} from '@angular/animations';
// #enddocregion imports
import { Hero } from './hero.service';
@Component({
selector: 'app-hero-list-inline-styles',
// #docregion template
template: `
<ul>
<li *ngFor="let hero of heroes"
[@heroState]="hero.state"
(click)="hero.toggleState()">
{{hero.name}}
</li>
</ul>
`,
// #enddocregion template
styleUrls: ['./hero-list.component.css'],
/**
* Define two states, "inactive" and "active", and the end
* styles that apply whenever the element is in those states.
* Then define an animation for the inactive => active transition.
* This animation has no end styles, but only styles that are
* defined inline inside the transition and thus are only kept
* as long as the animation is running.
*/
// #docregion animationdef
animations: [
trigger('heroState', [
// #docregion transitions
transition('inactive => active', [
style({
backgroundColor: '#cfd8dc',
transform: 'scale(1.3)'
}),
animate('80ms ease-in', style({
backgroundColor: '#eee',
transform: 'scale(1)'
}))
]),
// #enddocregion transitions
])
]
// #enddocregion animationdef
})
export class HeroListInlineStylesComponent {
@Input() heroes: Hero[];
}

View File

@ -1,71 +0,0 @@
import {
Component,
Input,
} from '@angular/core';
import {
trigger,
state,
style,
animate,
transition,
keyframes,
AnimationEvent
} from '@angular/animations';
import { Hero } from './hero.service';
@Component({
selector: 'app-hero-list-multistep',
// #docregion template
template: `
<ul>
<li *ngFor="let hero of heroes"
(@flyInOut.start)="animationStarted($event)"
(@flyInOut.done)="animationDone($event)"
[@flyInOut]="'in'">
{{hero.name}}
</li>
</ul>
`,
// #enddocregion template
styleUrls: ['./hero-list.component.css'],
/* The element here always has the state "in" when it
* is present. We animate two transitions: From void
* to in and from in to void, to achieve an animated
* enter and leave transition. Each transition is
* defined in terms of multiple keyframes, to give it
* a bounce effect.
*/
// #docregion animationdef
animations: [
trigger('flyInOut', [
state('in', style({transform: 'translateX(0)'})),
transition('void => *', [
animate(300, keyframes([
style({opacity: 0, transform: 'translateX(-100%)', offset: 0}),
style({opacity: 1, transform: 'translateX(15px)', offset: 0.3}),
style({opacity: 1, transform: 'translateX(0)', offset: 1.0})
]))
]),
transition('* => void', [
animate(300, keyframes([
style({opacity: 1, transform: 'translateX(0)', offset: 0}),
style({opacity: 1, transform: 'translateX(-15px)', offset: 0.7}),
style({opacity: 0, transform: 'translateX(100%)', offset: 1.0})
]))
])
])
]
// #enddocregion animationdef
})
export class HeroListMultistepComponent {
@Input() heroes: Hero[];
animationStarted(event: AnimationEvent) {
console.warn('Animation started: ', event);
}
animationDone(event: AnimationEvent) {
console.warn('Animation done: ', event);
}
}

View File

@ -0,0 +1,94 @@
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
position: relative;
height: 2.3em;
overflow:hidden;
margin: .5em;
}
.heroes li > .inner {
cursor: pointer;
background-color: #EEE;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
width: 19em;
}
.heroes li:hover > .inner {
color: #607D8B;
background-color: #DDD;
transform: translateX(.1em);
}
.heroes a {
color: #888;
text-decoration: none;
position: relative;
display: block;
width: 250px;
}
.heroes a:hover {
color:#607D8B;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
.button {
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
font-family: Arial;
}
button:hover {
background-color: #cfd8dc;
}
button.delete {
position: relative;
left: 24em;
top: -32px;
background-color: gray !important;
color: white;
display: inherit;
padding: 5px 8px;
width: 2em;
}
input {
font-size: 100%;
margin-bottom: 2px;
width: 11em;
}
.heroes input {
position: relative;
top: -3px;
width: 12em;
}

View File

@ -0,0 +1,19 @@
<!-- #docplaster -->
<h2>Filter/Stagger</h2>
<form>
<input #criteria (input)="updateCriteria(criteria.value)" placeholder="Search Heroes" />
</form>
<!-- #docregion filter-animations -->
<ul class="heroes" [@filterAnimation]="heroTotal">
<!-- #enddocregion filter-animations -->
<li *ngFor="let hero of heroes" class="hero">
<div class="inner">
<span class="badge">{{ hero.id }}</span>
<span>{{ hero.name }}</span>
</div>
</li>
<!-- #docregion filter-animations -->
</ul>
<!-- #enddocregion filter-animations -->

View File

@ -0,0 +1,81 @@
// #docplaster
import { Component, HostBinding, OnInit } from '@angular/core';
import { trigger, transition, animate, style, query, stagger } from '@angular/animations';
import { HEROES } from './mock-heroes';
// #docregion filter-animations
@Component({
// #enddocregion filter-animations
selector: 'app-hero-list-page',
templateUrl: 'hero-list-page.component.html',
styleUrls: ['hero-list-page.component.css'],
// #docregion page-animations, filter-animations
animations: [
// #enddocregion filter-animations
trigger('pageAnimations', [
transition(':enter', [
query('.hero, form', [
style({opacity: 0, transform: 'translateY(-100px)'}),
stagger(-30, [
animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({ opacity: 1, transform: 'none' }))
])
])
])
]),
// #enddocregion page-animations
// #docregion increment
// #docregion filter-animations
trigger('filterAnimation', [
transition(':enter, * => 0, * => -1', []),
transition(':increment', [
query(':enter', [
style({ opacity: 0, width: '0px' }),
stagger(50, [
animate('300ms ease-out', style({ opacity: 1, width: '*' })),
]),
], { optional: true })
]),
transition(':decrement', [
query(':leave', [
stagger(50, [
animate('300ms ease-out', style({ opacity: 0, width: '0px' })),
]),
])
]),
]),
// #enddocregion increment
// #docregion page-animations
]
})
export class HeroListPageComponent implements OnInit {
// #enddocregion filter-animations
@HostBinding('@pageAnimations')
public animatePage = true;
_heroes = [];
// #docregion filter-animations
heroTotal = -1;
// #enddocregion filter-animations
get heroes() {
return this._heroes;
}
ngOnInit() {
this._heroes = HEROES;
}
updateCriteria(criteria: string) {
criteria = criteria ? criteria.trim() : '';
this._heroes = HEROES.filter(hero => hero.name.toLowerCase().includes(criteria.toLowerCase()));
const newTotal = this.heroes.length;
if (this.heroTotal !== newTotal) {
this.heroTotal = newTotal;
} else if (!criteria) {
this.heroTotal = -1;
}
}
// #docregion filter-animations
}
// #enddocregion filter-animations

View File

@ -1,58 +0,0 @@
import {
Component,
Input
} from '@angular/core';
import {
trigger,
state,
style,
animate,
transition
} from '@angular/animations';
import { Hero } from './hero.service';
@Component({
selector: 'app-hero-list-timings',
template: `
<ul>
<li *ngFor="let hero of heroes"
[@flyInOut]="'in'"
(click)="hero.toggleState()">
{{hero.name}}
</li>
</ul>
`,
styleUrls: ['./hero-list.component.css'],
/* The element here always has the state "in" when it
* is present. We animate two transitions: From void
* to in and from in to void, to achieve an animated
* enter and leave transition. The element enters from
* the left and leaves to the right using translateX,
* and fades in/out using opacity. We use different easings
* for enter and leave.
*/
// #docregion animationdef
animations: [
trigger('flyInOut', [
state('in', style({opacity: 1, transform: 'translateX(0)'})),
transition('void => *', [
style({
opacity: 0,
transform: 'translateX(-100%)'
}),
animate('0.2s ease-in')
]),
transition('* => void', [
animate('0.2s 0.1s ease-out', style({
opacity: 0,
transform: 'translateX(100%)'
}))
])
])
]
// #enddocregion animationdef
})
export class HeroListTimingsComponent {
@Input() heroes: Hero[];
}

View File

@ -1,58 +0,0 @@
// #docregion
// #docregion imports
import {
Component,
Input
} from '@angular/core';
import {
trigger,
state,
style,
animate,
transition
} from '@angular/animations';
// #enddocregion imports
import { Hero } from './hero.service';
@Component({
selector: 'app-hero-list-twoway',
// #docregion template
template: `
<ul>
<li *ngFor="let hero of heroes"
[@heroState]="hero.state"
(click)="hero.toggleState()">
{{hero.name}}
</li>
</ul>
`,
// #enddocregion template
styleUrls: ['./hero-list.component.css'],
/*
* Define two states, "inactive" and "active", and the end
* styles that apply whenever the element is in those states.
* Then define an animated transition between these two
* states, in *both* directions.
*/
// #docregion animationdef
animations: [
trigger('heroState', [
state('inactive', style({
backgroundColor: '#eee',
transform: 'scale(1)'
})),
state('active', style({
backgroundColor: '#cfd8dc',
transform: 'scale(1.1)'
})),
// #docregion transitions
transition('inactive <=> active', animate('100ms ease-out'))
// #enddocregion transitions
])
]
// #enddocregion animationdef
})
export class HeroListTwowayComponent {
@Input() heroes: Hero[];
}

View File

@ -1,99 +0,0 @@
import { Component } from '@angular/core';
import { Hero, HeroService } from './hero.service';
@Component({
selector: 'app-root',
template: `
<div class="buttons">
<button [disabled]="!heroService.canAdd()" (click)="heroService.addInactive()">Add inactive hero</button>
<button [disabled]="!heroService.canAdd()" (click)="heroService.addActive()">Add active hero</button>
<button [disabled]="!heroService.canRemove()" (click)="heroService.remove()">Remove hero</button>
</div>
<div class="columns">
<div class="column">
<h4>Basic State</h4>
<p>Switch between active/inactive on click.</p>
<app-hero-list-basic [heroes]="heroes"></app-hero-list-basic>
</div>
<div class="column">
<h4>Styles inline in transitions</h4>
<p>Animated effect on click, no persistend end styles.</p>
<app-hero-list-inline-styles [heroes]="heroes"></app-hero-list-inline-styles>
</div>
<div class="column">
<h4>Combined transition syntax</h4>
<p>Switch between active/inactive on click. Define just one transition used in both directions.</p>
<app-hero-list-combined-transitions [heroes]="heroes"></app-hero-list-combined-transitions>
</div>
<div class="column">
<h4>Two-way transition syntax</h4>
<p>Switch between active/inactive on click. Define just one transition used in both directions using the <=> syntax.</p>
<app-hero-list-twoway [heroes]="heroes"></app-hero-list-twoway>
</div>
<div class="column">
<h4>Enter & Leave</h4>
<p>Enter and leave animations using the void state.</p>
<app-hero-list-enter-leave [heroes]="heroes"></app-hero-list-enter-leave>
</div>
</div>
<div class="columns">
<div class="column">
<h4>Enter & Leave & States</h4>
<p>
Enter and leave animations combined with active/inactive state animations.
Different enter and leave transitions depending on state.
</p>
<app-hero-list-enter-leave-states [heroes]="heroes"></app-hero-list-enter-leave-states>
</div>
<div class="column">
<h4>Auto Style Calc</h4>
<p>Leave animation from the current computed height using the auto-style value *.</p>
<app-hero-list-auto [heroes]="heroes"></app-hero-list-auto>
</div>
<div class="column">
<h4>Different Timings</h4>
<p>Enter and leave animations with different easings, ease-in for enter, ease-out for leave.</p>
<app-hero-list-timings [heroes]="heroes"></app-hero-list-timings>
</div>
<div class="column">
<h4>Multiple Keyframes</h4>
<p>Enter and leave animations with three keyframes in each, to give the transition some bounce.</p>
<app-hero-list-multistep [heroes]="heroes"></app-hero-list-multistep>
</div>
<div class="column">
<h4>Parallel Groups</h4>
<p>Enter and leave animations with multiple properties animated in parallel with different timings.</p>
<app-hero-list-groups [heroes]="heroes"></app-hero-list-groups>
</div>
</div>
`,
styles: [`
.buttons {
text-align: center;
}
button {
padding: 1.5em 3em;
}
.columns {
display: flex;
flex-direction: row;
}
.column {
flex: 1;
padding: 10px;
}
.column p {
min-height: 6em;
}
`],
providers: [HeroService]
})
export class HeroTeamBuilderComponent {
heroes: Hero[];
constructor(private heroService: HeroService) {
this.heroes = heroService.heroes;
}
}

View File

@ -1,54 +0,0 @@
import { Injectable } from '@angular/core';
// #docregion hero
export class Hero {
constructor(public name: string, public state = 'inactive') { }
toggleState() {
this.state = this.state === 'active' ? 'inactive' : 'active';
}
}
// #enddocregion hero
const ALL_HEROES = [
'Windstorm',
'RubberMan',
'Bombasto',
'Magneta',
'Dynama',
'Narco',
'Celeritas',
'Dr IQ',
'Magma',
'Tornado',
'Mr. Nice'
].map(name => new Hero(name));
@Injectable()
export class HeroService {
heroes: Hero[] = [];
canAdd() {
return this.heroes.length < ALL_HEROES.length;
}
canRemove() {
return this.heroes.length > 0;
}
addActive(active = true) {
let hero = ALL_HEROES[this.heroes.length];
hero.state = active ? 'active' : 'inactive';
this.heroes.push(hero);
}
addInactive() {
this.addActive(false);
}
remove() {
this.heroes.length -= 1;
}
}

View File

@ -0,0 +1,4 @@
export class Hero {
id: number;
name: string;
}

View File

@ -0,0 +1,3 @@
<p>
Welcome to Animations in Angular!
</p>

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,12 @@
:host {
display: block;
}
.insert-remove-container {
border: 1px solid #dddddd;
margin-top: 1em;
padding: 20px 20px 0px 20px;
color: #000000;
font-weight: bold;
font-size: 20px;
}

View File

@ -0,0 +1,10 @@
<!-- #docplaster -->
<nav>
<button (click)="toggle()">Toggle Insert/Remove</button>
</nav>
<!-- #docregion insert-remove-->
<div @myInsertRemoveTrigger *ngIf="isShown" class="insert-remove-container">
<p>The box is inserted</p>
</div>
<!-- #enddocregion insert-remove-->

View File

@ -0,0 +1,29 @@
// #docplaster
import { Component } from '@angular/core';
import { trigger, transition, animate, style } from '@angular/animations';
@Component({
selector: 'app-insert-remove',
animations: [
// #docregion enter-leave-trigger
trigger('myInsertRemoveTrigger', [
transition(':enter', [
style({ opacity: 0 }),
animate('5s', style({ opacity: 1 })),
]),
transition(':leave', [
animate('5s', style({ opacity: 0 }))
])
]),
// #enddocregion enter-leave-trigger
],
templateUrl: 'insert-remove.component.html',
styleUrls: ['insert-remove.component.css']
})
export class InsertRemoveComponent {
isShown = false;
toggle() {
this.isShown = !this.isShown;
}
}

View File

@ -0,0 +1,15 @@
// #docregion
import { Hero } from './hero';
export const HEROES: Hero[] = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];

View File

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-open-close-page',
template: `
<section>
<h2>Open Close Component</h2>
<input type="checkbox" [checked]="logging" (click)="toggleLogging()"/> Console Log Animation Events
<app-open-close [logging]="logging"></app-open-close>
</section>
`
})
export class OpenClosePageComponent {
logging = false;
toggleLogging() {
this.logging = !this.logging;
}
}

View File

@ -0,0 +1,10 @@
<!-- #docplaster -->
<nav>
<button (click)="toggle()">Toggle Open/Close</button>
</nav>
<!-- #docregion compare, trigger -->
<div [@openClose]="isOpen ? 'open' : 'closed'" class="open-close-container">
<p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
</div>
<!-- #enddocregion compare, trigger -->

View File

@ -0,0 +1,40 @@
import { Component } from '@angular/core';
import { trigger, transition, state, animate, style, keyframes } from '@angular/animations';
@Component({
selector: 'app-open-close',
animations: [
// #docregion trigger
trigger('openClose', [
state('open', style({
height: '200px',
opacity: 1,
backgroundColor: 'yellow'
})),
state('close', style({
height: '100px',
opacity: 0.5,
backgroundColor: 'green'
})),
// ...
transition('* => *', [
animate('1s', keyframes ( [
style({ opacity: 0.1, offset: 0.1 }),
style({ opacity: 0.6, offset: 0.2 }),
style({ opacity: 1, offset: 0.5 }),
style({ opacity: 0.2, offset: 0.7 })
]))
])
])
// #enddocregion trigger
],
templateUrl: 'open-close.component.html',
styleUrls: ['open-close.component.css']
})
export class OpenCloseKeyframeComponent {
isOpen = false;
toggle() {
this.isOpen = !this.isOpen;
}
}

View File

@ -0,0 +1,12 @@
<!-- #docplaster -->
<nav>
<button (click)="toggle()">Toggle Boolean/Close</button>
</nav>
<!-- #docregion trigger-boolean -->
<div [@openClose]="isOpen ? true : false" class="open-close-container">
<!-- #enddocregion trigger-boolean -->
<p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
<!-- #docregion trigger-boolean -->
</div>
<!-- #enddocregion trigger-boolean -->

View File

@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import { trigger, transition, state, animate, style } from '@angular/animations';
@Component({
selector: 'app-open-close-boolean',
// #docregion trigger-boolean
animations: [
trigger('openClose', [
state('true', style({ height: '*' })),
state('false', style({ height: '0px' })),
transition('false <=> true', animate(500))
])
],
// #enddocregion trigger-boolean
templateUrl: 'open-close.component.2.html',
styleUrls: ['open-close.component.css']
})
export class OpenCloseBooleanComponent {
isOpen = false;
toggle() {
this.isOpen = !this.isOpen;
}
}

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