Compare commits

...

89 Commits
4.4.4 ... 4.4.x

Author SHA1 Message Date
018750154d test: fix firebase deployment script test
When I fixed the project id in 2c4850dc58,
I didn't realize we had a test that verified the wrong behavior.
2018-05-04 15:10:17 -07:00
b19216d58b fix(aio): correct project id for deployment of archive sites 2018-05-03 15:09:17 -07:00
84fc1a3663 docs: add changelog for the 4.4.7 release 2018-04-16 02:00:31 -06:00
1c40be26c6 release: cut the 4.4.7 release 2018-04-16 02:00:02 -06:00
2c5cf19c6d fix(core): use appropriate inert document strategy for Firefox & Safari (#22077)
Both Firefox and Safari are vulnerable to XSS if we use an inert document
created via `document.implementation.createHTMLDocument()`.

Now we check for those vulnerabilities and then use a DOMParser or XHR
strategy if needed.

Further the platform-server has its own library for parsing HTML, so we
sniff for that (by checking whether DOMParser exists) and fall back to
the standard strategy.

Thanks to @cure53 for the heads up on this issue.
2018-02-13 10:05:14 -08:00
0dacf6d5f1 ci: use sudo: false on Travis (#21641)
Related to #21422.

PR Close #21641
2018-02-11 21:18:03 +02:00
e9f1d44015 ci: downgrade Chromium to a version that does not cause flakes
There seems to be some issue that causes Chrome/ChromeDriver to
unexpectedly reload during the aio e2e tests, causing flakes. It is not
clear what exactly is causing the reloading, but to the best of my
knowledge it is something inside Chrome or ChromeDriver.

Pinning Chrome to r494239 (between 62.0.3185.0 and 62.0.3186.0) fixes
the flakes.

Fixes #20159
2018-02-11 21:18:03 +02:00
1d9024ee9a build: pin ChromeDriver version (#20940)
Since our version of Chromium is also pinned, a new ChromeDriver (that
drops support for our Chromium version) can cause random (and unrelated
to the corresponding changes) errors on CI.
This commit pins the version of ChromeDriver and it should now be
manually upgraded to a vrsion that is compatible with th currently used
Chromium version.

PR Close #20940
2018-02-11 21:18:03 +02:00
6a6164ab4f revert: ci: use chrome stable (#18307)
This reverts commit 8bcb268140.
2018-02-11 19:39:55 +02:00
7231f5e26a docs: add changelog for 4.4.6 2017-10-18 16:14:53 -07:00
86415223cb release: cut the 4.4.6 release 2017-10-18 16:12:10 -07:00
269f5acc54 fix(common): attempt to JSON.parse errors for JSON responses (#19773)
Prior behavior for HttpClient was to parse the body as JSON when
responseType was set to 'json', even if the response was
unsuccessful. This changed due to a recent bugfix, and
unsuccessful responses had their bodies treated as strings.

There is no guarantee that if a service returns JSON in the
successful case that it will do so for errors. However, users
indicate that most APIs in the wild do work this way. Therefore,
this commit changes the error case behavior to attempt a JSON
parsing of the response body, and falls back on returning it as
a string if that fails.

PR Close #19773
2017-10-18 12:58:49 -07:00
6e6c866de9 docs(aio): changed confusing term (#19762)
Controller should be decorator I believe

PR Close #19762
2017-10-18 12:58:40 -07:00
732ed92cb7 test(animations): ensure :enter callbacks fire on container insertion (#19674)
PR Close #19674
2017-10-18 12:58:26 -07:00
53a807ae09 fix(router): RouterLinkActive should update its state right after checking the children
Closes #18983
2017-10-18 12:57:53 -07:00
3342a8253b fix(aio): make tests less flaky (#19784)
PR Close #19784
2017-10-18 10:19:48 -07:00
630c19f52d build: remove required BrowserStack run as it fails with “Access denied” (#19769)
See #19768
PR Close #19769
2017-10-17 15:53:05 -07:00
af8c2fa4be build: don’t make BrowserStack required as it fails with “Access denied”
See #19768
2017-10-17 14:56:41 -07:00
0789601dd6 build: fix broken path for animations in .pullapprove (#19453)
PR Close #19453
2017-10-17 10:44:43 -07:00
ce0ac46e42 style: fix formatting of check-node-modules (#19720)
PR Close #19720
2017-10-17 10:41:18 -07:00
b531d87580 ci: validate commit messages correctly when not on master (#19720)
PR Close #19720
2017-10-17 10:41:18 -07:00
23a2154817 docs(aio): update 2018 events (#19706)
update ac 2017 dates

PR Close #19706
2017-10-17 10:41:09 -07:00
76d2496f24 perf(animations): reduce size of bundle by removing AST classes (#19673)
This CL refactors the animation AST code to make use of interfaces
instead of classes. Given that interfaces are not persisted during
runtime the removal of classes should nicely cut down on size for the
animations-browser bundle.

PR Close #19673
2017-10-17 10:41:01 -07:00
b85cb410f1 docs(aio): change in-mem-web-api version for examples (#19668)
PR Close #19668
2017-10-17 10:40:52 -07:00
1be22df0df ci(aio): raise payload limit to accommodate the new search feature (#19704)
PR Close #19704
2017-10-13 15:20:23 -07:00
a805839d38 feat(aio): add search to 404 page (#19704)
The 404 page will now run a search based on the given URL to offer
suggestions for the page that the user really wanted.

PR Close #19704
2017-10-13 15:20:23 -07:00
3ac61a7550 refactor(aio): move SearchResultsComponent into shared module (#19704)
This will allow it to be used by an embedded component.

PR Close #19704
2017-10-13 15:20:23 -07:00
57ea33bc5c feat(aio): allow SearchService to have multiple clients (#19704)
PR Close #19704
2017-10-13 15:20:23 -07:00
4891649d68 test(aio): tidy up e2e tests that used invalid URLs (#19704)
PR Close #19704
2017-10-13 15:20:23 -07:00
93aba1bb1c build(aio): remove unused imports and local variables (#19704)
PR Close #19704
2017-10-13 15:20:23 -07:00
f983a6c615 fix(animations): properly support boolean-based transitions and state changes (#19672)
Closes #9396
Closes #12337

PR Close #19672
2017-10-13 09:40:29 -07:00
18f1b016e5 build(aio): consider devDependencies when overwriting dependencies of local Angular packages (#19687)
Previously, only `dependencies` were taken into account.

PR Close #19687
2017-10-13 09:30:10 -07:00
591dcc26af docs(animations): add missing bracket to fadeAnimation
Closes #18899
2017-10-13 09:14:29 -07:00
4acd322128 fix(core): don't refer to hydration in docs anymore.
Closes #18458
2017-10-13 09:05:30 -07:00
32a814bdfa docs: removing unnecessary commits from change log 2017-10-12 13:32:23 -07:00
912068e71c docs: add changelog for 4.4.5 2017-10-12 10:26:21 -07:00
df8e57dc5d release: cut the 4.4.5 release 2017-10-12 10:07:33 -07:00
f27f6e498f docs(aio): improve the Angular Connect homepage banner (#19684)
The original logo was too small.
The conference is over two days.

PR Close #19684
2017-10-12 09:49:58 -07:00
01bfbcb84a build(aio): update yarn.lock (#19683)
PR Close #19683
2017-10-12 09:49:48 -07:00
a15abbb324 docs(aio): change Material 2 to Material (#19663)
it is just called Angular Material now
PR Close #19663
2017-10-11 16:54:09 -07:00
28c29d560e build: fix rxjs version for aio (#19585)
PR Close #19585
2017-10-11 15:30:52 -07:00
65ca7fd4aa docs(aio): update banner ad (#19669)
PR Close #19669
2017-10-11 14:45:06 -07:00
3e3f918bb3 ci: freeze yarn lockfile when installing dependencies (#19616)
PR Close #19616
2017-10-11 14:37:38 -07:00
fc1dcffbdd build(aio): freeze lockfile when setting up examples (#19616)
PR Close #19616
2017-10-11 14:37:38 -07:00
ee3c681f98 build(aio): freeze yarn lockfile for aio-builds-setup scripts (#19616)
PR Close #19616
2017-10-11 14:37:37 -07:00
6225fedcb8 build(aio): freeze lockfile when installing example dependencies (#19616)
PR Close #19616
2017-10-11 14:37:37 -07:00
2905069559 ci(aio): freeze the lockfile for CI builds (#19616)
The CI will now fail if the dependencies in the AIO
package.json have been modified without the
lockfile being updated.

PR Close #19616
2017-10-11 14:37:37 -07:00
47202dd747 build(aio): improve accessor rendering (#19637)
Includes an update to `dgeni-packages@0.22.0` which provides more info
about accessors if required.

PR Close #19637
2017-10-11 14:37:28 -07:00
fb130c4eae build(aio): append information about links in and out of docs (#19583)
Closes #19560

PR Close #19583
2017-10-11 14:37:16 -07:00
734378c90b fix(compiler): TestBed.overrideProvider should keep imported NgModules eager (#19624)
Before, as soon as a user called `TestBed.overrideProvider` for a provider
of a `NgModule` that was imported via `TestBed.configureTestingModule`,
that `NgModule` became lazy.

This commit changes this behavior to keep the `NgModule` eager,
with or without a call to `TestBed.overrideProvider`.

PR Close #19624
2017-10-11 14:01:04 -07:00
3959b7ef28 refactor(compiler): introduce TestBed.deprecatedOverrideProvider (#19558)
This allows use to fix `TestBed.overrideProvider` to keep imported `NgModule`s eager,
while allowing our users to still keep the old semantics until they have fixed their
tests.

PR Close #19558
2017-10-11 14:00:32 -07:00
e292548523 fix(compiler): correctly instantiate eager providers that are used via Injector.get (#19558)
Closes #15501

PR Close #19558
2017-10-11 14:00:32 -07:00
c2506a78a1 docs(aio): add copy about ngNoForm to NgForm API doc (#19561)
PR Close #19561
2017-10-11 14:00:12 -07:00
34f70c6de2 build(aio): sort API list alphabetically (#19566)
Closes #19559

PR Close #19566
2017-10-11 13:52:19 -07:00
3d9c2a6352 build: add a Git .mailmap with my new name (#19550)
In this way my past contribution is mapped correctly.

PR Close #19550
2017-10-11 13:52:06 -07:00
3232125650 build(aio): remove unnecessary -- from yarn commands (#19565)
Essentially backports ffceae0a0 and f7199aa8c to 4.4.x.

PR Close #19565
2017-10-11 13:26:07 -07:00
c9f8718d2a fix(tsc-wrapped): don't rewrite imports when annotating for closure (#19579)
Port of f24ea59f74 to 4.4.x

Closure no longer needs to have the imports rewritten so avoid
rewriting as this can cause issues when the source directory
structure differs from what is deployed.

Fixes: #17171

PR Close #19579
2017-10-11 13:25:01 -07:00
f89ac92e5e test(aio): temporarily disable failing examples e2e tests (#19600)
Temporarily disable a couple of failing upgrade example e2e tests
to unblock the 4.4.x branch while investigating and fixing the
issues.
It seems to be some typings-related issues that TypeScript 2.5.3
complains about.

(Note: The same tests are temporarily disabled on master too.)

PR Close #19600
2017-10-10 15:58:45 -07:00
f82efcf942 build(aio): do not fail on first yarn setup (#19600)
PR Close #19600
2017-10-10 15:58:45 -07:00
dc22f4dc69 build(aio): fix overwriting with local Angular packages that depend on other local ones (#19600)
PR Close #19600
2017-10-10 15:58:45 -07:00
173ccf03ab test(aio): fix testing of NgPackagesInstaller (#19600)
PR Close #19600
2017-10-10 15:58:44 -07:00
1d5c3c1c9b build(aio): example-boilerplate is no longer responsible for yarn install (#19600)
The tooling for boilerplate was also running `yarn install` on the examples'
shared folder. But since this is handled by `ng-packages-installer` this
commit refactors the tools so that the boilerplate no longer does this
anymore.

PR Close #19600
2017-10-10 15:58:44 -07:00
e4dac421db build(aio): ignore @angular/mobile-toolkit in dist builds (#19600)
The new v5 version of this toolkit is too incompatible for us to run
AIO against both versions. So for now we will ignore this libary when
running against local Angular distributions and continue to use the
version on npm.

PR Close #19600
2017-10-10 15:58:44 -07:00
c639cdd3b3 build(aio): support ignoring dist packages in "local" mode (#19600)
PR Close #19600
2017-10-10 15:58:44 -07:00
5bcfa7cdfe build(aio): provide cleaner scripts for local build switching (#19600)
PR Close #19600
2017-10-10 15:58:44 -07:00
d8fd892e71 ci(aio): use custom package.json to run with local distributables (#19600)
Closes #19388

PR Close #19600
2017-10-10 15:58:44 -07:00
074a997302 build(aio): add support for using the locally built Angular packages for aio (#19600)
This commit allows building angular.io against the locally built Angular
packages. It adds two new npm scripts:

- `setup-local`: Same as `setup`, but overwrites the Angular packages for both
  angular.io and the examples boilerplate with the locally built ones.
- `build-local`: Same as `build`, but uses `setup-local` instead of `setup`
  under the hood, thus overwriting installed Angular packages with locally built
  ones.

Fixes #18611

PR Close #19600
2017-10-10 15:58:44 -07:00
62616f541a build(aio): ensure webdriver is updated when switching between local and npm deps (#19600)
PR Close #19600
2017-10-10 15:58:44 -07:00
156442f80d docs(aio): fix animations example to work with Angular v5 (#19600)
PR Close #19600
2017-10-10 15:58:44 -07:00
37112f549a build(aio): upgrade ts-node to support newer TypeScript config (#19600)
This was causing `Cannot find type definition file for 'jasmine'. (2688)`
errors when running Protractor.

PR Close #19600
2017-10-10 15:58:44 -07:00
cf4b4d53ba fix(aio): fix SearchService to work with TypeScript 2.4 (#19600)
The call to `race` required a type parameter to disambiguate the return type.

PR Close #19600
2017-10-10 15:58:43 -07:00
d45e3aa433 fix(aio): downgrade yarn to 1.0.2 temporarily (#19600)
PR Close #19600
2017-10-10 15:58:43 -07:00
ccc25ee901 fix: reformat files from previous cherry-picks 2017-10-05 13:54:46 -07:00
3052063c05 refactor(compiler): remove stale metadata classes for animations
These are no longer needed as animations are a purely
runtime concept.
2017-10-05 13:54:46 -07:00
e107322f5c refactor(core): add a checkIndex to the compiler view nodes
Each node now has two index: nodeIndex and checkIndex.

nodeIndex is the index in both the view definition and the view data.
checkIndex is the index in in the update function (update directives and update
renderer).

While nodeIndex and checkIndex have the same value for now, having both of them
will allow changing the structure of view definition after compilation (ie for
runtime translations).
2017-10-05 13:54:46 -07:00
f0774254de refactor(compiler): misc minor refactor / cleanup (#19189)
PR Close #19189
2017-10-05 13:54:46 -07:00
6c66031c4a fix(core): make dynamic & inline code checking behave the same (#19189)
PR Close #19189
2017-10-05 13:54:46 -07:00
8f668807cf Revert "fix(platform-browser): support customEqualityTesters when overriding Jasmine toEqual"
This reverts commit cc8ae32503.

Reason: broke a few existing tests.
2017-10-05 13:25:05 -07:00
f98eb35179 docs(aio): document Custom Async validators
docs(aio): Custom Async validators edits

incorporating suggestions from kapunahelewong
2017-10-04 15:08:00 -07:00
cc8ae32503 fix(platform-browser): support customEqualityTesters when overriding Jasmine toEqual 2017-10-04 15:01:11 -07:00
95f3b1dbe6 fix(compiler): disallow references for select and index evaluation
Also fixes an issue where enum values of 0 or '``' where not treated
correctly.

Fixes: #18170
2017-10-04 14:54:53 -07:00
6b96b069bf docs(aio): fix typo in preview server config file comment 2017-10-04 12:43:37 -07:00
ecfe85b06c docs(aio): document the special treatment of undefined 2017-10-04 12:43:28 -07:00
5439d4cd49 docs(aio): fix broken link in toh-pt5 2017-10-04 12:43:10 -07:00
df91fd032d build: update npm dependencies 2017-10-04 10:22:48 -07:00
2d5ef15e08 build: switch from npm to yarn 2017-10-04 10:22:48 -07:00
544a7ad0a5 docs(aio): Fix incorrect wording 2017-10-03 08:16:16 -07:00
ec2c1bec4a build(aio): fix various API rendering issues
Upgrading to dgeni-packages 0.21.4 gives us
access to more properties on the API docs, which
allows us to fix the following issues:

Closes #19450
Closes #19452
Closes #19456
2017-10-03 08:15:25 -07:00
e064d2607d docs(aio): add ngVikings to the events page 2017-10-03 08:14:30 -07:00
261 changed files with 10242 additions and 16206 deletions

View File

@ -27,10 +27,9 @@ jobs:
- checkout:
<<: *post_checkout
- restore_cache:
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
- run: npm install
- run: npm run postinstall
- run: yarn install --freeze-lockfile --non-interactive
- run: ./node_modules/.bin/gulp lint
build:
@ -39,12 +38,12 @@ jobs:
- checkout:
<<: *post_checkout
- restore_cache:
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
- run: bazel run @build_bazel_rules_typescript_node//:bin/npm install
- run: bazel run @yarn//:yarn
- run: bazel build ...
- save_cache:
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
paths:
- "node_modules"

1
.mailmap Normal file
View File

@ -0,0 +1 @@
Michał Gołębiowski-Owczarek <m.goleb@gmail.com>

View File

@ -104,7 +104,7 @@ groups:
animations:
conditions:
files:
- "packages/animation/*"
- "packages/animations/*"
- "packages/platform-browser/animations/*"
users:
- matsko #primary

View File

@ -1,12 +1,10 @@
language: node_js
sudo: false
# force trusty as Google Chrome addon is not supported on Precise
dist: trusty
node_js:
- '6.9.5'
addons:
chrome: stable
# firefox: "38.0"
apt:
sources:
@ -50,7 +48,8 @@ env:
- CI_MODE=e2e_2
- CI_MODE=js
- CI_MODE=saucelabs_required
- CI_MODE=browserstack_required
# deactivated, see #19768
# - CI_MODE=browserstack_required
- CI_MODE=saucelabs_optional
- CI_MODE=browserstack_optional
- CI_MODE=aio_tools_test

View File

@ -1,3 +1,45 @@
<a name="4.4.7"></a>
## [4.4.7](https://github.com/angular/angular/compare/4.4.6...4.4.7) (2018-04-16)
### Bug Fixes
* **core:** use appropriate inert document strategy for Firefox & Safari ([#22077](https://github.com/angular/angular/issues/22077)) ([2c5cf19](https://github.com/angular/angular/commit/2c5cf19))
<a name="4.4.6"></a>
## [4.4.6](https://github.com/angular/angular/compare/4.4.5...4.4.6) (2017-10-18)
### Bug Fixes
* **animations:** properly support boolean-based transitions and state changes ([#19672](https://github.com/angular/angular/issues/19672)) ([f983a6c](https://github.com/angular/angular/commit/f983a6c)), closes [#9396](https://github.com/angular/angular/issues/9396) [#12337](https://github.com/angular/angular/issues/12337)
* **common:** attempt to JSON.parse errors for JSON responses ([#19773](https://github.com/angular/angular/issues/19773)) ([269f5ac](https://github.com/angular/angular/commit/269f5ac))
* **router:** RouterLinkActive should update its state right after checking the children ([53a807a](https://github.com/angular/angular/commit/53a807a)), closes [#18983](https://github.com/angular/angular/issues/18983)
### Performance Improvements
* **animations:** reduce size of bundle by removing AST classes ([#19673](https://github.com/angular/angular/issues/19673)) ([76d2496](https://github.com/angular/angular/commit/76d2496))
<a name="4.4.5"></a>
## [4.4.5](https://github.com/angular/angular/compare/4.4.4...4.4.5) (2017-10-12)
### Bug Fixes
* **compiler:** `TestBed.overrideProvider` should keep imported `NgModule`s eager ([#19624](https://github.com/angular/angular/issues/19624)) ([734378c](https://github.com/angular/angular/commit/734378c))
* **compiler:** correctly instantiate eager providers that are used via `Injector.get` ([#19558](https://github.com/angular/angular/issues/19558)) ([e292548](https://github.com/angular/angular/commit/e292548)), closes [#15501](https://github.com/angular/angular/issues/15501)
* **compiler:** disallow references for select and index evaluation ([95f3b1d](https://github.com/angular/angular/commit/95f3b1d))
* **core:** make dynamic & inline code checking behave the same ([#19189](https://github.com/angular/angular/issues/19189)) ([6c66031](https://github.com/angular/angular/commit/6c66031))
* **platform-browser:** support customEqualityTesters when overriding Jasmine toEqual ([cc8ae32](https://github.com/angular/angular/commit/cc8ae32))
* **tsc-wrapped:** don't rewrite imports when annotating for closure ([#19579](https://github.com/angular/angular/issues/19579)) ([c9f8718](https://github.com/angular/angular/commit/c9f8718))
<a name="4.4.4"></a>
## [4.4.4](https://github.com/angular/angular/compare/4.4.3...4.4.4) (2017-09-28)

View File

@ -13,7 +13,11 @@ You should run all these tasks from the `angular/aio` folder.
Here are the most important tasks you might need to use:
* `yarn` - install all the dependencies.
* `yarn setup` - Install all the dependencies, boilerplate, plunkers, zips and runs dgeni on the docs.
* `yarn setup` - install all the dependencies, boilerplate, plunkers, zips and run dgeni on the docs.
* `yarn setup-local` - same as `setup`, but use the locally built Angular packages for aio and docs examples boilerplate.
* `yarn build` - create a production build of the application (after installing dependencies, boilerplate, etc).
* `yarn build-local` - same as `build`, but use `setup-local` instead of `setup`.
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
* `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console.
@ -26,26 +30,26 @@ Here are the most important tasks you might need to use:
* `yarn docs-lint` - check that the doc gen code follows our style rules.
* `yarn docs-test` - run the unit tests for the doc generation code.
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally. Add the option `-- --local` to use your local version of Angular contained in the "dist" folder.
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally. Add the option `--local` to use your local version of Angular contained in the "dist" folder.
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
* `yarn example-e2e` - run all e2e tests for examples
- `yarn example-e2e -- --setup` - force webdriver update & other setup, then run tests
- `yarn example-e2e -- --filter=foo` - limit e2e tests to those containing the word "foo"
- `yarn example-e2e -- --setup --local` - run e2e tests with the local version of Angular contained in the "dist" folder
- `yarn example-e2e --setup` - force webdriver update & other setup, then run tests
- `yarn example-e2e --filter=foo` - limit e2e tests to those containing the word "foo"
- `yarn example-e2e --setup --local` - run e2e tests with the local version of Angular contained in the "dist" folder
* `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
## Using ServiceWorker locally
Since abb36e3cb, running `yarn start -- --prod` will no longer set up the ServiceWorker, which
Since abb36e3cb, running `yarn start --prod` will no longer set up the ServiceWorker, which
would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible
with webpack serving the files from memory).
If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/`
with `yarn http-server -- dist -p 4200`.
with `yarn http-server dist -p 4200`.
For more details see #16745.
@ -113,10 +117,10 @@ yarn serve-and-sync
```
* Open a browser at https://localhost:4200/ and navigate to the document on which you want to work.
You can automatically open the browser by using `yarn start -- -o` in the first terminal.
You can automatically open the browser by using `yarn start -o` in the first terminal.
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
be regenerated, the app will rebuild and the page will reload.
* If you get a build error complaining about examples or any other odd behavior, be sure to consult
the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
the [Authors Style Guide](https://angular.io/guide/docs-style-guide).

View File

@ -156,7 +156,7 @@ RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
# Set up the Node.js scripts
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
WORKDIR $AIO_SCRIPTS_JS_DIR/
RUN yarn install --production
RUN yarn install --production --freeze-lockfile
# Set up health check

View File

@ -6,7 +6,7 @@ server=8.8.4.4
# Listen for DHCP and DNS requests only on this address.
listen-address=127.0.0.1
# Force an IP addres for these domains.
# Force an IP address for these domains.
address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1
address=/{{$AIO_UPLOAD_HOSTNAME}}/127.0.0.1
address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1

View File

@ -9,7 +9,7 @@ VM host to update the preview server based on changes in the source code.
The script will pull the latest changes from the origin's master branch and examine if there have
been any changes in files inside the preview server source code directory (see below). If there are,
it will create a new image and verify that is works as expected. Finally, it will stop and remove
the old docker container and image, create and new container based on the new image and start it.
the old docker container and image, create a new container based on the new image and start it.
The script assumes that the preview server source code is in the repository's
`aio/aio-builds-setup/` directory and expects the following inputs:

View File

@ -9,7 +9,7 @@ readonly defaultImageNameAndTag="aio-builds:latest"
# (Necessary, because only `scripts-js/dist/` is copied to the docker image.)
(
cd "$SCRIPTS_JS_DIR"
yarn install
yarn install --freeze-lockfile --non-interactive
yarn build
)

View File

@ -7,6 +7,6 @@ source "`dirname $0`/_env.sh"
# Test `scripts-js/`
(
cd "$SCRIPTS_JS_DIR"
yarn install
yarn install --freeze-lockfile --non-interactive
yarn test
)

View File

@ -335,17 +335,17 @@ describe('Animation Tests', () => {
});
}
function getBoundingClientWidth(el: ElementFinder): promise.Promise<number> {
function getBoundingClientWidth(el: ElementFinder) {
return browser.executeScript(
'return arguments[0].getBoundingClientRect().width',
el.getWebElement()
);
) as PromiseLike<number>;
}
function getOffsetWidth(el: ElementFinder): promise.Promise<number> {
function getOffsetWidth(el: ElementFinder) {
return browser.executeScript(
'return arguments[0].offsetWidth',
el.getWebElement()
);
) as PromiseLike<number>;
}
});

View File

@ -12,8 +12,10 @@ describe('Component Style Tests', function () {
let componentH1 = element(by.css('hero-app > h1'));
let externalH1 = element(by.css('body > h1'));
expect(componentH1.getCssValue('fontWeight')).toEqual('normal');
expect(externalH1.getCssValue('fontWeight')).not.toEqual('normal');
// Note: sometimes webdriver returns the fontWeight as "normal",
// othertimes as "400", both of which are equal in CSS terms.
expect(componentH1.getCssValue('fontWeight')).toMatch(/normal|400/);
expect(externalH1.getCssValue('fontWeight')).not.toMatch(/normal|400/);
});

View File

@ -7,3 +7,4 @@
<p>We're sorry. The page you are looking for cannot be found.</p>
</div>
</div>
<aio-file-not-found-search></aio-file-not-found-search>

View File

@ -2,13 +2,13 @@
This page presents design and layout guidelines for Angular documentation pages. These guidelines should be followed by all guide page authors. Deviations must be approved by the documentation editor.
Most guide pages should have [accompanying sample code](#from-code-samples) with
[special markup](#source-code-markup) for the code snippets on the page.
Code samples should adhere to the
[style guide for Angular applications](guide/styleguide "Application Code Style Guide")
Most guide pages should have [accompanying sample code](#from-code-samples) with
[special markup](#source-code-markup) for the code snippets on the page.
Code samples should adhere to the
[style guide for Angular applications](guide/styleguide "Application Code Style Guide")
because readers expect consistency.
For clarity and precision, every guideline on _this_ page is illustrated with a working example,
For clarity and precision, every guideline on _this_ page is illustrated with a working example,
followed by the page markup for that example ... as shown here.
```html
@ -19,13 +19,13 @@ followed by the page markup for that example ... as shown here.
To make changes to the documentation pages and sample code, clone the [Angular github repository](https://github.com/angular/angular "Angular repo") and go to the `aio/` folder.
The [aio/README.md](https://github.com/angular/angular/blob/master/aio/README.md "AIO ReadMe") explains how to install and use the tools to edit and test your changes.
The [aio/README.md](https://github.com/angular/angular/blob/master/aio/README.md "AIO ReadMe") explains how to install and use the tools to edit and test your changes.
Here are a few essential commands for guide page authors.
1. `yarn setup` &mdash; installs packages; builds docs, plunkers, and zips.
1. `yarn docs-watch -- --watch-only` &mdash; watches for saved content changes and refreshes the browser. The (optional) `--watch-only` flag skips the initial docs rebuild.
1. `yarn docs-watch --watch-only` &mdash; watches for saved content changes and refreshes the browser. The (optional) `--watch-only` flag skips the initial docs rebuild.
1. `yarn start` &mdash; starts the doc viewer application so you can see your local changes in the browser.
@ -33,9 +33,9 @@ Here are a few essential commands for guide page authors.
## Guide pages
All but a few guide pages are [markdown](https://daringfireball.net/projects/markdown/syntax "markdown") files with an `.md` extension.
All but a few guide pages are [markdown](https://daringfireball.net/projects/markdown/syntax "markdown") files with an `.md` extension.
Every guide page file is stored in the `content/guide` directory. Although the [side navigation](#navigation) panel displays as a hierarchy, the directory is flat with no sub-folders.
Every guide page file is stored in the `content/guide` directory. Although the [side navigation](#navigation) panel displays as a hierarchy, the directory is flat with no sub-folders.
The flat folder approach allows us to shuffle the apparent navigation structure without moving page files or redirecting old page URLs.
The doc generation process consumes the markdown files in the `content/guide` directory and produces JSON files in the `src/generated/docs/guide` directory, which is also flat. Those JSON files contain a combination of document metadata and HTML content.
@ -52,7 +52,7 @@ _Tutorial_ pages are exactly like guide pages. The only difference is that they
_API_ pages are generated from Angular source code into the `src/generated/docs/api` directory.
The doc viewer translates URLs that begin `api/` into requests for document JSON files in that directory. This style guide does not discuss creation or maintenance of API pages.
_Marketing_ pages are similar to guide pages. They're located in the `content/marketing` directory. While they can be markdown files, they may be static HTML pages or dynamic HTML pages that render with JSON data.
_Marketing_ pages are similar to guide pages. They're located in the `content/marketing` directory. While they can be markdown files, they may be static HTML pages or dynamic HTML pages that render with JSON data.
Only a few people are authorized to write marketing pages. This style guide does not discuss creation or maintenance of marketing pages.
@ -70,14 +70,14 @@ Standard markdown processors don't allow you to put markdown _within_ HTML tags.
<div class="alert is-critical">
**Always** follow every opening and closing HTML tag with _a blank line_.
**Always** follow every opening and closing HTML tag with _a blank line_.
</div>
```html
<div class="alert is-critical">
**Always** follow every opening and closing HTML tag with _a blank line_.
**Always** follow every opening and closing HTML tag with _a blank line_.
</div>
```
@ -111,7 +111,7 @@ Title text should be in "Title Case", which means that you use capital letters t
## Sections
A typical document is divided into sections.
A typical document is divided into sections.
All section heading text should be in "Sentence case", which means the first word is capitalized and all other words are lower case.
@ -122,7 +122,7 @@ Main section heading
</h2>
There are usually one or more main sections that may be further divided into secondary sections.
Begin a main section heading with the markdown `##` characters. Alternatively, you can write the equivalent `<h2>` HTML tag.
Begin a main section heading with the markdown `##` characters. Alternatively, you can write the equivalent `<h2>` HTML tag.
The main section heading should be followed by a blank line and then the content for that heading.
@ -164,7 +164,7 @@ Try to minimize the heading depth, preferably only two. But more headings, such
Subsections typically present extra detail and references to other pages.
Use subsections for commentary that _enriches_ the reader's understanding of the text that precedes it.
Use subsections for commentary that _enriches_ the reader's understanding of the text that precedes it.
A subsection _must not_ contain anything _essential_ to that understanding. Don't put a critical instruction or a tutorial step in a subsection.
@ -192,7 +192,7 @@ Note that at least one blank line must follow the opening `<div>`. A blank line
Most pages display a table of contents (TOC). The TOC appears in the right panel when the viewport is wide. When narrow, the TOC appears in an expandable/collapsible region near the top of the page.
You should not create your own TOC by hand. The TOC is generated automatically from the page's main and secondary section headers.
You should not create your own TOC by hand. The TOC is generated automatically from the page's main and secondary section headers.
To exclude a heading from the TOC, create the heading as an `<h2>` or `<h3>` element with a class called 'no-toc'. You can't do this with markdown.
@ -214,7 +214,7 @@ A guide without a TOC
The navigation links at the top, left, and bottom of the screen are generated from the JSON configuration file, `content/navigation.json`.
The authority to change the `navigation.json` file is limited to a few core team members.
The authority to change the `navigation.json` file is limited to a few core team members.
But for a new guide page, you should suggest a navigation title and position in the left-side navigation panel called the "side nav".
Look for the `SideNav` node in `navigation.json`. The `SideNav` node is an array of navigation nodes. Each node is either an _item_ node for a single document or a _header_ node with child nodes.
@ -302,7 +302,7 @@ For terminal input and output, put the content between `<code-example>` tags, se
Inline, hand-coded snippets like this one are _not_ testable and, therefore, are intrinsically unreliable.
This example belongs to the small set of pre-approved, inline snippets that includes
user input in a command shell or the _output_ of some process.
user input in a command shell or the _output_ of some process.
**Do not write inline code snippets** unless you have a good reason and the editor's permission to do so.
In all other cases, code snippets should be generated automatically from tested code samples.
@ -341,8 +341,8 @@ The following _code-example_ displays the sample's `app.module.ts`.
Here's the brief markup that produced that lengthy snippet:
```html
<code-example
path="docs-style-guide/src/app/app.module.ts"
<code-example
path="docs-style-guide/src/app/app.module.ts"
title="src/app/app.module.ts">
</code-example>
```
@ -355,7 +355,7 @@ Following convention, you set the `title` attribute to the file's location withi
<div class="l-sub-section">
Unless otherwise noted, all code snippets in this page are derived from sample source code
Unless otherwise noted, all code snippets in this page are derived from sample source code
located in the `content/examples/docs-style-guide` directory.
</div>
@ -377,7 +377,7 @@ The preferred way to un-ignore a file is to update the `content/examples/.gitign
</div>
#### Code-example attributes
#### Code-example attributes
You control the _code-example_ output by setting one or more of its attributes:
@ -401,8 +401,8 @@ You control the _code-example_ output by setting one or more of its attributes:
Often you want to focus on a fragment of code within a sample code file. In this example, you focus on the `AppModule` class and its `NgModule` metadata.
<code-example
path="docs-style-guide/src/app/app.module.ts"
<code-example
path="docs-style-guide/src/app/app.module.ts"
region="class">
</code-example>
@ -411,8 +411,8 @@ Then you reference that _docregion_ in the `region` attribute of the `<code-exam
```html
<code-example
path="docs-style-guide/src/app/app.module.ts"
<code-example
path="docs-style-guide/src/app/app.module.ts"
region="class">
</code-example>
```
@ -430,20 +430,20 @@ There's no need to repeat the header.
Sometimes you want to display an example of bad code or bad design.
You should be careful. Readers don't always read carefully and are likely to copy and paste your example of bad code in their own applications. So don't display bad code often.
You should be careful. Readers don't always read carefully and are likely to copy and paste your example of bad code in their own applications. So don't display bad code often.
When you do, set the `class` to `avoid`. The code snippet will be framed in bright red to grab the reader's attention.
Here's the markup for an "avoid" example in the
Here's the markup for an "avoid" example in the
[_Angular Style Guide_](guide/styleguide#style-05-03 "Style 05-03: components as elements").
```html
<code-example
path="styleguide/src/05-03/app/heroes/shared/hero-button/hero-button.component.avoid.ts"
region="example"
<code-example
path="styleguide/src/05-03/app/heroes/shared/hero-button/hero-button.component.avoid.ts"
region="example"
title="app/heroes/hero-button/hero-button.component.ts">
</code-example>
```
```
<code-example path="styleguide/src/05-03/app/heroes/shared/hero-button/hero-button.component.avoid.ts" region="example" title="app/heroes/hero-button/hero-button.component.ts">
</code-example>
@ -467,22 +467,22 @@ The next example displays multiple code tabs, each with its own title.
It demonstrates control over display of line numbers at both the `<code-tabs>` and `<code-pane>` levels.
<code-tabs linenums="false">
<code-pane
title="app.component.html"
<code-pane
title="app.component.html"
path="docs-style-guide/src/app/app.component.html">
</code-pane>
<code-pane
title="app.component.ts"
<code-pane
title="app.component.ts"
path="docs-style-guide/src/app/app.component.ts"
linenums="true">
</code-pane>
<code-pane
title="app.component.css (heroes)"
path="docs-style-guide/src/app/app.component.css"
<code-pane
title="app.component.css (heroes)"
path="docs-style-guide/src/app/app.component.css"
region="heroes">
</code-pane>
<code-pane
title="package.json (scripts)"
<code-pane
title="package.json (scripts)"
path="docs-style-guide/package.1.json">
</code-pane>
</code-tabs>
@ -494,22 +494,22 @@ The `linenums` attribute in the second pane restores line numbering for _itself
```html
<code-tabs linenums="false">
<code-pane
title="app.component.html"
<code-pane
title="app.component.html"
path="docs-style-guide/src/app/app.component.html">
</code-pane>
<code-pane
title="app.component.ts"
<code-pane
title="app.component.ts"
path="docs-style-guide/src/app/app.component.ts"
linenums="true">
</code-pane>
<code-pane
title="app.component.css (heroes)"
path="docs-style-guide/src/app/app.component.css"
<code-pane
title="app.component.css (heroes)"
path="docs-style-guide/src/app/app.component.css"
region="heroes">
</code-pane>
<code-pane
title="package.json (scripts)"
<code-pane
title="package.json (scripts)"
path="docs-style-guide/package.1.json">
</code-pane>
</code-tabs>
@ -572,8 +572,8 @@ Every line of code _after_ that comment belongs in the region _until_ the code f
The `src/main.ts` is a simple example of a file with a single _#docregion_ at the top of the file.
<code-example
path="docs-style-guide/src/main.ts"
<code-example
path="docs-style-guide/src/main.ts"
title="src/main.ts"></code-example>
</div>
@ -592,8 +592,8 @@ You distinguish among them by giving each fragment its own _#docregion name_ as
Remember to refer to this region by name in the `region` attribute of the `<code-example>` or `<code-pane>` as you did in an example above like this:
```html
<code-example
path="docs-style-guide/src/app/app.module.ts"
<code-example
path="docs-style-guide/src/app/app.module.ts"
region="class"></code-example>
```
@ -603,7 +603,7 @@ The _#docregion_ with no name is the _default region_. Do _not_ set the `region`
You can nest _#docregions_ within _#docregions_
```
// #docregion
// #docregion
... some code ...
// #docregion inner-region
... more code ...
@ -647,12 +647,12 @@ export class AppComponent {
Here's are the two corresponding code snippets displayed side-by-side.
<code-tabs>
<code-pane
title="app.component.ts (class)"
<code-pane
title="app.component.ts (class)"
path="docs-style-guide/src/app/app.component.ts"
region="class">
</code-pane>
<code-pane
<code-pane
title="app.component.ts (class-skeleton)"
path="docs-style-guide/src/app/app.component.ts"
region="class-skeleton">
@ -661,7 +661,7 @@ Here's are the two corresponding code snippets displayed side-by-side.
Some observations:
* The `#docplaster` at the top is another bit of code snippet markup. It tells the processor how to join the fragments into a single snippet.
* The `#docplaster` at the top is another bit of code snippet markup. It tells the processor how to join the fragments into a single snippet.
In this example, we tell the processor to put the fragments together without anything in between - without any "plaster". Most sample files define this _empty plaster_.
@ -673,7 +673,7 @@ Some observations:
Code snippet markup is not supported for JSON files because comments are forbidden in JSON files.
You can display an entire JSON file by referring to it in the `src` attribute.
You can display an entire JSON file by referring to it in the `src` attribute.
But you can't display JSON fragments because you can't add `#docregion` tags to the file.
If the JSON file is too big, you could copy the nodes-of-interest into markdown backticks.
@ -684,12 +684,12 @@ You can't test this partial file and you'll never use it in the application. But
Here's an example that excerpts certain scripts from `package.json` into a partial file named `package.1.json`.
<code-example
<code-example
path="docs-style-guide/package.1.json"
title="package.json (selected scripts)"></code-example>
```html
<code-example
<code-example
path="docs-style-guide/package.1.json"
title="package.json (selected scripts)"></code-example>
```
@ -713,7 +713,7 @@ You'll find many such files among the samples in the Angular documentation.
Remember to exclude these files from plunkers by listing them in the `plnkr.json` as illustrated here.
<code-example
<code-example
path="docs-style-guide/plnkr.json"
title="plnkr.json"></code-example>
@ -722,7 +722,7 @@ Remember to exclude these files from plunkers by listing them in the `plnkr.json
By adding `<live-example>` to the page you generate links that run sample code in the Plunker live coding environment and download that code to the reader's file system.
Live examples (AKA "plunkers") are defined by one or more `plnkr.json` files in the root of a code sample folder. Each sample folder usually has a single unnamed definition file, the default `plnkr.json`.
Live examples (AKA "plunkers") are defined by one or more `plnkr.json` files in the root of a code sample folder. Each sample folder usually has a single unnamed definition file, the default `plnkr.json`.
<div class="l-sub-section">
@ -745,7 +745,7 @@ Clicking the first link opens the code sample in a new browser tab in the "embed
You can change the appearance and behavior of the live example with attributes and classes.
<h3 class="no-toc">Custom label and tooltip</h3>
<h3 class="no-toc">Custom label and tooltip</h3>
Give the live example anchor a custom label and tooltip by setting the `title` attribute.
@ -763,7 +763,7 @@ You can achieve the same effect by putting the label between the `<live-example>
<live-example>Live example with content label</live-example>
```
<h3 class="no-toc">Live example from another guide</h3>
<h3 class="no-toc">Live example from another guide</h3>
To link to a plunker in a folder whose name is not the same as the current guide page, set the `name` attribute to the name of that folder.
@ -773,7 +773,7 @@ To link to a plunker in a folder whose name is not the same as the current guide
<live-example name="router">Live Example from the Router guide</live-example>
```
<h3 class="no-toc">Live Example for named plunker</h3>
<h3 class="no-toc">Live Example for named plunker</h3>
To link to a plunker defined by a named `plnkr.json` file, set the `plnkr` attribute. The following example links to the plunker defined by `second.plnkr.json` in the current guide's directory.
@ -783,7 +783,7 @@ To link to a plunker defined by a named `plnkr.json` file, set the `plnkr` attri
<live-example plnkr="second"></live-example>
```
<h3 class="no-toc">Live Example without download</h3>
<h3 class="no-toc">Live Example without download</h3>
To skip the download link, add the `noDownload` attribute.
@ -793,7 +793,7 @@ To skip the download link, add the `noDownload` attribute.
<live-example noDownload>Just the plunker</live-example>
```
<h3 class="no-toc">Live Example with download-only</h3>
<h3 class="no-toc">Live Example with download-only</h3>
To skip the live plunker link and only link to the download, add the `downloadOnly` attribute.
@ -803,9 +803,9 @@ To skip the live plunker link and only link to the download, add the `downloadOn
<live-example downloadOnly>Download only</live-example>
```
<h3 class="no-toc">Embedded live example</h3>
<h3 class="no-toc">Embedded live example</h3>
By default, a live example link opens a plunker in a separate browser tab.
By default, a live example link opens a plunker in a separate browser tab.
You can embed the plunker within the guide page itself by adding the `embedded` attribute.
For performance reasons, the plunker does not start right away. The reader sees an image instead. Clicking the image starts the sometimes-slow process of launching the embedded plunker within an iframe on the page.
@ -853,10 +853,10 @@ When navigating within the page, you can omit the page URL when specifying the l
It is often a good idea to *lock-in* a good anchor name.
Sometimes the section header text makes for an unattractive anchor. [This one](#ugly-long-section-header-anchors) is pretty bad.
Sometimes the section header text makes for an unattractive anchor. [This one](#ugly-long-section-header-anchors) is pretty bad.
```html
[This one](#ugly-long-section-header-anchors) is pretty bad.
[This one](#ugly-long-section-header-anchors) is pretty bad.
```
The greater danger is that **a future rewording of the header text would break** a link to this section.
@ -922,7 +922,7 @@ A helpful, informational alert.
```
Alerts are meant to grab the user's attention and should be used sparingly.
They are not for casual asides or commentary. Use [subsections](#subsections "subsections") for commentary.
They are not for casual asides or commentary. Use [subsections](#subsections "subsections") for commentary.
## Callouts
@ -1163,9 +1163,9 @@ src="generated/images/guide/docs-style-guide/flying-hero.png"
**Do not use the markdown image syntax, \!\[\.\.\.\]\(\.\.\.\).**
Images should be specified in an `<img>` tag.
Images should be specified in an `<img>` tag.
For accessibility, always set the `alt` attribute with a meaningful description of the image.
For accessibility, always set the `alt` attribute with a meaningful description of the image.
You should nest the `<img>` tag within a `<figure>` tag, which styles the image within a drop-shadow frame. You'll need the editor's permission to skip the `<figure>` tag.
@ -1177,7 +1177,7 @@ Here's a conforming example
```html
<figure>
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying hero">
</figure>
```
@ -1197,13 +1197,13 @@ Here's the "flying hero" at a more reasonable scale.
```html
<figure>
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
width="200">
</figure>
```
Wide images can be a problem. Most browsers try to rescale the image but wide images may overflow the document in certain viewports.
Wide images can be a problem. Most browsers try to rescale the image but wide images may overflow the document in certain viewports.
**Do not set a width greater than 700px**. If you wish to display a larger image, provide a link to the actual image that the user can click on to see the full size image separately as in this example of `source-map-explorer` output from the "Ahead-of-time Compilation" guide:
@ -1222,11 +1222,11 @@ Consider using an image compression web site such as [tinypng](https://tinypng.c
You can float the image to the left or right of text by applying the class="left" or class="right" attributes respectively.
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
width="200"
class="left">
This text wraps around to the right of the floating "flying hero" image.
Headings and code-examples automatically clear a floating image. If you need to force a piece of text to clear a floating image, add `<br class="clear">` where the text should break.
@ -1236,11 +1236,11 @@ Headings and code-examples automatically clear a floating image. If you need to
The markup for the above example is:
```html
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
width="200"
class="left">
This text wraps around to the right of the floating "flying hero" image.
Headings and code-examples automatically clear a floating image. If you need to force a piece of text to clear a floating image, add `<br class="clear">` where the text should break.
@ -1256,8 +1256,8 @@ If you have a floating image inside an alert, callout, or a subsection, it is a
<div class="l-sub-section clear-fix">
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
width="100"
class="right">
@ -1268,12 +1268,12 @@ If you have a floating image inside an alert, callout, or a subsection, it is a
```html
<div class="l-sub-section clear-fix">
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
<img src="generated/images/guide/docs-style-guide/flying-hero.png"
alt="flying Angular hero"
width="100"
class="right">
A subsection with **markdown** formatted text.
</div>
```
```

View File

@ -139,6 +139,10 @@ null if the control value is valid _or_ a validation error object.
The validation error object typically has a property whose name is the validation key, `'forbiddenName'`,
and whose value is an arbitrary dictionary of values that you could insert into an error message, `{name}`.
Custom async validators are similar to sync validators, but they must instead return a Promise or Observable
that later emits null or a validation error object. In the case of an Observable, the Observable must complete,
at which point the form uses the last value emitted for validation.
### Adding to reactive forms
In reactive forms, custom validators are fairly simple to add. All you have to do is pass the function directly

View File

@ -117,7 +117,7 @@ Now the extractor tool and compiler will generate a translation unit with _your
<code-example path="i18n/src/locale/messages.es.xlf.html" region="custom-id" linenums="false">
</code-example>
Here is the `i18n` attribute with a _definition_, followed by the custom `id`:
Here is the `i18n` attribute with a _description_, followed by the custom `id`:
<code-example path='i18n/src/app/app.component.1.html' region='i18n-attribute-id' title='app/app.component.html' linenums="false">
</code-example>

View File

@ -4502,8 +4502,8 @@ helps instantly identify which members of the component serve which purpose.
**Why?** The property associated with `@HostBinding` or the method associated with `@HostListener`
can be modified only in a single place&mdash;in the directive's class.
If you use the `host` metadata property, you must modify both the property declaration inside the controller,
and the metadata associated with the directive.
If you use the `host` metadata property, you must modify both the property/method declaration in the
directive's class and the metadata in the decorator associated with the directive.
</div>

View File

@ -142,7 +142,7 @@ The `hero` in `{{hero.name}}`
refers to the template input variable, not the component's property.
Template expressions cannot refer to anything in
the global namespace. They can't refer to `window` or `document`. They
the global namespace (except `undefined`). They can't refer to `window` or `document`. They
can't call `console.log` or `Math.max`. They are restricted to referencing
members of the expression context.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -13,18 +13,6 @@
</tr>
</thead>
<tbody>
<!-- ngJapan -->
<tr>
<th><a href="http://ngjapan.org/" title="ng-Japan">ng-Japan</a></th>
<td>Tokyo, Japan</td>
<td>June 17, 2017</td>
</tr>
<!-- AngularMix -->
<tr>
<th><a href="https://angularmix.com/" title="AngularMix">AngularMix</a></th>
<td>Universal Studios, Orlando, Florida</td>
<td>October 8, 2017</td>
</tr>
<!-- ReactiveConf -->
<tr>
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
@ -35,7 +23,7 @@
<tr>
<th><a href="http://angularconnect.com" title="AngularConnect">AngularConnect</a></th>
<td>London, United Kingdom</td>
<td>November 07, 2017</td>
<td>November 7-8, 2017</td>
</tr>
<!-- ngAtlanta-->
<tr>
@ -43,6 +31,30 @@
<td>Atlanta, Georgia</td>
<td>January 30, 2018</td>
</tr>
<!-- ngVikings-->
<tr>
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
<td>Helsinki, Finland</td>
<td>March 1-2, 2018</td>
</tr>
<!-- ngconf 2018-->
<tr>
<th><a href="https://www.ng-conf.org/" title="ng-conf">ng-conf</a></th>
<td>Salt Lake City, UT</td>
<td>April 18-20, 2018</td>
</tr>
<!-- WeRDevs-->
<tr>
<th><a href="https://www.wearedevelopers.com/" title="WeAreDevs">WeAreDevelopers</a></th>
<td>Vienna</td>
<td>May 16-18, 2018</td>
</tr>
<!-- AngularConnect-->
<tr>
<th><a href="http://angularconnect.com" title="AngularConnect">AngularConnect</a></th>
<td>London, United Kingdom</td>
<td>November 5-7, 2018</td>
</tr>
</tbody>
</table>
</article>

View File

@ -30,10 +30,10 @@
<!--Announcement Bar-->
<div class="homepage-container">
<div class="announcement-bar">
<img src="generated/images/marketing/home/angular-mix.png" height="40" width="151">
<p>Join us at our newest event, October 2017</p>
<a class="button" href="https://angularmix.com/">Learn More</a>
</div>
<img src="generated/images/marketing/home/angular-connect.png">
<p>Join us in London for AngularConnect<br>November 7-8, 2017</p>
<a class="button" href="https://angularconnect.com/">Learn More</a>
</div>
</div>
<!-- Group 1-->

View File

@ -317,7 +317,7 @@
"desc": "Material Design components for Angular",
"logo": "",
"rev": true,
"title": "Angular Material 2",
"title": "Angular Material",
"url": "https://github.com/angular/material2"
},
"aggrid": {

View File

@ -1207,7 +1207,7 @@ Here's an excerpt:
Create the file <code>styles.css</code>.
Ensure that the file contains the [master styles provided here](https://raw.githubusercontent.com/angular/angular/master/aio/tools/examples/shared/boilerplate/src/styles.css).
Ensure that the file contains the [master styles provided here](https://raw.githubusercontent.com/angular/angular/master/aio/tools/examples/shared/boilerplate/common/src/styles.css).
Also edit <code>index.html</code> to refer to this stylesheet.

View File

@ -21,4 +21,24 @@ describe('Api pages', function() {
const page = new ApiPage('api/animations/AnimationPlayer');
expect(page.getDescendants('class')).toEqual(['NoopAnimationPlayer', 'MockAnimationPlayer'] as any);
});
it('should show type params of type-aliases', () => {
const page = new ApiPage('api/common/http/HttpEvent');
expect(page.getOverview('type-alias').getText()).toContain('type HttpEvent<T>');
});
it('should show readonly properties as getters', () => {
const page = new ApiPage('api/common/http/HttpRequest');
expect(page.getOverview('class').getText()).toContain('get body: T|null');
});
it('should not show parenthesis for getters', () => {
const page = new ApiPage('api/core/NgModuleRef');
expect(page.getOverview('class').getText()).toContain('get injector: Injector');
});
it('should show both type and initializer if set', () => {
const page = new ApiPage('api/common/HashLocationStrategy');
expect(page.getOverview('class').getText()).toContain('path(includeHash: boolean = false): string');
});
});

View File

@ -27,4 +27,8 @@ export class ApiPage extends SitePage {
const selector = `.descendants.${docType} ${onlyDirect ? '>' : ''} li > :not(ul) code`;
return element.all(by.css(selector)).map<string>(item => item.getText());
}
getOverview(docType) {
return element(by.css(`.${docType}-overview`));
}
}

View File

@ -1,4 +1,4 @@
import { browser, element, by, promise } from 'protractor';
import { element, by } from 'protractor';
import { SitePage } from './app.po';
describe('site App', function() {
@ -41,20 +41,20 @@ describe('site App', function() {
describe('scrolling to the top', () => {
it('should scroll to the top when navigating to another page', () => {
page.navigateTo('guide/docs');
page.navigateTo('guide/security');
page.scrollToBottom();
page.getScrollTop().then(scrollTop => expect(scrollTop).toBeGreaterThan(0));
page.navigateTo('guide/api');
page.navigateTo('api');
page.getScrollTop().then(scrollTop => expect(scrollTop).toBe(0));
});
it('should scroll to the top when navigating to the same page', () => {
page.navigateTo('guide/docs');
page.navigateTo('guide/security');
page.scrollToBottom();
page.getScrollTop().then(scrollTop => expect(scrollTop).toBeGreaterThan(0));
page.navigateTo('guide/docs');
page.navigateTo('guide/security');
page.getScrollTop().then(scrollTop => expect(scrollTop).toBe(0));
});
});
@ -66,7 +66,9 @@ describe('site App', function() {
});
});
describe('google analytics', () => {
// TODO(https://github.com/angular/angular/issues/19785): Activate this again
// once it is no more flaky.
xdescribe('google analytics', () => {
beforeEach(done => page.gaReady.then(done));
it('should call ga', done => {
@ -100,4 +102,12 @@ describe('site App', function() {
expect(page.getSearchResults().map(link => link.getText())).toContain('ControlValueAccessor');
});
});
describe('404 page', () => {
it('should search the index for words found in the url', () => {
page.navigateTo('http/router');
expect(page.getSearchResults().map(link => link.getText())).toContain('Http');
expect(page.getSearchResults().map(link => link.getText())).toContain('Router');
});
});
});

View File

@ -30,8 +30,14 @@ module.exports = function (config) {
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
browsers: ['CustomChrome'],
browserNoActivityTimeout: 60000,
singleRun: false
singleRun: false,
customLaunchers: {
CustomChrome: {
base: 'Chrome',
flags: process.env.TRAVIS && ['--no-sandbox']
}
}
});
};

View File

@ -6,33 +6,45 @@
"author": "Angular",
"license": "MIT",
"scripts": {
"aio-use-local": "node tools/ng-packages-installer overwrite . --debug --ignore-packages @angular/service-worker",
"aio-use-npm": "node tools/ng-packages-installer restore .",
"aio-check-local": "node tools/ng-packages-installer check .",
"ng": "yarn check-env && ng",
"start": "yarn check-env && ng serve",
"prebuild": "yarn check-env && yarn setup",
"build": "ng build --target=production --environment=stable -sm --build-optimizer",
"postbuild": "yarn sw-manifest && yarn sw-copy",
"prebuild": "yarn setup",
"build": "yarn ~~build",
"prebuild-local": "yarn setup-local",
"build-local": "yarn ~~build",
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint",
"test": "yarn check-env && ng test",
"pree2e": "yarn check-env && yarn ~~update-webdriver",
"e2e": "ng e2e --no-webdriver-update",
"setup": "yarn && yarn build-ie-polyfills && yarn boilerplate:remove && yarn boilerplate:add && yarn generate-plunkers && yarn generate-zips && yarn docs",
"pretest-pwa-score-local": "yarn build",
"test-pwa-score-local": "concurrently --kill-others --success first \"http-server dist -p 4200 --silent\" \"yarn test-pwa-score -- http://localhost:4200 90\"",
"presetup": "yarn install --freeze-lockfile && yarn ~~check-env && yarn boilerplate:remove",
"setup": "yarn aio-use-npm && yarn example-use-npm",
"postsetup": "yarn boilerplate:add && yarn build-ie-polyfills && yarn generate-plunkers && yarn generate-zips && yarn docs",
"presetup-local": "yarn presetup",
"setup-local": "yarn aio-use-local && yarn example-use-local",
"postsetup-local": "yarn postsetup",
"pretest-pwa-score-localhost": "yarn build",
"test-pwa-score-localhost": "concurrently --kill-others --success first \"http-server dist -p 4200 --silent\" \"yarn test-pwa-score http://localhost:4200 90\"",
"test-pwa-score": "node scripts/test-pwa-score",
"example-e2e": "node ./tools/examples/run-example-e2e",
"example-e2e": "yarn example-check-local && node ./tools/examples/run-example-e2e",
"example-lint": "tslint -c \"content/examples/tslint.json\" \"content/examples/**/*.ts\" -e \"content/examples/styleguide/**/*.avoid.ts\"",
"example-use-local": "node tools/ng-packages-installer overwrite ./tools/examples/shared",
"example-use-npm": "node tools/ng-packages-installer restore ./tools/examples/shared",
"example-check-local": "node tools/ng-packages-installer check ./tools/examples/shared",
"deploy-preview": "scripts/deploy-preview.sh",
"deploy-production": "scripts/deploy-to-firebase.sh",
"check-env": "node scripts/check-environment",
"check-env": "yarn ~~check-env",
"postcheck-env": "yarn aio-check-local",
"payload-size": "scripts/payload.sh",
"predocs": "rimraf src/generated/{docs,*.json}",
"docs": "dgeni ./tools/transforms/angular.io-package",
"docs-watch": "node tools/transforms/authors-package/watchr.js",
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
"docs-test": "node tools/transforms/test.js",
"tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test",
"tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer/index.spec.js",
"serve-and-sync": "concurrently --kill-others \"yarn docs-watch\" \"yarn start\"",
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false",
"boilerplate:add": "node ./tools/examples/example-boilerplate add",
"boilerplate:remove": "node ./tools/examples/example-boilerplate remove",
"boilerplate:test": "node tools/examples/test.js",
@ -41,7 +53,11 @@
"sw-manifest": "ngu-sw-manifest --dist dist --in ngsw-manifest.json --out dist/ngsw-manifest.json",
"sw-copy": "cp node_modules/@angular/service-worker/bundles/worker-basic.min.js dist/",
"postinstall": "uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map",
"build-ie-polyfills": "node node_modules/webpack/bin/webpack.js -p src/ie-polyfills.js src/generated/ie-polyfills.min.js"
"build-ie-polyfills": "node node_modules/webpack/bin/webpack.js -p src/ie-polyfills.js src/generated/ie-polyfills.min.js",
"~~check-env": "node scripts/check-environment",
"~~build": "ng build --target=production --environment=stable -sm --build-optimizer",
"post~~build": "yarn sw-manifest && yarn sw-copy",
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG"
},
"engines": {
"node": ">=6.9.5 <7.0.0",
@ -66,7 +82,7 @@
"core-js": "^2.4.1",
"jasmine": "^2.6.0",
"ng-pwa-tools": "^0.0.10",
"rxjs": "^5.2.0",
"rxjs": "^5.4.3",
"tslib": "^1.7.1",
"web-animations-js": "^2.2.5",
"zone.js": "^0.8.16"
@ -78,11 +94,12 @@
"@types/node": "~6.0.60",
"archiver": "^1.3.0",
"canonical-path": "^0.0.2",
"chalk": "^2.1.0",
"codelyzer": "~2.0.0",
"concurrently": "^3.4.0",
"cross-spawn": "^5.1.0",
"dgeni": "^0.4.7",
"dgeni-packages": "^0.21.3",
"dgeni-packages": "0.22.0",
"entities": "^1.1.1",
"eslint": "^3.19.0",
"eslint-plugin-jasmine": "^2.2.0",
@ -117,7 +134,7 @@
"semver": "^5.3.0",
"shelljs": "^0.7.7",
"tree-kill": "^1.1.0",
"ts-node": "~2.0.0",
"ts-node": "^3.3.0",
"tslint": "~4.5.0",
"typescript": "2.3.2",
"uglify-js": "^3.0.15",

View File

@ -12,7 +12,8 @@ exports.config = {
browserName: 'chrome',
// For Travis
chromeOptions: {
binary: process.env.CHROME_BIN
binary: process.env.CHROME_BIN,
args: ['--no-sandbox']
}
},
directConnect: true,

View File

@ -3,7 +3,7 @@
set -u -e -o pipefail
declare -A limitUncompressed
limitUncompressed=(["inline"]=1600 ["main"]=525000 ["polyfills"]=38000)
limitUncompressed=(["inline"]=1600 ["main"]=525500 ["polyfills"]=38000)
declare -A limitGzip7
limitGzip7=(["inline"]=1000 ["main"]=127000 ["polyfills"]=12500)
declare -A limitGzip9

View File

@ -51,6 +51,6 @@ readonly relevantChangedFilesCount=$(git diff --name-only $TRAVIS_COMMIT_RANGE |
# Run PWA-score tests (unless the deployment is not public yet;
# i.e. it could not be automatically verified).
if [[ $httpCode -ne 202 ]] && [[ "$isHidden" != "true" ]]; then
yarn test-pwa-score -- "$DEPLOYED_URL" "$MIN_PWA_SCORE"
yarn test-pwa-score "$DEPLOYED_URL" "$MIN_PWA_SCORE"
fi
)

View File

@ -67,7 +67,7 @@ case $deployEnv in
readonly firebaseToken=$FIREBASE_TOKEN
;;
archive)
readonly projectId=angular-io-${majorVersion}
readonly projectId=v${majorVersion}-angular-io
readonly deployedUrl=https://v${majorVersion}.angular.io/
readonly firebaseToken=$FIREBASE_TOKEN
;;
@ -87,7 +87,7 @@ fi
cd "`dirname $0`/.."
# Build the app
yarn build -- --env=$deployEnv
yarn build --env=$deployEnv
# Include any mode-specific files
cp -rf src/extra-files/$deployEnv/. dist/
@ -100,5 +100,5 @@ fi
firebase deploy --message "Commit: $TRAVIS_COMMIT" --non-interactive --token "$firebaseToken"
# Run PWA-score tests
yarn test-pwa-score -- "$deployedUrl" "$MIN_PWA_SCORE"
yarn test-pwa-score "$deployedUrl" "$MIN_PWA_SCORE"
)

View File

@ -94,7 +94,7 @@ Deployment URL : https://angular.io/"
)
expected="Git branch : 2.4.x
Build/deploy mode : archive
Firebase project : angular-io-2
Firebase project : v2-angular-io
Deployment URL : https://v2.angular.io/"
check "$actual" "$expected"
)

View File

@ -17,8 +17,16 @@ const printer = require('lighthouse/lighthouse-cli/printer');
const config = require('lighthouse/lighthouse-core/config/default.js');
// Constants
const CHROME_LAUNCH_OPTS = {};
const VIEWER_URL = 'https://googlechrome.github.io/lighthouse/viewer/';
// Specify the path and flags for Chrome on Travis
if (process.env.TRAVIS) {
process.env.LIGHTHOUSE_CHROMIUM_PATH = process.env.CHROME_BIN;
CHROME_LAUNCH_OPTS.chromeFlags = ['--no-sandbox'];
}
// Run
_main(process.argv.slice(2));
@ -66,7 +74,7 @@ function ignoreHttpsAudits(config) {
}
function launchChromeAndRunLighthouse(url, flags, config) {
return chromeLauncher.launch().then(chrome => {
return chromeLauncher.launch(CHROME_LAUNCH_OPTS).then(chrome => {
flags.port = chrome.port;
return lighthouse(url, flags, config).
then(results => chrome.kill().then(() => results)).

View File

@ -13,7 +13,7 @@
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
</md-toolbar>
<aio-search-results #searchResults *ngIf="showSearchResults" (resultSelected)="hideSearchResults()"></aio-search-results>
<aio-search-results #searchResultsView *ngIf="showSearchResults" [searchResults]="searchResults | async" (resultSelected)="hideSearchResults()"></aio-search-results>
<md-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main">

View File

@ -1,12 +1,11 @@
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
import { async, inject, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { inject, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { Title } from '@angular/platform-browser';
import { APP_BASE_HREF } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { MdProgressBar, MdSidenav } from '@angular/material';
import { By } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { of } from 'rxjs/observable/of';
import { AppComponent } from './app.component';
@ -22,9 +21,9 @@ import { MockSearchService } from 'testing/search.service';
import { NavigationNode } from 'app/navigation/navigation.service';
import { ScrollService } from 'app/shared/scroll.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
import { SearchService } from 'app/search/search.service';
import { SelectComponent, Option } from 'app/shared/select/select.component';
import { SelectComponent } from 'app/shared/select/select.component';
import { TocComponent } from 'app/embedded/toc/toc.component';
import { TocItem, TocService } from 'app/shared/toc.service';
@ -1054,11 +1053,6 @@ class TestGaService {
locationChanged = jasmine.createSpy('locationChanged');
}
class TestSearchService {
initWorker = jasmine.createSpy('initWorker');
loadIndex = jasmine.createSpy('loadIndex');
}
class TestHttpClient {
static versionInfo = {

View File

@ -2,18 +2,18 @@ import { Component, ElementRef, HostBinding, HostListener, OnInit,
QueryList, ViewChild, ViewChildren } from '@angular/core';
import { MdSidenav } from '@angular/material';
import { CurrentNodes, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
import { CurrentNodes, NavigationService, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
import { DocumentService, DocumentContents } from 'app/documents/document.service';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { Deployment } from 'app/shared/deployment.service';
import { LocationService } from 'app/shared/location.service';
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
import { ScrollService } from 'app/shared/scroll.service';
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchResults } from 'app/search/interfaces';
import { SearchService } from 'app/search/search.service';
import { TocService } from 'app/shared/toc.service';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { combineLatest } from 'rxjs/observable/combineLatest';
@ -89,10 +89,9 @@ export class AppComponent implements OnInit {
// Search related properties
showSearchResults = false;
@ViewChildren('searchBox, searchResults', { read: ElementRef })
searchResults: Observable<SearchResults>;
@ViewChildren('searchBox, searchResultsView', { read: ElementRef })
searchElements: QueryList<ElementRef>;
@ViewChild(SearchResultsComponent)
searchResults: SearchResultsComponent;
@ViewChild(SearchBoxComponent)
searchBox: SearchBoxComponent;
@ -332,7 +331,7 @@ export class AppComponent implements OnInit {
}
doSearch(query) {
this.searchService.search(query);
this.searchResults = this.searchService.search(query);
this.showSearchResults = !!query;
}

View File

@ -43,7 +43,6 @@ import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
import { ScrollService } from 'app/shared/scroll.service';
import { ScrollSpyService } from 'app/shared/scroll-spy.service';
import { SearchResultsComponent } from './search/search-results/search-results.component';
import { SearchBoxComponent } from './search/search-box/search-box.component';
import { TocService } from 'app/shared/toc.service';
@ -95,7 +94,6 @@ export const svgIconProviders = [
ModeBannerComponent,
NavMenuComponent,
NavItemComponent,
SearchResultsComponent,
SearchBoxComponent,
TopMenuComponent,
],

View File

@ -1,10 +1,7 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ApiListComponent } from './api-list.component';

View File

@ -13,7 +13,7 @@ import { ReplaySubject } from 'rxjs/ReplaySubject';
import { combineLatest } from 'rxjs/observable/combineLatest';
import { LocationService } from 'app/shared/location.service';
import { ApiItem, ApiSection, ApiService } from './api.service';
import { ApiSection, ApiService } from './api.service';
import { Option } from 'app/shared/select/select.component';

View File

@ -1,6 +1,6 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { TestBed, inject } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';

View File

@ -1,11 +1,9 @@
import { Component, ElementRef, ViewChild, OnChanges, OnDestroy, Input } from '@angular/core';
import { Component, ElementRef, ViewChild, OnChanges, Input } from '@angular/core';
import { Logger } from 'app/shared/logger.service';
import { PrettyPrinter } from './pretty-printer.service';
import { CopierService } from 'app/shared/copier.service';
import { MdSnackBar } from '@angular/material';
const originalLabel = 'Copy Code';
const copiedLabel = 'Copied!';
const defaultLineNumsCount = 10; // by default, show linenums over this number
/**

View File

@ -3,7 +3,7 @@ import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ContributorService } from './contributor.service';
import { Contributor, ContributorGroup } from './contributors.model';
import { ContributorGroup } from './contributors.model';
describe('ContributorService', () => {

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';
import { CurrentLocationComponent } from './current-location.component';

View File

@ -20,6 +20,7 @@ import { CodeTabsComponent } from './code/code-tabs.component';
import { ContributorListComponent } from './contributor/contributor-list.component';
import { ContributorComponent } from './contributor/contributor.component';
import { CurrentLocationComponent } from './current-location.component';
import { FileNotFoundSearchComponent } from './search/file-not-found-search.component';
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
import { ResourceListComponent } from './resource/resource-list.component';
import { ResourceService } from './resource/resource.service';
@ -30,7 +31,8 @@ import { TocComponent } from './toc/toc.component';
*/
export const embeddedComponents: any[] = [
ApiListComponent, CodeExampleComponent, CodeTabsComponent, ContributorListComponent,
CurrentLocationComponent, LiveExampleComponent, ResourceListComponent, TocComponent
CurrentLocationComponent, FileNotFoundSearchComponent, LiveExampleComponent, ResourceListComponent,
TocComponent
];
/** Injectable class w/ property returning components that can be embedded in docs */

View File

@ -1,6 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component, DebugElement, ElementRef } from '@angular/core';
import { Component, DebugElement } from '@angular/core';
import { Location } from '@angular/common';
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example.component';
@ -71,7 +71,6 @@ describe('LiveExampleComponent', () => {
describe('when not embedded', () => {
function getLiveExampleAnchor() { return getAnchors()[0]; }
function getDownloadAnchor() { return getAnchors()[1]; }
it('should create LiveExampleComponent', () => {
testComponent(() => {

View File

@ -1,7 +1,6 @@
import { ReflectiveInjector } from '@angular/core';
import { PlatformLocation } from '@angular/common';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { ResourceListComponent } from './resource-list.component';

View File

@ -3,7 +3,7 @@ import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ResourceService } from './resource.service';
import { Category, SubCategory, Resource } from './resource.model';
import { Category } from './resource.model';
describe('ResourceService', () => {

View File

@ -0,0 +1,49 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Subject } from 'rxjs/Subject';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';
import { SearchResults } from 'app/search/interfaces';
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
import { SearchService } from 'app/search/search.service';
import { FileNotFoundSearchComponent } from './file-not-found-search.component';
describe('FileNotFoundSearchComponent', () => {
let element: HTMLElement;
let fixture: ComponentFixture<FileNotFoundSearchComponent>;
let searchService: SearchService;
let searchResultSubject: Subject<SearchResults>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ FileNotFoundSearchComponent, SearchResultsComponent ],
providers: [
{ provide: LocationService, useValue: new MockLocationService('base/initial-url?some-query') },
SearchService
]
});
fixture = TestBed.createComponent(FileNotFoundSearchComponent);
searchService = TestBed.get(SearchService);
searchResultSubject = new Subject<SearchResults>();
spyOn(searchService, 'search').and.callFake(() => searchResultSubject.asObservable());
fixture.detectChanges();
element = fixture.nativeElement;
});
it('should run a search with a query built from the current url', () => {
expect(searchService.search).toHaveBeenCalledWith('base initial url');
});
it('should pass through any results to the `aio-search-results` component', () => {
const searchResultsComponent = fixture.debugElement.query(By.directive(SearchResultsComponent)).componentInstance;
expect(searchResultsComponent.searchResults).toBe(null);
const results = { query: 'base initial url', results: []};
searchResultSubject.next(results);
fixture.detectChanges();
expect(searchResultsComponent.searchResults).toEqual(results);
});
});

View File

@ -0,0 +1,23 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { LocationService } from 'app/shared/location.service';
import { SearchResults } from 'app/search/interfaces';
import { SearchService } from 'app/search/search.service';
@Component({
selector: 'aio-file-not-found-search',
template:
`<p>Let's see if any of these search results help...</p>
<aio-search-results class="embedded" [searchResults]="searchResults | async"></aio-search-results>`
})
export class FileNotFoundSearchComponent implements OnInit {
searchResults: Observable<SearchResults>;
constructor(private location: LocationService, private search: SearchService) {}
ngOnInit() {
this.searchResults = this.location.currentPath.switchMap(path => {
const query = path.split(/\W+/).join(' ');
return this.search.search(query);
});
}
}

View File

@ -1,6 +1,6 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By, DOCUMENT } from '@angular/platform-browser';
import { By } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { asap } from 'rxjs/scheduler/asap';

View File

@ -1,11 +1,8 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
Component, ComponentFactoryResolver, DebugElement,
ElementRef, Injector, NgModule, OnInit, ViewChild } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement, ElementRef, NgModule, OnInit, ViewChild } from '@angular/core';
import { DocViewerComponent } from './doc-viewer.component';
import { DocumentContents } from 'app/documents/document.service';
import { EmbeddedModule, embeddedComponents, EmbeddedComponents } from 'app/embedded/embedded.module';
import { EmbeddedModule, EmbeddedComponents } from 'app/embedded/embedded.module';
import { Title } from '@angular/platform-browser';
import { TocService } from 'app/shared/toc.service';

View File

@ -1,7 +1,7 @@
import {
Component, ComponentFactory, ComponentFactoryResolver, ComponentRef,
DoCheck, ElementRef, EventEmitter, Injector, Input, OnDestroy,
Output, ViewEncapsulation
Output
} from '@angular/core';
import { EmbeddedComponents } from 'app/embedded/embedded.module';

View File

@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SimpleChange, SimpleChanges, NO_ERRORS_SCHEMA } from '@angular/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NavItemComponent } from './nav-item.component';
import { NavigationNode } from 'app/navigation/navigation.model';

View File

@ -1,9 +1,9 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { TopMenuComponent } from './top-menu.component';
import { NavigationService, NavigationViews, NavigationNode } from 'app/navigation/navigation.service';
import { NavigationService, NavigationViews } from 'app/navigation/navigation.service';
describe('TopMenuComponent', () => {
let component: TopMenuComponent;

View File

@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { AsyncSubject } from 'rxjs/AsyncSubject';
import { combineLatest } from 'rxjs/observable/combineLatest';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/publishLast';

View File

@ -0,0 +1,19 @@
export interface SearchResults {
query: string;
results: SearchResult[];
}
export interface SearchResult {
path: string;
title: string;
type: string;
titleWords: string;
keywords: string;
}
export interface SearchArea {
name: string;
pages: SearchResult[];
priorityPages: SearchResult[];
}

View File

@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SearchBoxComponent } from './search-box.component';
import { MockSearchService } from 'testing/search.service';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';

View File

@ -1,6 +1,7 @@
import { ReflectiveInjector, NgZone } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { SearchService } from './search.service';
import { WebWorkerClient } from 'app/shared/web-worker';
@ -36,27 +37,29 @@ describe('SearchService', () => {
describe('search', () => {
beforeEach(() => {
// We must initialize the service before calling search
service.initWorker('some/url', 100);
// We must initialize the service before calling connectSearches
service.initWorker('some/url', 1000);
// Simulate the index being ready so that searches get sent to the worker
(service as any).ready = Observable.of(true);
});
it('should trigger a `loadIndex` synchronously', () => {
service.search('some query');
it('should trigger a `loadIndex` synchronously (not waiting for the delay)', () => {
expect(mockWorker.sendMessage).not.toHaveBeenCalled();
service.search('some query').subscribe();
expect(mockWorker.sendMessage).toHaveBeenCalledWith('load-index');
});
it('should send a "query-index" message to the worker', () => {
service.search('some query');
service.search('some query').subscribe();
expect(mockWorker.sendMessage).toHaveBeenCalledWith('query-index', 'some query');
});
it('should push the response to the `searchResults` observable', () => {
it('should push the response to the returned observable', () => {
const mockSearchResults = { results: ['a', 'b'] };
let actualSearchResults;
(mockWorker.sendMessage as jasmine.Spy).and.returnValue(Observable.of(mockSearchResults));
let searchResults: any;
service.searchResults.subscribe(results => searchResults = results);
service.search('some query');
expect(searchResults).toEqual(mockSearchResults);
service.search('some query').subscribe(results => actualSearchResults = results);
expect(actualSearchResults).toEqual(mockSearchResults);
});
});
});

View File

@ -4,34 +4,21 @@ Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/
import { NgZone, Injectable, Type } from '@angular/core';
import { NgZone, Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/observable/race';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/publish';
import 'rxjs/add/operator/publishReplay';
import { WebWorkerClient } from 'app/shared/web-worker';
export interface SearchResults {
query: string;
results: SearchResult[];
}
export interface SearchResult {
path: string;
title: string;
type: string;
titleWords: string;
keywords: string;
}
import { SearchResults } from 'app/search/interfaces';
@Injectable()
export class SearchService {
private ready: Observable<boolean>;
private searchesSubject = new ReplaySubject<string>(1);
searchResults: Observable<SearchResults>;
private worker: WebWorkerClient;
constructor(private zone: NgZone) {}
/**
@ -43,36 +30,32 @@ export class SearchService {
* @param initDelay the number of milliseconds to wait before we load the WebWorker and generate the search index
*/
initWorker(workerUrl: string, initDelay: number) {
const searchResults = Observable
const ready = this.ready = Observable
// Wait for the initDelay or the first search
.race(
.race<any>(
Observable.timer(initDelay),
this.searchesSubject.first()
(this.searchesSubject as Observable<string>).first()
)
.concatMap(() => {
// Create the worker and load the index
const worker = WebWorkerClient.create(workerUrl, this.zone);
return worker.sendMessage('load-index').concatMap(() =>
// Once the index has loaded, switch to listening to the searches coming in
this.searchesSubject.switchMap((query) =>
// Each search gets switched to a web worker message, whose results are returned via an observable
worker.sendMessage<SearchResults>('query-index', query)
)
);
}).publish();
this.worker = WebWorkerClient.create(workerUrl, this.zone);
return this.worker.sendMessage<boolean>('load-index');
}).publishReplay(1);
// Connect to the observable to kick off the timer
searchResults.connect();
// Expose the connected observable to the rest of the world
this.searchResults = searchResults;
// Connect to the observable to kick off the timer
ready.connect();
return ready;
}
/**
* Send a search query to the index.
* The results will appear on the `searchResults` observable.
* Search the index using the given query and emit results on the observable that is returned.
* @param query The query to run against the index.
* @returns an observable collection of search results
*/
search(query: string) {
search(query: string): Observable<SearchResults> {
// Trigger the searches subject to override the init delay timer
this.searchesSubject.next(query);
// Once the index has loaded, switch to listening to the searches coming in.
return this.ready.concatMap(() => this.worker.sendMessage<SearchResults>('query-index', query));
}
}

View File

@ -1,5 +1,5 @@
import { MdIconRegistry } from '@angular/material';
import { CustomMdIconRegistry, SVG_ICONS, SvgIconInfo } from './custom-md-icon-registry';
import { CustomMdIconRegistry, SvgIconInfo } from './custom-md-icon-registry';
describe('CustomMdIconRegistry', () => {
it('should get the SVG element for a preloaded icon from the cache', () => {

View File

@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import { Location, PlatformLocation } from '@angular/common';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/operator/do';

View File

@ -3,7 +3,7 @@ import { fakeAsync, tick } from '@angular/core/testing';
import { DOCUMENT } from '@angular/platform-browser';
import { ScrollService } from 'app/shared/scroll.service';
import { ScrollItem, ScrollSpiedElement, ScrollSpiedElementGroup, ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-spy.service';
import { ScrollItem, ScrollSpiedElement, ScrollSpiedElementGroup, ScrollSpyService } from 'app/shared/scroll-spy.service';
describe('ScrollSpiedElement', () => {
@ -197,6 +197,7 @@ describe('ScrollSpyService', () => {
.and.callFake(() => actions.push('calibrate'));
expect(onResizeSpy).not.toHaveBeenCalled();
expect(calibrateSpy).not.toHaveBeenCalled();
scrollSpyService.spyOn([]);
expect(actions).toEqual(['onResize', 'calibrate']);

View File

@ -1,16 +1,12 @@
import { DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { SearchService, SearchResult, SearchResults } from '../search.service';
import { SearchResultsComponent, SearchArea } from './search-results.component';
import { MockSearchService } from 'testing/search.service';
import { SearchResult } from 'app/search/interfaces';
import { SearchResultsComponent } from './search-results.component';
describe('SearchResultsComponent', () => {
let component: SearchResultsComponent;
let fixture: ComponentFixture<SearchResultsComponent>;
let searchResults: Subject<SearchResults>;
/** Get all text from component element */
function getText() { return fixture.debugElement.nativeElement.textContent; }
@ -38,27 +34,26 @@ describe('SearchResultsComponent', () => {
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
}
function setSearchResults(query: string, results: SearchResult[]) {
component.searchResults = {query, results};
component.ngOnChanges({});
fixture.detectChanges();
}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ SearchResultsComponent ],
providers: [
{ provide: SearchService, useFactory: () => new MockSearchService() }
]
declarations: [ SearchResultsComponent ]
});
});
beforeEach(() => {
fixture = TestBed.createComponent(SearchResultsComponent);
component = fixture.componentInstance;
searchResults = TestBed.get(SearchService).searchResults;
fixture.detectChanges();
});
it('should map the search results into groups based on their containing folder', () => {
const results = getTestResults(3);
searchResults.next({ query: '', results: results});
setSearchResults('', getTestResults(3));
expect(component.searchAreas).toEqual([
{ name: 'api', priorityPages: [
{ path: 'api/d', title: 'API D', type: '', keywords: '', titleWords: '' }
@ -71,10 +66,10 @@ describe('SearchResultsComponent', () => {
});
it('should special case results that are top level folders', () => {
searchResults.next({ query: '', results: [
setSearchResults('', [
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' },
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '' },
]});
]);
expect(component.searchAreas).toEqual([
{ name: 'tutorial', priorityPages: [
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' },
@ -85,21 +80,21 @@ describe('SearchResultsComponent', () => {
it('should put first 5 results for each area into priorityPages', () => {
const results = getTestResults();
searchResults.next({ query: '', results: results });
setSearchResults('', results);
expect(component.searchAreas[0].priorityPages).toEqual(results.filter(p => p.path.startsWith('api')).slice(0, 5));
expect(component.searchAreas[1].priorityPages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(0, 5));
});
it('should put the nonPriorityPages into the pages array, sorted by title', () => {
const results = getTestResults();
searchResults.next({ query: '', results: results });
setSearchResults('', results);
expect(component.searchAreas[0].pages).toEqual([]);
expect(component.searchAreas[1].pages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(5).sort(compareTitle));
});
it('should put a total count in the header of each area of search results', () => {
const results = getTestResults();
searchResults.next({ query: '', results: results });
setSearchResults('', results);
fixture.detectChanges();
const headers = fixture.debugElement.queryAll(By.css('h3'));
expect(headers.length).toEqual(2);
@ -112,7 +107,7 @@ describe('SearchResultsComponent', () => {
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
];
searchResults.next({ query: '', results: results });
setSearchResults('', results);
expect(component.searchAreas).toEqual([
{ name: 'other', priorityPages: [
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
@ -125,7 +120,7 @@ describe('SearchResultsComponent', () => {
{ path: 'news', title: undefined, type: 'marketing', keywords: '', titleWords: '' }
];
searchResults.next({ query: 'something', results: results });
setSearchResults('something', results);
expect(component.searchAreas).toEqual([]);
});
@ -144,7 +139,7 @@ describe('SearchResultsComponent', () => {
selected = null;
searchResult = { path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' };
searchResults.next({ query: 'something', results: [searchResult] });
setSearchResults('something', [searchResult]);
fixture.detectChanges();
anchor = fixture.debugElement.query(By.css('a'));
@ -179,10 +174,8 @@ describe('SearchResultsComponent', () => {
describe('when no query results', () => {
it('should display "not found" message', () => {
searchResults.next({ query: 'something', results: [] });
fixture.detectChanges();
setSearchResults('something', []);
expect(getText()).toContain('No results');
});
});
});

View File

@ -1,28 +1,20 @@
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { SearchResult, SearchResults, SearchService } from '../search.service';
export interface SearchArea {
name: string;
pages: SearchResult[];
priorityPages: SearchResult[];
}
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { SearchResult, SearchResults, SearchArea } from 'app/search/interfaces';
/**
* A component to display the search results
* A component to display search results in groups
*/
@Component({
selector: 'aio-search-results',
templateUrl: './search-results.component.html',
})
export class SearchResultsComponent implements OnInit, OnDestroy {
export class SearchResultsComponent implements OnChanges {
private resultsSubscription: Subscription;
readonly defaultArea = 'other';
notFoundMessage = 'Searching ...';
readonly topLevelFolders = ['guide', 'tutorial'];
/**
* The results to display
*/
@Input()
searchResults: SearchResults;
/**
* Emitted when the user selects a search result
@ -30,20 +22,13 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
@Output()
resultSelected = new EventEmitter<SearchResult>();
/**
* A mapping of the search results grouped into areas
*/
readonly defaultArea = 'other';
notFoundMessage = 'Searching ...';
readonly topLevelFolders = ['guide', 'tutorial'];
searchAreas: SearchArea[] = [];
constructor(private searchService: SearchService) {}
ngOnInit() {
this.resultsSubscription = this.searchService.searchResults
.subscribe(search => this.searchAreas = this.processSearchResults(search));
}
ngOnDestroy() {
this.resultsSubscription.unsubscribe();
ngOnChanges(changes: SimpleChanges) {
this.searchAreas = this.processSearchResults(this.searchResults);
}
onResultSelected(page: SearchResult, event: MouseEvent) {
@ -55,6 +40,9 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
// Map the search results into groups by area
private processSearchResults(search: SearchResults) {
if (!search) {
return [];
}
this.notFoundMessage = 'No results found.';
const searchAreaMap = {};
search.results.forEach(result => {
@ -84,6 +72,6 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
}
}
function compareResults(l: {title: string}, r: {title: string}) {
function compareResults(l: SearchResult, r: SearchResult) {
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
}

View File

@ -1,5 +1,5 @@
import { Component, DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SelectComponent, Option } from './select.component';

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SearchResultsComponent } from './search-results/search-results.component';
import { SelectComponent } from './select/select.component';
@NgModule({
@ -7,9 +8,11 @@ import { SelectComponent } from './select/select.component';
CommonModule
],
exports: [
SearchResultsComponent,
SelectComponent
],
declarations: [
SearchResultsComponent,
SelectComponent
]
})

View File

@ -1,4 +1,4 @@
import { ReflectiveInjector, SecurityContext } from '@angular/core';
import { ReflectiveInjector } from '@angular/core';
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Subject } from 'rxjs/Subject';
@ -268,7 +268,6 @@ describe('TocService', () => {
});
it('should calculate and set id of heading without an id', () => {
const tocItem = lastTocList.find(item => item.title === 'H2 Two');
const id = headings[2].getAttribute('id');
expect(id).toEqual('h2-two');
});

View File

@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/
import {NgZone, Injectable} from '@angular/core';
import {NgZone} from '@angular/core';
import {Observable} from 'rxjs/Observable';
export interface WebWorkerMessage {

View File

@ -1,4 +1,4 @@
import { Inject, Injectable, OnDestroy } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { NgServiceWorker } from '@angular/service-worker';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

View File

@ -29,6 +29,24 @@ aio-search-results {
}
}
aio-search-results.embedded .search-results {
padding: 0;
color: inherit;
width: auto;
max-height: 100%;
position: relative;
background-color: inherit;
box-shadow: none;
box-sizing: border-box;
.search-area a {
color: lighten($darkgray, 10);
&:hover {
color: $accentblue;
}
}
}
.search-area {
display: flex;
flex-direction: column;

View File

@ -1,5 +1,5 @@
import { Subject } from 'rxjs/Subject';
import { SearchResults } from 'app/search/search.service';
import { SearchResults } from 'app/search/interfaces';
export class MockSearchService {
searchResults = new Subject<SearchResults>();

View File

@ -27,49 +27,22 @@ const BOILERPLATE_TEST_PATHS = [
'karma.conf.js'
];
const ANGULAR_DIST_PATH = path.resolve(__dirname, '../../../dist');
const ANGULAR_PACKAGES_PATH = path.resolve(ANGULAR_DIST_PATH, 'packages-dist');
const ANGULAR_PACKAGES = [
'animations',
'common',
'compiler',
'compiler-cli',
'core',
'forms',
'http',
'platform-browser',
'platform-browser-dynamic',
'platform-server',
'router',
'upgrade',
];
const ANGULAR_TOOLS_PACKAGES_PATH = path.resolve(ANGULAR_DIST_PATH, 'tools', '@angular');
const ANGULAR_TOOLS_PACKAGES = [
'tsc-wrapped'
];
const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
class ExampleBoilerPlate {
/**
* Add boilerplate files to all the examples
*
* @param useLocal if true then overwrite the Angular library files with locally built ones
*/
add(useLocal) {
// first install the shared node_modules
this.installNodeModules(SHARED_PATH);
// Replace the Angular packages with those from the dist folder, if necessary
if (useLocal) {
ANGULAR_PACKAGES.forEach(packageName => this.overridePackage(ANGULAR_PACKAGES_PATH, packageName));
ANGULAR_TOOLS_PACKAGES.forEach(packageName => this.overridePackage(ANGULAR_TOOLS_PACKAGES_PATH, packageName));
}
add() {
// Get all the examples folders, indicated by those that contain a `example-config.json` file
const exampleFolders = this.getFoldersContaining(EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, 'node_modules');
exampleFolders.forEach(exampleFolder => {
if (!fs.existsSync(SHARED_NODE_MODULES_PATH)) {
throw new Error(`The shared node_modules folder for the examples (${SHARED_NODE_MODULES_PATH}) is missing.\n` +
`Perhaps you need to run "yarn example-use-npm" or "yarn example-use-local" to install the dependencies?`);
}
// Link the node modules - requires admin access (on Windows) because it adds symlinks
const destinationNodeModules = path.resolve(exampleFolder, 'node_modules');
fs.ensureSymlinkSync(SHARED_NODE_MODULES_PATH, destinationNodeModules);
@ -95,25 +68,12 @@ class ExampleBoilerPlate {
main() {
yargs
.usage('$0 <cmd> [args]')
.command('add [--local]', 'add the boilerplate to each example',
{ local: { describe: 'Use the locally built Angular libraries, rather than ones from npm.' } },
argv => this.add(argv.local))
.command('add', 'add the boilerplate to each example', () => this.add())
.command('remove', 'remove the boilerplate from each example', () => this.remove())
.demandCommand(1, 'Please supply a command from the list above')
.argv;
}
installNodeModules(basePath) {
shelljs.exec('yarn', {cwd: basePath});
}
overridePackage(basePath, packageName) {
const sourceFolder = path.resolve(basePath, packageName);
const destinationFolder = path.resolve(SHARED_NODE_MODULES_PATH, '@angular', packageName);
shelljs.rm('-rf', destinationFolder);
fs.copySync(sourceFolder, destinationFolder);
}
getFoldersContaining(basePath, filename, ignore) {
const pattern = path.resolve(basePath, '**', filename);
const ignorePattern = path.resolve(basePath, '**', ignore, '**');

View File

@ -1,50 +1,46 @@
const exampleBoilerPlate = require('./example-boilerplate');
const shelljs = require('shelljs');
const path = require('canonical-path');
const fs = require('fs-extra');
const glob = require('glob');
const path = require('canonical-path');
const shelljs = require('shelljs');
const exampleBoilerPlate = require('./example-boilerplate');
describe('example-boilerplate tool', () => {
describe('add', () => {
const sharedDir = path.resolve(__dirname, 'shared');
const sharedNodeModulesDir = path.resolve(sharedDir, 'node_modules');
const numberOfBoilerPlateFiles = 8;
const numberOfBoilerPlateTestFiles = 3;
const exampleFolders = ['a/b', 'c/d'];
beforeEach(() => {
spyOn(exampleBoilerPlate, 'installNodeModules');
spyOn(exampleBoilerPlate, 'overridePackage');
spyOn(exampleBoilerPlate, 'getFoldersContaining').and.returnValue(exampleFolders);
spyOn(fs, 'ensureSymlinkSync');
spyOn(fs, 'existsSync').and.returnValue(true);
spyOn(exampleBoilerPlate, 'copyFile');
spyOn(exampleBoilerPlate, 'getFoldersContaining').and.returnValue(exampleFolders);
spyOn(exampleBoilerPlate, 'loadJsonFile').and.returnValue({});
});
it('should install the node modules', () => {
exampleBoilerPlate.add();
expect(exampleBoilerPlate.installNodeModules).toHaveBeenCalledWith(path.resolve(__dirname, 'shared'));
});
it('should override the Angular node_modules with the locally built Angular packages if `useLocal` is true', () => {
const numberOfAngularPackages = 12;
const numberOfAngularToolsPackages = 1;
exampleBoilerPlate.add(true);
expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledTimes(numberOfAngularPackages + numberOfAngularToolsPackages);
// for example
expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledWith(path.resolve(__dirname, '../../../dist/packages-dist'), 'common');
expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledWith(path.resolve(__dirname, '../../../dist/packages-dist'), 'core');
expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledWith(path.resolve(__dirname, '../../../dist/tools/@angular'), 'tsc-wrapped');
});
it('should process all the example folders', () => {
const examplesDir = path.resolve(__dirname, '../../content/examples');
exampleBoilerPlate.add();
expect(exampleBoilerPlate.getFoldersContaining).toHaveBeenCalledWith(path.resolve(__dirname, '../../content/examples'), 'example-config.json', 'node_modules');
expect(exampleBoilerPlate.getFoldersContaining)
.toHaveBeenCalledWith(examplesDir, 'example-config.json', 'node_modules');
});
it('should symlink the node_modules', () => {
exampleBoilerPlate.add();
expect(fs.ensureSymlinkSync).toHaveBeenCalledTimes(exampleFolders.length);
expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/node_modules'), path.resolve('a/b/node_modules'));
expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/node_modules'), path.resolve('c/d/node_modules'));
expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(sharedNodeModulesDir, path.resolve('a/b/node_modules'));
expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(sharedNodeModulesDir, path.resolve('c/d/node_modules'));
});
it('should error if the node_modules folder is missing', () => {
fs.existsSync.and.returnValue(false);
expect(() => exampleBoilerPlate.add()).toThrowError(
`The shared node_modules folder for the examples (${sharedNodeModulesDir}) is missing.\n` +
`Perhaps you need to run "yarn example-use-npm" or "yarn example-use-local" to install the dependencies?`);
expect(fs.ensureSymlinkSync).not.toHaveBeenCalled();
});
it('should copy all the source boilerplate files', () => {
@ -80,31 +76,6 @@ describe('example-boilerplate tool', () => {
});
});
describe('installNodeModules', () => {
it('should run `yarn` in the base path', () => {
spyOn(shelljs, 'exec');
exampleBoilerPlate.installNodeModules('some/base/path');
expect(shelljs.exec).toHaveBeenCalledWith('yarn', { cwd: 'some/base/path' });
});
});
describe('overridePackage', () => {
beforeEach(() => {
spyOn(shelljs, 'rm');
spyOn(fs, 'copySync');
});
it('should remove the original package from the shared node_modules folder', () => {
exampleBoilerPlate.overridePackage('base/path', 'somePackage');
expect(shelljs.rm).toHaveBeenCalledWith('-rf', path.resolve(__dirname, 'shared/node_modules/@angular/somePackage'));
});
it('should copy the source folder to the shared node_modules folder', () => {
exampleBoilerPlate.overridePackage('base/path', 'somePackage');
expect(fs.copySync).toHaveBeenCalledWith(path.resolve('base/path/somePackage'), path.resolve(__dirname, 'shared/node_modules/@angular/somePackage'));
});
});
describe('getFoldersContaining', () => {
it('should use glob.sync', () => {
spyOn(glob, 'sync').and.returnValue(['a/b/config.json', 'c/d/config.json']);

View File

@ -4,6 +4,9 @@ const argv = require('yargs').argv;
const globby = require('globby');
const xSpawn = require('cross-spawn');
const treeKill = require('tree-kill');
const shelljs = require('shelljs');
shelljs.set('-e');
const AIO_PATH = path.join(__dirname, '../../');
const SHARED_PATH = path.join(__dirname, '/shared');
@ -12,6 +15,7 @@ const PROTRACTOR_CONFIG_FILENAME = path.join(__dirname, './shared/protractor.con
const SPEC_FILENAME = 'e2e-spec.ts';
const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
const IGNORED_EXAMPLES = [
'upgrade-p', // Temporarily disabled to unblock 4.4.x while fixing.
'ts-to-js/'
];
@ -35,21 +39,18 @@ const IGNORED_EXAMPLES = [
* e.g. --shard=1/3 // the second of every three specs: 1, 4, 7, etc
*/
function runE2e() {
let promise = Promise.resolve();
if (argv.setup) {
// Run setup.
console.log('runE2e: copy boilerplate');
const spawnInfo = spawnExt('yarn', ['boilerplate:add', '--', argv.local ? '--local' : ''], { cwd: AIO_PATH });
promise = spawnInfo.promise
.then(() => {
console.log('runE2e: update webdriver');
return spawnExt('yarn', ['webdriver:update'], { cwd: SHARED_PATH }).promise;
});
};
console.log('runE2e: setup boilerplate');
const installPackagesCommand = `example-use-${argv.local ? 'local' : 'npm'}`;
const addBoilerplateCommand = 'boilerplate:add';
shelljs.exec(`yarn ${installPackagesCommand}`, { cwd: AIO_PATH });
shelljs.exec(`yarn ${addBoilerplateCommand}`, { cwd: AIO_PATH });
}
const outputFile = path.join(AIO_PATH, './protractor-results.txt');
return promise
return Promise.resolve()
.then(() => findAndRunE2eTests(argv.filter, outputFile, argv.shard))
.then((status) => {
reportStatus(status, outputFile);
@ -105,7 +106,7 @@ function runE2eTests(appDir, outputFile) {
const config = loadExampleConfig(appDir);
const appBuildSpawnInfo = spawnExt('yarn', [config.build], { cwd: appDir });
const appRunSpawnInfo = spawnExt('yarn', [config.run, '--', '-s'], { cwd: appDir }, true);
const appRunSpawnInfo = spawnExt('yarn', [config.run, '-s'], { cwd: appDir }, true);
let run = runProtractor(appBuildSpawnInfo.promise, appDir, appRunSpawnInfo, outputFile);
@ -124,12 +125,12 @@ function runProtractor(prepPromise, appDir, appRunSpawnInfo, outputFile) {
fs.appendFileSync(outputFile, emsg);
return Promise.reject(emsg);
})
.then(function (data) {
.then(function () {
let transpileError = false;
// Start protractor.
const spawnInfo = spawnExt('yarn', ['protractor', '--',
const spawnInfo = spawnExt('yarn', ['protractor',
PROTRACTOR_CONFIG_FILENAME,
`--specs=${specFilename}`,
'--params.appDir=' + appDir,
@ -139,19 +140,19 @@ function runProtractor(prepPromise, appDir, appRunSpawnInfo, outputFile) {
spawnInfo.proc.stderr.on('data', function (data) {
transpileError = transpileError || /npm ERR! Exit status 100/.test(data.toString());
});
return spawnInfo.promise.catch(function (err) {
return spawnInfo.promise.catch(function () {
if (transpileError) {
const emsg = `${specFilename} failed to transpile.\n\n`;
console.log(emsg);
fs.appendFileSync(outputFile, emsg);
}
return Promise.reject(emsg);
return Promise.reject();
});
})
.then(
function () { return finish(true); },
function () { return finish(false); }
)
);
function finish(ok) {
// Ugh... proc.kill does not work properly on windows with child processes.
@ -201,7 +202,7 @@ function reportStatus(status, outputFile) {
function spawnExt(command, args, options, ignoreClose = false) {
let proc;
const promise = new Promise((resolve, reject) => {
let descr = command + " " + args.join(' ');
let descr = command + ' ' + args.join(' ');
console.log('running: ' + descr);
try {
proc = xSpawn.spawn(command, args, options);
@ -237,7 +238,7 @@ function getE2eSpecPaths(basePath, filter) {
const e2eSpecGlob = `${filter ? `*${filter}*` : '*'}/${SPEC_FILENAME}`;
return globby(e2eSpecGlob, { cwd: basePath, nodir: true })
.then(paths => paths
.filter(file => IGNORED_EXAMPLES.some(ignored => !file.startsWith(ignored)))
.filter(file => !IGNORED_EXAMPLES.some(ignored => file.startsWith(ignored)))
.map(file => path.join(basePath, file))
);
}

View File

@ -68,7 +68,7 @@
// other libraries
'rxjs': 'npm:rxjs@5.0.1',
'tslib': 'npm:tslib/tslib.js',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api@0.4/bundles/in-memory-web-api.umd.js',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.3.2/lib/typescript.js',

View File

@ -54,7 +54,7 @@
// other libraries
'rxjs': 'npm:rxjs@5.0.1',
'tslib': 'npm:tslib/tslib.js',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api@0.4/bundles/in-memory-web-api.umd.js',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.3.2/lib/typescript.js',

View File

@ -6,7 +6,8 @@
"scripts": {
"http-server": "http-server",
"protractor": "protractor",
"webdriver:update": "webdriver-manager update --standalone false --gecko false"
"webdriver:update": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG",
"postinstall": "yarn webdriver:update"
},
"keywords": [],
"author": "",
@ -27,7 +28,7 @@
"@angular/upgrade": "~4.3.1",
"angular-in-memory-web-api": "~0.4.0",
"core-js": "^2.4.1",
"rxjs": "^5.1.0",
"rxjs": "^5.4.3",
"systemjs": "0.19.39",
"zone.js": "^0.8.4"
},
@ -78,7 +79,7 @@
"rollup-plugin-uglify": "^1.0.1",
"source-map-explorer": "^1.3.2",
"style-loader": "^0.13.1",
"ts-node": "~3.0.4",
"ts-node": "^3.3.0",
"tslint": "^3.15.1",
"typescript": "~2.3.2",
"webpack": "2.2.1",

View File

@ -20,7 +20,12 @@ exports.config = {
// Capabilities to be passed to the webdriver instance.
capabilities: {
'browserName': 'chrome'
'browserName': 'chrome',
// For Travis
chromeOptions: {
binary: process.env.CHROME_BIN,
args: ['--no-sandbox']
}
},
// Framework to use. Jasmine is recommended.

View File

@ -1390,6 +1390,14 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e"
dependencies:
ansi-styles "^3.1.0"
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
chalk@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d"
@ -2950,6 +2958,12 @@ home-or-tmp@^2.0.0:
os-homedir "^1.0.0"
os-tmpdir "^1.0.1"
homedir-polyfill@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
dependencies:
parse-passwd "^1.0.0"
hosted-git-info@^2.1.4:
version "2.4.2"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.4.2.tgz#0076b9f46a270506ddbaaea56496897460612a67"
@ -4383,6 +4397,10 @@ parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
parse-passwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
parse5@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510"
@ -5372,12 +5390,18 @@ rx@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
rxjs@^5.0.1, rxjs@^5.1.0:
rxjs@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.1.0.tgz#0aa9018b7f440b505fa42bd742b6738be550e720"
dependencies:
symbol-observable "^1.0.1"
rxjs@^5.4.3:
version "5.4.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f"
dependencies:
symbol-observable "^1.0.1"
safe-buffer@^5.0.1, safe-buffer@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223"
@ -6101,19 +6125,19 @@ trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
ts-node@~3.0.4:
version "3.0.6"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.0.6.tgz#55127ff790c7eebf6ba68c1e6dde94b09aaa21e0"
ts-node@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.3.0.tgz#c13c6a3024e30be1180dd53038fc209289d4bf69"
dependencies:
arrify "^1.0.0"
chalk "^1.1.1"
chalk "^2.0.0"
diff "^3.1.0"
make-error "^1.1.1"
minimist "^1.2.0"
mkdirp "^0.5.1"
source-map-support "^0.4.0"
tsconfig "^6.0.0"
v8flags "^2.0.11"
v8flags "^3.0.0"
yn "^2.0.0"
tsconfig@^6.0.0:
@ -6341,12 +6365,18 @@ uuid@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
v8flags@^2.0.10, v8flags@^2.0.11:
v8flags@^2.0.10:
version "2.1.1"
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"
dependencies:
user-home "^1.1.1"
v8flags@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.0.1.tgz#dce8fc379c17d9f2c9e9ed78d89ce00052b1b76b"
dependencies:
homedir-polyfill "^1.0.1"
validate-npm-package-license@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"

View File

@ -0,0 +1,291 @@
'use strict';
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('canonical-path');
const shelljs = require('shelljs');
const yargs = require('yargs');
const PACKAGE_JSON = 'package.json';
const LOCAL_MARKER_PATH = 'node_modules/_local_.json';
const PACKAGE_JSON_REGEX = /^[^/]+\/package\.json$/;
const ANGULAR_ROOT_DIR = path.resolve(__dirname, '../../..');
const ANGULAR_DIST_PACKAGES = path.resolve(ANGULAR_ROOT_DIR, 'dist/packages-dist');
const ANGULAR_DIST_TOOLS = path.resolve(ANGULAR_ROOT_DIR, 'dist/tools/@angular');
/**
* A tool that can install Angular dependencies for a project from NPM or from the
* locally built distributables.
*
* This tool is used to change dependencies of the `aio` application and the example
* applications.
*/
class NgPackagesInstaller {
/**
* Create a new installer for a project in the specified directory.
*
* @param {string} projectDir - the path to the directory containing the project.
* @param {object} options - a hash of options for the install
* * `debug` (`boolean`) - whether to display debug messages.
* * `force` (`boolean`) - whether to force a local installation
* even if there is a local marker file.
* * `ignorePackages` (`string[]`) - a collection of names of packages
* that should not be copied over.
*/
constructor(projectDir, options = {}) {
this.debug = options.debug;
this.force = options.force;
this.ignorePackages = options.ignorePackages || [];
this.projectDir = path.resolve(projectDir);
this.localMarkerPath = path.resolve(this.projectDir, LOCAL_MARKER_PATH);
this._log('Project directory:', this.projectDir);
}
// Public methods
/**
* Check whether the dependencies have been overridden with locally built
* Angular packages. This is done by checking for the `_local_.json` marker file.
* This will emit a warning to the console if the dependencies have been overridden.
*/
checkDependencies() {
if (this._checkLocalMarker()) {
this._printWarning();
}
}
/**
* Install locally built Angular dependencies, overriding the dependencies in the package.json
* This will also write a "marker" file (`_local_.json`), which contains the overridden package.json
* contents and acts as an indicator that dependencies have been overridden.
*/
installLocalDependencies() {
if (this._checkLocalMarker() !== true || this.force) {
const pathToPackageConfig = path.resolve(this.projectDir, PACKAGE_JSON);
const packages = this._getDistPackages();
try {
// Overwrite local Angular packages dependencies to other Angular packages with local files.
Object.keys(packages).forEach(key => {
const pkg = packages[key];
const tmpConfig = JSON.parse(JSON.stringify(pkg.config));
// Prevent accidental publishing of the package, if something goes wrong.
tmpConfig.private = true;
// Overwrite project dependencies/devDependencies to Angular packages with local files.
['dependencies', 'devDependencies'].forEach(prop => {
const deps = tmpConfig[prop] || {};
Object.keys(deps).forEach(key2 => {
const pkg2 = packages[key2];
if (pkg2) {
// point the core Angular packages at the distributable folder
deps[key2] = `file:${pkg2.parentDir}/${key2.replace('@angular/', '')}`;
this._log(`Overriding dependency of local ${key} with local package: ${key2}: ${deps[key2]}`);
}
});
});
fs.writeFileSync(pkg.packageJsonPath, JSON.stringify(tmpConfig));
});
const packageConfigFile = fs.readFileSync(pathToPackageConfig);
const packageConfig = JSON.parse(packageConfigFile);
const [dependencies, peers] = this._collectDependencies(packageConfig.dependencies || {}, packages);
const [devDependencies, devPeers] = this._collectDependencies(packageConfig.devDependencies || {}, packages);
this._assignPeerDependencies(peers, dependencies, devDependencies);
this._assignPeerDependencies(devPeers, dependencies, devDependencies);
const localPackageConfig = Object.assign(Object.create(null), packageConfig, { dependencies, devDependencies });
localPackageConfig.__angular = { local: true };
const localPackageConfigJson = JSON.stringify(localPackageConfig, null, 2);
try {
this._log(`Writing temporary local ${PACKAGE_JSON} to ${pathToPackageConfig}`);
fs.writeFileSync(pathToPackageConfig, localPackageConfigJson);
this._installDeps('--no-lockfile', '--check-files');
this._setLocalMarker(localPackageConfigJson);
} finally {
this._log(`Restoring original ${PACKAGE_JSON} to ${pathToPackageConfig}`);
fs.writeFileSync(pathToPackageConfig, packageConfigFile);
}
} finally {
// Restore local Angular packages dependencies to other Angular packages.
this._log(`Restoring original ${PACKAGE_JSON} for local Angular packages.`);
Object.keys(packages).forEach(key => {
const pkg = packages[key];
fs.writeFileSync(pkg.packageJsonPath, JSON.stringify(pkg.config));
});
}
}
}
/**
* Reinstall the original package.json depdendencies
* Yarn will also delete the local marker file for us.
*/
restoreNpmDependencies() {
this._installDeps('--freeze-lockfile', '--check-files');
}
// Protected helpers
_assignPeerDependencies(peerDependencies, dependencies, devDependencies) {
Object.keys(peerDependencies).forEach(key => {
// If there is already an equivalent dependency then override it - otherwise assign/override the devDependency
if (dependencies[key]) {
this._log(`Overriding dependency with peerDependency: ${key}: ${peerDependencies[key]}`);
dependencies[key] = peerDependencies[key];
} else {
this._log(`${devDependencies[key] ? 'Overriding' : 'Assigning'} devDependency with peerDependency: ${key}: ${peerDependencies[key]}`);
devDependencies[key] = peerDependencies[key];
}
});
}
_collectDependencies(dependencies, packages) {
const peerDependencies = Object.create(null);
const mergedDependencies = Object.assign(Object.create(null), dependencies);
Object.keys(dependencies).forEach(key => {
const sourcePackage = packages[key];
if (sourcePackage) {
// point the core Angular packages at the distributable folder
mergedDependencies[key] = `file:${sourcePackage.parentDir}/${key.replace('@angular/', '')}`;
this._log(`Overriding dependency with local package: ${key}: ${mergedDependencies[key]}`);
// grab peer dependencies
const sourcePackagePeerDeps = sourcePackage.config.peerDependencies || {};
Object.keys(sourcePackagePeerDeps)
// ignore peerDependencies which are already core Angular packages
.filter(key => !packages[key])
.forEach(key => peerDependencies[key] = sourcePackagePeerDeps[key]);
}
});
return [mergedDependencies, peerDependencies];
}
/**
* A hash of Angular package configs.
* (Detected as directories in '/packages/' that contain a top-level 'package.json' file.)
*/
_getDistPackages() {
const packageConfigs = Object.create(null);
[ANGULAR_DIST_PACKAGES, ANGULAR_DIST_TOOLS].forEach(distDir => {
this._log(`Angular distributable directory: ${distDir}.`);
shelljs
.find(distDir)
.map(filePath => filePath.slice(distDir.length + 1))
.filter(filePath => PACKAGE_JSON_REGEX.test(filePath))
.forEach(packagePath => {
const packageName = `@angular/${packagePath.slice(0, -PACKAGE_JSON.length -1)}`;
if (this.ignorePackages.indexOf(packageName) === -1) {
const packageConfig = require(path.resolve(distDir, packagePath));
packageConfigs[packageName] = {
parentDir: distDir,
packageJsonPath: path.resolve(distDir, packagePath),
config: packageConfig
};
} else {
this._log('Ignoring package', packageName);
}
});
});
this._log('Found the following Angular distributables:', Object.keys(packageConfigs).map(key => `\n - ${key}`));
return packageConfigs;
}
_installDeps(...options) {
const command = 'yarn install ' + options.join(' ');
this._log('Installing dependencies with:', command);
shelljs.exec(command, {cwd: this.projectDir});
}
/**
* Log a message if the `debug` property is set to true.
* @param {...string[]} messages - The messages to be logged.
*/
_log(...messages) {
if (this.debug) {
const header = ` [${NgPackagesInstaller.name}]: `;
const indent = ' '.repeat(header.length);
const message = messages.join(' ');
console.info(`${header}${message.split('\n').join(`\n${indent}`)}`);
}
}
_printWarning() {
const relativeScriptPath = path.relative('.', __filename.replace(/\.js$/, ''));
const absoluteProjectDir = path.resolve(this.projectDir);
const restoreCmd = `node ${relativeScriptPath} restore ${absoluteProjectDir}`;
// Log a warning.
console.warn(chalk.yellow([
'',
'!'.repeat(110),
'!!!',
'!!! WARNING',
'!!!',
`!!! The project at "${absoluteProjectDir}" is running against the local Angular build.`,
'!!!',
'!!! To restore the npm packages run:',
'!!!',
`!!! "${restoreCmd}"`,
'!!!',
'!'.repeat(110),
'',
].join('\n')));
}
// Local marker helpers
_checkLocalMarker() {
this._log('Checking for local marker at', this.localMarkerPath);
return fs.existsSync(this.localMarkerPath);
}
_setLocalMarker(contents) {
this._log('Writing local marker file to', this.localMarkerPath);
fs.writeFileSync(this.localMarkerPath, contents);
}
}
function main() {
shelljs.set('-e');
yargs
.usage('$0 <cmd> [args]')
.option('debug', { describe: 'Print additional debug information.', default: false })
.option('force', { describe: 'Force the command to execute even if not needed.', default: false })
.option('ignore-packages', { describe: 'List of Angular packages that should not be used in local mode.', default: [], array: true })
.command('overwrite <projectDir> [--force] [--debug] [--ignore-packages package1 package2]', 'Install dependencies from the locally built Angular distributables.', () => {}, argv => {
const installer = new NgPackagesInstaller(argv.projectDir, argv);
installer.installLocalDependencies();
})
.command('restore <projectDir> [--debug]', 'Install dependencies from the npm registry.', () => {}, argv => {
const installer = new NgPackagesInstaller(argv.projectDir, argv);
installer.restoreNpmDependencies();
})
.command('check <projectDir> [--debug]', 'Check that dependencies came from npm. Otherwise display a warning message.', () => {}, argv => {
const installer = new NgPackagesInstaller(argv.projectDir, argv);
installer.checkDependencies();
})
.demandCommand(1, 'Please supply a command from the list above.')
.strict()
.wrap(yargs.terminalWidth())
.argv;
}
module.exports = NgPackagesInstaller;
if (require.main === module) {
main();
}

View File

@ -0,0 +1,324 @@
'use strict';
const fs = require('fs-extra');
const path = require('canonical-path');
const shelljs = require('shelljs');
const NgPackagesInstaller = require('./index');
describe('NgPackagesInstaller', () => {
const rootDir = 'root/dir';
const absoluteRootDir = path.resolve(rootDir);
const nodeModulesDir = path.resolve(absoluteRootDir, 'node_modules');
const packageJsonPath = path.resolve(absoluteRootDir, 'package.json');
const packagesDir = path.resolve(path.resolve(__dirname, '../../../dist/packages-dist'));
const toolsDir = path.resolve(path.resolve(__dirname, '../../../dist/tools/@angular'));
let installer;
beforeEach(() => {
spyOn(fs, 'existsSync');
spyOn(fs, 'readFileSync');
spyOn(fs, 'writeFileSync');
spyOn(shelljs, 'exec');
spyOn(shelljs, 'rm');
spyOn(console, 'log');
spyOn(console, 'warn');
installer = new NgPackagesInstaller(rootDir);
});
describe('checkDependencies()', () => {
beforeEach(() => {
spyOn(installer, '_printWarning');
});
it('should not print a warning if there is no _local_.json file', () => {
fs.existsSync.and.returnValue(false);
installer.checkDependencies();
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(rootDir, 'node_modules/_local_.json'));
expect(installer._printWarning).not.toHaveBeenCalled();
});
it('should print a warning if there is a _local_.json file', () => {
fs.existsSync.and.returnValue(true);
installer.checkDependencies();
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(rootDir, 'node_modules/_local_.json'));
expect(installer._printWarning).toHaveBeenCalled();
});
});
describe('installLocalDependencies()', () => {
const copyJsonObj = obj => JSON.parse(JSON.stringify(obj));
let dummyNgPackages, dummyPackage, dummyPackageJson, expectedModifiedPackage, expectedModifiedPackageJson;
beforeEach(() => {
spyOn(installer, '_checkLocalMarker');
// These are the packages that are "found" in the dist directory
dummyNgPackages = {
'@angular/core': {
parentDir: packagesDir,
packageJsonPath: `${packagesDir}/core/package.json`,
config: { peerDependencies: { rxjs: '5.0.1' } }
},
'@angular/common': {
parentDir: packagesDir,
packageJsonPath: `${packagesDir}/common/package.json`,
config: { peerDependencies: { '@angular/core': '4.4.4-1ab23cd4' } }
},
'@angular/compiler': {
parentDir: packagesDir,
packageJsonPath: `${packagesDir}/compiler/package.json`,
config: { peerDependencies: { '@angular/common': '4.4.4-1ab23cd4' } }
},
'@angular/compiler-cli': {
parentDir: toolsDir,
packageJsonPath: `${toolsDir}/compiler-cli/package.json`,
config: {
dependencies: { '@angular/tsc-wrapped': '4.4.4-1ab23cd4' },
peerDependencies: { typescript: '^2.4.2', '@angular/compiler': '4.4.4-1ab23cd4' }
}
},
'@angular/tsc-wrapped': {
parentDir: toolsDir,
packageJsonPath: `${toolsDir}/tsc-wrapped/package.json`,
config: {
devDependencies: { '@angular/common': '4.4.4-1ab23cd4' },
peerDependencies: { tsickle: '^1.4.0' }
}
}
};
spyOn(installer, '_getDistPackages').and.callFake(() => copyJsonObj(dummyNgPackages));
// This is the package.json in the "test" folder
dummyPackage = {
dependencies: {
'@angular/core': '4.4.1',
'@angular/common': '4.4.1'
},
devDependencies: {
'@angular/compiler-cli': '4.4.1'
}
};
dummyPackageJson = JSON.stringify(dummyPackage);
fs.readFileSync.and.returnValue(dummyPackageJson);
// This is the package.json that is temporarily written to the "test" folder
// Note that the Angular (dev)dependencies have been modified to use a "file:" path
// And that the peerDependencies from `dummyNgPackages` have been added as (dev)dependencies.
expectedModifiedPackage = {
dependencies: {
'@angular/core': `file:${packagesDir}/core`,
'@angular/common': `file:${packagesDir}/common`
},
devDependencies: {
'@angular/compiler-cli': `file:${toolsDir}/compiler-cli`,
rxjs: '5.0.1',
typescript: '^2.4.2'
},
__angular: { local: true }
};
expectedModifiedPackageJson = JSON.stringify(expectedModifiedPackage, null, 2);
});
describe('when there is a local package marker', () => {
it('should not continue processing', () => {
installer._checkLocalMarker.and.returnValue(true);
installer.installLocalDependencies();
expect(installer._checkLocalMarker).toHaveBeenCalled();
expect(installer._getDistPackages).not.toHaveBeenCalled();
});
});
describe('when there is no local package marker', () => {
let log;
beforeEach(() => {
log = [];
fs.writeFileSync.and.callFake((filePath, contents) => filePath === packageJsonPath && log.push(`writeFile: ${contents}`));
spyOn(installer, '_installDeps').and.callFake(() => log.push('installDeps:'));
spyOn(installer, '_setLocalMarker');
installer._checkLocalMarker.and.returnValue(false);
installer.installLocalDependencies();
});
it('should get the dist packages', () => {
expect(installer._checkLocalMarker).toHaveBeenCalled();
expect(installer._getDistPackages).toHaveBeenCalled();
});
it('should temporarily overwrite the package.json files of local Angular packages', () => {
const pkgJsonFor = pkgName => dummyNgPackages[`@angular/${pkgName}`].packageJsonPath;
const pkgConfigFor = pkgName => copyJsonObj(dummyNgPackages[`@angular/${pkgName}`].config);
const overwriteConfigFor = (pkgName, newProps) => Object.assign(pkgConfigFor(pkgName), newProps);
const allArgs = fs.writeFileSync.calls.allArgs();
const firstFiveArgs = allArgs.slice(0, 5);
const lastFiveArgs = allArgs.slice(-5);
expect(firstFiveArgs).toEqual([
[pkgJsonFor('core'), JSON.stringify(overwriteConfigFor('core', {private: true}))],
[pkgJsonFor('common'), JSON.stringify(overwriteConfigFor('common', {private: true}))],
[pkgJsonFor('compiler'), JSON.stringify(overwriteConfigFor('compiler', {private: true}))],
[pkgJsonFor('compiler-cli'), JSON.stringify(overwriteConfigFor('compiler-cli', {
private: true,
dependencies: { '@angular/tsc-wrapped': `file:${toolsDir}/tsc-wrapped` }
}))],
[pkgJsonFor('tsc-wrapped'), JSON.stringify(overwriteConfigFor('tsc-wrapped', {
private: true,
devDependencies: { '@angular/common': `file:${packagesDir}/common` }
}))],
]);
expect(lastFiveArgs).toEqual(['core', 'common', 'compiler', 'compiler-cli', 'tsc-wrapped']
.map(pkgName => [pkgJsonFor(pkgName), JSON.stringify(pkgConfigFor(pkgName))]));
});
it('should load the package.json', () => {
expect(fs.readFileSync).toHaveBeenCalledWith(packageJsonPath);
});
it('should overwrite package.json with modified config', () => {
expect(fs.writeFileSync).toHaveBeenCalledWith(packageJsonPath, expectedModifiedPackageJson);
});
it('should restore original package.json', () => {
expect(fs.writeFileSync).toHaveBeenCalledWith(packageJsonPath, dummyPackageJson);
});
it('should overwrite package.json, then install deps, then restore original package.json', () => {
expect(log).toEqual([
`writeFile: ${expectedModifiedPackageJson}`,
`installDeps:`,
`writeFile: ${dummyPackageJson}`
]);
});
it('should set the local marker file with the contents of the modified package.json', () => {
expect(installer._setLocalMarker).toHaveBeenCalledWith(expectedModifiedPackageJson);
});
});
});
describe('restoreNpmDependencies()', () => {
it('should run `yarn install` in the specified directory, with the correct options', () => {
spyOn(installer, '_installDeps');
installer.restoreNpmDependencies();
expect(installer._installDeps).toHaveBeenCalledWith('--freeze-lockfile', '--check-files');
});
});
describe('_getDistPackages()', () => {
it('should include top level Angular packages', () => {
const ngPackages = installer._getDistPackages();
const expectedValue = jasmine.objectContaining({
parentDir: jasmine.any(String),
packageJsonPath: jasmine.any(String),
config: jasmine.any(Object),
});
// For example...
expect(ngPackages['@angular/common']).toEqual(expectedValue);
expect(ngPackages['@angular/core']).toEqual(expectedValue);
expect(ngPackages['@angular/router']).toEqual(expectedValue);
expect(ngPackages['@angular/upgrade']).toEqual(expectedValue);
expect(ngPackages['@angular/tsc-wrapped']).toEqual(expectedValue);
expect(ngPackages['@angular/upgrade/static']).not.toBeDefined();
});
it('should store each package\'s parent directory', () => {
const ngPackages = installer._getDistPackages();
// For example...
expect(ngPackages['@angular/core'].parentDir).toBe(packagesDir);
expect(ngPackages['@angular/tsc-wrapped'].parentDir).toBeDefined(toolsDir);
});
it('should not include packages that have been ignored', () => {
installer = new NgPackagesInstaller(rootDir, { ignorePackages: ['@angular/router'] });
const ngPackages = installer._getDistPackages();
expect(ngPackages['@angular/common']).toBeDefined();
expect(ngPackages['@angular/router']).toBeUndefined();
});
});
describe('_log()', () => {
beforeEach(() => {
spyOn(console, 'info');
});
it('should assign the debug property from the options', () => {
installer = new NgPackagesInstaller(rootDir, { debug: true });
expect(installer.debug).toBe(true);
installer = new NgPackagesInstaller(rootDir, { });
expect(installer.debug).toBe(undefined);
});
it('should log a message to the console if the `debug` property is true', () => {
installer._log('foo');
expect(console.info).not.toHaveBeenCalled();
installer.debug = true;
installer._log('bar');
expect(console.info).toHaveBeenCalledWith(' [NgPackagesInstaller]: bar');
});
});
describe('_printWarning()', () => {
it('should mention the message passed in the warning', () => {
installer._printWarning();
expect(console.warn.calls.argsFor(0)[0]).toContain('is running against the local Angular build');
});
it('should mention the command to restore the Angular packages in any warning', () => {
// When run for the current working directory...
const dir1 = '.';
const restoreCmdRe1 = RegExp('\\bnode .*?ng-packages-installer/index restore ' + path.resolve(dir1));
installer = new NgPackagesInstaller(dir1);
installer._printWarning('');
expect(console.warn.calls.argsFor(0)[0]).toMatch(restoreCmdRe1);
// When run for a different directory...
const dir2 = rootDir;
const restoreCmdRe2 = RegExp(`\\bnode .*?ng-packages-installer/index restore .*?${path.resolve(dir1)}\\b`);
installer = new NgPackagesInstaller(dir2);
installer._printWarning('');
expect(console.warn.calls.argsFor(1)[0]).toMatch(restoreCmdRe2);
});
});
describe('_installDeps()', () => {
it('should run yarn install with the given options', () => {
installer._installDeps('option-1', 'option-2');
expect(shelljs.exec).toHaveBeenCalledWith('yarn install option-1 option-2', { cwd: absoluteRootDir });
});
});
describe('local marker helpers', () => {
let installer;
beforeEach(() => {
installer = new NgPackagesInstaller(rootDir);
});
describe('_checkLocalMarker', () => {
it ('should return true if the local marker file exists', () => {
fs.existsSync.and.returnValue(true);
expect(installer._checkLocalMarker()).toEqual(true);
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(nodeModulesDir, '_local_.json'));
fs.existsSync.calls.reset();
fs.existsSync.and.returnValue(false);
expect(installer._checkLocalMarker()).toEqual(false);
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(nodeModulesDir, '_local_.json'));
});
});
describe('_setLocalMarker', () => {
it('should create a local marker file', () => {
installer._setLocalMarker('test contents');
expect(fs.writeFileSync).toHaveBeenCalledWith(path.resolve(nodeModulesDir, '_local_.json'), 'test contents');
});
});
});
});

View File

@ -4,7 +4,7 @@
*/
module.exports = function filterContainedDocs() {
return {
docTypes: ['member', 'function-overload'],
docTypes: ['member', 'function-overload', 'get-accessor-info', 'set-accessor-info'],
$runAfter: ['extra-docs-added'],
$runBefore: ['computing-paths'],
$process: function(docs) {

View File

@ -26,7 +26,9 @@ function getModuleInfo(moduleDoc) {
title: moduleName,
items: moduleDoc.exports
// Ignore internals and private exports (indicated by the ɵ prefix)
.filter(doc => !doc.internal && !doc.privateExport).map(getExportInfo)
.filter(doc => !doc.internal && !doc.privateExport)
.map(getExportInfo)
.sort((a, b) => a.name === b.name ? 0 : a.name > b.name ? 1 : -1)
};
}

View File

@ -113,7 +113,6 @@ describe('generateApiListDoc processor', () => {
]);
});
it('should convert security to a boolean securityRisk', () => {
const processor = processorFactory();
const docs = [
@ -129,7 +128,6 @@ describe('generateApiListDoc processor', () => {
]);
});
it('should convert stability tags to the stable string property', () => {
const processor = processorFactory();
const docs = [
@ -148,5 +146,24 @@ describe('generateApiListDoc processor', () => {
{ docType: 'class', title: 'DddDdd', name: 'dddddd', path: 'ddd', stability: '', securityRisk: false },
]);
});
it('should sort items in each group alphabetically', () => {
const processor = processorFactory();
const docs = [
{ docType: 'module', id: '@angular/common/index', exports: [
{ docType: 'class', name: 'DddDdd', path: 'uuu' },
{ docType: 'class', name: 'BbbBbb', path: 'vvv' },
{ docType: 'class', name: 'AaaAaa', path: 'xxx' },
{ docType: 'class', name: 'CccCcc', path: 'yyy' },
]}
];
processor.$process(docs);
expect(docs[1].data[0].items).toEqual([
{ docType: 'class', title: 'AaaAaa', name: 'aaaaaa', path: 'xxx', stability: '', securityRisk: false },
{ docType: 'class', title: 'BbbBbb', name: 'bbbbbb', path: 'vvv', stability: '', securityRisk: false },
{ docType: 'class', title: 'CccCcc', name: 'cccccc', path: 'yyy', stability: '', securityRisk: false },
{ docType: 'class', title: 'DddDdd', name: 'dddddd', path: 'uuu', stability: '', securityRisk: false },
]);
});
});

View File

@ -29,6 +29,7 @@ module.exports = new Package('angular-base', [
.processor(require('./processors/convertToJson'))
.processor(require('./processors/fixInternalDocumentLinks'))
.processor(require('./processors/copyContentAssets'))
.processor(require('./processors/renderLinkInfo'))
// overrides base packageInfo and returns the one for the 'angular/angular' repo.
.factory('packageInfo', function() { return require(path.resolve(PROJECT_ROOT, 'package.json')); })

View File

@ -0,0 +1,52 @@
/**
* @dgProcessor renderLinkInfo
* @description For each doc that has one of the specified docTypes,
* add HTML comments that describe the links to and from the doc.
*/
module.exports = function renderLinkInfo(extractLinks) {
return {
docTypes: [],
$runBefore: ['convertToJsonProcessor'],
$runAfter: ['fixInternalDocumentLinks'],
$process(docs) {
const toLinks = {};
const fromLinks = {};
const docsToCheck = docs.filter(doc => this.docTypes.indexOf(doc.docType) !== -1);
// Extract and store all links found in each doc in hashes
docsToCheck.forEach(doc => {
const linksFromDoc = extractLinks(doc.renderedContent).hrefs;
// Update the hashes
fromLinks[doc.path] = linksFromDoc;
linksFromDoc.forEach(linkPath => {
linkPath = linkPath.match(/^[^#?]+/)[0]; // remove the query and hash from the link
(toLinks[linkPath] = toLinks[linkPath] || []).push(doc.path);
});
});
// Add HTML comments to the end of the rendered content that list the links found above
docsToCheck.forEach(doc => {
const linksFromDoc = getLinks(fromLinks, doc.path);
const linksToDoc = getLinks(toLinks, doc.path);
doc.renderedContent +=
`\n<!-- links to this doc:${linksToDoc.map(link => `\n - ${link}`).join('')}\n-->\n` +
`<!-- links from this doc:${linksFromDoc.map(link => `\n - ${link}`).join('')}\n-->`;
});
}
};
};
function getLinks(hash, docPath) {
const links = (hash[docPath] || []).filter(link => link !== docPath);
const internal = {};
const external = {};
links.forEach(link => {
if (/^[^:/#?]+:/.test(link)) {
external[link] = true;
} else {
internal[link] = true;
}
});
return Object.keys(internal).sort()
.concat(Object.keys(external).sort());
}

View File

@ -0,0 +1,222 @@
const testPackage = require('../../helpers/test-package');
const processorFactory = require('./renderLinkInfo');
const extractLinks = require('dgeni-packages/base/services/extractLinks')();
const Dgeni = require('dgeni');
describe('renderLinkInfo processor', () => {
it('should be available on the injector', () => {
const dgeni = new Dgeni([testPackage('angular-base-package')]);
const injector = dgeni.configureInjector();
const processor = injector.get('renderLinkInfo');
expect(processor.$process).toBeDefined();
});
it('should run before the correct processor', () => {
const processor = processorFactory(extractLinks);
expect(processor.$runBefore).toEqual(['convertToJsonProcessor']);
});
it('should run after the correct processor', () => {
const processor = processorFactory(extractLinks);
expect(processor.$runAfter).toEqual(['fixInternalDocumentLinks']);
});
it('should add HTML comments for links out of docs', () => {
const processor = processorFactory(extractLinks);
processor.docTypes = ['test'];
const docs = [
{ path: 'test-1', docType: 'test', renderedContent: '<a href="a/b/c"></a><a href="x/y/z"></a>' },
{ path: 'test-2', docType: 'test', renderedContent: '<a href="foo"></a><a href="bar"></a>' },
];
processor.$process(docs);
expect(docs).toEqual([
{
path: 'test-1',
docType: 'test',
renderedContent: '<a href="a/b/c"></a><a href="x/y/z"></a>\n' +
'<!-- links to this doc:\n-->\n' +
'<!-- links from this doc:\n - a/b/c\n - x/y/z\n-->'
},
{
path: 'test-2',
docType: 'test',
renderedContent: '<a href="foo"></a><a href="bar"></a>\n' +
'<!-- links to this doc:\n-->\n' +
'<!-- links from this doc:\n - bar\n - foo\n-->'
},
]);
});
it('should order links alphabetically', () => {
const processor = processorFactory(extractLinks);
processor.docTypes = ['test'];
const docs = [
{ path: 'test-1', docType: 'test', renderedContent: '<a href="orange"></a><a href="apple"></a><a href="banana"></a>' },
];
processor.$process(docs);
expect(docs).toEqual([
{
path: 'test-1',
docType: 'test',
renderedContent: '<a href="orange"></a><a href="apple"></a><a href="banana"></a>\n' +
'<!-- links to this doc:\n-->\n' +
'<!-- links from this doc:\n - apple\n - banana\n - orange\n-->'
},
]);
});
it('should list repeated links only once', () => {
const processor = processorFactory(extractLinks);
processor.docTypes = ['test'];
const docs = [
{ path: 'test-1', docType: 'test', renderedContent: '<a href="banana"></a><a href="apple"></a><a href="banana"></a>' },
];
processor.$process(docs);
expect(docs).toEqual([
{
path: 'test-1',
docType: 'test',
renderedContent: '<a href="banana"></a><a href="apple"></a><a href="banana"></a>\n' +
'<!-- links to this doc:\n-->\n' +
'<!-- links from this doc:\n - apple\n - banana\n-->'
},
]);
});
it('should list internal links before external', () => {
const processor = processorFactory(extractLinks);
processor.docTypes = ['test'];
const docs = [
{ path: 'test-1', docType: 'test', renderedContent: '<a href="https://www.google.com"></a><a href="apple"></a><a href="ftp://myfile.org"></a>' },
];
processor.$process(docs);
expect(docs).toEqual([
{
path: 'test-1',
docType: 'test',
renderedContent: '<a href="https://www.google.com"></a><a href="apple"></a><a href="ftp://myfile.org"></a>\n' +
'<!-- links to this doc:\n-->\n' +
'<!-- links from this doc:\n - apple\n - ftp://myfile.org\n - https://www.google.com\n-->'
},
]);
});
it('should ignore docs that do not have the specified docType', () => {
const processor = processorFactory(extractLinks);
processor.docTypes = ['test'];
const docs = [
{ path: 'test-1', docType: 'test', renderedContent: '<a href="a/b/c"></a><a href="x/y/z"></a>' },
{ path: 'test-2', docType: 'test2', renderedContent: '<a href="foo"></a><a href="bar"></a>' },
];
processor.$process(docs);
expect(docs).toEqual([
{
path: 'test-1',
docType: 'test',
renderedContent: '<a href="a/b/c"></a><a href="x/y/z"></a>\n' +
'<!-- links to this doc:\n-->\n' +
'<!-- links from this doc:\n - a/b/c\n - x/y/z\n-->'
},
{
path: 'test-2',
docType: 'test2',
renderedContent: '<a href="foo"></a><a href="bar"></a>'
},
]);
});
it('should add HTML comments for links into docs', () => {
const processor = processorFactory(extractLinks);
processor.docTypes = ['test'];
const docs = [
{ path: 'test-1', docType: 'test', renderedContent: '<a href="test-2"></a>' },
{ path: 'test-2', docType: 'test', renderedContent: '<a href="test-1"></a><a href="test-3"></a>' },
{ path: 'test-3', docType: 'test', renderedContent: '<a href="test-1"></a><a href="test-2"></a>' },
];
processor.$process(docs);
expect(docs).toEqual([
{
path: 'test-1',
docType: 'test',
renderedContent: '<a href="test-2"></a>\n' +
'<!-- links to this doc:\n - test-2\n - test-3\n-->\n' +
'<!-- links from this doc:\n - test-2\n-->'
},
{
path: 'test-2',
docType: 'test',
renderedContent: '<a href="test-1"></a><a href="test-3"></a>\n' +
'<!-- links to this doc:\n - test-1\n - test-3\n-->\n' +
'<!-- links from this doc:\n - test-1\n - test-3\n-->'
},
{
path: 'test-3',
docType: 'test',
renderedContent: '<a href="test-1"></a><a href="test-2"></a>\n' +
'<!-- links to this doc:\n - test-2\n-->\n' +
'<!-- links from this doc:\n - test-1\n - test-2\n-->'
},
]);
});
it('should not include links to themselves', () => {
const processor = processorFactory(extractLinks);
processor.docTypes = ['test'];
const docs = [
{ path: 'test-1', docType: 'test', renderedContent: '<a href="test-2"></a>' },
{ path: 'test-2', docType: 'test', renderedContent: '<a href="test-1"></a><a href="test-2"></a>' },
];
processor.$process(docs);
expect(docs).toEqual([
{
path: 'test-1',
docType: 'test',
renderedContent: '<a href="test-2"></a>\n' +
'<!-- links to this doc:\n - test-2\n-->\n' +
'<!-- links from this doc:\n - test-2\n-->'
},
{
path: 'test-2',
docType: 'test',
renderedContent: '<a href="test-1"></a><a href="test-2"></a>\n' +
'<!-- links to this doc:\n - test-1\n-->\n' +
'<!-- links from this doc:\n - test-1\n-->'
},
]);
});
it('should match links that contain fragments or queries', () => {
const processor = processorFactory(extractLinks);
processor.docTypes = ['test'];
const docs = [
{ path: 'test-1', docType: 'test', renderedContent: '<a href="test-2#foo"></a>' },
{ path: 'test-2', docType: 'test', renderedContent: '<a href="test-1?some-query"></a>' },
{ path: 'test-3', docType: 'test', renderedContent: '<a href="test-1?some-query#foo"></a>' },
];
processor.$process(docs);
expect(docs).toEqual([
{
path: 'test-1',
docType: 'test',
renderedContent: '<a href="test-2#foo"></a>\n' +
'<!-- links to this doc:\n - test-2\n - test-3\n-->\n' +
'<!-- links from this doc:\n - test-2#foo\n-->'
},
{
path: 'test-2',
docType: 'test',
renderedContent: '<a href="test-1?some-query"></a>\n' +
'<!-- links to this doc:\n - test-1\n-->\n' +
'<!-- links from this doc:\n - test-1?some-query\n-->'
},
{
path: 'test-3',
docType: 'test',
renderedContent: '<a href="test-1?some-query#foo"></a>\n' +
'<!-- links to this doc:\n-->\n' +
'<!-- links from this doc:\n - test-1?some-query#foo\n-->'
},
]);
});
});

View File

@ -48,4 +48,8 @@ module.exports = new Package('angular.io', [gitPackage, apiPackage, contentPacka
});
checkAnchorLinksProcessor.pathVariants = ['', '/', '.html', '/index.html', '#top-of-page'];
checkAnchorLinksProcessor.errorOnUnmatchedLinks = true;
})
.config(function(renderLinkInfo, postProcessHtml) {
renderLinkInfo.docTypes = postProcessHtml.docTypes;
});

View File

@ -23,11 +23,11 @@ function next(error) {
let p = Promise.resolve();
if (process.argv.indexOf('--watch-only') === -1) {
console.log('===================================================================');
console.log('================================================================');
console.log('Running initial doc generation');
console.log('-------------------------------------------------------------------');
console.log('Skip the full doc-gen by running: `yarn docs-watch -- --watch-only`');
console.log('===================================================================');
console.log('----------------------------------------------------------------');
console.log('Skip the full doc-gen by running: `yarn docs-watch --watch-only`');
console.log('================================================================');
const {Dgeni} = require('dgeni');
var dgeni = new Dgeni([require('../angular.io-package')]);
p = dgeni.generate();
@ -44,4 +44,4 @@ p.then(() => {
watchr.open(CONTENTS_PATH, listener, next);
watchr.open(API_SOURCE_PATH, listener, next);
});
});

View File

@ -18,10 +18,10 @@
{%- macro renderMember(member, truncateLines) -%}
{%- if member.accessibility !== 'public' %}{$ member.accessibility $} {% endif -%}
{%- if member.isGetAccessor %}get {% endif -%}
{%- if member.isSetAccessor %}set {% endif -%}
{%- if (member.isGetAccessor or member.isReadonly) and not member.isSetAccessor %}get {% endif -%}
{%- if member.isSetAccessor and not member.isGetAccessor %}set {% endif -%}
{%- if member.isStatic %}static {% endif -%}
{$ member.name $}{$ member.typeParameters | escape $}{$ params.paramList(member.parameters, truncateLines) | trim $}
{$ member.name $}{$ member.typeParameters | escape $}{% if not member.isGetAccessor %}{$ params.paramList(member.parameters, truncateLines) | trim $}{% endif %}
{%- if member.isOptional %}?{% endif -%}
{$ params.returnType(member.type) | trim | truncateCode(truncateLines) $}
{%- endmacro -%}

View File

@ -1,9 +1,11 @@
{% extends 'export-base.template.html' %}
{% block overview %}
<code-example language="ts" hideCopy="true">
type {$ doc.name $}{% if doc.typeDefinition %} = {$ doc.typeDefinition | escape $}{% endif %};
</code-example>
<section class="{$ doc.docType $}-overview">
<code-example language="ts" hideCopy="true">
type {$ doc.name $}{$ doc.typeParameters | escape $}{% if doc.typeDefinition %} = {$ doc.typeDefinition | escape $}{% endif %};
</code-example>
</section>
{% endblock %}
{% block details %}

View File

@ -8,6 +8,7 @@
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noUnusedLocals": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"

View File

@ -380,10 +380,6 @@ ansi-styles@^3.1.0:
dependencies:
color-convert "^1.9.0"
any-promise@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
anymatch@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507"
@ -1085,7 +1081,7 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^2.0.0, chalk@^2.0.1:
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e"
dependencies:
@ -2002,9 +1998,9 @@ devtools-timeline-model@1.1.6:
chrome-devtools-frontend "1.0.401423"
resolve "1.1.7"
dgeni-packages@^0.21.3:
version "0.21.3"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.21.3.tgz#49d5264400cdd8c8a2f66040267e38c099d540f4"
dgeni-packages@0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.22.0.tgz#7ed07af9074f6547847256c1a65b488a5a17ad03"
dependencies:
canonical-path "0.0.2"
catharsis "^0.8.1"
@ -3442,6 +3438,12 @@ home-dir@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/home-dir/-/home-dir-1.0.0.tgz#2917eb44bdc9072ceda942579543847e3017fe4e"
homedir-polyfill@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
dependencies:
parse-passwd "^1.0.0"
hosted-git-info@^2.1.4:
version "2.4.2"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.4.2.tgz#0076b9f46a270506ddbaaea56496897460612a67"
@ -5464,6 +5466,10 @@ parse-json@^2.1.0, parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
parse-passwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
parse5@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
@ -5599,7 +5605,7 @@ pinkie-promise@^2.0.0:
dependencies:
pinkie "^2.0.0"
pinkie@^2.0.0, pinkie@^2.0.4:
pinkie@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
@ -6558,13 +6564,7 @@ rx@2.3.24:
version "2.3.24"
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
rxjs@^5.2.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.0.tgz#a7db14ab157f9d7aac6a56e655e7a3860d39bf26"
dependencies:
symbol-observable "^1.0.1"
rxjs@^5.4.2:
rxjs@^5.4.2, rxjs@^5.4.3:
version "5.4.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f"
dependencies:
@ -7514,31 +7514,20 @@ ts-node@^3.0.2:
v8flags "^2.0.11"
yn "^1.2.0"
ts-node@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-2.0.0.tgz#16e4fecc949088238b4cbf1c39c9582526b66f74"
ts-node@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.3.0.tgz#c13c6a3024e30be1180dd53038fc209289d4bf69"
dependencies:
arrify "^1.0.0"
chalk "^1.1.1"
chalk "^2.0.0"
diff "^3.1.0"
make-error "^1.1.1"
minimist "^1.2.0"
mkdirp "^0.5.1"
pinkie "^2.0.4"
source-map-support "^0.4.0"
tsconfig "^5.0.2"
v8flags "^2.0.11"
xtend "^4.0.0"
yn "^1.2.0"
tsconfig@^5.0.2:
version "5.0.3"
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-5.0.3.tgz#5f4278e701800967a8fc383fd19648878f2a6e3a"
dependencies:
any-promise "^1.3.0"
parse-json "^2.2.0"
strip-bom "^2.0.0"
strip-json-comments "^2.0.0"
tsconfig "^6.0.0"
v8flags "^3.0.0"
yn "^2.0.0"
tsconfig@^6.0.0:
version "6.0.0"
@ -7965,6 +7954,12 @@ v8flags@^2.0.11:
dependencies:
user-home "^1.1.1"
v8flags@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.0.1.tgz#dce8fc379c17d9f2c9e9ed78d89ce00052b1b76b"
dependencies:
homedir-polyfill "^1.0.1"
valid-url@^1:
version "1.0.9"
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
@ -8507,6 +8502,10 @@ yn@^1.2.0:
dependencies:
object-assign "^4.1.1"
yn@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"
zip-stream@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.1.1.tgz#5216b48bbb4d2651f64d5c6e6f09eb4a7399d557"

View File

@ -30,7 +30,7 @@ var CIconfiguration = {
'Safari8': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'Safari9': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'Safari10': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'iOS7': {unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}},
'iOS7': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'iOS8': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'iOS9': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'iOS10': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},

View File

@ -27,9 +27,9 @@ repository (google3).
That repository defines dependencies on specific versions of
all the tools. You can run the tools Bazel installed, for
example rather than `npm install` (which depends on whatever
example rather than `yarn install` (which depends on whatever
version you have installed on your machine), you can
`bazel run @build_bazel_rules_typescript_node//:bin/npm install`.
`bazel run @yarn//:yarn`.
Bazel accepts a lot of options. We check in some options in the
`.bazelrc` file. See the [bazelrc doc]. For example, if you don't

View File

@ -26,6 +26,8 @@ following products on your development machine:
(version `>=3.10.7 <4.0.0`), which comes with Node. Depending on your system, you can install Node either from
source or as a pre-packaged bundle.
* [Yarn](https://yarnpkg.com) (version `>=1.0.2 <2.0.0`) which is used to install dependencies.
* [Java Development Kit](http://www.oracle.com/technetwork/es/java/javase/downloads/index.html) which is used
to execute the selenium standalone server for e2e testing.
@ -56,29 +58,13 @@ Next, install the JavaScript modules needed to build and test Angular:
```shell
# Install Angular project dependencies (package.json)
npm install
yarn install
```
**Optional**: In this document, we make use of project local `npm` package scripts and binaries
(stored under `./node_modules/.bin`) by prefixing these command invocations with `$(npm bin)`; in
particular `gulp` and `protractor` commands. If you prefer, you can drop this path prefix by either:
*Option 1*: globally installing these two packages as follows:
* `npm install -g gulp` (you might need to prefix this command with `sudo`)
* `npm install -g protractor` (you might need to prefix this command with `sudo`)
Since global installs can become stale, and required versions can vary by project, we avoid their
use in these instructions.
*Option 2*: globally installing the package `npm-run` by running `npm install -g npm-run`
(you might need to prefix this command with `sudo`). You will then be able to run locally installed
package scripts by invoking: e.g., `npm-run gulp build`
(see [npm-run project page](https://github.com/timoxley/npm-run) for more details).
*Option 3*: defining a bash alias like `alias nbin='PATH=$(npm bin):$PATH'` as detailed in this
[Stack Overflow answer](http://stackoverflow.com/questions/9679932/how-to-use-package-installed-locally-in-node-modules/15157360#15157360) and used like this: e.g., `nbin gulp build`.
## Installing Bower Modules

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