Compare commits
58 Commits
Author | SHA1 | Date | |
---|---|---|---|
d033176774 | |||
5dd1744a0b | |||
e8b98a71db | |||
5b03219790 | |||
1a09b237ec | |||
d83f7a1859 | |||
9dba26b686 | |||
1c8186150b | |||
62edafd49f | |||
c8b4e21a9a | |||
0558c6f42a | |||
9e72bad661 | |||
37a44f2f15 | |||
afd5d72bf9 | |||
3f8c8e5d5c | |||
c755cc3cb6 | |||
21433d0ffa | |||
81c20c3b4e | |||
cd2ed6f07c | |||
b01eb6cd0c | |||
3155255dfc | |||
e166da2c71 | |||
ef0e4ebbc1 | |||
959d39cda1 | |||
ff6c0de659 | |||
62f3ab5a3f | |||
2ed16a51af | |||
27885c1d15 | |||
e5ce728c9e | |||
bd1ee3abad | |||
a3ad121c04 | |||
6218299796 | |||
ac862798e9 | |||
911ce36b42 | |||
dbfd7179a8 | |||
207e2febb3 | |||
39c9860b9d | |||
3f29348526 | |||
09261e6c4d | |||
b61c19a86e | |||
f6ebb40fbc | |||
0ce4542990 | |||
8072fe120a | |||
50e79ace12 | |||
aa39449ce4 | |||
d1434e82df | |||
6a0d5b5825 | |||
5e4c41fcdf | |||
4c8413986e | |||
c6aacf5b17 | |||
bd4ec5c14c | |||
406dd45df1 | |||
9948ccf365 | |||
9e8d773e4e | |||
aea11bca87 | |||
08b4e1edc9 | |||
274c229463 | |||
ab13f1f659 |
@ -1,18 +1,31 @@
|
||||
defaults: &defaults
|
||||
# Configuration file for https://circleci.com/gh/angular/angular
|
||||
|
||||
# Note: YAML anchors allow an object to be re-used, reducing duplication.
|
||||
# The ampersand declares an alias for an object, then later the `<<: *name`
|
||||
# syntax dereferences it.
|
||||
# See http://blog.daemonl.com/2016/02/yaml.html
|
||||
# To validate changes, use an online parser, eg.
|
||||
# http://yaml-online-parser.appspot.com/
|
||||
|
||||
# Settings common to each job
|
||||
anchor_1: &job_defaults
|
||||
working_directory: ~/ng
|
||||
docker:
|
||||
- image: angular/ngcontainer
|
||||
|
||||
# After checkout, rebase on top of master.
|
||||
# Similar to travis behavior, but not quite the same.
|
||||
# See https://discuss.circleci.com/t/1662
|
||||
anchor_2: &post_checkout
|
||||
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
lint:
|
||||
<<: *defaults
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout:
|
||||
# After checkout, rebase on top of master.
|
||||
# Similar to travis behavior, but not quite the same.
|
||||
# See https://discuss.circleci.com/t/1662
|
||||
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||
|
||||
@ -21,9 +34,10 @@ jobs:
|
||||
- run: ./node_modules/.bin/gulp lint
|
||||
|
||||
build:
|
||||
<<: *defaults
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||
|
||||
|
14
.github/ISSUE_TEMPLATE.md
vendored
14
.github/ISSUE_TEMPLATE.md
vendored
@ -1,14 +1,14 @@
|
||||
<!--
|
||||
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
||||
|
||||
ISSUES MISSING IMPORTANT INFORMATION MIGHT BE CLOSED WITHOUT INVESTIGATION.
|
||||
ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
|
||||
-->
|
||||
|
||||
## I'm submitting a ...
|
||||
## I'm submitting a...
|
||||
<!-- Check one of the following options with "x" -->
|
||||
<pre><code>
|
||||
[ ] Regression (behavior that used to work and stopped working in a new release)
|
||||
[ ] Bug report <!-- Please search github for a similar issue or PR before submitting -->
|
||||
[ ] Regression (a behavior that used to work and stopped working in a new release)
|
||||
[ ] Bug report <!-- Please search GitHub for a similar issue or PR before submitting -->
|
||||
[ ] Feature request
|
||||
[ ] Documentation issue or request
|
||||
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||
@ -32,7 +32,7 @@ https://plnkr.co or similar (you can use this template as a starting point: http
|
||||
<!-- Describe the motivation or the concrete use case. -->
|
||||
|
||||
|
||||
## Please tell us about your environment
|
||||
## Environment
|
||||
|
||||
<pre><code>
|
||||
Angular version: X.Y.Z
|
||||
@ -49,8 +49,8 @@ Browser:
|
||||
- [ ] Edge version XX
|
||||
|
||||
For Tooling issues:
|
||||
- Node version: XX <!-- use `node --version` -->
|
||||
- Platform: <!-- Mac, Linux, Windows -->
|
||||
- Node version: XX <!-- run `node --version` -->
|
||||
- Platform: <!-- Mac, Linux, Windows -->
|
||||
|
||||
Others:
|
||||
<!-- Anything else relevant? Operating system version, IDE, package manager, HTTP server, ... -->
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,5 +1,5 @@
|
||||
## PR Checklist
|
||||
Does please check if your PR fulfills the following requirements:
|
||||
Please check if your PR fulfills the following requirements:
|
||||
|
||||
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
|
||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
|
||||
/dist/
|
||||
bazel-*
|
||||
e2e_test.*
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
|
@ -256,8 +256,6 @@ groups:
|
||||
files:
|
||||
include:
|
||||
- "aio/*"
|
||||
exclude:
|
||||
- "aio/content/*"
|
||||
users:
|
||||
- petebacondarwin #primary
|
||||
- IgorMinar
|
||||
@ -268,11 +266,11 @@ groups:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "aio/content/examples/*"
|
||||
- "aio/content/guide/*"
|
||||
- "aio/content/images/*"
|
||||
- "aio/content/tutorial/*"
|
||||
- "aio/content/file-not-found.md"
|
||||
- "aio/content/*"
|
||||
exclude:
|
||||
- "aio/content/marketing/*"
|
||||
- "aio/content/navigation.json"
|
||||
- "aio/content/license.md"
|
||||
users:
|
||||
- juleskremer #primary
|
||||
- Foxandxss
|
||||
@ -285,13 +283,9 @@ groups:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "aio/content/*"
|
||||
exclude:
|
||||
- "aio/content/examples/*"
|
||||
- "aio/content/guide/*"
|
||||
- "aio/content/images/*"
|
||||
- "aio/content/tutorial/*"
|
||||
- "aio/content/file-not-found.md"
|
||||
- "aio/content/marketing/*"
|
||||
- "aio/content/navigation.json"
|
||||
- "aio/content/license.md"
|
||||
users:
|
||||
- juleskremer #primary
|
||||
- stephenfluin
|
||||
|
@ -8,7 +8,7 @@
|
||||
* **animations:** properly cleanup query artificats when animation construction fails ([00de9ff](https://github.com/angular/angular/commit/00de9ff))
|
||||
* **animations:** properly detect state transition changes for object literals ([00c9741](https://github.com/angular/angular/commit/00c9741))
|
||||
* **animations:** properly handle cancelled animation style application ([cf57527](https://github.com/angular/angular/commit/cf57527))
|
||||
* **compiler:** emits quoted keys only iff they are quoted in the original template ([45ae14c](https://github.com/angular/angular/commit/45ae14c)), closes [#14292](https://github.com/angular/angular/issues/14292)
|
||||
* **compiler:** emits quoted keys only if they are quoted in the original template ([45ae14c](https://github.com/angular/angular/commit/45ae14c)), closes [#14292](https://github.com/angular/angular/issues/14292)
|
||||
* **compiler:** fix merge error ([6307581](https://github.com/angular/angular/commit/6307581))
|
||||
* **compiler:** fix types ([5ea9b62](https://github.com/angular/angular/commit/5ea9b62))
|
||||
* **compiler:** remove i18n markup even if no translations ([#17999](https://github.com/angular/angular/issues/17999)) ([2763577](https://github.com/angular/angular/commit/2763577)), closes [#11042](https://github.com/angular/angular/issues/11042)
|
||||
|
@ -17,7 +17,7 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
|
||||
|
||||
## <a name="question"></a> Got a Question or Problem?
|
||||
|
||||
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
|
||||
Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
|
||||
|
||||
Stack Overflow is a much better place to ask questions since:
|
||||
|
||||
@ -25,7 +25,7 @@ Stack Overflow is a much better place to ask questions since:
|
||||
- questions and answers stay available for public viewing so your question / answer might help someone else
|
||||
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||
|
||||
To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to Stack Overflow.
|
||||
To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow.
|
||||
|
||||
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
|
||||
|
||||
|
10
README.md
10
README.md
@ -4,22 +4,20 @@
|
||||
[](http://issuestats.com/github/angular/angular)
|
||||
[](http://issuestats.com/github/angular/angular)
|
||||
[](https://www.npmjs.com/@angular/core)
|
||||
)
|
||||
|
||||
|
||||
[](https://saucelabs.com/u/angular2-ci)
|
||||
|
||||
*Safari (7+), iOS (7+), Edge (14) and IE mobile (11) are tested on [BrowserStack][browserstack].*
|
||||
|
||||
Angular
|
||||
=========
|
||||
|
||||
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript (JS) and other languages.
|
||||
# Angular
|
||||
|
||||
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript and other languages.
|
||||
|
||||
## Quickstart
|
||||
|
||||
[Get started in 5 minutes][quickstart].
|
||||
|
||||
|
||||
## Want to help?
|
||||
|
||||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
||||
|
@ -88,6 +88,21 @@ server {
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
||||
# Notify about PR changes
|
||||
location "~^/pr-updated/?$" {
|
||||
if ($request_method != "POST") {
|
||||
add_header Allow "POST";
|
||||
return 405;
|
||||
}
|
||||
|
||||
proxy_pass_request_headers on;
|
||||
proxy_redirect off;
|
||||
proxy_method POST;
|
||||
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
|
||||
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
||||
# Everything else
|
||||
location / {
|
||||
return 404;
|
||||
|
@ -18,45 +18,17 @@ export class BuildCreator extends EventEmitter {
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public changePrVisibility(pr: string, makePublic: boolean): Promise<void> {
|
||||
const {oldPrDir, newPrDir} = this.getCandidatePrDirs(pr, makePublic);
|
||||
|
||||
return Promise.
|
||||
all([this.exists(oldPrDir), this.exists(newPrDir)]).
|
||||
then(([oldPrDirExisted, newPrDirExisted]) => {
|
||||
if (!oldPrDirExisted) {
|
||||
throw new UploadError(404, `Request to move non-existing directory '${oldPrDir}' to '${newPrDir}'.`);
|
||||
} else if (newPrDirExisted) {
|
||||
throw new UploadError(409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||
}
|
||||
|
||||
return Promise.resolve().
|
||||
then(() => shell.mv(oldPrDir, newPrDir)).
|
||||
then(() => this.listShasByDate(newPrDir)).
|
||||
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
|
||||
then(() => undefined);
|
||||
}).
|
||||
catch(err => {
|
||||
if (!(err instanceof UploadError)) {
|
||||
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
|
||||
// Use only part of the SHA for more readable URLs.
|
||||
sha = sha.substr(0, SHORT_SHA_LEN);
|
||||
|
||||
const {oldPrDir: otherVisPrDir, newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
|
||||
const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
|
||||
const shaDir = path.join(prDir, sha);
|
||||
let dirToRemoveOnError: string;
|
||||
|
||||
return Promise.resolve().
|
||||
then(() => this.exists(otherVisPrDir)).
|
||||
// If the same PR exists with different visibility, update the visibility first.
|
||||
then(otherVisPrDirExisted => (otherVisPrDirExisted && this.changePrVisibility(pr, isPublic)) as any).
|
||||
then(() => this.updatePrVisibility(pr, isPublic)).
|
||||
then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
|
||||
then(([prDirExisted, shaDirExisted]) => {
|
||||
if (shaDirExisted) {
|
||||
@ -84,6 +56,36 @@ export class BuildCreator extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
public updatePrVisibility(pr: string, makePublic: boolean): Promise<boolean> {
|
||||
const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
|
||||
|
||||
return Promise.
|
||||
all([this.exists(otherVisPrDir), this.exists(targetVisPrDir)]).
|
||||
then(([otherVisPrDirExisted, targetVisPrDirExisted]) => {
|
||||
if (!otherVisPrDirExisted) {
|
||||
// No visibility change: Either the visibility is up-to-date or the PR does not exist.
|
||||
return false;
|
||||
} else if (targetVisPrDirExisted) {
|
||||
// Error: Directories for both visibilities exist.
|
||||
throw new UploadError(409, `Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
|
||||
}
|
||||
|
||||
// Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
|
||||
return Promise.resolve().
|
||||
then(() => shell.mv(otherVisPrDir, targetVisPrDir)).
|
||||
then(() => this.listShasByDate(targetVisPrDir)).
|
||||
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
|
||||
then(() => true);
|
||||
}).
|
||||
catch(err => {
|
||||
if (!(err instanceof UploadError)) {
|
||||
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected exists(fileOrDir: string): Promise<boolean> {
|
||||
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Imports
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
@ -84,6 +85,7 @@ class UploadServerFactory {
|
||||
|
||||
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
||||
const middleware = express();
|
||||
const jsonParser = bodyParser.json();
|
||||
|
||||
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
|
||||
const pr = req.params[0];
|
||||
@ -96,8 +98,8 @@ class UploadServerFactory {
|
||||
} else if (!archive) {
|
||||
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
||||
} else {
|
||||
buildVerifier.
|
||||
verify(+pr, authHeader).
|
||||
Promise.resolve().
|
||||
then(() => buildVerifier.verify(+pr, authHeader)).
|
||||
then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted).
|
||||
then(isPublic => buildCreator.create(pr, sha, archive, isPublic).
|
||||
then(() => res.sendStatus(isPublic ? 201 : 202))).
|
||||
@ -105,8 +107,23 @@ class UploadServerFactory {
|
||||
}
|
||||
});
|
||||
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
||||
middleware.get('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
||||
middleware.all('*', req => this.throwRequestError(405, 'Unsupported method', req));
|
||||
middleware.post(/^\/pr-updated\/?$/, jsonParser, (req, res) => {
|
||||
const {action, number: prNo}: {action?: string, number?: number} = req.body;
|
||||
const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled');
|
||||
|
||||
if (!visMayHaveChanged) {
|
||||
res.sendStatus(200);
|
||||
} else if (!prNo) {
|
||||
this.throwRequestError(400, `Missing or empty 'number' field`, req);
|
||||
} else {
|
||||
Promise.resolve().
|
||||
then(() => buildVerifier.getPrIsTrusted(prNo)).
|
||||
then(isPublic => buildCreator.updatePrVisibility(String(prNo), isPublic)).
|
||||
then(() => res.sendStatus(200)).
|
||||
catch(err => this.respondWithError(res, err));
|
||||
}
|
||||
});
|
||||
middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
||||
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
|
||||
|
||||
return middleware;
|
||||
@ -125,7 +142,10 @@ class UploadServerFactory {
|
||||
}
|
||||
|
||||
protected throwRequestError(status: number, error: string, req: express.Request) {
|
||||
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
|
||||
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
|
||||
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
|
||||
|
||||
throw new UploadError(status, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,16 @@
|
||||
// Using the values below, we can fake the response of the corresponding methods in tests. This is
|
||||
// necessary, because the test upload-server will be running as a separate node process, so we will
|
||||
// not have direct access to the code (e.g. for mocking).
|
||||
// (See also 'lib/verify-setup/start-test-upload-server.ts'.)
|
||||
|
||||
/* tslint:disable: variable-name */
|
||||
|
||||
// Special values to be used as `authHeader` in `BuildVerifier#verify()`.
|
||||
export const BV_verify_error = 'FAKE_VERIFICATION_ERROR';
|
||||
export const BV_verify_verifiedNotTrusted = 'FAKE_VERIFIED_NOT_TRUSTED';
|
||||
|
||||
// Special values to be used as `pr` in `BuildVerifier#getPrIsTrusted()`.
|
||||
export const BV_getPrIsTrusted_error = 32203;
|
||||
export const BV_getPrIsTrusted_notTrusted = 72457;
|
||||
|
||||
/* tslint:enable: variable-name */
|
@ -317,6 +317,51 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/pr-updated`, () => {
|
||||
const url = `${scheme}://${host}/pr-updated`;
|
||||
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should pass requests through to the upload server', done => {
|
||||
const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`;
|
||||
|
||||
const cmd1 = `${cmdPrefix} ${url}`;
|
||||
const cmd2 = `${cmdPrefix} --data '{"number":${pr}}' ${url}`;
|
||||
const cmd3 = `${cmdPrefix} --data '{"number":${pr},"action":"foo"}' ${url}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)),
|
||||
h.runCmd(cmd2).then(h.verifyResponse(200)),
|
||||
h.runCmd(cmd3).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for unknown URLs (even if the resource exists)', done => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
// Imports
|
||||
import * as path from 'path';
|
||||
import * as c from './constants';
|
||||
import {helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
@ -14,12 +15,14 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
const getFile = (pr: string, sha: string, file: string) =>
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`);
|
||||
const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => {
|
||||
// Using `FAKE_VERIFICATION_ERROR` or `FAKE_VERIFIED_NOT_TRUSTED` as `authHeader`,
|
||||
// we can fake the response of the overwritten `BuildVerifier.verify()` method.
|
||||
// (See 'lib/upload-server/index-test.ts'.)
|
||||
const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`;
|
||||
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
|
||||
};
|
||||
const prUpdated = (pr: number, action?: string) => {
|
||||
const url = `${scheme}://${host}/pr-updated`;
|
||||
const payloadStr = JSON.stringify({number: pr, action});
|
||||
return h.runCmd(`curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`);
|
||||
};
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => {
|
||||
@ -29,7 +32,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
});
|
||||
|
||||
|
||||
describe('for a new PR', () => {
|
||||
describe('for a new/non-existing PR', () => {
|
||||
|
||||
it('should be able to upload and serve a public build', done => {
|
||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
@ -54,7 +57,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||
@ -74,7 +77,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
|
||||
then(h.verifyResponse(403, errorRegex9)).
|
||||
then(() => {
|
||||
expect(h.buildExists(pr9)).toBe(false);
|
||||
@ -83,6 +86,18 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should be able to notify that a PR has been updated (and do nothing)', done => {
|
||||
prUpdated(+pr9).
|
||||
then(h.verifyResponse(200)).
|
||||
then(() => {
|
||||
// The PR should still not exist.
|
||||
expect(h.buildExists(pr9, '', false)).toBe(false);
|
||||
expect(h.buildExists(pr9, '', true)).toBe(false);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -123,7 +138,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
h.createDummyBuild(pr9, sha0, false);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)),
|
||||
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||
@ -148,7 +163,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
h.createDummyBuild(pr9, sha0);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
|
||||
then(h.verifyResponse(403, errorRegex9)).
|
||||
then(() => {
|
||||
expect(h.buildExists(pr9)).toBe(true);
|
||||
@ -186,7 +201,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
h.createDummyBuild(pr9, sha9, false);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
|
||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
||||
then(h.verifyResponse(409)).
|
||||
then(() => {
|
||||
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
||||
@ -195,6 +210,110 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should be able to request re-checking visibility (if outdated)', done => {
|
||||
const publicPr = pr9;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
|
||||
h.createDummyBuild(publicPr, sha9, false);
|
||||
h.createDummyBuild(hiddenPr, sha9, true);
|
||||
|
||||
// PR visibilities are outdated (i.e. the opposte of what the should).
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
||||
|
||||
Promise.
|
||||
all([
|
||||
prUpdated(+publicPr).then(h.verifyResponse(200)),
|
||||
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
|
||||
]).
|
||||
then(() => {
|
||||
// PR visibilities should have been updated.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
}).
|
||||
then(() => {
|
||||
h.deletePrDir(publicPr, true);
|
||||
h.deletePrDir(hiddenPr, false);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should be able to request re-checking visibility (if up-to-date)', done => {
|
||||
const publicPr = pr9;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
|
||||
h.createDummyBuild(publicPr, sha9, true);
|
||||
h.createDummyBuild(hiddenPr, sha9, false);
|
||||
|
||||
// PR visibilities are already up-to-date.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
|
||||
Promise.
|
||||
all([
|
||||
prUpdated(+publicPr).then(h.verifyResponse(200)),
|
||||
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
|
||||
]).
|
||||
then(() => {
|
||||
// PR visibilities are still up-to-date.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject a request if re-checking visibility fails', done => {
|
||||
const errorPr = String(c.BV_getPrIsTrusted_error);
|
||||
|
||||
h.createDummyBuild(errorPr, sha9, true);
|
||||
|
||||
expect(h.buildExists(errorPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(errorPr, '', true)).toBe(true);
|
||||
|
||||
prUpdated(+errorPr).
|
||||
then(h.verifyResponse(500, /Test/)).
|
||||
then(() => {
|
||||
// PR visibility should not have been updated.
|
||||
expect(h.buildExists(errorPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(errorPr, '', true)).toBe(true);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject a request if updating visibility fails', done => {
|
||||
// One way to cause an error is to have both a public and a hidden directory for the same PR.
|
||||
h.createDummyBuild(pr9, sha9, false);
|
||||
h.createDummyBuild(pr9, sha9, true);
|
||||
|
||||
const hiddenPrDir = h.getPrDir(pr9, false);
|
||||
const publicPrDir = h.getPrDir(pr9, true);
|
||||
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
|
||||
|
||||
expect(h.buildExists(pr9, '', false)).toBe(true);
|
||||
expect(h.buildExists(pr9, '', true)).toBe(true);
|
||||
|
||||
prUpdated(+pr9).
|
||||
then(h.verifyResponse(409, bodyRegex)).
|
||||
then(() => {
|
||||
// PR visibility should not have been updated.
|
||||
expect(h.buildExists(pr9, '', false)).toBe(true);
|
||||
expect(h.buildExists(pr9, '', true)).toBe(true);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}));
|
||||
|
@ -1,17 +1,31 @@
|
||||
// Imports
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
|
||||
import {UploadError} from './upload-error';
|
||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../upload-server/build-verifier';
|
||||
import {UploadError} from '../upload-server/upload-error';
|
||||
import * as c from './constants';
|
||||
|
||||
// Run
|
||||
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
||||
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
||||
BuildVerifier.prototype.getPrIsTrusted = (pr: number) => {
|
||||
switch (pr) {
|
||||
case c.BV_getPrIsTrusted_error:
|
||||
// For e2e tests, fake an error.
|
||||
return Promise.reject('Test');
|
||||
case c.BV_getPrIsTrusted_notTrusted:
|
||||
// For e2e tests, fake an untrusted PR (`false`).
|
||||
return Promise.resolve(false);
|
||||
default:
|
||||
// For e2e tests, default to trusted PRs (`true`).
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
|
||||
switch (authHeader) {
|
||||
case 'FAKE_VERIFICATION_ERROR':
|
||||
case c.BV_verify_error:
|
||||
// For e2e tests, fake a verification error.
|
||||
return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`));
|
||||
case 'FAKE_VERIFIED_NOT_TRUSTED':
|
||||
case c.BV_verify_verifiedNotTrusted:
|
||||
// For e2e tests, fake a `verifiedNotTrusted` verification status.
|
||||
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
|
||||
default:
|
||||
@ -21,4 +35,4 @@ BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
require('./index');
|
||||
require('../upload-server/index');
|
@ -1,6 +1,7 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as c from './constants';
|
||||
import {CmdResult, helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
@ -25,13 +26,13 @@ describe('upload-server (on HTTP)', () => {
|
||||
|
||||
it('should disallow non-GET requests', done => {
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = /^Unsupported method/;
|
||||
const bodyRegex = /^Unknown resource/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
@ -63,7 +64,7 @@ describe('upload-server (on HTTP)', () => {
|
||||
|
||||
|
||||
it('should reject requests for which the PR verification fails', done => {
|
||||
const headers = `--header "Authorization: FAKE_VERIFICATION_ERROR" ${xFileHeader}`;
|
||||
const headers = `--header "Authorization: ${c.BV_verify_error}" ${xFileHeader}`;
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`);
|
||||
|
||||
@ -107,7 +108,7 @@ describe('upload-server (on HTTP)', () => {
|
||||
|
||||
[true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => {
|
||||
const authorizationHeader2 = isPublic ?
|
||||
authorizationHeader : '--header "Authorization: FAKE_VERIFIED_NOT_TRUSTED"';
|
||||
authorizationHeader : `--header "Authorization: ${c.BV_verify_verifiedNotTrusted}"`;
|
||||
const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`);
|
||||
|
||||
|
||||
@ -373,27 +374,194 @@ describe('upload-server (on HTTP)', () => {
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/pr-updated`, () => {
|
||||
const url = `http://${host}/pr-updated`;
|
||||
|
||||
// Helpers
|
||||
const curl = (payload?: {number: number, action?: string}) => {
|
||||
const payloadStr = payload && JSON.stringify(payload) || '';
|
||||
return `curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`;
|
||||
};
|
||||
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
const bodyRegex = /^Unknown resource in request/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without a payload', done => {
|
||||
const bodyRegex = /^Missing or empty 'number' field in request/;
|
||||
|
||||
h.runCmd(curl()).
|
||||
then(h.verifyResponse(400, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without a \'number\' field', done => {
|
||||
const bodyRegex = /^Missing or empty 'number' field in request/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(curl({} as any)).then(h.verifyResponse(400, bodyRegex)),
|
||||
h.runCmd(curl({number: null} as any)).then(h.verifyResponse(400, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject requests for which checking the PR visibility fails', done => {
|
||||
h.runCmd(curl({number: c.BV_getPrIsTrusted_error})).
|
||||
then(h.verifyResponse(500, /Test/)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const mockPayload = JSON.stringify({number: +pr});
|
||||
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" http://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if PR\'s visibility is already up-to-date', done => {
|
||||
const publicPr = pr;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
const checkVisibilities = () => {
|
||||
// Public build is already public.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
// Hidden build is already hidden.
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
};
|
||||
|
||||
h.createDummyBuild(publicPr, sha9, true);
|
||||
h.createDummyBuild(hiddenPr, sha9, false);
|
||||
checkVisibilities();
|
||||
|
||||
Promise.
|
||||
all([
|
||||
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||
]).
|
||||
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
||||
then(checkVisibilities).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if \'action\' implies no visibility change', done => {
|
||||
const publicPr = pr;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
const checkVisibilities = () => {
|
||||
// Public build is hidden atm.
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
||||
// Hidden build is public atm.
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
||||
};
|
||||
|
||||
h.createDummyBuild(publicPr, sha9, false);
|
||||
h.createDummyBuild(hiddenPr, sha9, true);
|
||||
checkVisibilities();
|
||||
|
||||
Promise.
|
||||
all([
|
||||
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||
]).
|
||||
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
||||
then(checkVisibilities).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('when the visiblity has changed', () => {
|
||||
const publicPr = pr;
|
||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||
|
||||
beforeEach(() => {
|
||||
// Create initial PR builds with opposite visibilities as the ones that will be reported:
|
||||
// - The now public PR was previously hidden.
|
||||
// - The now hidden PR was previously public.
|
||||
h.createDummyBuild(publicPr, sha9, false);
|
||||
h.createDummyBuild(hiddenPr, sha9, true);
|
||||
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
||||
});
|
||||
afterEach(() => {
|
||||
// Expect PRs' visibility to have been updated:
|
||||
// - The public PR should be actually public (previously it was hidden).
|
||||
// - The hidden PR should be actually hidden (previously it was public).
|
||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||
|
||||
h.deletePrDir(publicPr, true);
|
||||
h.deletePrDir(hiddenPr, false);
|
||||
});
|
||||
|
||||
|
||||
it('should update the PR\'s visibility (action: undefined)', done => {
|
||||
Promise.all([
|
||||
h.runCmd(curl({number: +publicPr})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr})).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should update the PR\'s visibility (action: labeled)', done => {
|
||||
Promise.all([
|
||||
h.runCmd(curl({number: +publicPr, action: 'labeled'})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr, action: 'labeled'})).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should update the PR\'s visibility (action: unlabeled)', done => {
|
||||
Promise.all([
|
||||
h.runCmd(curl({number: +publicPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
|
||||
h.runCmd(curl({number: +hiddenPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for GET requests to unknown URLs', done => {
|
||||
it('should respond with 404 for requests to unknown URLs', done => {
|
||||
const bodyRegex = /^Unknown resource/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests to any URL', done => {
|
||||
const bodyRegex = /^Unsupported method/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
@ -20,12 +20,14 @@
|
||||
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.17.2",
|
||||
"express": "^4.14.1",
|
||||
"jasmine": "^2.5.3",
|
||||
"jsonwebtoken": "^7.3.0",
|
||||
"shelljs": "^0.7.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.16.4",
|
||||
"@types/express": "^4.0.35",
|
||||
"@types/jasmine": "^2.5.43",
|
||||
"@types/jsonwebtoken": "^7.2.0",
|
||||
|
@ -43,178 +43,25 @@ describe('BuildCreator', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('changePrVisibility()', () => {
|
||||
let bcEmitSpy: jasmine.Spy;
|
||||
let bcExistsSpy: jasmine.Spy;
|
||||
let bcListShasByDate: jasmine.Spy;
|
||||
let shellMvSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bcEmitSpy = spyOn(bc, 'emit');
|
||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
|
||||
shellMvSpy = spyOn(shell, 'mv');
|
||||
|
||||
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
bcListShasByDate.and.returnValue([]);
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.changePrVisibility(pr, true);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `extractArchive()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
[true, false].forEach(makePublic => {
|
||||
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
|
||||
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
|
||||
|
||||
|
||||
it('should rename the directory', done => {
|
||||
bc.changePrVisibility(pr, makePublic).
|
||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should emit a ChangedPrVisibilityEvent on success', done => {
|
||||
let emitted = false;
|
||||
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toEqual(jasmine.any(Array));
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.changePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should include all shas in the emitted event', done => {
|
||||
const shas = ['foo', 'bar', 'baz'];
|
||||
let emitted = false;
|
||||
|
||||
bcListShasByDate.and.returnValue(Promise.resolve(shas));
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
||||
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toBe(shas);
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.changePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if the old directory does not exist', done => {
|
||||
bcExistsSpy.and.callFake((dir: string) => dir !== oldPrDir);
|
||||
bc.changePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 404, `Request to move non-existing directory '${oldPrDir}' to '${newPrDir}'.`);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if the new directory does already exist', done => {
|
||||
bcExistsSpy.and.returnValue(true);
|
||||
bc.changePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to rename the directory', done => {
|
||||
shellMvSpy.and.throwError('');
|
||||
bc.changePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to list the SHAs', done => {
|
||||
bcListShasByDate.and.throwError('');
|
||||
bc.changePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an UploadError', done => {
|
||||
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
||||
bc.changePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pass UploadError instances unmodified', done => {
|
||||
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
||||
bc.changePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('create()', () => {
|
||||
let bcChangePrVisibilitySpy: jasmine.Spy;
|
||||
let bcEmitSpy: jasmine.Spy;
|
||||
let bcExistsSpy: jasmine.Spy;
|
||||
let bcExtractArchiveSpy: jasmine.Spy;
|
||||
let bcUpdatePrVisibilitySpy: jasmine.Spy;
|
||||
let shellMkdirSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bcChangePrVisibilitySpy = spyOn(bc, 'changePrVisibility');
|
||||
bcEmitSpy = spyOn(bc, 'emit');
|
||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
|
||||
bcUpdatePrVisibilitySpy = spyOn(bc, 'updatePrVisibility');
|
||||
shellMkdirSpy = spyOn(shell, 'mkdir');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
});
|
||||
|
||||
|
||||
[true, false].forEach(isPublic => {
|
||||
const otherVisPrDir = isPublic ? hiddenPrDir : publicPrDir;
|
||||
const prDir = isPublic ? publicPrDir : hiddenPrDir;
|
||||
const shaDir = isPublic ? publicShaDir : hiddenShaDir;
|
||||
|
||||
@ -228,20 +75,12 @@ describe('BuildCreator', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should not update the PR\'s visibility first if not necessary', done => {
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(bcChangePrVisibilitySpy).not.toHaveBeenCalled()).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should update the PR\'s visibility first if necessary', done => {
|
||||
bcChangePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
|
||||
bcExistsSpy.and.callFake((dir: string) => dir === otherVisPrDir);
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => {
|
||||
expect(bcChangePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
@ -286,7 +125,6 @@ describe('BuildCreator', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
existsValues = {
|
||||
[otherVisPrDir]: false,
|
||||
[prDir]: false,
|
||||
[shaDir]: false,
|
||||
};
|
||||
@ -297,14 +135,12 @@ describe('BuildCreator', () => {
|
||||
|
||||
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
|
||||
const mockError = new UploadError(543, 'Test');
|
||||
|
||||
existsValues[otherVisPrDir] = true;
|
||||
bcChangePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
|
||||
bcUpdatePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expect(err).toBe(mockError);
|
||||
|
||||
expect(bcExistsSpy).toHaveBeenCalledTimes(1);
|
||||
expect(bcExistsSpy).not.toHaveBeenCalled();
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
@ -327,8 +163,10 @@ describe('BuildCreator', () => {
|
||||
|
||||
|
||||
it('should detect existing build directory after visibility change', done => {
|
||||
existsValues[otherVisPrDir] = true;
|
||||
bcChangePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
|
||||
|
||||
expect(bcExistsSpy(prDir)).toBe(false);
|
||||
expect(bcExistsSpy(shaDir)).toBe(false);
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
|
||||
@ -406,6 +244,190 @@ describe('BuildCreator', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('updatePrVisibility()', () => {
|
||||
let bcEmitSpy: jasmine.Spy;
|
||||
let bcExistsSpy: jasmine.Spy;
|
||||
let bcListShasByDate: jasmine.Spy;
|
||||
let shellMvSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bcEmitSpy = spyOn(bc, 'emit');
|
||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
|
||||
shellMvSpy = spyOn(shell, 'mv');
|
||||
|
||||
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
bcListShasByDate.and.returnValue([]);
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.updatePrVisibility(pr, true);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `extractArchive()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
[true, false].forEach(makePublic => {
|
||||
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
|
||||
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
|
||||
|
||||
|
||||
it('should rename the directory', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('when the visibility is updated', () => {
|
||||
|
||||
it('should resolve to true', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => expect(result).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rename the directory', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should emit a ChangedPrVisibilityEvent on success', done => {
|
||||
let emitted = false;
|
||||
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toEqual(jasmine.any(Array));
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should include all shas in the emitted event', done => {
|
||||
const shas = ['foo', 'bar', 'baz'];
|
||||
let emitted = false;
|
||||
|
||||
bcListShasByDate.and.returnValue(Promise.resolve(shas));
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
||||
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toBe(shas);
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if the visibility is already up-to-date', done => {
|
||||
bcExistsSpy.and.callFake((dir: string) => dir === newPrDir);
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => {
|
||||
expect(result).toBe(false);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if the PR directory does not exist', done => {
|
||||
bcExistsSpy.and.returnValue(false);
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => {
|
||||
expect(result).toBe(false);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if both directories exist', done => {
|
||||
bcExistsSpy.and.returnValue(true);
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to rename the directory', done => {
|
||||
shellMvSpy.and.throwError('');
|
||||
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to list the SHAs', done => {
|
||||
bcListShasByDate.and.throwError('');
|
||||
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an UploadError', done => {
|
||||
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pass UploadError instances unmodified', done => {
|
||||
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBeUploadError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('exists()', () => {
|
||||
|
@ -258,12 +258,12 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests', done => {
|
||||
it('should respond with 404 for non-GET requests', done => {
|
||||
verifyRequests([
|
||||
agent.put(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.post(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.patch(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.delete(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.put(`/create-build/${pr}/${sha}`).expect(404),
|
||||
agent.post(`/create-build/${pr}/${sha}`).expect(404),
|
||||
agent.patch(`/create-build/${pr}/${sha}`).expect(404),
|
||||
agent.delete(`/create-build/${pr}/${sha}`).expect(404),
|
||||
], done);
|
||||
});
|
||||
|
||||
@ -418,12 +418,12 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests', done => {
|
||||
it('should respond with 404 for non-GET requests', done => {
|
||||
verifyRequests([
|
||||
agent.put('/health-check').expect(405),
|
||||
agent.post('/health-check').expect(405),
|
||||
agent.patch('/health-check').expect(405),
|
||||
agent.delete('/health-check').expect(405),
|
||||
agent.put('/health-check').expect(404),
|
||||
agent.post('/health-check').expect(404),
|
||||
agent.patch('/health-check').expect(404),
|
||||
agent.delete('/health-check').expect(404),
|
||||
], done);
|
||||
});
|
||||
|
||||
@ -442,11 +442,141 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('GET *', () => {
|
||||
describe('POST /pr-updated', () => {
|
||||
const pr = '9';
|
||||
const url = '/pr-updated';
|
||||
let bvGetPrIsTrustedSpy: jasmine.Spy;
|
||||
let bcUpdatePrVisibilitySpy: jasmine.Spy;
|
||||
|
||||
// Helpers
|
||||
const createRequest = (num: number, action?: string) =>
|
||||
agent.post(url).send({number: num, action});
|
||||
|
||||
beforeEach(() => {
|
||||
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted');
|
||||
bcUpdatePrVisibilitySpy = spyOn(buildCreator, 'updatePrVisibility');
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for non-POST requests', done => {
|
||||
verifyRequests([
|
||||
agent.get(url).expect(404),
|
||||
agent.put(url).expect(404),
|
||||
agent.patch(url).expect(404),
|
||||
agent.delete(url).expect(404),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without a payload', done => {
|
||||
const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`;
|
||||
|
||||
const request1 = agent.post(url);
|
||||
const request2 = agent.post(url).send();
|
||||
|
||||
verifyRequests([
|
||||
request1.expect(400, responseBody),
|
||||
request2.expect(400, responseBody),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without a \'number\' field', done => {
|
||||
const responseBodyPrefix = `Missing or empty 'number' field in request: POST ${url}`;
|
||||
|
||||
const request1 = agent.post(url).send({});
|
||||
const request2 = agent.post(url).send({number: null});
|
||||
|
||||
verifyRequests([
|
||||
request1.expect(400, `${responseBodyPrefix} {}`),
|
||||
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', done => {
|
||||
const req = createRequest(+pr);
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9)).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildVerifier', done => {
|
||||
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = createRequest(+pr).expect(500, 'Test');
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => {
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', done => {
|
||||
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
||||
|
||||
const req1 = createRequest(24);
|
||||
const req2 = createRequest(42);
|
||||
|
||||
Promise.all([
|
||||
promisifyRequest(req1).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('24', false)),
|
||||
promisifyRequest(req2).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('42', true)),
|
||||
]).then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildCreator', done => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = createRequest(+pr).expect(500, 'Test');
|
||||
verifyRequests([req], done);
|
||||
});
|
||||
|
||||
|
||||
describe('on success', () => {
|
||||
|
||||
it('should respond with 200 (action: undefined)', done => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
||||
verifyRequests(reqs, done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (action: labeled)', done => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
||||
verifyRequests(reqs, done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (action: unlabeled)', done => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
||||
verifyRequests(reqs, done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', done => {
|
||||
const promises = ['foo', 'notlabeled'].
|
||||
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])).
|
||||
map(promisifyRequest);
|
||||
|
||||
Promise.all(promises).
|
||||
then(() => {
|
||||
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
it('should respond with 404', done => {
|
||||
const responseBody = 'Unknown resource in request: GET /some/url';
|
||||
verifyRequests([agent.get('/some/url').expect(404, responseBody)], done);
|
||||
});
|
||||
|
||||
});
|
||||
@ -454,14 +584,15 @@ describe('uploadServerFactory', () => {
|
||||
|
||||
describe('ALL *', () => {
|
||||
|
||||
it('should respond with 405', done => {
|
||||
const responseFor = (method: string) => `Unsupported method in request: ${method.toUpperCase()} /some/url`;
|
||||
it('should respond with 404', done => {
|
||||
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
|
||||
|
||||
verifyRequests([
|
||||
agent.put('/some/url').expect(405, responseFor('put')),
|
||||
agent.post('/some/url').expect(405, responseFor('post')),
|
||||
agent.patch('/some/url').expect(405, responseFor('patch')),
|
||||
agent.delete('/some/url').expect(405, responseFor('delete')),
|
||||
agent.get('/some/url').expect(404, responseFor('get')),
|
||||
agent.put('/some/url').expect(404, responseFor('put')),
|
||||
agent.post('/some/url').expect(404, responseFor('post')),
|
||||
agent.patch('/some/url').expect(404, responseFor('patch')),
|
||||
agent.delete('/some/url').expect(404, responseFor('delete')),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
@ -2,13 +2,20 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/body-parser@^1.16.4":
|
||||
version "1.16.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.4.tgz#96f3660e6f88a677fee7250f5a5e6d6bda3c76bb"
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/express-serve-static-core@*":
|
||||
version "4.0.48"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.48.tgz#b4fa06b0fce282e582b4535ff7fac85cc90173e9"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/express@^4.0.35":
|
||||
"@types/express@*", "@types/express@^4.0.35":
|
||||
version "4.0.36"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.36.tgz#14eb47de7ecb10319f0a2fb1cf971aa8680758c2"
|
||||
dependencies:
|
||||
@ -236,6 +243,21 @@ block-stream@*:
|
||||
dependencies:
|
||||
inherits "~2.0.0"
|
||||
|
||||
body-parser@^1.17.2:
|
||||
version "1.17.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee"
|
||||
dependencies:
|
||||
bytes "2.4.0"
|
||||
content-type "~1.0.2"
|
||||
debug "2.6.7"
|
||||
depd "~1.1.0"
|
||||
http-errors "~1.6.1"
|
||||
iconv-lite "0.4.15"
|
||||
on-finished "~2.3.0"
|
||||
qs "6.4.0"
|
||||
raw-body "~2.2.0"
|
||||
type-is "~1.6.15"
|
||||
|
||||
boom@2.x.x:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
|
||||
@ -273,6 +295,10 @@ buffer-equal-constant-time@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
||||
|
||||
bytes@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
|
||||
|
||||
caller-path@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
|
||||
@ -1158,6 +1184,10 @@ http-signature@~1.1.0:
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
iconv-lite@0.4.15:
|
||||
version "0.4.15"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
|
||||
|
||||
ignore-by-default@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||
@ -1958,6 +1988,14 @@ range-parser@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
|
||||
|
||||
raw-body@~2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96"
|
||||
dependencies:
|
||||
bytes "2.4.0"
|
||||
iconv-lite "0.4.15"
|
||||
unpipe "1.0.0"
|
||||
|
||||
rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
|
||||
@ -2477,7 +2515,7 @@ unique-string@^1.0.0:
|
||||
dependencies:
|
||||
crypto-random-string "^1.0.0"
|
||||
|
||||
unpipe@~1.0.0:
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
||||
|
@ -21,7 +21,7 @@ appName=aio-upload-server-test
|
||||
if [[ "$1" == "stop" ]]; then
|
||||
pm2 delete $appName
|
||||
else
|
||||
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server/index-test.js \
|
||||
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup/start-test-upload-server.js \
|
||||
--log /var/log/aio/upload-server-test.log \
|
||||
--name $appName \
|
||||
--no-autorestart \
|
||||
|
@ -3,10 +3,9 @@
|
||||
|
||||
TODO (gkalpak): Add docs. Mention:
|
||||
- Travis' JWT addon (+ limitations).
|
||||
Relevant files: `.travis.yml`
|
||||
Relevant files: `.travis.yml`, `scripts/ci/env.sh`
|
||||
- Testing on CI.
|
||||
Relevant files: `ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
||||
- Preverifying on CI.
|
||||
Relevant files: `ci/deploy.sh`, `aio/aio-builds-setup/scripts/travis-preverify-pr.sh`
|
||||
Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
||||
- Deploying from CI.
|
||||
Relevant files: `ci/deploy.sh`, `aio/scripts/deploy-preview.sh`
|
||||
Relevant files: `scripts/ci/deploy.sh`, `aio/scripts/deploy-preview.sh`,
|
||||
`aio/scripts/deploy-to-firebase.sh`
|
||||
|
@ -80,13 +80,31 @@ More info on the possible HTTP status codes and their meaning can be found
|
||||
[here](overview--http-status-codes.md).
|
||||
|
||||
|
||||
### Updating PR visibility
|
||||
- nginx receives a natification that a PR has been updated and passes it through to the
|
||||
upload-server. This could, for example, be sent by a GitHub webhook every time a PR's labels
|
||||
change.
|
||||
E.g.: `ngbuilds.io/pr-updated` (payload: `{"number":<PR>,"action":"labeled"}`)
|
||||
- The request contains the PR number (as `number`) and optionally the action that triggered the
|
||||
request (as `action`) in the payload.
|
||||
- The upload-server verifies the payload and determines whether the `action` (if specified) could
|
||||
have led to PR visibility changes. Only requests that omit the `action` field altogether or
|
||||
specify an action that can affect visibility are further processed.
|
||||
(Currently, the only actions that are considered capable of affecting visibility are `labeled` and
|
||||
`unlabeled`.)
|
||||
- The upload-server re-checks and if necessary updates the PR's visibility.
|
||||
|
||||
More info on the possible HTTP status codes and their meaning can be found
|
||||
[here](overview--http-status-codes.md).
|
||||
|
||||
|
||||
### Serving build artifacts
|
||||
- nginx receives a request for an uploaded resource on a subdomain corresponding to the PR and SHA.
|
||||
E.g.: `pr<PR>-<SHA>.ngbuilds.io/path/to/resource`
|
||||
- nginx maps the subdomain to the correct sub-directory and serves the resource.
|
||||
E.g.: `/<PR>/<SHA>/path/to/resource`
|
||||
|
||||
Again, more info on the possible HTTP status codes and their meaning can be found
|
||||
More info on the possible HTTP status codes and their meaning can be found
|
||||
[here](overview--http-status-codes.md).
|
||||
|
||||
|
||||
|
@ -42,10 +42,6 @@ with a bried explanation of what they mean:
|
||||
- **403 (Forbidden)**:
|
||||
Unable to verify build (e.g. invalid JWT token, or unable to talk to 3rd-party APIs, etc).
|
||||
|
||||
- **404 (Not Found)**:
|
||||
Tried to change PR visibility but the source directory did not exist.
|
||||
(Currently, this can only happen as a rare race condition during build deployment.)
|
||||
|
||||
- **405 (Method Not Allowed)**:
|
||||
Request method other than POST.
|
||||
|
||||
@ -57,6 +53,28 @@ with a bried explanation of what they mean:
|
||||
Payload larger than size specified in `AIO_UPLOAD_MAX_SIZE`.
|
||||
|
||||
|
||||
## `https://ngbuilds.io/health-check`
|
||||
|
||||
- **200 (OK)**:
|
||||
The server is healthy (i.e. up and running and processing requests).
|
||||
|
||||
|
||||
## `https://ngbuilds.io/pr-updated`
|
||||
|
||||
- **200 (OK)**:
|
||||
Request processed successfully. Processing may or may not have resulted in further actions.
|
||||
|
||||
- **400 (Bad Request)**:
|
||||
No payload or no `number` field in payload.
|
||||
|
||||
- **405 (Method Not Allowed)**:
|
||||
Request method other than POST.
|
||||
|
||||
- **409 (Conflict)**:
|
||||
Request to overwrite existing directory (i.e. directories for both visibilities exist).
|
||||
(Normally, this should not happen.)
|
||||
|
||||
|
||||
## `https://*.ngbuilds.io/*`
|
||||
|
||||
- **404 (Not Found)**:
|
||||
|
@ -16,13 +16,6 @@ available:
|
||||
Can be used for running the tests for `<aio-builds-setup-dir>/dockerbuild/scripts-js/`. This is
|
||||
useful for CI integration. See [here](misc--integrate-with-ci.md) for more info.
|
||||
|
||||
- `travis-preverify-pr.sh`:
|
||||
Can be used for "pre-verifying" a PR before uploading the artifacts to the server. It checks
|
||||
whether the author of the PR is a member of one of the specified GitHub teams (therefore allowed
|
||||
to upload build artifacts) or the PR has the specified "trusted PR" label (meaning it has been
|
||||
manually verified by a trusted member). This is useful for CI integration.
|
||||
See [here](misc--integrate-with-ci.md) for more info.
|
||||
|
||||
- `update-preview-server.sh`:
|
||||
Can be used for updating the docker container (and image) based on the latest changes checked out
|
||||
from a git repository. See [here](vm-setup--update-docker-container.md) for more info.
|
||||
|
@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/_env.sh"
|
||||
|
||||
# Build `scripts-js/`
|
||||
(
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn build
|
||||
)
|
||||
|
||||
# Preverify PR
|
||||
AIO_GITHUB_ORGANIZATION="angular" \
|
||||
AIO_GITHUB_TEAM_SLUGS="angular-core,aio-contributors" \
|
||||
AIO_GITHUB_TOKEN=$(echo ${GITHUB_TEAM_MEMBERSHIP_CHECK_KEY} | rev) \
|
||||
AIO_REPO_SLUG=$TRAVIS_REPO_SLUG \
|
||||
AIO_TRUSTED_PR_LABEL="aio: preview" \
|
||||
AIO_PREVERIFY_PR=$TRAVIS_PULL_REQUEST \
|
||||
node "$SCRIPTS_JS_DIR/dist/lib/upload-server/index-preverify-pr"
|
||||
|
||||
# Exit codes:
|
||||
# - 0: The PR can be automatically trusted (i.e. author belongs to trusted team or PR has the "trusted PR" label).
|
||||
# - 1: An error occurred.
|
||||
# - 2: The PR cannot be automatically trusted.
|
@ -12,7 +12,7 @@ import { UserService } from './user.service';
|
||||
})
|
||||
export class TitleComponent {
|
||||
@Input() subtitle = '';
|
||||
title = 'Angular Modules';
|
||||
title = 'NgModules';
|
||||
// #enddocregion v1
|
||||
user = '';
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { Pipe, PipeTransform } from '@angular/core';
|
||||
* Usage:
|
||||
* value | exponentialStrength:exponent
|
||||
* Example:
|
||||
* {{ 2 | exponentialStrength:10}}
|
||||
* {{ 2 | exponentialStrength:10 }}
|
||||
* formats to: 1024
|
||||
*/
|
||||
@Pipe({name: 'exponentialStrength'})
|
||||
|
@ -16,7 +16,7 @@ import { HeroService } from './hero.service'; // <-- #1 import service
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
ReactiveFormsModule // <-- #2 add to Angular module imports
|
||||
ReactiveFormsModule // <-- #2 add to @NgModule imports
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
@ -4,7 +4,7 @@ import { Directive } from '@angular/core';
|
||||
@Directive({
|
||||
selector: '[tohValidator2]',
|
||||
host: {
|
||||
'attr.role': 'button',
|
||||
'[attr.role]': 'role',
|
||||
'(mouseenter)': 'onMouseEnter()'
|
||||
}
|
||||
})
|
||||
|
@ -1000,7 +1000,7 @@ For more information on pipes, see [Pipes](guide/pipes).
|
||||
|
||||
|
||||
## Modules/controllers/components
|
||||
In both AngularJS and Angular, Angular modules help you organize your application into cohesive blocks of functionality.
|
||||
In both AngularJS and Angular, modules help you organize your application into cohesive blocks of functionality.
|
||||
|
||||
In AngularJS, you write the code that provides the model and the methods for the view in a **controller**.
|
||||
In Angular, you build a **component**.
|
||||
@ -1080,18 +1080,18 @@ The Angular code is shown using TypeScript.
|
||||
<td>
|
||||
|
||||
|
||||
### Angular modules
|
||||
### NgModules
|
||||
<code-example hideCopy path="ajs-quick-reference/src/app/app.module.1.ts" linenums="false">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
Angular modules, defined with the `NgModule` decorator, serve the same purpose:
|
||||
NgModules, defined with the `NgModule` decorator, serve the same purpose:
|
||||
|
||||
* `imports`: specifies the list of other modules that this module depends upon
|
||||
* `declaration`: keeps track of your components, pipes, and directives.
|
||||
|
||||
For more information on modules, see [Angular Modules (NgModule)](guide/ngmodule).
|
||||
For more information on modules, see [NgModules](guide/ngmodule).
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
@ -475,7 +475,7 @@ You'll need separate TypeScript configuration files such as these:
|
||||
<div class="callout is-helpful">
|
||||
|
||||
<header>
|
||||
@Types and node modules
|
||||
`@types` and node modules
|
||||
</header>
|
||||
|
||||
In the file structure of _this particular sample project_,
|
||||
@ -568,7 +568,7 @@ Run the following command to generate the map.
|
||||
</code-example>
|
||||
|
||||
The `source-map-explorer` analyzes the source map generated with the bundle and draws a map of all dependencies,
|
||||
showing exactly which application and Angular modules and classes are included in the bundle.
|
||||
showing exactly which application and NgModules and classes are included in the bundle.
|
||||
|
||||
Here's the map for _Tour of Heroes_.
|
||||
|
||||
|
@ -31,21 +31,21 @@ You'll learn the details in the pages that follow. For now, focus on the big pic
|
||||
<img src="generated/images/guide/architecture/module.png" alt="Component" class="left">
|
||||
|
||||
|
||||
Angular apps are modular and Angular has its own modularity system called _Angular modules_ or _NgModules_.
|
||||
Angular apps are modular and Angular has its own modularity system called _NgModules_.
|
||||
|
||||
_Angular modules_ are a big deal.
|
||||
This page introduces modules; the [Angular modules](guide/ngmodule) page covers them in depth.
|
||||
NgModules are a big deal.
|
||||
This page introduces modules; the [NgModules](guide/ngmodule) page covers them in depth.
|
||||
|
||||
<br class="clear">
|
||||
|
||||
Every Angular app has at least one Angular module class, [the _root module_](guide/bootstrapping "AppModule: the root module"),
|
||||
Every Angular app has at least one NgModule class, [the _root module_](guide/bootstrapping "Bootstrapping"),
|
||||
conventionally named `AppModule`.
|
||||
|
||||
While the _root module_ may be the only module in a small application, most apps have many more
|
||||
_feature modules_, each a cohesive block of code dedicated to an application domain,
|
||||
a workflow, or a closely related set of capabilities.
|
||||
|
||||
An Angular module, whether a _root_ or _feature_, is a class with an `@NgModule` decorator.
|
||||
An NgModule, whether a _root_ or _feature_, is a class with an `@NgModule` decorator.
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
@ -87,12 +87,12 @@ During development you're likely to bootstrap the `AppModule` in a `main.ts` fil
|
||||
|
||||
<code-example path="architecture/src/main.ts" title="src/main.ts" linenums="false"></code-example>
|
||||
|
||||
### Angular modules vs. JavaScript modules
|
||||
### NgModules vs. JavaScript modules
|
||||
|
||||
The Angular module — a class decorated with `@NgModule` — is a fundamental feature of Angular.
|
||||
The NgModule — a class decorated with `@NgModule` — is a fundamental feature of Angular.
|
||||
|
||||
JavaScript also has its own module system for managing collections of JavaScript objects.
|
||||
It's completely different and unrelated to the Angular module system.
|
||||
It's completely different and unrelated to the NgModule system.
|
||||
|
||||
In JavaScript each _file_ is a module and all objects defined in the file belong to that module.
|
||||
The module declares some objects to be public by marking them with the `export` key word.
|
||||
@ -124,7 +124,7 @@ For example, import Angular's `Component` decorator from the `@angular/core` lib
|
||||
|
||||
<code-example path="architecture/src/app/app.component.ts" region="import" linenums="false"></code-example>
|
||||
|
||||
You also import Angular _modules_ from Angular _libraries_ using JavaScript import statements:
|
||||
You also import NgModules_ from Angular _libraries_ using JavaScript import statements:
|
||||
|
||||
<code-example path="architecture/src/app/mini-app.ts" region="import-browser-module" linenums="false"></code-example>
|
||||
|
||||
@ -139,7 +139,7 @@ Hang in there. The confusion yields to clarity with time and experience.
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
Learn more from the [Angular modules](guide/ngmodule) page.
|
||||
Learn more from the [NgModules](guide/ngmodule) page.
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Bootstrapping
|
||||
|
||||
An Angular module class describes how the application parts fit together.
|
||||
Every application has at least one Angular module, the _root_ module
|
||||
that you [bootstrap](guide/bootstrapping#main) to launch the application.
|
||||
An NgModule class describes how the application parts fit together.
|
||||
Every application has at least one NgModule, the _root_ module
|
||||
that you [bootstrap](guide/appmodule#main) to launch the application.
|
||||
You can call it anything you want. The conventional name is `AppModule`.
|
||||
|
||||
The [setup](guide/setup) instructions produce a new project with the following minimal `AppModule`.
|
||||
@ -17,15 +17,15 @@ You'll evolve this module as your application grows.
|
||||
After the `import` statements, you come to a class adorned with the
|
||||
**`@NgModule`** [_decorator_](guide/glossary#decorator '"Decorator" explained').
|
||||
|
||||
The `@NgModule` decorator identifies `AppModule` as an Angular module class (also called an `NgModule` class).
|
||||
The `@NgModule` decorator identifies `AppModule` as an `NgModule` class.
|
||||
`@NgModule` takes a _metadata_ object that tells Angular how to compile and launch the application.
|
||||
|
||||
* **_imports_** — the `BrowserModule` that this and every application needs to run in a browser.
|
||||
* **_declarations_** — the application's lone component, which is also ...
|
||||
* **_bootstrap_** — the _root_ component that Angular creates and inserts into the `index.html` host web page.
|
||||
|
||||
The [Angular Modules (NgModule)](guide/ngmodule) guide dives deeply into the details of Angular modules.
|
||||
All you need to know at the moment is a few basics about these three properties.
|
||||
The [NgModules](guide/ngmodule) guide dives deeply into the details of NgModules.
|
||||
All you need to know at the moment is a few basics about these three properties.
|
||||
|
||||
|
||||
{@a imports}
|
||||
@ -33,8 +33,8 @@ All you need to know at the moment is a few basics about these three properties.
|
||||
|
||||
### The _imports_ array
|
||||
|
||||
Angular modules are a way to consolidate features that belong together into discrete units.
|
||||
Many features of Angular itself are organized as Angular modules.
|
||||
NgModules are a way to consolidate features that belong together into discrete units.
|
||||
Many features of Angular itself are organized as NgModules.
|
||||
HTTP services are in the `HttpModule`. The router is in the `RouterModule`.
|
||||
Eventually you may create a feature module.
|
||||
|
||||
@ -61,7 +61,7 @@ Other guide and cookbook pages will tell you when you need to add additional mod
|
||||
|
||||
|
||||
|
||||
The `import` statements at the top of the file and the Angular module's `imports` array
|
||||
The `import` statements at the top of the file and the NgModule's `imports` array
|
||||
are unrelated and have completely different jobs.
|
||||
|
||||
The _JavaScript_ `import` statements give you access to symbols _exported_ by other files
|
||||
@ -70,8 +70,8 @@ You add `import` statements to almost every application file.
|
||||
They have nothing to do with Angular and Angular knows nothing about them.
|
||||
|
||||
The _module's_ `imports` array appears _exclusively_ in the `@NgModule` metadata object.
|
||||
It tells Angular about specific _other_ Angular modules — all of them classes decorated with `@NgModule` —
|
||||
that the application needs to function properly.
|
||||
It tells Angular about specific _other_ NgModules—all of them classes decorated
|
||||
with `@NgModule`—that the application needs to function properly.
|
||||
|
||||
</div>
|
||||
|
||||
@ -178,11 +178,11 @@ This file is very stable. Once you've set it up, you may never change it again.
|
||||
|
||||
|
||||
|
||||
## More about Angular Modules
|
||||
## More about NgModules
|
||||
|
||||
Your initial app has only a single module, the _root_ module.
|
||||
As your app grows, you'll consider subdividing it into multiple "feature" modules,
|
||||
some of which can be loaded later ("lazy loaded") if and when the user chooses
|
||||
to visit those features.
|
||||
|
||||
When you're ready to explore these possibilities, visit the [Angular Modules (NgModule)](guide/ngmodule) guide.
|
||||
When you're ready to explore these possibilities, visit the [NgModules](guide/ngmodule) guide.
|
||||
|
@ -199,7 +199,7 @@ The new "angular-in-memory-web-api" has new features.
|
||||
|
||||
## "Style Guide" with _NgModules_ (2016-09-27)
|
||||
|
||||
[StyleGuide](guide/styleguide) explains recommended conventions for Angular modules (NgModule).
|
||||
[StyleGuide](guide/styleguide) explains recommended conventions for NgModules.
|
||||
Barrels now are far less useful and have been removed from the style guide;
|
||||
they remain valuable but are not a matter of Angular style.
|
||||
Also relaxed the rule that discouraged use of the `@Component.host` property.
|
||||
|
@ -64,7 +64,7 @@ This gives you a reference to the Angular `NgModel` directive
|
||||
associated with this control that you can use _in the template_
|
||||
to check for control states such as `valid` and `dirty`.
|
||||
|
||||
* The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
|
||||
* The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
|
||||
but only if there are `name` errors and
|
||||
the control is either `dirty` or `touched`.
|
||||
|
||||
@ -321,7 +321,7 @@ This allows you to do the following:
|
||||
|
||||
* Add, change, and remove validation functions on the fly.
|
||||
* Manipulate the control model dynamically from within the component.
|
||||
* [Test](guide/form-validation#testing) validation and control logic with isolated unit tests.
|
||||
* [Test](guide/form-validation#testing-considerations) validation and control logic with isolated unit tests.
|
||||
|
||||
The following sample re-writes the hero form in Reactive Forms style.
|
||||
|
||||
@ -386,7 +386,7 @@ but rather for css styling and accessibility.
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
Currently, Reactive Forms doesn't add the `required` or `aria-required`
|
||||
Currently, Reactive Forms doesn't add the `required` or `aria-required`
|
||||
HTML validation attribute to the DOM element
|
||||
when the control has the `required` validator function.
|
||||
|
||||
@ -455,12 +455,12 @@ to set error messages for the new control model.
|
||||
|
||||
## Built-in validators
|
||||
|
||||
Angular forms include a number of built-in validator functions, which are functions
|
||||
that help you check common user input in forms. In addition to the built-in
|
||||
validators covered here of `minlength`, `maxlength`,
|
||||
and `required`, there are others such as `email` and `pattern`
|
||||
for Reactive Forms.
|
||||
For a full list of built-in validators,
|
||||
Angular forms include a number of built-in validator functions, which are functions
|
||||
that help you check common user input in forms. In addition to the built-in
|
||||
validators covered here of `minlength`, `maxlength`,
|
||||
and `required`, there are others such as `email` and `pattern`
|
||||
for Reactive Forms.
|
||||
For a full list of built-in validators,
|
||||
see the [Validators](api/forms/Validators) API reference.
|
||||
|
||||
|
||||
@ -486,7 +486,7 @@ Learn more about `FormBuilder` in the [Introduction to FormBuilder](guide/reacti
|
||||
#### Committing hero value changes
|
||||
|
||||
In two-way data binding, the user's changes flow automatically from the controls back to the data model properties.
|
||||
A Reactive Forms component should not use data binding to
|
||||
A Reactive Forms component should not use data binding to
|
||||
automatically update data model properties.
|
||||
The developer decides _when and how_ to update the data model from control values.
|
||||
|
||||
|
@ -214,10 +214,10 @@ There are three changes:
|
||||
|
||||
1. You import `FormsModule` and the new `HeroFormComponent`.
|
||||
|
||||
1. You add the `FormsModule` to the list of `imports` defined in the `ngModule` decorator. This gives the application
|
||||
1. You add the `FormsModule` to the list of `imports` defined in the `@NgModule` decorator. This gives the application
|
||||
access to all of the template-driven forms features, including `ngModel`.
|
||||
|
||||
1. You add the `HeroFormComponent` to the list of `declarations` defined in the `ngModule` decorator. This makes
|
||||
1. You add the `HeroFormComponent` to the list of `declarations` defined in the `@NgModule` decorator. This makes
|
||||
the `HeroFormComponent` component visible throughout this module.
|
||||
|
||||
|
||||
|
@ -25,15 +25,8 @@ to a module factory, meaning you don't need to include the Angular compiler in y
|
||||
Ahead-of-time compiled applications also benefit from decreased load time and increased performance.
|
||||
|
||||
|
||||
## Angular module
|
||||
|
||||
Helps you organize an application into cohesive blocks of functionality.
|
||||
An Angular module identifies the components, directives, and pipes that the application uses along with the list of external Angular modules that the application needs, such as `FormsModule`.
|
||||
|
||||
Every Angular application has an application root-module class. By convention, the class is
|
||||
called `AppModule` and resides in a file named `app.module.ts`.
|
||||
|
||||
For details and examples, see the [Angular Modules (NgModule)](guide/ngmodule) page.
|
||||
</div>
|
||||
|
||||
|
||||
## Annotation
|
||||
@ -115,7 +108,7 @@ The Angular [scoped packages](guide/glossary#scoped-package) each have a barrel
|
||||
|
||||
|
||||
|
||||
You can often achieve the same result using [Angular modules](guide/glossary#angular-module) instead.
|
||||
You can often achieve the same result using [NgModules](guide/glossary#ngmodule) instead.
|
||||
|
||||
|
||||
</div>
|
||||
@ -132,7 +125,11 @@ between a "token"—also referred to as a "key"—and a dependency [prov
|
||||
|
||||
## Bootstrap
|
||||
|
||||
You launch an Angular application by "bootstrapping" it using the application root Angular module (`AppModule`).
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
You launch an Angular application by "bootstrapping" it using the application root NgModule (`AppModule`).
|
||||
|
||||
Bootstrapping identifies an application's top level "root" [component](guide/glossary#component),
|
||||
which is the first component that is loaded for the application.
|
||||
For more information, see the [Setup](guide/setup) page.
|
||||
@ -346,13 +343,13 @@ elements and their children.
|
||||
The [official JavaScript language specification](https://en.wikipedia.org/wiki/ECMAScript).
|
||||
|
||||
The latest approved version of JavaScript is
|
||||
[ECMAScript 2016](http://www.ecma-international.org/ecma-262/7.0/)
|
||||
(also known as "ES2016" or "ES7"). Many Angular developers write their applications
|
||||
in ES7 or a dialect that strives to be
|
||||
[ECMAScript 2017](http://www.ecma-international.org/ecma-262/8.0/)
|
||||
(also known as "ES2017" or "ES8"). Many Angular developers write their applications
|
||||
in ES8 or a dialect that strives to be
|
||||
compatible with it, such as [TypeScript](guide/glossary#typescript).
|
||||
|
||||
Most modern browsers only support the much older "ECMAScript 5" (also known as "ES5") standard.
|
||||
Applications written in ES2016, ES2015, or one of their dialects must be [transpiled](guide/glossary#transpile)
|
||||
Applications written in ES2017, ES2016, ES2015, or one of their dialects must be [transpiled](guide/glossary#transpile)
|
||||
to ES5 JavaScript.
|
||||
|
||||
Angular developers can write in ES5 directly.
|
||||
@ -475,8 +472,8 @@ Read more in the [Lifecycle Hooks](guide/lifecycle-hooks) page.
|
||||
|
||||
Angular has the following types of modules:
|
||||
|
||||
* [Angular modules](guide/glossary#angular-module).
|
||||
For details and examples, see the [Angular Modules](guide/ngmodule) page.
|
||||
* [NgModules](guide/glossary#ngmodule).
|
||||
For details and examples, see the [NgModules](guide/ngmodule) page.
|
||||
* ES2015 modules, as described in this section.
|
||||
|
||||
|
||||
@ -493,7 +490,7 @@ In general, you assemble an application from many modules, both the ones you wri
|
||||
A module *exports* something of value in that code, typically one thing such as a class;
|
||||
a module that needs that class *imports* it.
|
||||
|
||||
The structure of Angular modules and the import/export syntax
|
||||
The structure of NgModules and the import/export syntax
|
||||
is based on the [ES2015 module standard](http://www.2ality.com/2014/09/es6-modules-final.html).
|
||||
|
||||
An application that adheres to this standard requires a module loader to
|
||||
@ -511,6 +508,24 @@ You rarely access Angular feature modules directly. You usually import them from
|
||||
|
||||
{@a N}
|
||||
|
||||
|
||||
## NgModule
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
|
||||
|
||||
Helps you organize an application into cohesive blocks of functionality.
|
||||
An NgModule identifies the components, directives, and pipes that the application uses along with the list of external NgModules that the application needs, such as `FormsModule`.
|
||||
|
||||
Every Angular application has an application root-module class. By convention, the class is
|
||||
called `AppModule` and resides in a file named `app.module.ts`.
|
||||
|
||||
For details and examples, see [NgModules](guide/ngmodule).
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{@a O}
|
||||
|
||||
## Observable
|
||||
@ -614,7 +629,9 @@ For more information, see the [Routing & Navigation](guide/router) page.
|
||||
|
||||
## Router module
|
||||
|
||||
A separate [Angular module](guide/glossary#angular-module) that provides the necessary service providers and directives for navigating through application views.
|
||||
<div class="l-sub-section">
|
||||
|
||||
A separate [NgModule](guide/glossary#ngmodule) that provides the necessary service providers and directives for navigating through application views.
|
||||
|
||||
For more information, see the [Routing & Navigation](guide/router) page.
|
||||
|
||||
@ -633,7 +650,7 @@ For more information, see the [Routing & Navigation](guide/router) page.
|
||||
A way to group related *npm* packages.
|
||||
Read more at the [npm-scope](https://docs.npmjs.com/misc/scope) page.
|
||||
|
||||
Angular modules are delivered within *scoped packages* such as `@angular/core`,
|
||||
NgModules are delivered within *scoped packages* such as `@angular/core`,
|
||||
`@angular/common`, `@angular/platform-browser-dynamic`, `@angular/http`, and `@angular/router`.
|
||||
|
||||
Import a scoped package the same way that you import a normal package.
|
||||
@ -795,4 +812,4 @@ asynchronous events by checking for data changes and updating
|
||||
the information it displays via [data bindings](guide/glossary#data-binding).
|
||||
|
||||
Learn more about zones in this
|
||||
[Brian Ford video](https://www.youtube.com/watch?v=3IqtmUscE_U).
|
||||
[Brian Ford video](https://www.youtube.com/watch?v=3IqtmUscE_U).
|
||||
|
@ -1327,7 +1327,7 @@ Here's an _NgModule_ class with imports, exports, and declarations.
|
||||
|
||||
|
||||
|
||||
Of course you use _JavaScript_ modules to write _Angular_ modules as seen in the complete `contact.module.ts` file:
|
||||
Of course you use _JavaScript_ modules to write NgModules as seen in the complete `contact.module.ts` file:
|
||||
|
||||
<code-example path="ngmodule/src/app/contact/contact.module.2.ts" title="src/app/contact/contact.module.ts" linenums="false">
|
||||
|
||||
|
@ -464,7 +464,7 @@ These files go in the root folder next to `src/`.
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Inside `e2e/` live the End-to-End tests.
|
||||
Inside `e2e/` live the end-to-end tests.
|
||||
They shouldn't be inside `src/` because e2e tests are really a separate app that
|
||||
just so happens to test your main app.
|
||||
That's also why they have their own `tsconfig.e2e.json`.
|
||||
@ -493,7 +493,7 @@ These files go in the root folder next to `src/`.
|
||||
|
||||
Configuration for Angular CLI.
|
||||
In this file you can set several defaults and also configure what files are included
|
||||
when your project is build.
|
||||
when your project is built.
|
||||
Check out the official documentation if you want to know more.
|
||||
|
||||
</td>
|
||||
|
@ -347,7 +347,7 @@ Here are the key `Router` terms and their meanings:
|
||||
</td>
|
||||
|
||||
<td>
|
||||
A separate Angular module that provides the necessary service providers
|
||||
A separate NgModule that provides the necessary service providers
|
||||
and directives for navigating through application views.
|
||||
</td>
|
||||
|
||||
@ -3794,7 +3794,7 @@ Take the final step and detach the admin feature set from the main application.
|
||||
The root `AppModule` must neither load nor reference the `AdminModule` or its files.
|
||||
|
||||
In `app.module.ts`, remove the `AdminModule` import statement from the top of the file
|
||||
and remove the `AdminModule` from the Angular module's `imports` array.
|
||||
and remove the `AdminModule` from the NgModule's `imports` array.
|
||||
|
||||
|
||||
{@a can-load-guard}
|
||||
|
@ -2129,12 +2129,12 @@ discourage the `I` prefix.
|
||||
<a href="#toc">Back to top</a>
|
||||
|
||||
|
||||
## Application structure and Angular modules
|
||||
## Application structure and NgModules
|
||||
|
||||
Have a near-term view of implementation and a long-term vision. Start small but keep in mind where the app is heading down the road.
|
||||
|
||||
All of the app's code goes in a folder named `src`.
|
||||
All feature areas are in their own folder, with their own Angular module.
|
||||
All feature areas are in their own folder, with their own NgModule.
|
||||
|
||||
All content is one asset per file. Each component, service, and pipe is in its own file.
|
||||
All third party vendor scripts are stored in another folder and not in the `src` folder.
|
||||
@ -2779,7 +2779,7 @@ and more difficult in a flat structure.
|
||||
|
||||
|
||||
|
||||
**Do** create an Angular module for each feature area.
|
||||
**Do** create an NgModule for each feature area.
|
||||
|
||||
|
||||
</div>
|
||||
@ -2790,7 +2790,7 @@ and more difficult in a flat structure.
|
||||
|
||||
|
||||
|
||||
**Why?** Angular modules make it easy to lazy load routable features.
|
||||
**Why?** NgModules make it easy to lazy load routable features.
|
||||
|
||||
|
||||
</div>
|
||||
@ -2801,7 +2801,7 @@ and more difficult in a flat structure.
|
||||
|
||||
|
||||
|
||||
**Why?** Angular modules make it easier to isolate, test, and re-use features.
|
||||
**Why?** NgModules make it easier to isolate, test, and re-use features.
|
||||
|
||||
|
||||
</div>
|
||||
@ -2827,7 +2827,7 @@ and more difficult in a flat structure.
|
||||
|
||||
|
||||
|
||||
**Do** create an Angular module in the app's root folder,
|
||||
**Do** create an NgModule in the app's root folder,
|
||||
for example, in `/src/app`.
|
||||
|
||||
|
||||
@ -2839,7 +2839,7 @@ for example, in `/src/app`.
|
||||
|
||||
|
||||
|
||||
**Why?** Every app requires at least one root Angular module.
|
||||
**Why?** Every app requires at least one root NgModule.
|
||||
|
||||
|
||||
</div>
|
||||
@ -2888,7 +2888,7 @@ for example, in `/src/app`.
|
||||
|
||||
|
||||
|
||||
**Do** create an Angular module for all distinct features in an application;
|
||||
**Do** create an NgModule for all distinct features in an application;
|
||||
for example, a `Heroes` feature.
|
||||
|
||||
|
||||
|
@ -12,8 +12,8 @@ component class instance (the *component*) and its user-facing template.
|
||||
You may be familiar with the component/template duality from your experience with model-view-controller (MVC) or model-view-viewmodel (MVVM).
|
||||
In Angular, the component plays the part of the controller/viewmodel, and the template represents the view.
|
||||
|
||||
This page is a comprehensive technical reference to the Angular template language.
|
||||
It explains basic principles of the template language and describes most of the syntax that you'll encounter elsewhere in the documentation.
|
||||
This page is a comprehensive technical reference to the Angular template language.
|
||||
It explains basic principles of the template language and describes most of the syntax that you'll encounter elsewhere in the documentation.
|
||||
|
||||
Many code snippets illustrate the points and concepts, all of them available
|
||||
in the <live-example title="Template Syntax Live Code"></live-example>.
|
||||
@ -1155,7 +1155,7 @@ other HTML elements, attributes, properties, and components.
|
||||
They are usually applied to elements as if they were HTML attributes, hence the name.
|
||||
|
||||
Many details are covered in the [_Attribute Directives_](guide/attribute-directives) guide.
|
||||
Many Angular modules such as the [`RouterModule`](guide/router "Routing and Navigation")
|
||||
Many NgMdules such as the [`RouterModule`](guide/router "Routing and Navigation")
|
||||
and the [`FormsModule`](guide/forms "Forms") define their own attribute directives.
|
||||
This section is an introduction to the most commonly used attribute directives:
|
||||
|
||||
@ -1260,7 +1260,7 @@ Two-way data binding with the `NgModel` directive makes that easy. Here's an exa
|
||||
#### _FormsModule_ is required to use _ngModel_
|
||||
|
||||
Before using the `ngModel` directive in a two-way data binding,
|
||||
you must import the `FormsModule` and add it to the Angular module's `imports` list.
|
||||
you must import the `FormsModule` and add it to the NgModule's `imports` list.
|
||||
Learn more about the `FormsModule` and `ngModel` in the
|
||||
[Forms](guide/forms#ngModel) guide.
|
||||
|
||||
@ -1947,12 +1947,12 @@ As of Typescript 2.0, you can enforce [strict null checking](http://www.typescri
|
||||
|
||||
In this mode, typed variables disallow null and undefined by default. The type checker throws an error if you leave a variable unassigned or try to assign null or undefined to a variable whose type disallows null and undefined.
|
||||
|
||||
The type checker also throws an error if it can't determine whether a variable will be null or undefined at runtime.
|
||||
You may know that can't happen but the type checker doesn't know.
|
||||
The type checker also throws an error if it can't determine whether a variable will be null or undefined at runtime.
|
||||
You may know that can't happen but the type checker doesn't know.
|
||||
You tell the type checker that it can't happen by applying the post-fix
|
||||
[_non-null assertion operator (!)_]((http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator "Non-null assertion operator").
|
||||
[_non-null assertion operator (!)_](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator "Non-null assertion operator").
|
||||
|
||||
The _Angular_ **non-null assertion operator (`!`)** serves the same purpose in an Angular template.
|
||||
The _Angular_ **non-null assertion operator (`!`)** serves the same purpose in an Angular template.
|
||||
|
||||
For example, after you use [*ngIf](guide/template-syntax#ngIf) to check that `hero` is defined, you can assert that
|
||||
`hero` properties are also defined.
|
||||
|
@ -427,7 +427,7 @@ and re-attach it to a dynamically-constructed Angular test module
|
||||
tailored specifically for this battery of tests.
|
||||
|
||||
The `configureTestingModule` method takes an `@NgModule`-like metadata object.
|
||||
The metadata object can have most of the properties of a normal [Angular module](guide/ngmodule).
|
||||
The metadata object can have most of the properties of a normal [NgModule](guide/ngmodule).
|
||||
|
||||
_This metadata object_ simply declares the component to test, `BannerComponent`.
|
||||
The metadata lack `imports` because (a) the default testing module configuration already has what `BannerComponent` needs
|
||||
|
@ -62,7 +62,7 @@ There are a few rules in particular that will make it much easier to do
|
||||
* The [Folders-by-Feature Structure](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#folders-by-feature-structure)
|
||||
and [Modularity](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#modularity)
|
||||
rules define similar principles on a higher level of abstraction: Different parts of the
|
||||
application should reside in different directories and Angular modules.
|
||||
application should reside in different directories and NgModules.
|
||||
|
||||
When an application is laid out feature per feature in this way, it can also be
|
||||
migrated one feature at a time. For applications that don't already look like
|
||||
@ -382,12 +382,12 @@ that describes Angular assets in metadata. The differences blossom from there.
|
||||
|
||||
In a hybrid application you run both versions of Angular at the same time.
|
||||
That means that you need at least one module each from both AngularJS and Angular.
|
||||
You will import `UpgradeModule` inside the Angular module, and then use it for
|
||||
You will import `UpgradeModule` inside the NgModule, and then use it for
|
||||
bootstrapping the AngularJS module.
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
Learn more about Angular modules at the [NgModule guide](guide/ngmodule).
|
||||
Read more about [NgModules](guide/ngmodule).
|
||||
|
||||
</div>
|
||||
|
||||
@ -485,7 +485,7 @@ Because `HeroDetailComponent` is an Angular component, you must also add it to t
|
||||
|
||||
And because this component is being used from the AngularJS module, and is an entry point into
|
||||
the Angular application, you must add it to the `entryComponents` for the
|
||||
Angular module.
|
||||
NgModule.
|
||||
|
||||
<code-example path="upgrade-module/src/app/downgrade-static/app.module.ts" region="ngmodule" title="app.module.ts">
|
||||
</code-example>
|
||||
|
@ -316,7 +316,14 @@
|
||||
"desc": "Angular UI Components including Grids, Charts, Scheduling and more.",
|
||||
"rev": true,
|
||||
"title": "jQWidgets",
|
||||
"url": "https://www.jqwidgets.com/angular/"
|
||||
"url": "http://www.jqwidgets.com/angular/"
|
||||
},
|
||||
"amexio": {
|
||||
"desc": "Amexio (Angular MetaMagic EXtensions for Inputs and Outputs) is a rich set of Angular components powered by Bootstrap for Responsive Design. UI Components include Standard Form Components, Data Grids, Tree Grids, Tabs etc. Open Source (Apache 2 License) & Free and backed by MetaMagic Global Inc",
|
||||
"rev": true,
|
||||
"title": "Amexio - Angular Extensions",
|
||||
"url": "http://www.amexio.tech/",
|
||||
"logo": "http://www.amexio.org/amexio-logo.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -508,12 +515,6 @@
|
||||
"title": "Learn Angular (francais)",
|
||||
"url": "http://www.learn-angular.fr/"
|
||||
},
|
||||
"sad200": {
|
||||
"desc": "Free Angular training delivered by SFEIR in France",
|
||||
"rev": true,
|
||||
"title": "SFEIR School (French)",
|
||||
"url": "https://school.sfeir.com/project/sad-200/"
|
||||
},
|
||||
"toddmotto-ultimateangular": {
|
||||
"desc": "Online courses providing in-depth coverage of the Angular ecosystem, AngularJS, Angular and TypeScript, with functional code samples and a full-featured seed environment. Get a deep understanding of Angular and TypeScript from foundation to functional application, then move on to advanced topics with Todd Motto and collaborators.",
|
||||
"rev": true,
|
||||
@ -600,6 +601,12 @@
|
||||
"rev": true,
|
||||
"title": "Learn Javascript (Russian)",
|
||||
"url": "https://learn.javascript.ru/courses/angular"
|
||||
},
|
||||
"sa200": {
|
||||
"desc": "Free Angular training delivered by SFEIR in France",
|
||||
"rev": true,
|
||||
"title": "SFEIR School (French)",
|
||||
"url": "https://school.sfeir.com/project/sa200/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -470,6 +470,8 @@
|
||||
],
|
||||
|
||||
"docVersions": [
|
||||
{ "title": "v2", "url": "https://v2.angular.io" }
|
||||
{ "title": "v2", "url": "https://v2.angular.io" },
|
||||
{ "title": "AngularDart", "url": "https://webdev.dartlang.org/angular" }
|
||||
|
||||
]
|
||||
}
|
||||
|
@ -249,7 +249,7 @@ Here's the complete `HeroDetailComponent`.
|
||||
|
||||
|
||||
## Declare _HeroDetailComponent_ in the _AppModule_
|
||||
Every component must be declared in one—and only one—Angular module.
|
||||
Every component must be declared in one—and only one—NgModule.
|
||||
|
||||
Open `app.module.ts` in your editor and import the `HeroDetailComponent` so you can refer to it.
|
||||
|
||||
@ -276,7 +276,7 @@ This module declares only the two application components, `AppComponent` and `He
|
||||
|
||||
|
||||
|
||||
Read more about Angular modules in the [NgModules](guide/ngmodule "Angular Modules") guide.
|
||||
Read more about NgModules in the [NgModules](guide/ngmodule "NgModules") guide.
|
||||
|
||||
|
||||
</div>
|
||||
@ -449,8 +449,8 @@ Here's what you achieved in this page:
|
||||
|
||||
* You created a reusable component.
|
||||
* You learned how to make a component accept input.
|
||||
* You learned to declare the required application directives in an Angular module. You
|
||||
listed the directives in the `NgModule` decorator's `declarations` array.
|
||||
* You learned to declare the required application directives in an NgModule. You
|
||||
listed the directives in the `@NgModule` decorator's `declarations` array.
|
||||
* You learned to bind a parent component to a child component.
|
||||
|
||||
Your app should look like this <live-example></live-example>.
|
||||
|
@ -30,7 +30,7 @@ You can keep building the Tour of Heroes without pausing to recompile or refresh
|
||||
|
||||
## Providing HTTP Services
|
||||
|
||||
The `HttpModule` is not a core Angular module.
|
||||
The `HttpModule` is not a core NgModule.
|
||||
`HttpModule` is Angular's optional approach to web access. It exists as a separate add-on module called `@angular/http`
|
||||
and is shipped in a separate script file as part of the Angular npm package.
|
||||
|
||||
|
@ -81,7 +81,7 @@
|
||||
"concurrently": "^3.4.0",
|
||||
"cross-spawn": "^5.1.0",
|
||||
"dgeni": "^0.4.7",
|
||||
"dgeni-packages": "^0.19.1",
|
||||
"dgeni-packages": "^0.20.0-rc.5",
|
||||
"entities": "^1.1.1",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-plugin-jasmine": "^2.2.0",
|
||||
|
@ -1,7 +1,6 @@
|
||||
/* tslint:disable:no-unused-variable */
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Component, DebugElement, Input } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Component, DebugElement, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { CodeExampleComponent } from './code-example.component';
|
||||
|
||||
@ -75,13 +74,12 @@ describe('CodeExampleComponent', () => {
|
||||
});
|
||||
|
||||
//// Test helpers ////
|
||||
// tslint:disable:member-ordering
|
||||
@Component({
|
||||
selector: 'aio-code',
|
||||
template: `
|
||||
<div>lang: {{language}}</div>
|
||||
<div>linenums: {{linenums}}</div>
|
||||
code: <pre>{{someCode}}</pre>
|
||||
<div>lang: {{language}}</div>
|
||||
<div>linenums: {{linenums}}</div>
|
||||
code: <pre>{{someCode}}</pre>
|
||||
`
|
||||
})
|
||||
class TestCodeComponent {
|
||||
|
245
aio/src/app/embedded/code/code-tabs.component.spec.ts
Normal file
245
aio/src/app/embedded/code/code-tabs.component.spec.ts
Normal file
@ -0,0 +1,245 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, DebugElement, Input, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MdTabGroup, MdTabsModule } from '@angular/material';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { CodeTabsComponent } from './code-tabs.component';
|
||||
|
||||
|
||||
describe('CodeTabsComponent', () => {
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
let hostComponent: HostComponent;
|
||||
let codeTabsDe: DebugElement;
|
||||
let codeTabsComponent: CodeTabsComponent;
|
||||
|
||||
const createComponentBasic = (codeTabsContent = '') => {
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
hostComponent = fixture.componentInstance;
|
||||
codeTabsDe = fixture.debugElement.children[0];
|
||||
codeTabsComponent = codeTabsDe.componentInstance;
|
||||
|
||||
// Copy the CodeTab's innerHTML (content)
|
||||
// into the `codeTabsContent` property as the DocViewer does.
|
||||
codeTabsDe.nativeElement.codeTabsContent = codeTabsContent;
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CodeTabsComponent, HostComponent, TestCodeComponent ],
|
||||
imports: [ CommonModule ],
|
||||
schemas: [ NO_ERRORS_SCHEMA ],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create CodeTabsComponent', () => {
|
||||
createComponentBasic();
|
||||
expect(codeTabsComponent).toBeTruthy('CodeTabsComponent');
|
||||
});
|
||||
|
||||
describe('(tab labels)', () => {
|
||||
let labelElems: HTMLSpanElement[];
|
||||
|
||||
const createComponent = (codeTabsContent?: string) => {
|
||||
createComponentBasic(codeTabsContent);
|
||||
const labelDes = codeTabsDe.queryAll(By.css('.mat-tab-label'));
|
||||
labelElems = labelDes.map(de => de.nativeElement.querySelector('span'));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ MdTabsModule, NoopAnimationsModule ]
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a label for each tab', () => {
|
||||
createComponent(`
|
||||
<code-pane>foo</code-pane>
|
||||
<code-pane>bar</code-pane>
|
||||
<code-pane>baz</code-pane>
|
||||
`);
|
||||
|
||||
expect(labelElems.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should use the `title` as label', () => {
|
||||
createComponent(`
|
||||
<code-pane title="foo-title">foo</code-pane>
|
||||
<code-pane title="bar-title">bar</code-pane>
|
||||
`);
|
||||
const texts = labelElems.map(s => s.textContent);
|
||||
|
||||
expect(texts).toEqual(['foo-title', 'bar-title']);
|
||||
});
|
||||
|
||||
it('should add the `class` to the label element', () => {
|
||||
createComponent(`
|
||||
<code-pane class="foo-class">foo</code-pane>
|
||||
<code-pane class="bar-class">bar</code-pane>
|
||||
`);
|
||||
const classes = labelElems.map(s => s.className);
|
||||
|
||||
expect(classes).toEqual(['foo-class', 'bar-class']);
|
||||
});
|
||||
|
||||
it('should disable ripple effect on tab labels', () => {
|
||||
createComponent();
|
||||
const tabsGroupComponent = codeTabsDe.query(By.directive(MdTabGroup)).componentInstance;
|
||||
|
||||
expect(tabsGroupComponent.disableRipple).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(tab content)', () => {
|
||||
let codeDes: DebugElement[];
|
||||
let codeComponents: TestCodeComponent[];
|
||||
|
||||
const createComponent = (codeTabsContent?: string) => {
|
||||
createComponentBasic(codeTabsContent);
|
||||
codeDes = codeTabsDe.queryAll(By.directive(TestCodeComponent));
|
||||
codeComponents = codeDes.map(de => de.componentInstance);
|
||||
};
|
||||
|
||||
it('should pass `class` to CodeComponent (<aio-code>)', () => {
|
||||
createComponent(`
|
||||
<code-pane class="foo-class">foo</code-pane>
|
||||
<code-pane class="bar-class">bar</code-pane>
|
||||
`);
|
||||
const classes = codeDes.map(de => de.nativeElement.className);
|
||||
|
||||
expect(classes).toEqual(['foo-class', 'bar-class']);
|
||||
});
|
||||
|
||||
it('should pass content to CodeComponent (<aio-code>)', () => {
|
||||
createComponent(`
|
||||
<code-pane>foo</code-pane>
|
||||
<code-pane>bar</code-pane>
|
||||
`);
|
||||
const codes = codeComponents.map(c => c.code);
|
||||
|
||||
expect(codes).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should pass `language` to CodeComponent (<aio-code>)', () => {
|
||||
createComponent(`
|
||||
<code-pane language="foo-lang">foo</code-pane>
|
||||
<code-pane language="bar-lang">bar</code-pane>
|
||||
`);
|
||||
const langs = codeComponents.map(c => c.language);
|
||||
|
||||
expect(langs).toEqual(['foo-lang', 'bar-lang']);
|
||||
});
|
||||
|
||||
it('should pass `linenums` to CodeComponent (<aio-code>)', () => {
|
||||
createComponent(`
|
||||
<code-pane linenums="foo-lnums">foo</code-pane>
|
||||
<code-pane linenums="bar-lnums">bar</code-pane>
|
||||
<code-pane linenums="">baz</code-pane>
|
||||
<code-pane linenums>qux</code-pane>
|
||||
`);
|
||||
const lnums = codeComponents.map(c => c.linenums);
|
||||
|
||||
expect(lnums).toEqual(['foo-lnums', 'bar-lnums', '', '']);
|
||||
});
|
||||
|
||||
it('should use the default value (if present on <code-tabs>) if `linenums` is not specified', () => {
|
||||
TestBed.overrideComponent(HostComponent, {
|
||||
set: { template: '<code-tabs linenums="default-lnums"></code-tabs>' }
|
||||
});
|
||||
|
||||
createComponent(`
|
||||
<code-pane linenums="foo-lnums">foo</code-pane>
|
||||
<code-pane linenums>bar</code-pane>
|
||||
<code-pane>baz</code-pane>
|
||||
`);
|
||||
const lnums = codeComponents.map(c => c.linenums);
|
||||
|
||||
expect(lnums).toEqual(['foo-lnums', '', 'default-lnums']);
|
||||
});
|
||||
|
||||
it('should pass `path` to CodeComponent (<aio-code>)', () => {
|
||||
createComponent(`
|
||||
<code-pane path="foo-path">foo</code-pane>
|
||||
<code-pane path="bar-path">bar</code-pane>
|
||||
`);
|
||||
const paths = codeComponents.map(c => c.path);
|
||||
|
||||
expect(paths).toEqual(['foo-path', 'bar-path']);
|
||||
});
|
||||
|
||||
it('should default to an empty string if `path` is not spcified', () => {
|
||||
createComponent(`
|
||||
<code-pane>foo</code-pane>
|
||||
<code-pane>bar</code-pane>
|
||||
`);
|
||||
const paths = codeComponents.map(c => c.path);
|
||||
|
||||
expect(paths).toEqual(['', '']);
|
||||
});
|
||||
|
||||
it('should pass `region` to CodeComponent (<aio-code>)', () => {
|
||||
createComponent(`
|
||||
<code-pane region="foo-region">foo</code-pane>
|
||||
<code-pane region="bar-region">bar</code-pane>
|
||||
`);
|
||||
const regions = codeComponents.map(c => c.region);
|
||||
|
||||
expect(regions).toEqual(['foo-region', 'bar-region']);
|
||||
});
|
||||
|
||||
it('should default to an empty string if `region` is not spcified', () => {
|
||||
createComponent(`
|
||||
<code-pane>foo</code-pane>
|
||||
<code-pane>bar</code-pane>
|
||||
`);
|
||||
const regions = codeComponents.map(c => c.region);
|
||||
|
||||
expect(regions).toEqual(['', '']);
|
||||
});
|
||||
|
||||
it('should pass `title` to CodeComponent (<aio-code>)', () => {
|
||||
createComponent(`
|
||||
<code-pane title="foo-title">foo</code-pane>
|
||||
<code-pane title="bar-title">bar</code-pane>
|
||||
`);
|
||||
const titles = codeComponents.map(c => c.title);
|
||||
|
||||
expect(titles).toEqual(['foo-title', 'bar-title']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//// Test helpers ////
|
||||
@Component({
|
||||
selector: 'aio-code',
|
||||
template: `
|
||||
<div>lang: {{ language }}</div>
|
||||
<div>linenums: {{ linenums }}</div>
|
||||
code: <pre>{{ someCode }}</pre>
|
||||
`
|
||||
})
|
||||
class TestCodeComponent {
|
||||
@Input() code = '';
|
||||
@Input() hideCopy: boolean;
|
||||
@Input() language: string;
|
||||
@Input() linenums: string;
|
||||
@Input() path: string;
|
||||
@Input() region: string;
|
||||
@Input() title: string;
|
||||
|
||||
get someCode() {
|
||||
if (this.code && this.code.length > 30) {
|
||||
return `${this.code.substring(0, 30)}...`;
|
||||
}
|
||||
|
||||
return this.code;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aio-host-comp',
|
||||
template: `<code-tabs></code-tabs>`
|
||||
})
|
||||
class HostComponent {}
|
@ -21,16 +21,21 @@ export interface TabInfo {
|
||||
@Component({
|
||||
selector: 'code-tabs',
|
||||
template: `
|
||||
<md-tab-group class="code-tab-group">
|
||||
<md-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
|
||||
<ng-template md-tab-label>
|
||||
<span class="{{tab.class}}">{{ tab.title }}</span>
|
||||
</ng-template>
|
||||
<aio-code [code]="tab.code" [language]="tab.language" [linenums]="tab.linenums"
|
||||
[path]="tab.path" [region]="tab.region" [title]="tab.title"
|
||||
class="{{ tab.class }}"></aio-code>
|
||||
</md-tab>
|
||||
</md-tab-group>
|
||||
<md-tab-group class="code-tab-group" disableRipple>
|
||||
<md-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
|
||||
<ng-template md-tab-label>
|
||||
<span class="{{ tab.class }}">{{ tab.title }}</span>
|
||||
</ng-template>
|
||||
<aio-code class="{{ tab.class }}"
|
||||
[code]="tab.code"
|
||||
[language]="tab.language"
|
||||
[linenums]="tab.linenums"
|
||||
[path]="tab.path"
|
||||
[region]="tab.region"
|
||||
[title]="tab.title">
|
||||
</aio-code>
|
||||
</md-tab>
|
||||
</md-tab-group>
|
||||
`
|
||||
})
|
||||
export class CodeTabsComponent implements OnInit {
|
||||
@ -52,7 +57,7 @@ export class CodeTabsComponent implements OnInit {
|
||||
processContent(content: string) {
|
||||
// We add it to an element so that we can easily parse the HTML
|
||||
const element = document.createElement('div');
|
||||
// **Security:** `codeTabsContent` is provided by docs authors and as such its considered to
|
||||
// **Security:** `codeTabsContent` is provided by docs authors and as such is considered to
|
||||
// be safe for innerHTML purposes.
|
||||
element.innerHTML = content;
|
||||
|
||||
@ -61,8 +66,8 @@ export class CodeTabsComponent implements OnInit {
|
||||
for (let i = 0; i < codeExamples.length; i++) {
|
||||
const codeExample = codeExamples.item(i);
|
||||
const tab = {
|
||||
code: codeExample.innerHTML,
|
||||
class: codeExample.getAttribute('class'),
|
||||
code: codeExample.innerHTML,
|
||||
language: codeExample.getAttribute('language'),
|
||||
linenums: this.getLinenums(codeExample),
|
||||
path: codeExample.getAttribute('path') || '',
|
||||
|
@ -1,8 +1,7 @@
|
||||
/* tslint:disable:no-unused-variable */
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MdSnackBarModule, MdSnackBar } from '@angular/material';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { CodeComponent } from './code.component';
|
||||
@ -268,9 +267,9 @@ describe('CodeComponent', () => {
|
||||
@Component({
|
||||
selector: 'aio-host-comp',
|
||||
template: `
|
||||
<aio-code md-no-ink [code]="code" [language]="language"
|
||||
[linenums]="linenums" [path]="path" [region]="region"
|
||||
[hideCopy]="hideCopy" [title]="title"></aio-code>
|
||||
<aio-code md-no-ink [code]="code" [language]="language"
|
||||
[linenums]="linenums" [path]="path" [region]="region"
|
||||
[hideCopy]="hideCopy" [title]="title"></aio-code>
|
||||
`
|
||||
})
|
||||
class HostComponent {
|
||||
|
@ -18,7 +18,8 @@ self.onmessage = handleMessage;
|
||||
function createIndex(addFn) {
|
||||
return lunr(/** @this */function() {
|
||||
this.ref('path');
|
||||
this.field('titleWords', {boost: 50});
|
||||
this.field('titleWords', {boost: 100});
|
||||
this.field('headingWords', {boost: 50});
|
||||
this.field('members', {boost: 40});
|
||||
this.field('keywords', {boost: 20});
|
||||
addFn(this);
|
||||
@ -80,17 +81,7 @@ function loadIndex(searchInfo) {
|
||||
|
||||
// Query the index and return the processed results
|
||||
function queryIndex(query) {
|
||||
// The index requires the query to be lowercase
|
||||
var terms = query.toLowerCase().split(/\s+/);
|
||||
var results = index.query(function(qb) {
|
||||
terms.forEach(function(term) {
|
||||
// Only include terms that are longer than 2 characters, if there is more than one term
|
||||
// Add trailing wildcard to each term so that it will match more results
|
||||
if (terms.length === 1 || term.trim().length > 2) {
|
||||
qb.term(term, { wildcard: lunr.Query.wildcard.TRAILING });
|
||||
}
|
||||
});
|
||||
});
|
||||
var results = index.search(query);
|
||||
// Only return the array of paths to pages
|
||||
return results.map(function(hit) { return pages[hit.ref]; });
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
.api-info-bar {
|
||||
max-width: 800px;
|
||||
text-align: left;
|
||||
|
||||
span {
|
||||
margin: 0 16px 0 0;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
23
aio/src/styles/2-modules/_api-pages.scss
Normal file
23
aio/src/styles/2-modules/_api-pages.scss
Normal file
@ -0,0 +1,23 @@
|
||||
.api-info-bar {
|
||||
max-width: 800px;
|
||||
text-align: left;
|
||||
|
||||
span {
|
||||
margin: 0 16px 0 0;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.api-heading {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 18px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.overloads .detail-contents {
|
||||
padding-top: 0;
|
||||
}
|
@ -112,11 +112,6 @@ aio-code pre {
|
||||
}
|
||||
|
||||
|
||||
// REMOVE RIPPLE EFFECT FROM MATERIAL TABS
|
||||
code-tabs md-tab-group *.mat-ripple-element, code-tabs md-tab-group *.mat-tab-body-active, code-tabs md-tab-group *.mat-tab-body-content, code-tabs md-tab-group *.mat-tab-body-content {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
[role="tabpanel"] {
|
||||
transition: none;
|
||||
}
|
||||
|
51
aio/src/styles/2-modules/_details.scss
Normal file
51
aio/src/styles/2-modules/_details.scss
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* General styling to make detail/summary tags look a bit more material
|
||||
* To get the best out of it you should structure your usage like this:
|
||||
*
|
||||
* ```
|
||||
* <details>
|
||||
* <summary>Some title</summary>
|
||||
* <div class="details-content">
|
||||
* Some content
|
||||
* </div>
|
||||
* </details>
|
||||
*
|
||||
*/
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
padding: 16px 24px;
|
||||
color: $black;
|
||||
height: 16px;
|
||||
display: block; // Remove the built in details marker in FF
|
||||
|
||||
&::-webkit-details-marker {
|
||||
display: none; // Remove the built in details marker in webkit
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '\E5CE'; // See https://material.io/icons/#ic_expand_less
|
||||
font-family: 'Material Icons';
|
||||
font-size: 24px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@include rotate(0deg); // We will rotate 180 degrees when details is open
|
||||
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
details {
|
||||
box-shadow: 0 1px 4px 0 rgba($black, 0.37);
|
||||
|
||||
.detail-contents {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
&[open] > summary::after {
|
||||
@include rotate(180deg); // Rotate the icon
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
============================== */
|
||||
|
||||
@import 'alert';
|
||||
@import 'api-info-bar';
|
||||
@import 'api-pages';
|
||||
@import 'api-list';
|
||||
@import 'banner';
|
||||
@import 'buttons';
|
||||
@ -12,6 +12,7 @@
|
||||
@import 'code';
|
||||
@import 'contribute';
|
||||
@import 'contributor';
|
||||
@import 'details';
|
||||
@import 'edit-page-cta';
|
||||
@import 'features';
|
||||
@import 'filetree';
|
||||
|
@ -208,7 +208,7 @@ aio-resource-list {
|
||||
border-color: #2B85E7;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 8px 8px rgba(1, 67, 163, .24), 0 0 8px rgba(1, 67, 163, .12), 0 6px 18px rgba(43, 133, 231, .12);
|
||||
transform: translate3d(0, -2px, 0);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media(max-width: 900px) {
|
||||
|
@ -20,10 +20,11 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage])
|
||||
.processor(require('./processors/mergeDecoratorDocs'))
|
||||
.processor(require('./processors/extractDecoratedClasses'))
|
||||
.processor(require('./processors/matchUpDirectiveDecorators'))
|
||||
.processor(require('./processors/filterMemberDocs'))
|
||||
.processor(require('./processors/filterContainedDocs'))
|
||||
.processor(require('./processors/markBarredODocsAsPrivate'))
|
||||
.processor(require('./processors/filterPrivateDocs'))
|
||||
.processor(require('./processors/computeSearchTitle'))
|
||||
.processor(require('./processors/simplifyMemberAnchors'))
|
||||
|
||||
// Where do we get the source files?
|
||||
.config(function(readTypeScriptModules, readFilesProcessor, collectExamples) {
|
||||
|
17
aio/tools/transforms/angular-api-package/processors/filterContainedDocs.js
vendored
Normal file
17
aio/tools/transforms/angular-api-package/processors/filterContainedDocs.js
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Remove docs that are contained in (owned by) another doc
|
||||
* so that they don't get rendered as files in themselves.
|
||||
*/
|
||||
module.exports = function filterContainedDocs() {
|
||||
return {
|
||||
docTypes: ['member', 'function-overload'],
|
||||
$runAfter: ['extra-docs-added'],
|
||||
$runBefore: ['computing-paths'],
|
||||
$process: function(docs) {
|
||||
var docTypes = this.docTypes;
|
||||
return docs.filter(function(doc) {
|
||||
return docTypes.indexOf(doc.docType) === -1;
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
module.exports = function filterMemberDocs() {
|
||||
return {
|
||||
$runAfter: ['extra-docs-added'], $runBefore: ['computing-paths'], $process: function(docs) {
|
||||
return docs.filter(function(doc) { return doc.docType !== 'member'; });
|
||||
}
|
||||
};
|
||||
};
|
@ -2,7 +2,7 @@ const testPackage = require('../../helpers/test-package');
|
||||
const processorFactory = require('./markBarredODocsAsPrivate');
|
||||
const Dgeni = require('dgeni');
|
||||
|
||||
describe('generateApiListDoc processor', () => {
|
||||
describe('markBarredODocsAsPrivate processor', () => {
|
||||
|
||||
it('should be available on the injector', () => {
|
||||
const dgeni = new Dgeni([testPackage('angular-api-package')]);
|
||||
|
@ -1,61 +1,85 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
/**
|
||||
* @dgProcessor
|
||||
* @description
|
||||
* Directives in Angular are specified by various decorators. In particular the `@Directive()`
|
||||
* decorator on the class and various other property decorators, such as `@Input`.
|
||||
*
|
||||
* This processor will extract this decorator information and attach it as properties to the
|
||||
* directive document.
|
||||
*
|
||||
* Notably, the `input` and `output` binding information can be specified
|
||||
* either via property decorators (`@Input()`/`@Output()`) or by properties on the metadata
|
||||
* passed to the `@Directive` decorator. This processor will collect up info from both and
|
||||
* merge them.
|
||||
*/
|
||||
module.exports = function matchUpDirectiveDecoratorsProcessor() {
|
||||
|
||||
module.exports = function matchUpDirectiveDecorators() {
|
||||
return {
|
||||
$runAfter: ['ids-computed', 'paths-computed'],
|
||||
$runBefore: ['rendering-docs'],
|
||||
decoratorMappings: {'Inputs': 'inputs', 'Outputs': 'outputs'},
|
||||
$process: function(docs) {
|
||||
var decoratorMappings = this.decoratorMappings;
|
||||
_.forEach(docs, function(doc) {
|
||||
docs.forEach(function(doc) {
|
||||
if (doc.docType === 'directive') {
|
||||
doc.selector = doc.directiveOptions.selector;
|
||||
|
||||
for (let decoratorName in decoratorMappings) {
|
||||
var propertyName = decoratorMappings[decoratorName];
|
||||
doc[propertyName] =
|
||||
getDecoratorValues(doc.directiveOptions[propertyName], decoratorName, doc.members);
|
||||
}
|
||||
doc.selector = stripQuotes(doc.directiveOptions.selector);
|
||||
doc.exportAs = stripQuotes(doc.directiveOptions.exportAs);
|
||||
|
||||
doc.inputs = getBindingInfo(doc.directiveOptions.inputs, doc.members, 'Input');
|
||||
doc.outputs = getBindingInfo(doc.directiveOptions.outputs, doc.members, 'Output');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function getDecoratorValues(classDecoratorValues, memberDecoratorName, members) {
|
||||
var decoratorValues = {};
|
||||
function getBindingInfo(directiveBindings, members, bindingType) {
|
||||
const bindings = {};
|
||||
|
||||
// Parse the class decorator
|
||||
_.forEach(classDecoratorValues, function(option) {
|
||||
// Options are of the form: "propName : bindingName" (bindingName is optional)
|
||||
var optionPair = option.split(':');
|
||||
var propertyName = optionPair.shift().trim();
|
||||
var bindingName = (optionPair.shift() || '').trim() || propertyName;
|
||||
// Parse the bindings from the directive decorator
|
||||
if (directiveBindings) {
|
||||
directiveBindings.forEach(function(binding) {
|
||||
const bindingInfo = parseBinding(binding);
|
||||
bindings[bindingInfo.propertyName] = bindingInfo;
|
||||
});
|
||||
}
|
||||
|
||||
decoratorValues[propertyName] = {propertyName: propertyName, bindingName: bindingName};
|
||||
});
|
||||
if (members) {
|
||||
members.forEach(function(member) {
|
||||
if (member.decorators) {
|
||||
// Search for members with binding decorators
|
||||
member.decorators.forEach(function(decorator) {
|
||||
if (decorator.name === bindingType) {
|
||||
bindings[member.name] = createBindingInfo(member.name, decorator.arguments[0] || member.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_.forEach(members, function(member) {
|
||||
_.forEach(member.decorators, function(decorator) {
|
||||
if (decorator.name === memberDecoratorName) {
|
||||
decoratorValues[member.name] = {
|
||||
propertyName: member.name,
|
||||
bindingName: decorator.arguments[0] || member.name
|
||||
};
|
||||
// Now ensure that any bindings have the associated member attached
|
||||
// Note that this binding could have come from the directive decorator
|
||||
if (bindings[member.name]) {
|
||||
bindings[member.name].memberDoc = member;
|
||||
}
|
||||
});
|
||||
if (decoratorValues[member.name]) {
|
||||
decoratorValues[member.name].memberDoc = member;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(decoratorValues).length) {
|
||||
return decoratorValues;
|
||||
}
|
||||
|
||||
// Convert the map back to an array
|
||||
return Object.keys(bindings).map(function(key) { return bindings[key]; });
|
||||
}
|
||||
|
||||
function stripQuotes(value) {
|
||||
return (typeof(value) === 'string') ? value.trim().replace(/^(['"])(.*)\1$/, '$2') : value;
|
||||
}
|
||||
|
||||
function parseBinding(option) {
|
||||
// Directive decorator bindings are of the form: "propName : bindingName" (bindingName is optional)
|
||||
const optionPair = option.split(':');
|
||||
const propertyName = optionPair[0].trim();
|
||||
const bindingName = (optionPair[1] || '').trim() || propertyName;
|
||||
return createBindingInfo(propertyName, bindingName);
|
||||
}
|
||||
|
||||
function createBindingInfo(propertyName, bindingName) {
|
||||
return {
|
||||
propertyName: stripQuotes(propertyName),
|
||||
bindingName: stripQuotes(bindingName)
|
||||
};
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
const testPackage = require('../../helpers/test-package');
|
||||
const processorFactory = require('./matchUpDirectiveDecorators');
|
||||
const Dgeni = require('dgeni');
|
||||
|
||||
describe('matchUpDirectiveDecorators processor', () => {
|
||||
|
||||
it('should be available on the injector', () => {
|
||||
const dgeni = new Dgeni([testPackage('angular-api-package')]);
|
||||
const injector = dgeni.configureInjector();
|
||||
const processor = injector.get('matchUpDirectiveDecorators');
|
||||
expect(processor.$process).toBeDefined();
|
||||
expect(processor.$runAfter).toContain('ids-computed');
|
||||
expect(processor.$runAfter).toContain('paths-computed');
|
||||
expect(processor.$runBefore).toContain('rendering-docs');
|
||||
});
|
||||
|
||||
it('should extract selector and exportAs from the directive decorator on directive docs', () => {
|
||||
const docs = [{
|
||||
docType: 'directive',
|
||||
directiveOptions: { selector: 'a,b,c', exportAs: 'someExport' }
|
||||
}];
|
||||
processorFactory().$process(docs);
|
||||
expect(docs[0].selector).toEqual('a,b,c');
|
||||
expect(docs[0].exportAs).toEqual('someExport');
|
||||
});
|
||||
|
||||
it('should ignore properties from the directive decorator on non-directive docs', () => {
|
||||
const docs = [{
|
||||
docType: 'class',
|
||||
directiveOptions: { selector: 'a,b,c', exportAs: 'someExport' }
|
||||
}];
|
||||
processorFactory().$process(docs);
|
||||
expect(docs[0].selector).toBeUndefined();
|
||||
expect(docs[0].exportAs).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should strip whitespace and quotes off directive properties', () => {
|
||||
const docs = [
|
||||
{
|
||||
docType: 'directive',
|
||||
directiveOptions: { selector: '"a,b,c"', exportAs: '\'someExport\'' }
|
||||
},
|
||||
{
|
||||
docType: 'directive',
|
||||
directiveOptions: { selector: ' a,b,c ', exportAs: ' someExport ' }
|
||||
},
|
||||
{
|
||||
docType: 'directive',
|
||||
directiveOptions: { selector: ' "a,b,c" ', exportAs: ' \'someExport\' ' }
|
||||
}
|
||||
];
|
||||
processorFactory().$process(docs);
|
||||
expect(docs[0].selector).toEqual('a,b,c');
|
||||
expect(docs[0].exportAs).toEqual('someExport');
|
||||
expect(docs[1].selector).toEqual('a,b,c');
|
||||
expect(docs[1].exportAs).toEqual('someExport');
|
||||
expect(docs[2].selector).toEqual('a,b,c');
|
||||
expect(docs[2].exportAs).toEqual('someExport');
|
||||
});
|
||||
|
||||
it('should extract inputs and outputs from the directive decorator', () => {
|
||||
const docs = [{
|
||||
docType: 'directive',
|
||||
directiveOptions: {
|
||||
inputs: ['in1:in2', 'in3', ' in4:in5 ', ' in6 '],
|
||||
outputs: ['out1:out1', ' out2:out3 ', ' out4 ']
|
||||
},
|
||||
members: [
|
||||
{ name: 'in1' },
|
||||
{ name: 'in3' },
|
||||
{ name: 'in4' },
|
||||
{ name: 'in6' },
|
||||
{ name: 'out1' },
|
||||
{ name: 'out2' },
|
||||
{ name: 'out4' }
|
||||
]
|
||||
}];
|
||||
processorFactory().$process(docs);
|
||||
expect(docs[0].inputs).toEqual([
|
||||
{ propertyName: 'in1', bindingName: 'in2', memberDoc: docs[0].members[0] },
|
||||
{ propertyName: 'in3', bindingName: 'in3', memberDoc: docs[0].members[1] },
|
||||
{ propertyName: 'in4', bindingName: 'in5', memberDoc: docs[0].members[2] },
|
||||
{ propertyName: 'in6', bindingName: 'in6', memberDoc: docs[0].members[3] }
|
||||
]);
|
||||
|
||||
expect(docs[0].outputs).toEqual([
|
||||
{ propertyName: 'out1', bindingName: 'out1', memberDoc: docs[0].members[4] },
|
||||
{ propertyName: 'out2', bindingName: 'out3', memberDoc: docs[0].members[5] },
|
||||
{ propertyName: 'out4', bindingName: 'out4', memberDoc: docs[0].members[6] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract inputs and outputs from decorated properties', () => {
|
||||
const docs = [{
|
||||
docType: 'directive',
|
||||
directiveOptions: {},
|
||||
members: [
|
||||
{ name: 'a1', decorators: [{ name: 'Input', arguments: ['a2'] }] },
|
||||
{ name: 'b1', decorators: [{ name: 'Output', arguments: ['b2'] }] },
|
||||
{ name: 'c1', decorators: [{ name: 'Input', arguments: [] }] },
|
||||
{ name: 'd1', decorators: [{ name: 'Output', arguments: [] }] },
|
||||
]
|
||||
}];
|
||||
processorFactory().$process(docs);
|
||||
expect(docs[0].inputs).toEqual([
|
||||
{ propertyName: 'a1', bindingName: 'a2', memberDoc: docs[0].members[0] },
|
||||
{ propertyName: 'c1', bindingName: 'c1', memberDoc: docs[0].members[2] }
|
||||
]);
|
||||
|
||||
expect(docs[0].outputs).toEqual([
|
||||
{ propertyName: 'b1', bindingName: 'b2', memberDoc: docs[0].members[1] },
|
||||
{ propertyName: 'd1', bindingName: 'd1', memberDoc: docs[0].members[3] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should merge directive inputs/outputs with decorator property inputs/outputs', () => {
|
||||
const docs = [{
|
||||
docType: 'directive',
|
||||
directiveOptions: {
|
||||
inputs: ['a1:a2'],
|
||||
outputs: ['b1:b2']
|
||||
},
|
||||
members: [
|
||||
{ name: 'a1' },
|
||||
{ name: 'a3', decorators: [{ name: 'Input', arguments: ['a4'] }] },
|
||||
{ name: 'b1' },
|
||||
{ name: 'b3', decorators: [{ name: 'Output', arguments: ['b4'] }] },
|
||||
]
|
||||
}];
|
||||
processorFactory().$process(docs);
|
||||
expect(docs[0].inputs).toEqual([
|
||||
{ propertyName: 'a1', bindingName: 'a2', memberDoc: docs[0].members[0] },
|
||||
{ propertyName: 'a3', bindingName: 'a4', memberDoc: docs[0].members[1] }
|
||||
]);
|
||||
|
||||
expect(docs[0].outputs).toEqual([
|
||||
{ propertyName: 'b1', bindingName: 'b2', memberDoc: docs[0].members[2] },
|
||||
{ propertyName: 'b3', bindingName: 'b4', memberDoc: docs[0].members[3] }
|
||||
]);
|
||||
});
|
||||
});
|
@ -83,13 +83,14 @@ module.exports = function mergeDecoratorDocs(log) {
|
||||
if (docsToMerge[doc.name]) {
|
||||
// We have found an `XxxDecorator` document that will hold the call signature of the decorator
|
||||
var decoratorDoc = docsToMerge[doc.name];
|
||||
var callMember = doc.members.filter(function(member) { return member.isCallMember; })[0];
|
||||
log.debug(
|
||||
'mergeDecoratorDocs: merging', doc.name, 'into', decoratorDoc.name,
|
||||
doc.callMember.description.substring(0, 50));
|
||||
callMember.description.substring(0, 50));
|
||||
// Merge the documentation found in this call signature into the original decorator
|
||||
decoratorDoc.description = doc.callMember.description;
|
||||
decoratorDoc.howToUse = doc.callMember.howToUse;
|
||||
decoratorDoc.whatItDoes = doc.callMember.whatItDoes;
|
||||
decoratorDoc.description = callMember.description;
|
||||
decoratorDoc.howToUse = callMember.howToUse;
|
||||
decoratorDoc.whatItDoes = callMember.whatItDoes;
|
||||
|
||||
// remove doc from its module doc's exports
|
||||
doc.moduleDoc.exports =
|
||||
@ -108,8 +109,8 @@ module.exports = function mergeDecoratorDocs(log) {
|
||||
function getMakeDecoratorCall(doc, type) {
|
||||
var makeDecoratorFnName = 'make' + (type || '') + 'Decorator';
|
||||
|
||||
var initializer = doc.exportSymbol && doc.exportSymbol.valueDeclaration &&
|
||||
doc.exportSymbol.valueDeclaration.initializer;
|
||||
var initializer = doc.declaration &&
|
||||
doc.declaration.initializer;
|
||||
|
||||
if (initializer) {
|
||||
// There appear to be two forms of initializer:
|
||||
|
@ -15,10 +15,7 @@ describe('mergeDecoratorDocs processor', () => {
|
||||
name: 'Component',
|
||||
docType: 'const',
|
||||
description: 'A description of the metadata for the Component decorator',
|
||||
exportSymbol: {
|
||||
valueDeclaration:
|
||||
{initializer: {expression: {text: 'makeDecorator'}, arguments: [{text: 'X'}]}}
|
||||
},
|
||||
declaration: {initializer: {expression: {text: 'makeDecorator'}, arguments: [{text: 'X'}]}},
|
||||
members: [
|
||||
{ name: 'templateUrl', description: 'A description of the templateUrl property' }
|
||||
],
|
||||
@ -29,34 +26,30 @@ describe('mergeDecoratorDocs processor', () => {
|
||||
name: 'ComponentDecorator',
|
||||
docType: 'interface',
|
||||
description: 'A description of the interface for the call signature for the Component decorator',
|
||||
callMember: {
|
||||
description: 'The actual description of the call signature',
|
||||
whatItDoes: 'Does something cool...',
|
||||
howToUse: 'Use it like this...'
|
||||
},
|
||||
members: [
|
||||
{
|
||||
isCallMember: true,
|
||||
description: 'The actual description of the call signature',
|
||||
whatItDoes: 'Does something cool...',
|
||||
howToUse: 'Use it like this...'
|
||||
},
|
||||
{
|
||||
description: 'Some other member'
|
||||
}
|
||||
],
|
||||
moduleDoc
|
||||
};
|
||||
|
||||
decoratorDocWithTypeAssertion = {
|
||||
name: 'Y',
|
||||
docType: 'var',
|
||||
exportSymbol: {
|
||||
valueDeclaration: {
|
||||
initializer: {
|
||||
expression:
|
||||
{type: {}, expression: {text: 'makeDecorator'}, arguments: [{text: 'Y'}]}
|
||||
}
|
||||
}
|
||||
},
|
||||
docType: 'const',
|
||||
declaration: { initializer: { expression: {type: {}, expression: {text: 'makeDecorator'}, arguments: [{text: 'Y'}]} } },
|
||||
moduleDoc
|
||||
};
|
||||
otherDoc = {
|
||||
name: 'Y',
|
||||
docType: 'var',
|
||||
exportSymbol: {
|
||||
valueDeclaration:
|
||||
{initializer: {expression: {text: 'otherCall'}, arguments: [{text: 'param1'}]}}
|
||||
},
|
||||
docType: 'const',
|
||||
declaration: {initializer: {expression: {text: 'otherCall'}, arguments: [{text: 'param1'}]}},
|
||||
moduleDoc
|
||||
};
|
||||
|
||||
@ -68,7 +61,7 @@ describe('mergeDecoratorDocs processor', () => {
|
||||
processor.$process([decoratorDoc, metadataDoc, decoratorDocWithTypeAssertion, otherDoc]);
|
||||
expect(decoratorDoc.docType).toEqual('decorator');
|
||||
expect(decoratorDocWithTypeAssertion.docType).toEqual('decorator');
|
||||
expect(otherDoc.docType).toEqual('var');
|
||||
expect(otherDoc.docType).toEqual('const');
|
||||
});
|
||||
|
||||
it('should extract the "type" of the decorator meta data', () => {
|
||||
|
26
aio/tools/transforms/angular-api-package/processors/simplifyMemberAnchors.js
vendored
Normal file
26
aio/tools/transforms/angular-api-package/processors/simplifyMemberAnchors.js
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Members that have overloads get long unwieldy anchors because they must be distinguished
|
||||
* by their parameter lists.
|
||||
* But the primary overload doesn't not need this distinction, so can just be the name of the member.
|
||||
*/
|
||||
module.exports = function simplifyMemberAnchors() {
|
||||
return {
|
||||
$runAfter: ['extra-docs-added'],
|
||||
$runBefore: ['computing-paths'],
|
||||
$process: function(docs) {
|
||||
return docs.forEach(doc => {
|
||||
if (doc.members) {
|
||||
doc.members.forEach(member => member.anchor = computeAnchor(member));
|
||||
}
|
||||
if (doc.statics) {
|
||||
doc.statics.forEach(member => member.anchor = computeAnchor(member));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function computeAnchor(member) {
|
||||
// if the member is a "call" type then it has no name
|
||||
return encodeURI(member.name.trim() || 'call');
|
||||
}
|
@ -1,22 +1,28 @@
|
||||
const visit = require('unist-util-visit');
|
||||
const is = require('hast-util-is-element');
|
||||
const source = require('unist-util-source');
|
||||
const toString = require('hast-util-to-string');
|
||||
const filter = require('unist-util-filter');
|
||||
|
||||
module.exports = function h1CheckerPostProcessor() {
|
||||
return (ast, file) => {
|
||||
let h1s = [];
|
||||
file.headings = {
|
||||
h1: [],
|
||||
h2: [],
|
||||
h3: [],
|
||||
h4: [],
|
||||
h5: [],
|
||||
h6: [],
|
||||
hgroup: []
|
||||
};
|
||||
visit(ast, node => {
|
||||
if (is(node, 'h1')) {
|
||||
h1s.push(node);
|
||||
file.title = getText(node);
|
||||
if (is(node, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hgroup'])) {
|
||||
file.headings[node.tagName].push(getText(node));
|
||||
}
|
||||
});
|
||||
|
||||
if (h1s.length > 1) {
|
||||
const h1Src = h1s.map(node => source(node, file)).join(', ');
|
||||
file.fail(`More than one h1 found [${h1Src}]`);
|
||||
file.title = file.headings.h1[0];
|
||||
if (file.headings.h1.length > 1) {
|
||||
file.fail(`More than one h1 found in ${file}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ describe('h1Checker postprocessor', () => {
|
||||
<h1>Heading 1a</h1>
|
||||
`
|
||||
};
|
||||
expect(() => processor.$process([doc])).toThrowError(createDocMessage('More than one h1 found [<h1>Heading 1, <h1>Heading 1a</h1>]', doc));
|
||||
expect(() => processor.$process([doc])).toThrowError(createDocMessage('More than one h1 found in ' + doc.renderedContent, doc));
|
||||
});
|
||||
|
||||
it('should not complain if there is exactly one h1 in a document', () => {
|
||||
|
@ -50,19 +50,14 @@ module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
|
||||
|
||||
var ignoreWordsMap = convertToMap(wordsToIgnore);
|
||||
|
||||
// If the title contains a name starting with ng, e.g. "ngController", then add the module
|
||||
// name
|
||||
// without the ng to the title text, e.g. "controller".
|
||||
function extractTitleWords(title) {
|
||||
var match = /ng([A-Z]\w*)/.exec(title);
|
||||
if (match) {
|
||||
title = title + ' ' + match[1].toLowerCase();
|
||||
}
|
||||
return title;
|
||||
// If the heading contains a name starting with ng, e.g. "ngController", then add the
|
||||
// name without the ng to the text, e.g. "controller".
|
||||
function preprocessText(text) {
|
||||
return text.replace(/(^|\s)([nN]g([A-Z]\w*))/g, '$1$2 $3');
|
||||
}
|
||||
|
||||
function extractWords(text, words, keywordMap) {
|
||||
var tokens = text.toLowerCase().split(/[.\s,`'"#]+/mg);
|
||||
var tokens = preprocessText(text).toLowerCase().split(/[.\s,`'"#]+/mg);
|
||||
tokens.forEach(function(token) {
|
||||
var match = token.match(KEYWORD_REGEX);
|
||||
if (match) {
|
||||
@ -82,13 +77,15 @@ module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
|
||||
// Ignore internals and private exports (indicated by the ɵ prefix)
|
||||
.filter(function(doc) { return !doc.internal && !doc.privateExport; });
|
||||
|
||||
filteredDocs.forEach(function(doc) {
|
||||
|
||||
filteredDocs.forEach(function(doc) {
|
||||
|
||||
var words = [];
|
||||
var keywordMap = Object.assign({}, ignoreWordsMap);
|
||||
var members = [];
|
||||
var membersMap = {};
|
||||
var membersMap = Object.assign({}, ignoreWordsMap);
|
||||
const headingWords = [];
|
||||
const headingWordMap = Object.assign({}, ignoreWordsMap);
|
||||
|
||||
// Search each top level property of the document for search terms
|
||||
Object.keys(doc).forEach(function(key) {
|
||||
@ -98,26 +95,44 @@ module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
|
||||
extractWords(value, words, keywordMap);
|
||||
}
|
||||
|
||||
// Special case properties that contain content relating to "members"
|
||||
// of a doc that represents, say, a class or interface
|
||||
if (key === 'methods' || key === 'properties' || key === 'events') {
|
||||
value.forEach(function(member) { extractWords(member.name, members, membersMap); });
|
||||
}
|
||||
});
|
||||
|
||||
doc.searchTitle = doc.searchTitle || doc.title || doc.vFile && doc.vFile.title || doc.name;
|
||||
// Extract all the keywords from the headings
|
||||
if (doc.vFile && doc.vFile.headings) {
|
||||
Object.keys(doc.vFile.headings).forEach(function(headingTag) {
|
||||
doc.vFile.headings[headingTag].forEach(function(headingText) {
|
||||
extractWords(headingText, headingWords, headingWordMap);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Extract the title to use in searches
|
||||
doc.searchTitle = doc.searchTitle || doc.title || doc.vFile && doc.vFile.title || doc.name || '';
|
||||
|
||||
// Attach all this search data to the document
|
||||
doc.searchTerms = {
|
||||
titleWords: extractTitleWords(doc.searchTitle),
|
||||
titleWords: preprocessText(doc.searchTitle),
|
||||
headingWords: headingWords.sort().join(' '),
|
||||
keywords: words.sort().join(' '),
|
||||
members: members.sort().join(' ')
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
var searchData =
|
||||
filteredDocs.filter(function(page) { return page.searchTerms; }).map(function(page) {
|
||||
return Object.assign(
|
||||
{path: page.path, title: page.searchTitle, type: page.docType}, page.searchTerms);
|
||||
});
|
||||
// Now process all the search data and collect it up to be used in creating a new document
|
||||
var searchData = filteredDocs.map(function(page) {
|
||||
// Copy the properties from the searchTerms object onto the search data object
|
||||
return Object.assign({
|
||||
path: page.path,
|
||||
title: page.searchTitle,
|
||||
type: page.docType
|
||||
}, page.searchTerms);
|
||||
});
|
||||
|
||||
docs.push({
|
||||
docType: 'json-doc',
|
||||
|
@ -42,9 +42,9 @@ describe('generateKeywords processor', () => {
|
||||
it('should compute `doc.searchTitle` from the doc properties if not already provided', () => {
|
||||
const processor = processorFactory(mockLogger, mockReadFilesProcessor);
|
||||
const docs = [
|
||||
{ docType: 'class', name: 'A', searchTitle: 'searchTitle A', title: 'title A', vFile: { title: 'vFile A'} },
|
||||
{ docType: 'class', name: 'B', title: 'title B', vFile: { title: 'vFile B'} },
|
||||
{ docType: 'class', name: 'C', vFile: { title: 'vFile C'} },
|
||||
{ docType: 'class', name: 'A', searchTitle: 'searchTitle A', title: 'title A', vFile: { headings: { h1: ['vFile A'] } } },
|
||||
{ docType: 'class', name: 'B', title: 'title B', vFile: { headings: { h1: ['vFile B'] } } },
|
||||
{ docType: 'class', name: 'C', vFile: { title: 'vFile C', headings: { h1: ['vFile C'] } } },
|
||||
{ docType: 'class', name: 'D' },
|
||||
];
|
||||
processor.$process(docs);
|
||||
@ -62,19 +62,82 @@ describe('generateKeywords processor', () => {
|
||||
{ docType: 'class', name: 'PublicExport', searchTitle: 'class PublicExport' },
|
||||
];
|
||||
processor.$process(docs);
|
||||
expect(docs[docs.length - 1].data).toEqual([
|
||||
const keywordsDoc = docs[docs.length - 1];
|
||||
expect(keywordsDoc.data).toEqual([
|
||||
jasmine.objectContaining({ title: 'class PublicExport', type: 'class'})
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add title words to the search terms', () => {
|
||||
const processor = processorFactory(mockLogger, mockReadFilesProcessor);
|
||||
const docs = [
|
||||
{
|
||||
docType: 'class',
|
||||
name: 'PublicExport',
|
||||
searchTitle: 'class PublicExport',
|
||||
vFile: { headings: { h2: ['heading A', 'heading B'] } }
|
||||
},
|
||||
];
|
||||
processor.$process(docs);
|
||||
const keywordsDoc = docs[docs.length - 1];
|
||||
expect(keywordsDoc.data[0].titleWords).toEqual('class PublicExport');
|
||||
});
|
||||
|
||||
it('should add heading words to the search terms', () => {
|
||||
const processor = processorFactory(mockLogger, mockReadFilesProcessor);
|
||||
const docs = [
|
||||
{
|
||||
docType: 'class',
|
||||
name: 'PublicExport',
|
||||
searchTitle: 'class PublicExport',
|
||||
vFile: { headings: { h2: ['Important heading', 'Secondary heading'] } }
|
||||
},
|
||||
];
|
||||
processor.$process(docs);
|
||||
const keywordsDoc = docs[docs.length - 1];
|
||||
expect(keywordsDoc.data[0].headingWords).toEqual('heading important secondary');
|
||||
});
|
||||
|
||||
it('should process terms prefixed with "ng" to include the term stripped of "ng"', () => {
|
||||
const processor = processorFactory(mockLogger, mockReadFilesProcessor);
|
||||
const docs = [
|
||||
{
|
||||
docType: 'class',
|
||||
name: 'PublicExport',
|
||||
searchTitle: 'ngController',
|
||||
vFile: { headings: { h2: ['ngModel'] } },
|
||||
content: 'Some content with ngClass in it.'
|
||||
},
|
||||
];
|
||||
processor.$process(docs);
|
||||
const keywordsDoc = docs[docs.length - 1];
|
||||
expect(keywordsDoc.data[0].titleWords).toEqual('ngController Controller');
|
||||
expect(keywordsDoc.data[0].headingWords).toEqual('model ngmodel');
|
||||
expect(keywordsDoc.data[0].keywords).toContain('class');
|
||||
expect(keywordsDoc.data[0].keywords).toContain('ngclass');
|
||||
});
|
||||
|
||||
it('should generate renderedContent property', () => {
|
||||
const processor = processorFactory(mockLogger, mockReadFilesProcessor);
|
||||
const docs = [
|
||||
{ docType: 'class', name: 'SomeClass', description: 'The is the documentation for the SomeClass API.' },
|
||||
{
|
||||
docType: 'class',
|
||||
name: 'SomeClass',
|
||||
description: 'The is the documentation for the SomeClass API.',
|
||||
vFile: { headings: { h1: ['SomeClass'], h2: ['Some heading'] } }
|
||||
},
|
||||
];
|
||||
processor.$process(docs);
|
||||
expect(docs[docs.length - 1].renderedContent).toEqual(
|
||||
'[{"title":"SomeClass","type":"class","titleWords":"SomeClass","keywords":"api class documentation for is someclass the","members":""}]'
|
||||
const keywordsDoc = docs[docs.length - 1];
|
||||
expect(JSON.parse(keywordsDoc.renderedContent)).toEqual(
|
||||
[{
|
||||
'title':'SomeClass',
|
||||
'type':'class',
|
||||
'titleWords':'SomeClass',
|
||||
'headingWords':'heading some someclass',
|
||||
'keywords':'api class documentation for is someclass the',
|
||||
'members':''
|
||||
}]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
49
aio/tools/transforms/angular-base-package/rendering/truncateCode.js
vendored
Normal file
49
aio/tools/transforms/angular-base-package/rendering/truncateCode.js
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
module.exports = function() {
|
||||
return {
|
||||
name: 'truncateCode',
|
||||
process: function(str, lines) {
|
||||
if (lines === undefined) return str;
|
||||
|
||||
const parts = str && str.split && str.split(/\r?\n/);
|
||||
if (parts && parts.length > lines) {
|
||||
return balance(parts[0] + '...', ['{', '(', '['], ['}', ')', ']']);
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to balance the brackets by adding closers on to the end of a string
|
||||
* for every bracket that is left open.
|
||||
* The chars at each index in the openers and closers should match (i.e openers = ['{', '('], closers = ['}', ')'])
|
||||
*
|
||||
* @param {string} str The string to balance
|
||||
* @param {string[]} openers an array of chars that open a bracket
|
||||
* @param {string[]} closers an array of chars that close a brack
|
||||
* @returns the balanced string
|
||||
*/
|
||||
function balance(str, openers, closers) {
|
||||
const stack = [];
|
||||
|
||||
// Add each open bracket to the stack, removing them when there is a matching closer
|
||||
str.split('').forEach(function(char) {
|
||||
const closerIndex = closers.indexOf(char);
|
||||
if (closerIndex !== -1 && stack[stack.length-1] === closerIndex) {
|
||||
stack.pop();
|
||||
} else {
|
||||
const openerIndex = openers.indexOf(char);
|
||||
if (openerIndex !== -1) {
|
||||
stack.push(openerIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Now the stack should contain all the unclosed brackets
|
||||
while(stack.length) {
|
||||
str += closers[stack.pop()];
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
var factory = require('./truncateCode');
|
||||
|
||||
describe('truncateCode filter', function() {
|
||||
var filter;
|
||||
|
||||
beforeEach(function() { filter = factory(); });
|
||||
|
||||
it('should be called "truncateCode"',
|
||||
function() { expect(filter.name).toEqual('truncateCode'); });
|
||||
|
||||
it('should return the whole string given lines is undefined', function() {
|
||||
expect(filter.process('some text\n \nmore text\n \n'))
|
||||
.toEqual('some text\n \nmore text\n \n');
|
||||
});
|
||||
|
||||
it('should return the whole string if less than the given number of lines', function() {
|
||||
expect(filter.process('this is a pretty long string that only exists on one line', 1))
|
||||
.toEqual('this is a pretty long string that only exists on one line');
|
||||
|
||||
expect(filter.process('this is a pretty long string\nthat exists on two lines', 2))
|
||||
.toEqual('this is a pretty long string\nthat exists on two lines');
|
||||
});
|
||||
|
||||
it('should return the specified number of lines and an ellipsis if there are more lines', function() {
|
||||
expect(filter.process('some text\n \nmore text\n \n', 1)).toEqual('some text...');
|
||||
});
|
||||
|
||||
it('should add closing brackets for all the unclosed opening brackets after truncating', function() {
|
||||
expect(filter.process('()[]{}\nsecond line', 1)).toEqual('()[]{}...');
|
||||
expect(filter.process('([]{}\nsecond line', 1)).toEqual('([]{}...)');
|
||||
expect(filter.process('()[{}\nsecond line', 1)).toEqual('()[{}...]');
|
||||
expect(filter.process('()[]{\nsecond line', 1)).toEqual('()[]{...}');
|
||||
expect(filter.process('([{\nsecond line', 1)).toEqual('([{...}])');
|
||||
});
|
||||
});
|
@ -1,14 +1,14 @@
|
||||
{% import "lib/memberHelpers.html" as memberHelpers -%}
|
||||
{% import "lib/paramList.html" as params -%}
|
||||
{% extends 'export-base.template.html' -%}
|
||||
|
||||
{% block overview %}{% include "includes/class-overview.html" %}{% endblock %}
|
||||
{% block details %}
|
||||
|
||||
{% include "includes/class-overview.html" %}
|
||||
{% block additional %}{% endblock %}
|
||||
{% include "includes/description.html" %}
|
||||
{% include "includes/annotations.html" %}
|
||||
{% include "includes/statics.html" %}
|
||||
{$ memberHelpers.renderMemberDetails(doc.statics, 'static-members', 'static-member', 'Static Members') $}
|
||||
{% include "includes/constructor.html" %}
|
||||
{% include "includes/members.html" %}
|
||||
{$ memberHelpers.renderMemberDetails(doc.members, 'instance-members', 'instance-member', 'Members') $}
|
||||
{% include "includes/annotations.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
@ -1,7 +1,9 @@
|
||||
{% import "lib/memberHelpers.html" as memberHelper -%}
|
||||
{% import "lib/paramList.html" as params -%}
|
||||
{% extends 'export-base.template.html' %}
|
||||
|
||||
{% block overview %}{% include "includes/decorator-overview.html" %}{% endblock %}
|
||||
{% block details %}
|
||||
{% include "includes/description.html" %}
|
||||
{% include "includes/metadata.html" %}
|
||||
{$ memberHelper.renderMemberDetails(doc.members, 'metadata-members', 'metadata-member', 'Metadata Properties') $}
|
||||
{% endblock %}
|
||||
|
@ -1,9 +1,11 @@
|
||||
{% import "lib/directiveHelpers.html" as directiveHelper -%}
|
||||
{% import "lib/paramList.html" as params -%}
|
||||
{% extends 'class.template.html' -%}
|
||||
|
||||
{% block overview %}{% include "includes/directive-overview.html" %}{% endblock %}
|
||||
{% block additional -%}
|
||||
{% include "includes/selectors.html" %}
|
||||
{% include "includes/outputs.html" %}
|
||||
{% include "includes/inputs.html" %}
|
||||
{$ directiveHelper.renderBindings(doc.inputs, 'inputs', 'input', 'Inputs') $}
|
||||
{$ directiveHelper.renderBindings(doc.outputs, 'outputs', 'output', 'Outputs') $}
|
||||
{% include "includes/export-as.html" %}
|
||||
{% endblock %}
|
||||
|
@ -5,6 +5,7 @@
|
||||
{% include "includes/what-it-does.html" %}
|
||||
{% include "includes/security-notes.html" %}
|
||||
{% include "includes/deprecation.html" %}
|
||||
{% block overview %}{% endblock %}
|
||||
{% include "includes/how-to-use.html" %}
|
||||
{% block details %}{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -1,9 +1,24 @@
|
||||
{% import "lib/paramList.html" as params -%}
|
||||
{% extends 'export-base.template.html' -%}
|
||||
|
||||
{% block overview %}
|
||||
<code-example language="ts" hideCopy="true" class="no-box api-heading">
|
||||
function {$ doc.name $}{$ params.paramList(doc.parameters) $}
|
||||
{%- if doc.type %}: {$ doc.type | escape $}{% endif %};
|
||||
</code-example>
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<code-example language="ts" hideCopy="true">
|
||||
function {$ doc.name $}{$ params.paramList(doc.parameters) $}: {$ doc.returnType or 'any' $};
|
||||
{% include "includes/description.html" %}
|
||||
{% if doc.overloads.length %}
|
||||
<h2>Overloads</h2>{% for overload in doc.overloads %}
|
||||
<code-example language="ts" hideCopy="true" class="no-box api-heading">
|
||||
function {$ overload.name $}{$ params.paramList(overload.parameters) $}
|
||||
{%- if overload.type %}: {$ overload.type | escape $}{% endif %};
|
||||
</code-example>
|
||||
{% include "includes/description.html" %}
|
||||
<section class="description">
|
||||
{$ overload.description | trimBlankLines | marked $}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -2,7 +2,7 @@
|
||||
<section class="annotations">
|
||||
<h2>Annotations</h2>
|
||||
{%- for decorator in doc.decorators %}
|
||||
<code-example hideCopy="true">@{$ decorator.name $}{$ params.paramList(decorator.arguments) $}</code-example>
|
||||
<code-example hideCopy="true" class="no-box api-heading">@{$ decorator.name $}{$ params.paramList(decorator.arguments) $}</code-example>
|
||||
{% if not decorator.notYetDocumented %}{$ decorator.description | marked $}{% endif %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
@ -1,17 +1,13 @@
|
||||
{% macro renderMember(member) %}{% if not member.internal -%}
|
||||
<a class="code-anchor" href="#{$ member.name $}">{$ member.name $}</a>{$ params.paramList(member.parameters) | indent(4, false) | trim() $}{$ params.returnType(member.returnType) $}
|
||||
{%- endif %}{% endmacro -%}
|
||||
{% import "lib/memberHelpers.html" as memberHelper -%}
|
||||
|
||||
<section class="class-overview">
|
||||
<h2>Overview</h2>
|
||||
<code-example language="ts" hideCopy="true">
|
||||
{$ doc.docType $} {$ doc.name $}{$ doc.heritage $} {
|
||||
{%- if doc.statics.length %}{% for member in doc.statics %}
|
||||
static {$ renderMember(member) $}{% endfor %}{% endif %}
|
||||
{%- if doc.constructorDoc %}
|
||||
{$ renderMember(doc.constructorDoc) $}{% endif %}
|
||||
{%- if doc.members.length %}{% for member in doc.members %}
|
||||
{$ renderMember(member) $}{% endfor %}{% endif %}
|
||||
}
|
||||
</code-example>
|
||||
</section>
|
||||
<section class="{$ doc.docType $}-overview">
|
||||
<h2>Overview</h2>
|
||||
<code-example language="ts" hideCopy="true">
|
||||
{$ doc.docType $} {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} {
|
||||
{%- if doc.statics.length %}{% for member in doc.statics %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
{%- if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
}
|
||||
</code-example>
|
||||
</section>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<section class="constructor">
|
||||
<a id="{$ doc.constructorDoc.name $}"></a>
|
||||
<h2>Constructor</h2>
|
||||
<code-example hideCopy="true">{$ doc.constructorDoc.name $}{$ params.paramList(doc.constructorDoc.parameters) $}</code-example>
|
||||
<code-example hideCopy="true" class="no-box api-heading">{$ doc.constructorDoc.name $}{$ params.paramList(doc.constructorDoc.parameters) $}</code-example>
|
||||
{% if not doc.constructorDoc.notYetDocumented %}{$ doc.constructorDoc.description | marked $}{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
@ -0,0 +1,10 @@
|
||||
{% import "lib/memberHelpers.html" as memberHelper -%}
|
||||
|
||||
<section class="decorator-overview">
|
||||
<h2>Metadata Overview</h2>
|
||||
<code-example language="ts" hideCopy="true">
|
||||
@{$ doc.name $}{$ doc.typeParams | escape $}({ {% if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
})
|
||||
</code-example>
|
||||
</section>
|
@ -0,0 +1,14 @@
|
||||
{% import "lib/memberHelpers.html" as memberHelper -%}
|
||||
|
||||
<section class="{$ doc.docType $}-overview">
|
||||
<h2>Overview</h2>
|
||||
<code-example language="ts" hideCopy="true">{% for decorator in doc.decorators %}
|
||||
<a href="#annotations">@{$ decorator.name $}{$ params.paramList(decorator.arguments) $}</a>{% endfor %}
|
||||
class {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} {
|
||||
{%- if doc.statics.length %}{% for member in doc.statics %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
{%- if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
}
|
||||
</code-example>
|
||||
</section>
|
@ -1,8 +1,8 @@
|
||||
{%- if doc.directiveOptions.exportAs %}
|
||||
{%- if doc.exportAs %}
|
||||
<section class="export-as">
|
||||
<h2>Exported as</h2>
|
||||
<div>
|
||||
<code>{$ doc.directiveOptions.exportAs $}</code>
|
||||
<code>{$ doc.exportAs $}</code>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
@ -1,14 +1,27 @@
|
||||
{% import "lib/githubLinks.html" as github -%}
|
||||
|
||||
<!-- INFO BAR -->
|
||||
<div class="info-banner api-info-bar">
|
||||
<span class="info-bar-item">
|
||||
npm package: <code>@angular/{$ doc.moduleDoc.id $}</code>
|
||||
</span>
|
||||
<section class="info-bar">
|
||||
|
||||
{% if doc.ngModule %}
|
||||
<span class="info-bar-item">
|
||||
NgModule: {@link {$ doc.ngModule $}}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="is-full-width">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>npm Package</th>
|
||||
<td><a href="https://www.npmjs.com/package/@angular/{$ doc.moduleDoc.id.split('/')[0] $}">@angular/{$ doc.moduleDoc.id.split('/')[0] $}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Module</th>
|
||||
<td><code>import { {$ doc.name $} } from <a href="{$ doc.moduleDoc.path $}">@angular/{$ doc.moduleDoc.id $}</a>;</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<td>{$ github.githubViewLink(doc, versionInfo) $}</td>
|
||||
</tr>
|
||||
{% if doc.ngModule %}
|
||||
<tr>
|
||||
<th>NgModule</th>
|
||||
<td>{@link {$ doc.ngModule $}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -1,11 +0,0 @@
|
||||
{% if doc.inputs %}
|
||||
<section class="inputs">
|
||||
<h2>Inputs</h2>
|
||||
{% for binding, property in doc.inputs %}
|
||||
<div class="input">
|
||||
<code>{$ property.bindingName $}</code> bound to <code>{$ property.memberDoc.classDoc.name $}.{$ property.propertyName $}</code>
|
||||
{$ property.memberDoc.description | trimBlankLines | marked $}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
@ -1,10 +1,10 @@
|
||||
{% import "lib/memberHelpers.html" as memberHelper -%}
|
||||
|
||||
<section class="interface-overview">
|
||||
<h2>Interface Overview</h2>
|
||||
<code-example language="ts" hideCopy="true">
|
||||
interface {$ doc.name $}{$ doc.heritage $} { {% if doc.newMember %}
|
||||
<a class="code-anchor" href="#{$ doc.newMember.name $}">{$ doc.newMember.name | indent(6, false) | trim $}</a>{$ params.paramList(doc.newMember.parameters) | indent(8, false) | trim $}{$ params.returnType(doc.newMember.returnType) $}{% endif %}{% if doc.callMember %}
|
||||
<a class="code-anchor" href="#{$ doc.callMember.name $}">{$ doc.callMember.name | indent(6, false) | trim $}</a>{$ params.paramList(doc.callMember.parameters) | indent(8, false) | trim $}{$ params.returnType(doc.callMember.returnType) $}{% endif %}{% if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.name $}">{$ member.name | indent(6, false) | trim $}</a>{$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $}{% endif %}{% endfor %}{% endif %}
|
||||
}
|
||||
</code-example>
|
||||
<h2>Interface Overview</h2>
|
||||
<code-example language="ts" hideCopy="true">
|
||||
interface {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} { {% if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
}
|
||||
</code-example>
|
||||
</section>
|
@ -1,29 +0,0 @@
|
||||
{% if doc.members.length or doc.newMember or doc.callMember %}
|
||||
<section class="member-members">
|
||||
<h2>Members</h2>
|
||||
{% if doc.newMember %}
|
||||
<div class="new-member">
|
||||
<a id="{$ doc.newMember.name $}"></a>
|
||||
<code-example hideCopy="true">{$ doc.newMember.name $}{$ params.paramList(doc.newMember.parameters) | trim $}{$ params.returnType(doc.newMember.returnType) $}</code-example>
|
||||
{% if not doc.newMember.notYetDocumented %}{$ doc.newMember.description | marked $}{% endif %}
|
||||
</div>
|
||||
{% if doc.members.length or doc.callMember %}<hr>{% endif %}
|
||||
{% endif %}
|
||||
{% if doc.callMember %}
|
||||
<div class="call-member">
|
||||
<a id="{$ doc.callMember.name $}"></a>
|
||||
<code-example hideCopy="true">{$ doc.callMember.name $}{$ params.paramList(doc.callMember.parameters) | trim $}{$ params.returnType(doc.callMember.returnType) $}</code-example>
|
||||
{% if not doc.callMember.notYetDocumented %}{$ doc.callMember.description | marked $}{% endif %}
|
||||
</div>
|
||||
{% if doc.members.length %}<hr>{% endif %}
|
||||
{% endif %}
|
||||
{% for member in doc.members %}{% if not member.internal %}
|
||||
<div class="instance-member">
|
||||
<a id="{$ member.name $}"></a>
|
||||
<code-example hideCopy="true">{$ member.name $}{$ params.paramList(member.parameters) | trim $}{$ params.returnType(member.returnType) $}</code-example>
|
||||
{% if not member.notYetDocumented %}{$ member.description | marked $}{% endif %}
|
||||
</div>
|
||||
{% if not loop.last %}<hr>{% endif %}
|
||||
{% endif %}{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
@ -4,7 +4,7 @@
|
||||
{% for metadata in doc.members %}{% if not metadata.internal %}
|
||||
<div class="metadata-member">
|
||||
<a name="{$ metadata.name $}" class="anchor-offset"></a>
|
||||
<code-example hideCopy="true">{$ metadata.name $}{$ params.paramList(metadata.parameters) | trim $}{$ params.returnType(metadata.returnType) $}</code-example>
|
||||
<code-example hideCopy="true">{$ metadata.name $}{$ params.paramList(metadata.parameters) | trim $}{$ params.returnType(metadata.type) $}</code-example>
|
||||
{%- if not metadata.notYetDocumented %}{$ metadata.description | marked $}{% endif -%}
|
||||
</div>
|
||||
{% if not loop.last %}<hr class="hr-margin">{% endif %}
|
||||
|
@ -1,11 +0,0 @@
|
||||
{% if doc.outputs %}
|
||||
<section class="outputs">
|
||||
<h2>Outputs</h2>
|
||||
{% for binding, property in doc.outputs %}
|
||||
<div class="output">
|
||||
<code>{$ property.bindingName $}</code> bound to <code>{$ property.memberDoc.classDoc.name $}.{$ property.propertyName $}</code>
|
||||
{$ property.memberDoc.description | trimBlankLines | marked $}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
@ -1,7 +1,7 @@
|
||||
{%- if doc.directiveOptions.selector.split(',').length %}
|
||||
{%- if doc.selector %}
|
||||
<section class="selectors">
|
||||
<h2>Selectors</h2>
|
||||
{% for selector in doc.directiveOptions.selector.split(',') %}
|
||||
{% for selector in doc.selector.split(',') %}
|
||||
<div class="selector">
|
||||
<code>{$ selector $}</code>
|
||||
</div>
|
||||
|
@ -1,19 +0,0 @@
|
||||
{% if doc.statics.length %}
|
||||
<section class="static-members">
|
||||
<h2>Static Members</h2>
|
||||
{% for member in doc.statics %}{% if not member.internal %}
|
||||
<div class="static-member">
|
||||
<a id="{$ member.name $}"></a>
|
||||
<code-example hideCopy="true">{$ member.name $}{$ params.paramList(member.parameters) | trim $}{$ params.returnType(member.returnType) $}</code-example>
|
||||
{%- if not member.notYetDocumented %}
|
||||
{$ member.description | marked $}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not loop.last %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
@ -1,8 +1,9 @@
|
||||
{% import "lib/paramList.html" as params -%}
|
||||
{% import "lib/memberHelpers.html" as memberHelper -%}
|
||||
{% extends 'export-base.template.html' -%}
|
||||
|
||||
{% block overview %}{% include "includes/interface-overview.html" %}{% endblock %}
|
||||
{% block details %}
|
||||
{% include "includes/interface-overview.html" %}
|
||||
{% include "includes/description.html" %}
|
||||
{% include "includes/members.html" %}
|
||||
{$ memberHelper.renderMemberDetails(doc.members, 'instance-members', 'instance-member', 'Members') $}
|
||||
{% endblock %}
|
||||
|
13
aio/tools/transforms/templates/api/lib/directiveHelpers.html
Normal file
13
aio/tools/transforms/templates/api/lib/directiveHelpers.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% macro renderBindings(bindings, cssContainerClass, cssItemClass, title) -%}
|
||||
{% if bindings.length %}
|
||||
<section class="{$ cssContainerClass $}">
|
||||
<h2>{$ title $}</h2>
|
||||
{% for binding in bindings %}
|
||||
<div class="{$ cssItemClass $}">
|
||||
<code>{$ binding.bindingName $}</code> bound to <code>{$ binding.memberDoc.containerDoc.name $}.{$ binding.propertyName $}</code>
|
||||
{#{$ binding.memberDoc.description | trimBlankLines | marked $}#}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
@ -1,5 +1,5 @@
|
||||
{% macro githubHref(doc, versionInfo) -%}
|
||||
https://github.com/{$ versionInfo.gitRepoInfo.owner $}/{$ versionInfo.gitRepoInfo.repo $}/tree/{$ versionInfo.currentVersion.isSnapshot and versionInfo.currentVersion.SHA or versionInfo.currentVersion.raw $}/packages/{$ doc.fileInfo.projectRelativePath $}#L{$ doc.location.start.line+1 $}-L{$ doc.location.end.line+1 $}
|
||||
https://github.com/{$ versionInfo.gitRepoInfo.owner $}/{$ versionInfo.gitRepoInfo.repo $}/tree/{$ versionInfo.currentVersion.isSnapshot and versionInfo.currentVersion.SHA or versionInfo.currentVersion.raw $}/packages/{$ doc.fileInfo.projectRelativePath $}#L{$ doc.startingLine + 1 $}-L{$ doc.endingLine + 1 $}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro githubViewLink(doc, versionInfo) -%}
|
||||
|
52
aio/tools/transforms/templates/api/lib/memberHelpers.html
Normal file
52
aio/tools/transforms/templates/api/lib/memberHelpers.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% import "lib/paramList.html" as params -%}
|
||||
|
||||
{%- macro renderHeritage(exportDoc) -%}
|
||||
{%- if exportDoc.extendsClauses.length %} extends {% for clause in exportDoc.extendsClauses -%}
|
||||
{$ clause $}{% if not loop.last %}, {% endif -%}
|
||||
{% endfor %}{% endif %}
|
||||
{%- if exportDoc.implementsClauses.length %} implements {% for clause in exportDoc.implementsClauses -%}
|
||||
{$ clause $}{% if not loop.last %}, {% endif -%}
|
||||
{% endfor %}{% endif %}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro renderMember(member, truncateLines) -%}
|
||||
{%- if member.accessibility !== 'public' %}{$ member.accessibility $} {% endif -%}
|
||||
{%- if member.isGetAccessor %}get {% endif -%}
|
||||
{%- if member.isSetAccessor %}set {% endif -%}
|
||||
{%- if member.isStatic %}static {% endif -%}
|
||||
{$ member.name $}{$ member.typeParameters | escape $}{$ params.paramList(member.parameters, truncateLines) | trim $}
|
||||
{%- if member.isOptional %}?{% endif -%}
|
||||
{$ params.returnType(member.type) | trim | truncateCode(truncateLines) $}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro renderMemberDetail(member, cssClass) -%}
|
||||
<div class="{$ cssClass $}">
|
||||
<a id="{$ member.anchor $}"></a>
|
||||
<code-example hideCopy="true" class="no-box api-heading">{$ renderMember(member) $}</code-example>
|
||||
{%- if not member.notYetDocumented %}
|
||||
{$ member.description | marked $}
|
||||
{% endif -%}
|
||||
</div>
|
||||
{% endmacro -%}
|
||||
|
||||
{% macro renderMemberDetails(members, containerClass, itemClass, titleText) %}
|
||||
{% if members.length %}
|
||||
<section class="{$ containerClass $}">
|
||||
<h2>{$ titleText $}</h2>
|
||||
{% for member in members %}{% if not member.internal %}
|
||||
{$ renderMemberDetail(member, itemClass) $}
|
||||
{% if member.overloads.length %}
|
||||
<details class="overloads">
|
||||
<summary>Overloads</summary>
|
||||
<div class="detail-contents">
|
||||
{% for overload in member.overloads %}
|
||||
{$ renderMemberDetail(overload, itemClass + '-overload') $}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if not loop.last %}<hr class="hr-margin">{% endif %}
|
||||
{% endif %}{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
@ -1,7 +1,7 @@
|
||||
{% macro paramList(params) -%}
|
||||
{% macro paramList(params, truncateLines) -%}
|
||||
{%- if params -%}
|
||||
({%- for param in params -%}
|
||||
{$ param | escape $}{% if not loop.last %}, {% endif %}
|
||||
{$ param | escape | truncateCode(truncateLines) $}{% if not loop.last %}, {% endif %}
|
||||
{%- endfor %})
|
||||
{%- endif %}
|
||||
{%- endmacro -%}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user