Compare commits
200 Commits
10.1.1
...
11.0.0-nex
Author | SHA1 | Date | |
---|---|---|---|
4baabf9cd3 | |||
7244e1b4e3 | |||
b1682526dd | |||
75610505c6 | |||
ba3f4c26bb | |||
c8f056beb6 | |||
a2068523fd | |||
145ab3d7e0 | |||
3082f7378b | |||
297b123151 | |||
a93605f2a4 | |||
15dfd3439a | |||
123bff7cb6 | |||
6158dc16b4 | |||
b0a43872a8 | |||
856e74ac98 | |||
6ae3b68acf | |||
49f27e31ed | |||
1a62f74496 | |||
2b1b7180db | |||
fdd4fa0009 | |||
984ed39195 | |||
0c0c54d615 | |||
dfbfabc052 | |||
f3f6a42342 | |||
e498ea9b5a | |||
a91f0f6b82 | |||
d5ddb9f340 | |||
b68fab547a | |||
16a560119a | |||
88d7bb8386 | |||
2d6105a784 | |||
95b8a8706a | |||
d92a0dd72f | |||
55485713a3 | |||
d3169c533e | |||
e4424863c2 | |||
d795a00137 | |||
7fb388f929 | |||
02c6bff9cf | |||
ef28e15884 | |||
a33d630a21 | |||
c4b8964424 | |||
dd8d8c8289 | |||
129107191c | |||
722699fb0c | |||
5614258cc7 | |||
e62a918542 | |||
49ee90b1b5 | |||
7849fdde09 | |||
97adc27207 | |||
41bc2701c4 | |||
a765530277 | |||
5b33798796 | |||
44074499dc | |||
ba54671993 | |||
a85109fd72 | |||
85a2626620 | |||
d192c87f6a | |||
3817e5f1df | |||
171a0d0696 | |||
a1c1c450dc | |||
fd44d84a33 | |||
f0688b4d18 | |||
b9ca6d20cc | |||
e00535a2a4 | |||
593bd594e3 | |||
284c70ee9d | |||
78e1ecb161 | |||
3934f0a833 | |||
297c060ae7 | |||
077f51685a | |||
e162da0753 | |||
354138eba9 | |||
6768fe9927 | |||
57c442f930 | |||
f667e374a9 | |||
15207e3c9c | |||
66129f8ea6 | |||
da14b72550 | |||
27cc56b359 | |||
6acea54f62 | |||
2d52c80332 | |||
03447ba52f | |||
8f349b2375 | |||
19598b47ca | |||
f56ece4fdc | |||
cf2e8b99a8 | |||
c4556db9f5 | |||
a46e0e48a3 | |||
9e77bd3087 | |||
26f28200bf | |||
ce1efc1af2 | |||
d1415162cb | |||
db21c4fb44 | |||
281865bbcf | |||
3406ec15a4 | |||
85951a0465 | |||
0ae00bb1f7 | |||
1373a98e25 | |||
1ed6913b8b | |||
3c4b8b97c1 | |||
f9421184ef | |||
c6ebb77cec | |||
a69507a0ad | |||
886f58d4fe | |||
18f84a0328 | |||
b0ca3cd0c4 | |||
3d77b64fc3 | |||
f645d26e3f | |||
6d9bfb8368 | |||
7997fc5f97 | |||
3cb2a79399 | |||
d896c33b0e | |||
19a484302d | |||
ded075a79c | |||
ba95b79a21 | |||
4faac78e32 | |||
4360eed9b7 | |||
c880e393e9 | |||
b0c79f2373 | |||
a32a317ea1 | |||
bfb7eec698 | |||
7e0b3fd953 | |||
7a6a061a9e | |||
109555b33a | |||
bf31ef29f6 | |||
40096bee00 | |||
45a73dddfd | |||
687477279b | |||
4007422cc6 | |||
1150649139 | |||
7869de6136 | |||
2c4a98a285 | |||
92ff6d93eb | |||
83ace4ed30 | |||
926ffcd8ac | |||
97475d7408 | |||
a29f9a9fb3 | |||
9f28f82598 | |||
261f689e9b | |||
1d9873c44c | |||
d9da7e5a18 | |||
79d8795633 | |||
3e57ca1d98 | |||
c2d017de83 | |||
7baa7ebfc4 | |||
4e5286180b | |||
73001b42fe | |||
c90eb5450d | |||
3e97435f1c | |||
1c7e5cef3e | |||
2cb3d58b42 | |||
44bb85ade4 | |||
50f4d8a1ce | |||
fdea1804d6 | |||
1d8c5d88cd | |||
a68f1a78a7 | |||
86e11f1110 | |||
5da1934115 | |||
86e7cd8117 | |||
e6ee7c2aeb | |||
54687f7765 | |||
59c234cfb4 | |||
a6f3cd93a9 | |||
d9fea857db | |||
03dbcc7a56 | |||
c142b071eb | |||
71acf9dd49 | |||
f5a148b1b7 | |||
4f28192d62 | |||
0fc2bef0cd | |||
f5d1e9a2d1 | |||
036a2faf02 | |||
5be4edfa17 | |||
38d6596742 | |||
0a7a5e3aff | |||
d5fabc303d | |||
ebc0e46501 | |||
3487b549fd | |||
52c7a4bfc6 | |||
827ba05914 | |||
b2857b4e3a | |||
5d5caf21b8 | |||
c1bc070b40 | |||
930eeaf177 | |||
2dd29fbae7 | |||
9613660fee | |||
c0523fc3b4 | |||
de1cffb23b | |||
31f4557621 | |||
7723bfd9ba | |||
e8ea839df8 | |||
90cec40cce | |||
4036281007 | |||
164cd274a4 | |||
fedcfec346 | |||
618cb32407 | |||
4aee0087ea | |||
0681a20d28 |
@ -653,8 +653,10 @@ jobs:
|
||||
name: Starting Saucelabs tunnel service
|
||||
command: ./tools/saucelabs/sauce-service.sh run
|
||||
background: true
|
||||
- run: yarn tsc -p packages
|
||||
- run: yarn tsc -p modules
|
||||
# add module umd tsc compile option so the test can work
|
||||
# properly in the legacy browsers
|
||||
- run: yarn tsc -p packages --module UMD
|
||||
- run: yarn tsc -p modules --module UMD
|
||||
- run: yarn bazel build //packages/zone.js:npm_package
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
@ -747,6 +749,8 @@ jobs:
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-patch-electron.umd.js ./packages/zone.js/test/extra/ &&
|
||||
yarn --cwd packages/zone.js electrontest
|
||||
- run: yarn --cwd packages/zone.js jesttest
|
||||
- run: yarn --cwd packages/zone.js/test/typings install --frozen-lockfile --non-interactive
|
||||
- run: yarn --cwd packages/zone.js/test/typings test
|
||||
|
||||
# Windows jobs
|
||||
# Docs: https://circleci.com/docs/2.0/hello-world-windows/
|
||||
|
1
.github/angular-robot.yml
vendored
1
.github/angular-robot.yml
vendored
@ -68,6 +68,7 @@ merge:
|
||||
- "packages/**/integrationtest/**"
|
||||
- "packages/**/test/**"
|
||||
- "packages/zone.js/*"
|
||||
- "packages/zone.js/dist/**"
|
||||
- "packages/zone.js/doc/**"
|
||||
- "packages/zone.js/example/**"
|
||||
- "packages/zone.js/scripts/**"
|
||||
|
2
.github/workflows/lock-closed.yml
vendored
2
.github/workflows/lock-closed.yml
vendored
@ -10,6 +10,6 @@ jobs:
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/lock-closed@66462f6
|
||||
- uses: angular/dev-infra/github-actions/lock-closed@414834b2b24dd2df37c6ed00808387ee6fd91b66
|
||||
with:
|
||||
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -40,6 +40,9 @@ yarn-error.log
|
||||
# User specific bazel settings
|
||||
.bazelrc.user
|
||||
|
||||
# User specific ng-dev settings
|
||||
.ng-dev.user*
|
||||
|
||||
.notes.md
|
||||
baseline.json
|
||||
|
||||
|
@ -44654,7 +44654,7 @@ const FOLDERS_IGNORE = [
|
||||
const DEFAULT_IGNORE = (0, (_filter || _load_filter()).ignoreLinesToRegex)([...FOLDERS_IGNORE,
|
||||
|
||||
// ignore cruft
|
||||
'yarn.lock', '.lock-wscript', '.wafpickle-{0..9}', '*.swp', '._*', 'npm-debug.log', 'yarn-error.log', '.npmrc', '.yarnrc', '.npmignore', '.gitignore', '.DS_Store']);
|
||||
'yarn.lock', '.lock-wscript', '.wafpickle-{0..9}', '*.swp', '._*', 'npm-debug.log', 'yarn-error.log', '.npmrc', '.yarnrc', '.yarnrc.yml', '.npmignore', '.gitignore', '.DS_Store']);
|
||||
|
||||
const NEVER_IGNORE = (0, (_filter || _load_filter()).ignoreLinesToRegex)([
|
||||
// never ignore these files
|
||||
@ -44663,6 +44663,7 @@ const NEVER_IGNORE = (0, (_filter || _load_filter()).ignoreLinesToRegex)([
|
||||
function packWithIgnoreAndHeaders(cwd, ignoreFunction, { mapHeader } = {}) {
|
||||
return tar.pack(cwd, {
|
||||
ignore: ignoreFunction,
|
||||
sort: true,
|
||||
map: header => {
|
||||
const suffix = header.name === '.' ? '' : `/${header.name}`;
|
||||
header.name = `package${suffix}`;
|
||||
@ -46678,7 +46679,7 @@ function mkdirfix (name, opts, cb) {
|
||||
/* 194 */
|
||||
/***/ (function(module, exports) {
|
||||
|
||||
module.exports = {"name":"yarn","installationMethod":"unknown","version":"1.22.4","license":"BSD-2-Clause","preferGlobal":true,"description":"📦🐈 Fast, reliable, and secure dependency management.","dependencies":{"@zkochan/cmd-shim":"^3.1.0","babel-runtime":"^6.26.0","bytes":"^3.0.0","camelcase":"^4.0.0","chalk":"^2.1.0","cli-table3":"^0.4.0","commander":"^2.9.0","death":"^1.0.0","debug":"^3.0.0","deep-equal":"^1.0.1","detect-indent":"^5.0.0","dnscache":"^1.0.1","glob":"^7.1.1","gunzip-maybe":"^1.4.0","hash-for-dep":"^1.2.3","imports-loader":"^0.8.0","ini":"^1.3.4","inquirer":"^6.2.0","invariant":"^2.2.0","is-builtin-module":"^2.0.0","is-ci":"^1.0.10","is-webpack-bundle":"^1.0.0","js-yaml":"^3.13.1","leven":"^2.0.0","loud-rejection":"^1.2.0","micromatch":"^2.3.11","mkdirp":"^0.5.1","node-emoji":"^1.6.1","normalize-url":"^2.0.0","npm-logical-tree":"^1.2.1","object-path":"^0.11.2","proper-lockfile":"^2.0.0","puka":"^1.0.0","read":"^1.0.7","request":"^2.87.0","request-capture-har":"^1.2.2","rimraf":"^2.5.0","semver":"^5.1.0","ssri":"^5.3.0","strip-ansi":"^4.0.0","strip-bom":"^3.0.0","tar-fs":"^1.16.0","tar-stream":"^1.6.1","uuid":"^3.0.1","v8-compile-cache":"^2.0.0","validate-npm-package-license":"^3.0.4","yn":"^2.0.0"},"devDependencies":{"babel-core":"^6.26.0","babel-eslint":"^7.2.3","babel-loader":"^6.2.5","babel-plugin-array-includes":"^2.0.3","babel-plugin-inline-import":"^3.0.0","babel-plugin-transform-builtin-extend":"^1.1.2","babel-plugin-transform-inline-imports-commonjs":"^1.0.0","babel-plugin-transform-runtime":"^6.4.3","babel-preset-env":"^1.6.0","babel-preset-flow":"^6.23.0","babel-preset-stage-0":"^6.0.0","babylon":"^6.5.0","commitizen":"^2.9.6","cz-conventional-changelog":"^2.0.0","eslint":"^4.3.0","eslint-config-fb-strict":"^22.0.0","eslint-plugin-babel":"^5.0.0","eslint-plugin-flowtype":"^2.35.0","eslint-plugin-jasmine":"^2.6.2","eslint-plugin-jest":"^21.0.0","eslint-plugin-jsx-a11y":"^6.0.2","eslint-plugin-prefer-object-spread":"^1.2.1","eslint-plugin-prettier":"^2.1.2","eslint-plugin-react":"^7.1.0","eslint-plugin-relay":"^0.0.28","eslint-plugin-yarn-internal":"file:scripts/eslint-rules","execa":"^0.11.0","fancy-log":"^1.3.2","flow-bin":"^0.66.0","git-release-notes":"^3.0.0","gulp":"^4.0.0","gulp-babel":"^7.0.0","gulp-if":"^2.0.1","gulp-newer":"^1.0.0","gulp-plumber":"^1.0.1","gulp-sourcemaps":"^2.2.0","jest":"^22.4.4","jsinspect":"^0.12.6","minimatch":"^3.0.4","mock-stdin":"^0.3.0","prettier":"^1.5.2","string-replace-loader":"^2.1.1","temp":"^0.8.3","webpack":"^2.1.0-beta.25","yargs":"^6.3.0"},"resolutions":{"sshpk":"^1.14.2"},"engines":{"node":">=4.0.0"},"repository":"yarnpkg/yarn","bin":{"yarn":"./bin/yarn.js","yarnpkg":"./bin/yarn.js"},"scripts":{"build":"gulp build","build-bundle":"node ./scripts/build-webpack.js","build-chocolatey":"powershell ./scripts/build-chocolatey.ps1","build-deb":"./scripts/build-deb.sh","build-dist":"bash ./scripts/build-dist.sh","build-win-installer":"scripts\\build-windows-installer.bat","changelog":"git-release-notes $(git describe --tags --abbrev=0 $(git describe --tags --abbrev=0)^)..$(git describe --tags --abbrev=0) scripts/changelog.md","dupe-check":"yarn jsinspect ./src","lint":"eslint . && flow check","pkg-tests":"yarn --cwd packages/pkg-tests jest yarn.test.js","prettier":"eslint src __tests__ --fix","release-branch":"./scripts/release-branch.sh","test":"yarn lint && yarn test-only","test-only":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --verbose","test-only-debug":"node --inspect-brk --max_old_space_size=4096 node_modules/jest/bin/jest.js --runInBand --verbose","test-coverage":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --coverage --verbose","watch":"gulp watch","commit":"git-cz"},"jest":{"collectCoverageFrom":["src/**/*.js"],"testEnvironment":"node","modulePathIgnorePatterns":["__tests__/fixtures/","packages/pkg-tests/pkg-tests-fixtures","dist/"],"testPathIgnorePatterns":["__tests__/(fixtures|__mocks__)/","updates/","_(temp|mock|install|init|helpers).js$","packages/pkg-tests"]},"config":{"commitizen":{"path":"./node_modules/cz-conventional-changelog"}}}
|
||||
module.exports = {"name":"yarn","installationMethod":"unknown","version":"1.22.5","license":"BSD-2-Clause","preferGlobal":true,"description":"📦🐈 Fast, reliable, and secure dependency management.","dependencies":{"@zkochan/cmd-shim":"^3.1.0","babel-runtime":"^6.26.0","bytes":"^3.0.0","camelcase":"^4.0.0","chalk":"^2.1.0","cli-table3":"^0.4.0","commander":"^2.9.0","death":"^1.0.0","debug":"^3.0.0","deep-equal":"^1.0.1","detect-indent":"^5.0.0","dnscache":"^1.0.1","glob":"^7.1.1","gunzip-maybe":"^1.4.0","hash-for-dep":"^1.2.3","imports-loader":"^0.8.0","ini":"^1.3.4","inquirer":"^6.2.0","invariant":"^2.2.0","is-builtin-module":"^2.0.0","is-ci":"^1.0.10","is-webpack-bundle":"^1.0.0","js-yaml":"^3.13.1","leven":"^2.0.0","loud-rejection":"^1.2.0","micromatch":"^2.3.11","mkdirp":"^0.5.1","node-emoji":"^1.6.1","normalize-url":"^2.0.0","npm-logical-tree":"^1.2.1","object-path":"^0.11.2","proper-lockfile":"^2.0.0","puka":"^1.0.0","read":"^1.0.7","request":"^2.87.0","request-capture-har":"^1.2.2","rimraf":"^2.5.0","semver":"^5.1.0","ssri":"^5.3.0","strip-ansi":"^4.0.0","strip-bom":"^3.0.0","tar-fs":"^1.16.0","tar-stream":"^1.6.1","uuid":"^3.0.1","v8-compile-cache":"^2.0.0","validate-npm-package-license":"^3.0.4","yn":"^2.0.0"},"devDependencies":{"babel-core":"^6.26.0","babel-eslint":"^7.2.3","babel-loader":"^6.2.5","babel-plugin-array-includes":"^2.0.3","babel-plugin-inline-import":"^3.0.0","babel-plugin-transform-builtin-extend":"^1.1.2","babel-plugin-transform-inline-imports-commonjs":"^1.0.0","babel-plugin-transform-runtime":"^6.4.3","babel-preset-env":"^1.6.0","babel-preset-flow":"^6.23.0","babel-preset-stage-0":"^6.0.0","babylon":"^6.5.0","commitizen":"^2.9.6","cz-conventional-changelog":"^2.0.0","eslint":"^4.3.0","eslint-config-fb-strict":"^22.0.0","eslint-plugin-babel":"^5.0.0","eslint-plugin-flowtype":"^2.35.0","eslint-plugin-jasmine":"^2.6.2","eslint-plugin-jest":"^21.0.0","eslint-plugin-jsx-a11y":"^6.0.2","eslint-plugin-prefer-object-spread":"^1.2.1","eslint-plugin-prettier":"^2.1.2","eslint-plugin-react":"^7.1.0","eslint-plugin-relay":"^0.0.28","eslint-plugin-yarn-internal":"file:scripts/eslint-rules","execa":"^0.11.0","fancy-log":"^1.3.2","flow-bin":"^0.66.0","git-release-notes":"^3.0.0","gulp":"^4.0.0","gulp-babel":"^7.0.0","gulp-if":"^2.0.1","gulp-newer":"^1.0.0","gulp-plumber":"^1.0.1","gulp-sourcemaps":"^2.2.0","jest":"^22.4.4","jsinspect":"^0.12.6","minimatch":"^3.0.4","mock-stdin":"^0.3.0","prettier":"^1.5.2","string-replace-loader":"^2.1.1","temp":"^0.8.3","webpack":"^2.1.0-beta.25","yargs":"^6.3.0"},"resolutions":{"sshpk":"^1.14.2"},"engines":{"node":">=4.0.0"},"repository":"yarnpkg/yarn","bin":{"yarn":"./bin/yarn.js","yarnpkg":"./bin/yarn.js"},"scripts":{"build":"gulp build","build-bundle":"node ./scripts/build-webpack.js","build-chocolatey":"powershell ./scripts/build-chocolatey.ps1","build-deb":"./scripts/build-deb.sh","build-dist":"bash ./scripts/build-dist.sh","build-win-installer":"scripts\\build-windows-installer.bat","changelog":"git-release-notes $(git describe --tags --abbrev=0 $(git describe --tags --abbrev=0)^)..$(git describe --tags --abbrev=0) scripts/changelog.md","dupe-check":"yarn jsinspect ./src","lint":"eslint . && flow check","pkg-tests":"yarn --cwd packages/pkg-tests jest yarn.test.js","prettier":"eslint src __tests__ --fix","release-branch":"./scripts/release-branch.sh","test":"yarn lint && yarn test-only","test-only":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --verbose","test-only-debug":"node --inspect-brk --max_old_space_size=4096 node_modules/jest/bin/jest.js --runInBand --verbose","test-coverage":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --coverage --verbose","watch":"gulp watch","commit":"git-cz"},"jest":{"collectCoverageFrom":["src/**/*.js"],"testEnvironment":"node","modulePathIgnorePatterns":["__tests__/fixtures/","packages/pkg-tests/pkg-tests-fixtures","dist/"],"testPathIgnorePatterns":["__tests__/(fixtures|__mocks__)/","updates/","_(temp|mock|install|init|helpers).js$","packages/pkg-tests"]},"config":{"commitizen":{"path":"./node_modules/cz-conventional-changelog"}}}
|
||||
|
||||
/***/ }),
|
||||
/* 195 */
|
||||
@ -98338,7 +98339,7 @@ var _buildSubCommands = (0, (_buildSubCommands2 || _load_buildSubCommands()).def
|
||||
|
||||
const bundle = yield fetchBundle(config, bundleUrl);
|
||||
|
||||
const yarnPath = path.resolve(config.lockfileFolder, `.yarn/releases/yarn-${bundleVersion}.js`);
|
||||
const yarnPath = path.resolve(config.lockfileFolder, `.yarn/releases/yarn-${bundleVersion}.cjs`);
|
||||
reporter.log(`Saving it into ${chalk.magenta(yarnPath)}...`);
|
||||
yield (_fs || _load_fs()).mkdirp(path.dirname(yarnPath));
|
||||
yield (_fs || _load_fs()).writeFile(yarnPath, bundle);
|
||||
@ -100190,7 +100191,7 @@ let main = exports.main = (() => {
|
||||
|
||||
const config = new (_config || _load_config()).default(reporter);
|
||||
const outputWrapperEnabled = (0, (_conversion || _load_conversion()).boolifyWithDefault)(process.env.YARN_WRAP_OUTPUT, true);
|
||||
const shouldWrapOutput = outputWrapperEnabled && !(_commander || _load_commander()).default.json && command.hasWrapper((_commander || _load_commander()).default, (_commander || _load_commander()).default.args);
|
||||
const shouldWrapOutput = outputWrapperEnabled && !(_commander || _load_commander()).default.json && command.hasWrapper((_commander || _load_commander()).default, (_commander || _load_commander()).default.args) && !(commandName === 'init' && (_commander || _load_commander()).default[`2`]);
|
||||
|
||||
if (shouldWrapOutput) {
|
||||
reporter.header(commandName, { name: 'yarn', version: (_yarnVersion || _load_yarnVersion()).version });
|
||||
@ -100604,7 +100605,7 @@ let start = (() => {
|
||||
});
|
||||
|
||||
try {
|
||||
if (yarnPath.endsWith(`.js`)) {
|
||||
if (/\.[cm]?js$/.test(yarnPath)) {
|
||||
exitCode = yield (0, (_child || _load_child()).spawnp)(process.execPath, [yarnPath, ...argv], opts);
|
||||
} else {
|
||||
exitCode = yield (0, (_child || _load_child()).spawnp)(yarnPath, argv, opts);
|
2
.yarnrc
2
.yarnrc
@ -2,4 +2,4 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
yarn-path ".yarn/releases/yarn-1.22.4.js"
|
||||
yarn-path ".yarn/releases/yarn-1.22.5.js"
|
||||
|
@ -34,7 +34,7 @@ filegroup(
|
||||
filegroup(
|
||||
name = "angularjs_scripts",
|
||||
srcs = [
|
||||
# We also declare the unminfied AngularJS files since these can be used for
|
||||
# We also declare the unminified AngularJS files since these can be used for
|
||||
# local debugging (e.g. see: packages/upgrade/test/common/test_helpers.ts)
|
||||
"@npm//:node_modules/angular/angular.js",
|
||||
"@npm//:node_modules/angular/angular.min.js",
|
||||
|
164
CHANGELOG.md
164
CHANGELOG.md
@ -1,3 +1,142 @@
|
||||
<a name="11.0.0-next.3"></a>
|
||||
# 11.0.0-next.3 (2020-09-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** add `params` and `reportProgress` options to `HttpClient.put()` overload ([#37873](https://github.com/angular/angular/issues/37873)) ([dd8d8c8](https://github.com/angular/angular/commit/dd8d8c8)), closes [#23600](https://github.com/angular/angular/issues/23600)
|
||||
* **compiler-cli:** generate `let` statements in ES2015+ mode ([#38775](https://github.com/angular/angular/issues/38775)) ([123bff7](https://github.com/angular/angular/commit/123bff7))
|
||||
* **core:** ensure TestBed is not instantiated before override provider ([#38717](https://github.com/angular/angular/issues/38717)) ([c8f056b](https://github.com/angular/angular/commit/c8f056b))
|
||||
* **forms:** type NG_VALUE_ACCESSOR injection token as array ([#29723](https://github.com/angular/angular/issues/29723)) ([2b1b718](https://github.com/angular/angular/commit/2b1b718)), closes [#29351](https://github.com/angular/angular/issues/29351)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **common:** Add ISO week-numbering year formats support to formatDate ([#38828](https://github.com/angular/angular/issues/38828)) ([984ed39](https://github.com/angular/angular/commit/984ed39))
|
||||
* **compiler:** Parse and recover on incomplete opening HTML tags ([#38681](https://github.com/angular/angular/issues/38681)) ([6ae3b68](https://github.com/angular/angular/commit/6ae3b68)), closes [#38596](https://github.com/angular/angular/issues/38596)
|
||||
* **router:** add migration to update calls to navigateByUrl and createUrlTree with invalid parameters ([#38825](https://github.com/angular/angular/issues/38825)) ([7849fdd](https://github.com/angular/angular/commit/7849fdd)), closes [#38227](https://github.com/angular/angular/issues/38227)
|
||||
* **service-worker:** add the option to prefer network for navigation requests ([#38565](https://github.com/angular/angular/issues/38565)) ([a206852](https://github.com/angular/angular/commit/a206852)), closes [#38194](https://github.com/angular/angular/issues/38194)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **core:** If you call `TestBed.overrideProvider` after TestBed initialization, provider overrides are not applied. This
|
||||
behavior is consistent with other override methods (such as `TestBed.overrideDirective`, etc) but they
|
||||
throw an error to indicate that, when the check was missing in the `TestBed.overrideProvider` function.
|
||||
Now calling `TestBed.overrideProvider` after TestBed initialization also triggers an
|
||||
error, thus there is a chance that some tests (where `TestBed.overrideProvider` is
|
||||
called after TestBed initialization) will start to fail and require updates to move `TestBed.overrideProvider` calls
|
||||
before TestBed initialization is completed.
|
||||
|
||||
|
||||
|
||||
<a name="10.1.3"></a>
|
||||
## 10.1.3 (2020-09-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **http:** Fix error message when we call jsonp without importing HttpClientJsonpModule ([#38756](https://github.com/angular/angular/issues/38756)) ([3902ec0](https://github.com/angular/angular/commit/3902ec0))
|
||||
* **ngcc:** fix compilation of `ChangeDetectorRef` in pipe constructors ([#38892](https://github.com/angular/angular/issues/38892)) ([093c3a1](https://github.com/angular/angular/commit/093c3a1)), closes [#38666](https://github.com/angular/angular/issues/38666) [#38883](https://github.com/angular/angular/issues/38883)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* feat(router): better warning message when a router outlet has not been instantiated ([#38920](https://github.com/angular/angular/issues/38920)) ([04d0aa6](https://github.com/angular/angular/commit/04d0aa6))
|
||||
|
||||
|
||||
|
||||
<a name="11.0.0-next.2"></a>
|
||||
# 11.0.0-next.2 (2020-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** do not round up fractions of a millisecond in `DatePipe` ([#38009](https://github.com/angular/angular/issues/38009)) ([26f2820](https://github.com/angular/angular/commit/26f2820)), closes [/www.ecma-international.org/ecma-262/5.1/#sec-15](https://github.com//www.ecma-international.org/ecma-262/5.1//issues/sec-15) [#37989](https://github.com/angular/angular/issues/37989)
|
||||
* **common:** mark locale data arrays as readonly ([#30397](https://github.com/angular/angular/issues/30397)) ([6acea54](https://github.com/angular/angular/commit/6acea54)), closes [#27003](https://github.com/angular/angular/issues/27003)
|
||||
* **compiler:** source span for microsyntax text att should be key span ([#38766](https://github.com/angular/angular/issues/38766)) ([8f349b2](https://github.com/angular/angular/commit/8f349b2))
|
||||
* **router:** Fix arguments order for call to shouldReuseRoute ([#26949](https://github.com/angular/angular/issues/26949)) ([3817e5f](https://github.com/angular/angular/commit/3817e5f)), closes [#16192](https://github.com/angular/angular/issues/16192) [#16192](https://github.com/angular/angular/issues/16192)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** `TemplateTypeChecker` operation to get `Symbol` from a template node ([#38618](https://github.com/angular/angular/issues/38618)) ([c4556db](https://github.com/angular/angular/commit/c4556db))
|
||||
* **compiler-cli:** Add ability to get `Symbol` of `Template`s and `Element`s in component template ([#38618](https://github.com/angular/angular/issues/38618)) ([cf2e8b9](https://github.com/angular/angular/commit/cf2e8b9))
|
||||
* **compiler-cli:** Add ability to get `Symbol` of AST expression in component template ([#38618](https://github.com/angular/angular/issues/38618)) ([f56ece4](https://github.com/angular/angular/commit/f56ece4))
|
||||
* **compiler-cli:** add ability to get symbol of reference or variable ([#38618](https://github.com/angular/angular/issues/38618)) ([19598b4](https://github.com/angular/angular/commit/19598b4))
|
||||
* **compiler-cli:** define interfaces to be used for TemplateTypeChecker ([#38618](https://github.com/angular/angular/issues/38618)) ([9e77bd3](https://github.com/angular/angular/commit/9e77bd3))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **compiler-cli:** only emit directive/pipe references that are used ([#38539](https://github.com/angular/angular/issues/38539)) ([077f516](https://github.com/angular/angular/commit/077f516))
|
||||
* **compiler-cli:** optimize computation of type-check scope information ([#38539](https://github.com/angular/angular/issues/38539)) ([297c060](https://github.com/angular/angular/commit/297c060))
|
||||
* **router:** use `ngDevMode` to tree-shake error messages in router ([#38674](https://github.com/angular/angular/issues/38674)) ([db21c4f](https://github.com/angular/angular/commit/db21c4f))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **router:** This change corrects the argument order when calling
|
||||
RouteReuseStrategy#shouldReuseRoute. Previously, when evaluating child
|
||||
routes, they would be called with the future and current arguments would
|
||||
be swapped. If your RouteReuseStrategy relies specifically on only the future
|
||||
or current snapshot state, you may need to update the shouldReuseRoute
|
||||
implementation's use of "future" and "current" ActivateRouteSnapshots.
|
||||
* **common:** The locale data API has been marked as returning readonly arrays, rather
|
||||
than mutable arrays, since these arrays are shared across calls to the
|
||||
API. If you were mutating them (e.g. calling `sort()`, `push()`, `splice()`, etc)
|
||||
then your code will not longer compile. If you need to mutate the array, you
|
||||
should now take a copy (e.g. by calling `slice()`) and mutate the copy.
|
||||
* **common:** When passing a date-time formatted string to the `DatePipe` in a format that contains
|
||||
fractions of a millisecond, the milliseconds will now always be rounded down rather than
|
||||
to the nearest millisecond.
|
||||
|
||||
Most applications will not be affected by this change. If this is not the desired behaviour
|
||||
then consider pre-processing the string to round the millisecond part before passing
|
||||
it to the `DatePipe`.
|
||||
|
||||
|
||||
|
||||
<a name="10.1.2"></a>
|
||||
## 10.1.2 (2020-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** detect pipes in ICUs in template binder ([#38810](https://github.com/angular/angular/issues/38810)) ([ec2dbe7](https://github.com/angular/angular/commit/ec2dbe7)), closes [#38539](https://github.com/angular/angular/issues/38539) [#38539](https://github.com/angular/angular/issues/38539) [#38539](https://github.com/angular/angular/issues/38539)
|
||||
* **core:** clear the `RefreshTransplantedView` when detached ([#38768](https://github.com/angular/angular/issues/38768)) ([edb7f90](https://github.com/angular/angular/commit/edb7f90)), closes [#38619](https://github.com/angular/angular/issues/38619)
|
||||
* **localize:** ensure that `formatOptions` is optional ([#38787](https://github.com/angular/angular/issues/38787)) ([a47383d](https://github.com/angular/angular/commit/a47383d))
|
||||
* **router:** Ensure routes are processed in priority order and only if needed ([#38780](https://github.com/angular/angular/issues/38780)) ([9c51ba3](https://github.com/angular/angular/commit/9c51ba3)), closes [#38691](https://github.com/angular/angular/issues/38691)
|
||||
* **upgrade:** add try/catch when downgrading injectables ([#38671](https://github.com/angular/angular/issues/38671)) ([5de2ac3](https://github.com/angular/angular/commit/5de2ac3)), closes [#37579](https://github.com/angular/angular/issues/37579)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **compiler-cli:** only emit directive/pipe references that are used ([#38843](https://github.com/angular/angular/issues/38843)) ([5658405](https://github.com/angular/angular/commit/5658405))
|
||||
* **compiler-cli:** optimize computation of type-check scope information ([#38843](https://github.com/angular/angular/issues/38843)) ([ebede67](https://github.com/angular/angular/commit/ebede67))
|
||||
* **ngcc:** introduce cache for sharing data across entry-points ([#38840](https://github.com/angular/angular/issues/38840)) ([58411e7](https://github.com/angular/angular/commit/58411e7))
|
||||
* **ngcc:** reduce maximum worker count ([#38840](https://github.com/angular/angular/issues/38840)) ([ea36466](https://github.com/angular/angular/commit/ea36466))
|
||||
|
||||
|
||||
|
||||
<a name="11.0.0-next.1"></a>
|
||||
# 11.0.0-next.1 (2020-09-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** compute source-mappings for localized strings ([#38645](https://github.com/angular/angular/issues/38645)) ([7e0b3fd](https://github.com/angular/angular/commit/7e0b3fd)), closes [#38588](https://github.com/angular/angular/issues/38588)
|
||||
* **core:** remove CollectionChangeRecord symbol ([#38668](https://github.com/angular/angular/issues/38668)) ([fdea180](https://github.com/angular/angular/commit/fdea180))
|
||||
* **router:** support lazy loading for empty path named outlets ([#38379](https://github.com/angular/angular/issues/38379)) ([926ffcd](https://github.com/angular/angular/commit/926ffcd)), closes [#12842](https://github.com/angular/angular/issues/12842)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **core:** CollectionChangeRecord has been removed, use IterableChangeRecord
|
||||
instead
|
||||
|
||||
|
||||
|
||||
<a name="10.1.1"></a>
|
||||
## 10.1.1 (2020-09-09)
|
||||
|
||||
@ -23,6 +162,31 @@
|
||||
|
||||
|
||||
|
||||
<a name="11.0.0-next.0"></a>
|
||||
# 11.0.0-next.0 (2020-09-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **forms:** ensure to emit `statusChanges` on subsequent value update/validations ([#38354](https://github.com/angular/angular/issues/38354)) ([d9fea85](https://github.com/angular/angular/commit/d9fea85)), closes [#20424](https://github.com/angular/angular/issues/20424) [#14542](https://github.com/angular/angular/issues/14542)
|
||||
* **service-worker:** fix condition to check for a cache-busted request ([#36847](https://github.com/angular/angular/issues/36847)) ([5be4edf](https://github.com/angular/angular/commit/5be4edf))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **service-worker:** add `UnrecoverableStateError` ([#36847](https://github.com/angular/angular/issues/36847)) ([036a2fa](https://github.com/angular/angular/commit/036a2fa)), closes [#36539](https://github.com/angular/angular/issues/36539)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **forms:** Previously if FormControl, FormGroup and FormArray class instances had async validators
|
||||
defined at initialization time, the status change event was not emitted once async validator
|
||||
completed. After this change the status event is emitted into the `statusChanges` observable.
|
||||
If your code relies on the old behavior, you can filter/ignore this additional status change
|
||||
event.
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0"></a>
|
||||
# 10.1.0 (2020-09-02)
|
||||
|
||||
|
@ -21,11 +21,13 @@ import { ItemDirective } from './item.directive';
|
||||
ItemDirective
|
||||
],
|
||||
// #enddocregion declarations
|
||||
// #docregion imports
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpClientModule
|
||||
],
|
||||
// #enddocregion imports
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
@ -0,0 +1,9 @@
|
||||
/*
|
||||
* This example project is special in that it is not a cli app. To run tests appropriate for this
|
||||
* project, the test command is overwritten in `aio/content/examples/observables/example-config.json`.
|
||||
*
|
||||
* This is an empty placeholder file to ensure that `aio/tools/examples/run-example-e2e.js` runs
|
||||
* tests for this project.
|
||||
*
|
||||
* TODO: Fix our infrastructure/tooling, so that this hack is not necessary.
|
||||
*/
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"tests": [
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
|
||||
},
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": ["jasmine", "out-tsc/**/*.spec.js"]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { docRegionChain, docRegionObservable, docRegionUnsubscribe } from './observables';
|
||||
|
||||
describe('observables', () => {
|
||||
it('should print 2', (doneFn: DoneFn) => {
|
||||
const consoleLogSpy = spyOn(console, 'log');
|
||||
const observable = docRegionObservable(console);
|
||||
observable.subscribe(() => {
|
||||
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(2);
|
||||
doneFn();
|
||||
});
|
||||
});
|
||||
|
||||
it('should close the subscription', () => {
|
||||
const subscription = docRegionUnsubscribe();
|
||||
expect(subscription.closed).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should chain an observable', (doneFn: DoneFn) => {
|
||||
const observable = docRegionChain();
|
||||
observable.subscribe(value => {
|
||||
expect(value).toBe(4);
|
||||
doneFn();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,20 +1,37 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
// #docplaster
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export function docRegionObservable(console: Console) {
|
||||
// #docregion observable
|
||||
|
||||
// declare a publishing operation
|
||||
const observable = new Observable<number>(observer => {
|
||||
// Subscriber fn...
|
||||
// #enddocregion observable
|
||||
// The below code is used for unit testing only
|
||||
observer.next(2);
|
||||
// #docregion observable
|
||||
});
|
||||
|
||||
// initiate execution
|
||||
observable.subscribe(() => {
|
||||
observable.subscribe(value => {
|
||||
// observer handles notifications
|
||||
// #enddocregion observable
|
||||
// The below code is used for unit testing only
|
||||
console.log(value);
|
||||
// #docregion observable
|
||||
});
|
||||
|
||||
// #enddocregion observable
|
||||
return observable;
|
||||
}
|
||||
|
||||
export function docRegionUnsubscribe() {
|
||||
const observable = new Observable<number>(() => {
|
||||
// Subscriber fn...
|
||||
});
|
||||
// #docregion unsubscribe
|
||||
|
||||
const subscription = observable.subscribe(() => {
|
||||
@ -24,17 +41,32 @@ const subscription = observable.subscribe(() => {
|
||||
subscription.unsubscribe();
|
||||
|
||||
// #enddocregion unsubscribe
|
||||
return subscription;
|
||||
}
|
||||
|
||||
// #docregion error
|
||||
|
||||
observable.subscribe(() => {
|
||||
throw Error('my error');
|
||||
export function docRegionError() {
|
||||
const observable = new Observable<number>(() => {
|
||||
// Subscriber fn...
|
||||
});
|
||||
|
||||
// #docregion error
|
||||
observable.subscribe(() => {
|
||||
throw new Error('my error');
|
||||
});
|
||||
// #enddocregion error
|
||||
}
|
||||
|
||||
export function docRegionChain() {
|
||||
let observable = new Observable<number>(observer => {
|
||||
// Subscriber fn...
|
||||
observer.next(2);
|
||||
});
|
||||
|
||||
observable =
|
||||
// #docregion chain
|
||||
|
||||
observable.pipe(map(v => 2 * v));
|
||||
|
||||
// #enddocregion chain
|
||||
return observable;
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { docRegionError, docRegionPromise } from './promises';
|
||||
|
||||
describe('promises', () => {
|
||||
it('should print 2', (doneFn: DoneFn) => {
|
||||
const consoleLogSpy = spyOn(console, 'log');
|
||||
const pr = docRegionPromise(console, 2);
|
||||
pr.then((value) => {
|
||||
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(2);
|
||||
expect(value).toBe(4);
|
||||
doneFn();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error', (doneFn: DoneFn) => {
|
||||
const promise = docRegionError();
|
||||
promise
|
||||
.then(() => {
|
||||
throw new Error('Promise should be rejected.');
|
||||
},
|
||||
() => doneFn());
|
||||
});
|
||||
});
|
@ -1,25 +1,44 @@
|
||||
// #docplaster
|
||||
|
||||
export function docRegionPromise(console: Console, inputValue: number) {
|
||||
// #docregion promise
|
||||
// initiate execution
|
||||
const promise = new Promise<number>((resolve, reject) => {
|
||||
let promise = new Promise<number>((resolve, reject) => {
|
||||
// Executer fn...
|
||||
// #enddocregion promise
|
||||
// The below is used in the unit tests.
|
||||
resolve(inputValue);
|
||||
// #docregion promise
|
||||
});
|
||||
|
||||
// #enddocregion promise
|
||||
promise =
|
||||
// #docregion promise
|
||||
promise.then(value => {
|
||||
// handle result here
|
||||
});
|
||||
|
||||
// #enddocregion promise
|
||||
|
||||
// The below is used in the unit tests.
|
||||
console.log(value);
|
||||
return value;
|
||||
// #docregion promise
|
||||
});
|
||||
// #enddocregion promise
|
||||
promise =
|
||||
// #docregion chain
|
||||
|
||||
promise.then(v => 2 * v);
|
||||
|
||||
// #enddocregion chain
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
export function docRegionError() {
|
||||
let promise = Promise.resolve();
|
||||
promise =
|
||||
// #docregion error
|
||||
|
||||
promise.then(() => {
|
||||
throw Error('my error');
|
||||
throw new Error('my error');
|
||||
});
|
||||
|
||||
// #enddocregion error
|
||||
return promise;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// TODO: Add unit tests for this file.
|
||||
// tslint:disable: no-output-native
|
||||
// #docregion
|
||||
import { Component, Output, OnInit, EventEmitter, NgModule } from '@angular/core';
|
||||
|
@ -2,7 +2,11 @@
|
||||
"tests": [
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": [ "tsc", "--project", "./tsconfig.app.json" ]
|
||||
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
|
||||
},
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": ["jasmine", "out-tsc/**/*.spec.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
55
aio/content/examples/observables/src/creating.spec.ts
Normal file
55
aio/content/examples/observables/src/creating.spec.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { docRegionFromEvent, docRegionSubscriber } from './creating';
|
||||
|
||||
describe('observables', () => {
|
||||
it('should create an observable using the constructor', () => {
|
||||
const console = {log: jasmine.createSpy('log')};
|
||||
docRegionSubscriber(console);
|
||||
expect(console.log).toHaveBeenCalledTimes(4);
|
||||
expect(console.log.calls.allArgs()).toEqual([
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
['Finished sequence'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should listen to input changes', () => {
|
||||
let triggerInputChange;
|
||||
const input = {
|
||||
value: 'Test',
|
||||
addEventListener: jasmine
|
||||
.createSpy('addEvent')
|
||||
.and.callFake((eventName: string, cb: (e) => void) => {
|
||||
if (eventName === 'keydown') {
|
||||
triggerInputChange = cb;
|
||||
}
|
||||
}),
|
||||
removeEventListener: jasmine.createSpy('removeEventListener'),
|
||||
};
|
||||
|
||||
const document = { getElementById: () => input };
|
||||
docRegionFromEvent(document);
|
||||
triggerInputChange({keyCode: 65});
|
||||
expect(input.value).toBe('Test');
|
||||
|
||||
triggerInputChange({keyCode: 27});
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
it('should call removeEventListener when unsubscribing', (doneFn: DoneFn) => {
|
||||
const input = {
|
||||
addEventListener: jasmine.createSpy('addEvent'),
|
||||
removeEventListener: jasmine
|
||||
.createSpy('removeEvent')
|
||||
.and.callFake((eventName: string, cb: (e) => void) => {
|
||||
if (eventName === 'keydown') {
|
||||
doneFn();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const document = { getElementById: () => input };
|
||||
const subscription = docRegionFromEvent(document);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
@ -1,8 +1,9 @@
|
||||
// #docplaster
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export function docRegionSubscriber(console) {
|
||||
// #docregion subscriber
|
||||
|
||||
// This function runs when subscribe() is called
|
||||
function sequenceSubscriber(observer) {
|
||||
// synchronously deliver 1, 2, and 3, then complete
|
||||
@ -30,8 +31,8 @@ sequence.subscribe({
|
||||
// 2
|
||||
// 3
|
||||
// Finished sequence
|
||||
|
||||
// #enddocregion subscriber
|
||||
}
|
||||
|
||||
// #docregion fromevent
|
||||
|
||||
@ -51,16 +52,18 @@ function fromEvent(target, eventName) {
|
||||
|
||||
// #enddocregion fromevent
|
||||
|
||||
export function docRegionFromEvent(document) {
|
||||
// #docregion fromevent_use
|
||||
|
||||
const ESC_KEY = 27;
|
||||
const nameInput = document.getElementById('name') as HTMLInputElement;
|
||||
|
||||
const subscription = fromEvent(nameInput, 'keydown')
|
||||
.subscribe((e: KeyboardEvent) => {
|
||||
const subscription = fromEvent(nameInput, 'keydown').subscribe((e: KeyboardEvent) => {
|
||||
if (e.keyCode === ESC_KEY) {
|
||||
nameInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// #enddocregion fromevent_use
|
||||
return subscription;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
// TODO: Add unit tests for this file.
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// #docregion
|
||||
|
||||
// Create an Observable that will start listening to geolocation updates
|
||||
|
48
aio/content/examples/observables/src/multicasting.spec.ts
Normal file
48
aio/content/examples/observables/src/multicasting.spec.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { docRegionDelaySequence, docRegionMulticastSequence } from './multicasting';
|
||||
|
||||
describe('multicasting', () => {
|
||||
let console;
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
console = {log: jasmine.createSpy('log')};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should create an observable and emit in sequence', () => {
|
||||
docRegionDelaySequence(console);
|
||||
jasmine.clock().tick(10000);
|
||||
expect(console.log).toHaveBeenCalledTimes(12);
|
||||
expect(console.log.calls.allArgs()).toEqual([
|
||||
[1],
|
||||
['1st subscribe: 1'],
|
||||
['2nd subscribe: 1'],
|
||||
[2],
|
||||
['1st subscribe: 2'],
|
||||
['2nd subscribe: 2'],
|
||||
[3],
|
||||
['Finished sequence'],
|
||||
['1st subscribe: 3'],
|
||||
['1st sequence finished.'],
|
||||
['2nd subscribe: 3'],
|
||||
['2nd sequence finished.']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create an observable and multicast the emissions', () => {
|
||||
docRegionMulticastSequence(console);
|
||||
jasmine.clock().tick(10000);
|
||||
expect(console.log).toHaveBeenCalledTimes(7);
|
||||
expect(console.log.calls.allArgs()).toEqual([
|
||||
['1st subscribe: 1'],
|
||||
['1st subscribe: 2'],
|
||||
['2nd subscribe: 2'],
|
||||
['1st subscribe: 3'],
|
||||
['2nd subscribe: 3'],
|
||||
['1st sequence finished.'],
|
||||
['2nd sequence finished.']
|
||||
]);
|
||||
});
|
||||
});
|
@ -1,8 +1,9 @@
|
||||
// #docplaster
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export function docRegionDelaySequence(console) {
|
||||
// #docregion delay_sequence
|
||||
|
||||
function sequenceSubscriber(observer) {
|
||||
const seq = [1, 2, 3];
|
||||
let timeoutId;
|
||||
@ -23,9 +24,11 @@ function sequenceSubscriber(observer) {
|
||||
doInSequence(seq, 0);
|
||||
|
||||
// Unsubscribe should clear the timeout to stop execution
|
||||
return {unsubscribe() {
|
||||
return {
|
||||
unsubscribe() {
|
||||
clearTimeout(timeoutId);
|
||||
}};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Create a new Observable that will deliver the above sequence
|
||||
@ -71,9 +74,10 @@ setTimeout(() => {
|
||||
// (at 3.5 seconds): 2nd sequence finished
|
||||
|
||||
// #enddocregion subscribe_twice
|
||||
}
|
||||
|
||||
export function docRegionMulticastSequence(console) {
|
||||
// #docregion multicast_sequence
|
||||
|
||||
function multicastSequenceSubscriber() {
|
||||
const seq = [1, 2, 3];
|
||||
// Keep track of each observer (one for every active subscription)
|
||||
@ -84,7 +88,7 @@ function multicastSequenceSubscriber() {
|
||||
|
||||
// Return the subscriber function (runs when subscribe()
|
||||
// function is invoked)
|
||||
return (observer) => {
|
||||
return observer => {
|
||||
observers.push(observer);
|
||||
// When this is the first subscription, start the sequence
|
||||
if (observers.length === 1) {
|
||||
@ -153,3 +157,4 @@ setTimeout(() => {
|
||||
// (at 3 seconds): 2nd sequence finished
|
||||
|
||||
// #enddocregion multicast_sequence
|
||||
}
|
||||
|
19
aio/content/examples/observables/src/subscribing.spec.ts
Normal file
19
aio/content/examples/observables/src/subscribing.spec.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { docRegionObserver } from './subscribing';
|
||||
|
||||
describe('subscribing', () => {
|
||||
it('should subscribe and emit', () => {
|
||||
const console = {log: jasmine.createSpy('log')};
|
||||
docRegionObserver(console);
|
||||
expect(console.log).toHaveBeenCalledTimes(8);
|
||||
expect(console.log.calls.allArgs()).toEqual([
|
||||
['Observer got a next value: 1'],
|
||||
['Observer got a next value: 2'],
|
||||
['Observer got a next value: 3'],
|
||||
['Observer got a complete notification'],
|
||||
['Observer got a next value: 1'],
|
||||
['Observer got a next value: 2'],
|
||||
['Observer got a next value: 3'],
|
||||
['Observer got a complete notification'],
|
||||
]);
|
||||
});
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
// #docplaster
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
export function docRegionObserver(console) {
|
||||
// #docregion observer
|
||||
|
||||
// Create simple observable that emits three values
|
||||
@ -15,6 +16,7 @@ const myObserver = {
|
||||
|
||||
// Execute with the observer object
|
||||
myObservable.subscribe(myObserver);
|
||||
|
||||
// Logs:
|
||||
// Observer got a next value: 1
|
||||
// Observer got a next value: 2
|
||||
@ -30,3 +32,4 @@ myObservable.subscribe(
|
||||
() => console.log('Observer got a complete notification')
|
||||
);
|
||||
// #enddocregion sub_fn
|
||||
}
|
||||
|
@ -2,7 +2,11 @@
|
||||
"tests": [
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": [ "tsc", "--project", "./tsconfig.app.json" ]
|
||||
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
|
||||
},
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": ["jasmine", "out-tsc/**/*.spec.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
|
||||
// TODO: Add unit tests for this file.
|
||||
import { pipe, range, timer, zip } from 'rxjs';
|
||||
import { ajax } from 'rxjs/ajax';
|
||||
import { retryWhen, map, mergeMap } from 'rxjs/operators';
|
||||
|
@ -0,0 +1,72 @@
|
||||
import { of } from 'rxjs';
|
||||
import { docRegionTypeahead } from './typeahead';
|
||||
|
||||
describe('typeahead', () => {
|
||||
let document;
|
||||
let ajax;
|
||||
let triggertInputChange;
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
const input = {
|
||||
addEventListener: jasmine
|
||||
.createSpy('addEvent')
|
||||
.and.callFake((eventName: string, cb: (e) => void) => {
|
||||
if (eventName === 'input') {
|
||||
triggertInputChange = cb;
|
||||
}
|
||||
}),
|
||||
removeEventListener: jasmine.createSpy('removeEvent'),
|
||||
};
|
||||
|
||||
document = { getElementById: (id: string) => input };
|
||||
ajax = jasmine.createSpy('ajax').and.callFake((url: string) => of('foo bar'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should make an ajax call to the corrent endpoint', () => {
|
||||
docRegionTypeahead(document, ajax);
|
||||
triggertInputChange({ target: { value: 'foo' } });
|
||||
jasmine.clock().tick(11);
|
||||
expect(ajax).toHaveBeenCalledWith('/api/endpoint?search=foo');
|
||||
});
|
||||
|
||||
it('should not make an ajax call, when the input length < 3', () => {
|
||||
docRegionTypeahead(document, ajax);
|
||||
triggertInputChange({ target: { value: '' } });
|
||||
jasmine.clock().tick(11);
|
||||
expect(ajax).not.toHaveBeenCalled();
|
||||
triggertInputChange({ target: { value: 'fo' } });
|
||||
jasmine.clock().tick(11);
|
||||
expect(ajax).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not make an ajax call for intermediate values when debouncing', () => {
|
||||
docRegionTypeahead(document, ajax);
|
||||
triggertInputChange({ target: { value: 'foo' } });
|
||||
jasmine.clock().tick(9);
|
||||
triggertInputChange({ target: { value: 'bar' } });
|
||||
jasmine.clock().tick(9);
|
||||
triggertInputChange({ target: { value: 'baz' } });
|
||||
jasmine.clock().tick(9);
|
||||
triggertInputChange({ target: { value: 'qux' } });
|
||||
expect(ajax).not.toHaveBeenCalled();
|
||||
jasmine.clock().tick(10);
|
||||
expect(ajax).toHaveBeenCalledTimes(1);
|
||||
expect(ajax).toHaveBeenCalledWith('/api/endpoint?search=qux');
|
||||
});
|
||||
|
||||
it('should not make an ajax call, when the input value has not changed', () => {
|
||||
docRegionTypeahead(document, ajax);
|
||||
triggertInputChange({ target: { value: 'foo' } });
|
||||
jasmine.clock().tick(11);
|
||||
expect(ajax).toHaveBeenCalled();
|
||||
ajax.calls.reset();
|
||||
triggertInputChange({ target: { value: 'foo' } });
|
||||
jasmine.clock().tick(11);
|
||||
expect(ajax).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -1,8 +1,18 @@
|
||||
/*
|
||||
Because of how the code is merged together using the doc regions,
|
||||
we need to indent the imports with the function below.
|
||||
*/
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import { fromEvent } from 'rxjs';
|
||||
import { ajax } from 'rxjs/ajax';
|
||||
import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
|
||||
// #enddocregion
|
||||
/* tslint:disable:no-shadowed-variable */
|
||||
/* tslint:disable:align */
|
||||
export function docRegionTypeahead(document, ajax) {
|
||||
// #docregion
|
||||
const searchBox = document.getElementById('search-box');
|
||||
|
||||
const typeahead = fromEvent(searchBox, 'input').pipe(
|
||||
@ -10,9 +20,13 @@ const typeahead = fromEvent(searchBox, 'input').pipe(
|
||||
filter(text => text.length > 2),
|
||||
debounceTime(10),
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => ajax('/api/endpoint'))
|
||||
switchMap(searchTerm => ajax(`/api/endpoint?search=${searchTerm}`))
|
||||
);
|
||||
|
||||
typeahead.subscribe(data => {
|
||||
// Handle the data from the API
|
||||
});
|
||||
|
||||
// #enddocregion
|
||||
return typeahead;
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
|
||||
function notifyUser(message: string): void { }
|
||||
|
||||
// #docregion sw-unrecoverable-state
|
||||
@Injectable()
|
||||
export class HandleUnrecoverableStateService {
|
||||
constructor(updates: SwUpdate) {
|
||||
updates.unrecoverable.subscribe(event => {
|
||||
notifyUser(
|
||||
`An error occurred that we cannot recover from:\n${event.reason}\n\n` +
|
||||
'Please reload the page.');
|
||||
});
|
||||
}
|
||||
}
|
||||
// #enddocregion sw-unrecoverable-state
|
@ -28,7 +28,7 @@ import {
|
||||
ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync
|
||||
} from '@angular/core/testing';
|
||||
|
||||
import { addMatchers, newEvent, click } from '../../testing';
|
||||
import { addMatchers, click } from '../../testing';
|
||||
|
||||
export class NotProvided extends ValueService { /* example below */ }
|
||||
beforeEach(addMatchers);
|
||||
@ -274,8 +274,10 @@ describe('demo (with TestBed):', () => {
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// Dispatch a DOM event so that Angular learns of input value change.
|
||||
// then wait while ngModel pushes input.box value to comp.name
|
||||
// In older browsers, such as IE, you might need a CustomEvent instead. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
|
||||
input.dispatchEvent(new Event('input'));
|
||||
return fixture.whenStable();
|
||||
})
|
||||
@ -312,8 +314,10 @@ describe('demo (with TestBed):', () => {
|
||||
expect(comp.name).toBe(expectedOrigName,
|
||||
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// Dispatch a DOM event so that Angular learns of input value change.
|
||||
// then wait a tick while ngModel pushes input.box value to comp.name
|
||||
// In older browsers, such as IE, you might need a CustomEvent instead. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
|
||||
input.dispatchEvent(new Event('input'));
|
||||
tick();
|
||||
expect(comp.name).toBe(expectedNewName,
|
||||
@ -335,9 +339,11 @@ describe('demo (with TestBed):', () => {
|
||||
// simulate user entering new name in input
|
||||
input.value = inputText;
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// Dispatch a DOM event so that Angular learns of input value change.
|
||||
// then wait a tick while ngModel pushes input.box value to comp.text
|
||||
// and Angular updates the output span
|
||||
// In older browsers, such as IE, you might need a CustomEvent instead. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
|
||||
input.dispatchEvent(new Event('input'));
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
@ -3,7 +3,7 @@ import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import {
|
||||
ActivatedRoute, ActivatedRouteStub, asyncData, click, newEvent
|
||||
ActivatedRoute, ActivatedRouteStub, asyncData, click
|
||||
} from '../../testing';
|
||||
|
||||
import { Hero } from '../model/hero';
|
||||
@ -99,6 +99,9 @@ function overrideSetup() {
|
||||
const newName = 'New Name';
|
||||
|
||||
page.nameInput.value = newName;
|
||||
|
||||
// In older browsers, such as IE, you might need a CustomEvent instead. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
|
||||
page.nameInput.dispatchEvent(new Event('input')); // tell Angular
|
||||
|
||||
expect(component.hero.name).toBe(newName, 'component hero has new name');
|
||||
@ -197,8 +200,9 @@ function heroModuleSetup() {
|
||||
// simulate user entering a new name into the input box
|
||||
nameInput.value = 'quick BROWN fOx';
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// use newEvent utility function (not provided by Angular) for better browser compatibility
|
||||
// Dispatch a DOM event so that Angular learns of input value change.
|
||||
// In older browsers, such as IE, you might need a CustomEvent instead. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
|
||||
nameInput.dispatchEvent(new Event('input'));
|
||||
|
||||
// Tell Angular to update the display binding through the title pipe
|
||||
|
@ -6,7 +6,7 @@ import { DebugElement } from '@angular/core';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { addMatchers, newEvent } from '../../testing';
|
||||
import { addMatchers } from '../../testing';
|
||||
import { HeroService } from '../model/hero.service';
|
||||
import { getTestHeroes, TestHeroService } from '../model/testing/test-hero.service';
|
||||
|
||||
@ -53,6 +53,9 @@ describe('HeroListComponent', () => {
|
||||
it('should select hero on click', fakeAsync(() => {
|
||||
const expectedHero = HEROES[1];
|
||||
const li = page.heroRows[1];
|
||||
|
||||
// In older browsers, such as IE, you might need a CustomEvent instead. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
|
||||
li.dispatchEvent(new Event('click'));
|
||||
tick();
|
||||
// `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService
|
||||
@ -62,6 +65,9 @@ describe('HeroListComponent', () => {
|
||||
it('should navigate to selected hero detail on click', fakeAsync(() => {
|
||||
const expectedHero = HEROES[1];
|
||||
const li = page.heroRows[1];
|
||||
|
||||
// In older browsers, such as IE, you might need a CustomEvent instead. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
|
||||
li.dispatchEvent(new Event('click'));
|
||||
tick();
|
||||
|
||||
|
@ -3,7 +3,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { HighlightDirective } from './highlight.directive';
|
||||
import { newEvent } from '../../testing';
|
||||
|
||||
// #docregion test-component
|
||||
@Component({
|
||||
@ -59,8 +58,11 @@ describe('HighlightDirective', () => {
|
||||
const input = des[2].nativeElement as HTMLInputElement;
|
||||
expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');
|
||||
|
||||
// dispatch a DOM event so that Angular responds to the input value change.
|
||||
input.value = 'green';
|
||||
|
||||
// Dispatch a DOM event so that Angular responds to the input value change.
|
||||
// In older browsers, such as IE, you might need a CustomEvent instead. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
|
||||
input.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
|
||||
|
@ -14,18 +14,6 @@ export function advance(f: ComponentFixture<any>): void {
|
||||
f.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom DOM event the old fashioned way
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent
|
||||
* Although officially deprecated, some browsers (phantom) don't accept the preferred "new Event(eventName)"
|
||||
*/
|
||||
export function newEvent(eventName: string, bubbles = false, cancelable = false) {
|
||||
const evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent'
|
||||
evt.initCustomEvent(eventName, bubbles, cancelable, null);
|
||||
return evt;
|
||||
}
|
||||
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
|
||||
// #docregion click-event
|
||||
/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
|
||||
|
@ -18,8 +18,6 @@ When you use the [Angular CLI](cli) command `ng new` to generate an app, the def
|
||||
/* JavaScript imports */
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@ -29,9 +27,7 @@ import { AppComponent } from './app.component';
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpClientModule
|
||||
BrowserModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
@ -120,9 +116,6 @@ Now you could use your `ItemDirective` in a component. This example uses `AppMod
|
||||
|
||||
Remember, components, directives, and pipes belong to one module only. You only need to declare them once in your app because you share them by importing the necessary modules. This saves you time and helps keep your app lean.
|
||||
|
||||
|
||||
|
||||
|
||||
{@a imports}
|
||||
|
||||
## The `imports` array
|
||||
@ -130,6 +123,12 @@ Remember, components, directives, and pipes belong to one module only. You only
|
||||
The module's `imports` array appears exclusively in the `@NgModule` metadata object.
|
||||
It tells Angular about other NgModules that this particular module needs to function properly.
|
||||
|
||||
<code-example
|
||||
path="bootstrapping/src/app/app.module.ts"
|
||||
region="imports"
|
||||
header="src/app/app.module.ts (excerpt)">
|
||||
</code-example>
|
||||
|
||||
This list of modules are those that export components, directives, or pipes
|
||||
that component templates in this module reference. In this case, the component is
|
||||
`AppComponent`, which references components, directives, or pipes in `BrowserModule`,
|
||||
@ -138,6 +137,8 @@ A component template can reference another component, directive,
|
||||
or pipe when the referenced class is declared in this module or
|
||||
the class was imported from another module.
|
||||
|
||||
|
||||
|
||||
{@a bootstrap-array}
|
||||
|
||||
## The `providers` array
|
||||
|
@ -38,7 +38,6 @@ v9 - v12
|
||||
| `@angular/bazel` | [`Bazel builder and schematics`](#bazelbuilder) | v10 |
|
||||
| `@angular/common` | [`ReflectiveInjector`](#reflectiveinjector) | <!--v8--> v11 |
|
||||
| `@angular/common` | [`CurrencyPipe` - `DEFAULT_CURRENCY_CODE`](api/common/CurrencyPipe#currency-code-deprecation) | <!--v9--> v11 |
|
||||
| `@angular/core` | [`CollectionChangeRecord`](#core) | <!--v7--> v11 |
|
||||
| `@angular/core` | [`DefaultIterableDiffer`](#core) | <!--v7--> v11 |
|
||||
| `@angular/core` | [`ReflectiveKey`](#core) | <!--v8--> v11 |
|
||||
| `@angular/core` | [`RenderComponentType`](#core) | <!--v7--> v11 |
|
||||
@ -89,7 +88,6 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i
|
||||
|
||||
| API | Replacement | Deprecation announced | Notes |
|
||||
| --- | ----------- | --------------------- | ----- |
|
||||
| [`CollectionChangeRecord`](api/core/CollectionChangeRecord) | [`IterableChangeRecord`](api/core/IterableChangeRecord) | v4 | none |
|
||||
| [`DefaultIterableDiffer`](api/core/DefaultIterableDiffer) | n/a | v4 | Not part of public API. |
|
||||
| [`ReflectiveInjector`](api/core/ReflectiveInjector) | [`Injector.create`](api/core/Injector#create) | v5 | See [`ReflectiveInjector`](#reflectiveinjector) |
|
||||
| [`ReflectiveKey`](api/core/ReflectiveKey) | none | v5 | none |
|
||||
|
@ -119,7 +119,14 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
|
||||
|
||||
In browsers that support Custom Elements natively, the specification requires developers use ES2015 classes to define Custom Elements - developers can opt-in to this by setting the `target: "es2015"` property in their project's [TypeScript configuration file](/guide/typescript-configuration). As Custom Element and ES2015 support may not be available in all browsers, developers can instead choose to use a polyfill to support older browsers and ES5 code.
|
||||
|
||||
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill: `ng add @angular/elements --project=*your_project_name*`.
|
||||
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill:
|
||||
|
||||
<code-example language="sh">
|
||||
|
||||
ng add @angular/elements --project=*your_project_name*
|
||||
|
||||
</code-example>
|
||||
|
||||
- For more information about polyfills, see [polyfill documentation](https://www.webcomponents.org/polyfills).
|
||||
|
||||
- For more information about Angular browser support, see [Browser Support](guide/browser-support).
|
||||
|
@ -627,6 +627,11 @@ The [npm package manager](https://docs.npmjs.com/getting-started/what-is-npm) is
|
||||
|
||||
Learn more about how Angular uses [Npm Packages](guide/npm-packages).
|
||||
|
||||
{@ ngc}
|
||||
## ngc
|
||||
`ngc` is a Typescript-to-Javascript transpiler that processes Angular decorators, metadata, and templates, and emits JavaScript code.
|
||||
The most recent implementation is internally refered to as `ngtsc` because it's a minimalistic wrapper around the TypeScript compiler `tsc` that adds a transform for processing Angular code.
|
||||
|
||||
{@a O}
|
||||
|
||||
{@a observable}
|
||||
|
@ -94,7 +94,7 @@ All of our major releases are supported for 18 months.
|
||||
|
||||
* 6 months of *active support*, during which regularly-scheduled updates and patches are released.
|
||||
|
||||
* 12 months of *long-term support (LTS)*, during which only critical fixes and security patches are released.
|
||||
* 12 months of *long-term support (LTS)*, during which only [critical fixes and security patches](#lts-fixes) are released.
|
||||
|
||||
The following table provides the status for Angular versions under support.
|
||||
|
||||
@ -107,6 +107,13 @@ Version | Status | Released | Active Ends | LTS Ends
|
||||
|
||||
Angular versions ^4.0.0, ^5.0.0, ^6.0.0 and ^7.0.0 are no longer under support.
|
||||
|
||||
### LTS fixes
|
||||
|
||||
As a general rule, a fix is considered for an LTS version if it resolves one of:
|
||||
|
||||
* a newly identified security vulnerability,
|
||||
* a regression, since the start of LTS, caused by a 3rd party change, such as a new browser version.
|
||||
|
||||
{@a deprecation}
|
||||
## Deprecation practices
|
||||
|
||||
|
@ -67,6 +67,33 @@ Therefore, it is recommended to reload the page once the promise returned by `ac
|
||||
|
||||
</div>
|
||||
|
||||
### Handling an unrecoverable state
|
||||
|
||||
In some cases, the version of the app used by the service worker to serve a client might be in a broken state that cannot be recovered from without a full page reload.
|
||||
|
||||
For example, imagine the following scenario:
|
||||
- A user opens the app for the first time and the service worker caches the latest version of the app.
|
||||
Let's assume the app's cached assets include `index.html`, `main.<main-hash-1>.js` and `lazy-chunk.<lazy-hash-1>.js`.
|
||||
- The user closes the app and does not open it for a while.
|
||||
- After some time, a new version of the app is deployed to the server.
|
||||
This newer version includes the files `index.html`, `main.<main-hash-2>.js` and `lazy-chunk.<lazy-hash-2>.js` (note that the hashes are different now, because the content of the files has changed).
|
||||
The old version is no longer available on the server.
|
||||
- In the meantime, the user's browser decides to evict `lazy-chunk.<lazy-hash-1>.js` from its cache.
|
||||
Browsers may decide to evict specific (or all) resources from a cache in order to reclaim disk space.
|
||||
- The user opens the app again.
|
||||
The service worker serves the latest version known to it at this point, namely the old version (`index.html` and `main.<main-hash-1>.js`).
|
||||
- At some later point, the app requests the lazy bundle, `lazy-chunk.<lazy-hash-1>.js`.
|
||||
- The service worker is unable to find the asset in the cache (remember that the browser evicted it).
|
||||
Nor is it able to retrieve it from the server (since the server now only has `lazy-chunk.<lazy-hash-2>.js` from the newer version).
|
||||
|
||||
In the above scenario, the service worker is not able to serve an asset that would normally be cached.
|
||||
That particular app version is broken and there is no way to fix the state of the client without reloading the page.
|
||||
In such cases, the service worker notifies the client by sending an `UnrecoverableStateEvent` event.
|
||||
You can subscribe to `SwUpdate#unrecoverable` to be notified and handle these errors.
|
||||
|
||||
<code-example path="service-worker-getting-started/src/app/handle-unrecoverable-state.service.ts" header="handle-unrecoverable-state.service.ts" region="sw-unrecoverable-state"></code-example>
|
||||
|
||||
|
||||
## More on Angular service workers
|
||||
|
||||
You may also be interested in the following:
|
||||
|
@ -267,6 +267,12 @@ By default, these criteria are:
|
||||
1. The URL must not contain a file extension (i.e. a `.`) in the last path segment.
|
||||
2. The URL must not contain `__`.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
To configure whether navigation requests are sent through to the network or not, see the [navigationRequestStrategy](#navigation-request-strategy) section.
|
||||
|
||||
</div>
|
||||
|
||||
### Matching navigation request URLs
|
||||
|
||||
While these default criteria are fine in most cases, it is sometimes desirable to configure different rules. For example, you may want to ignore specific routes (that are not part of the Angular app) and pass them through to the server.
|
||||
@ -285,3 +291,32 @@ If the field is omitted, it defaults to:
|
||||
'!/**/*__*/**', // Exclude URLs containing `__` in any other segment.
|
||||
]
|
||||
```
|
||||
|
||||
{@a navigation-request-strategy}
|
||||
|
||||
## `navigationRequestStrategy`
|
||||
|
||||
This optional property enables you to configure how the service worker handles navigation requests:
|
||||
|
||||
```json
|
||||
{
|
||||
"navigationRequestStrategy": "freshness"
|
||||
}
|
||||
```
|
||||
|
||||
Possible values:
|
||||
|
||||
- `'performance'`: The default setting. Serves the specified [index file](#index-file), which is typically cached.
|
||||
- `'freshness'`: Passes the requests through to the network and falls back to the `performance` behavior when offline.
|
||||
This value is useful when the server redirects the navigation requests elsewhere using an HTTP redirect (3xx status code).
|
||||
Reasons for using this value include:
|
||||
- Redirecting to an authentication website when authentication is not handled by the application.
|
||||
- Redirecting specific URLs to avoid breaking existing links/bookmarks after a website redesign.
|
||||
- Redirecting to a different website, such as a server-status page, while a page is temporarily down.
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
The `freshness` strategy usually results in more requests sent to the server, which can increase response latency.
|
||||
It is recommended that you use the default performance strategy whenever possible.
|
||||
|
||||
</div>
|
@ -324,5 +324,5 @@ These techniques are useful for small-scale demonstrations, but they
|
||||
quickly become verbose and clumsy when handling large amounts of user input.
|
||||
Two-way data binding is a more elegant and compact way to move
|
||||
values between data entry fields and model properties.
|
||||
The next page, `Forms`, explains how to write
|
||||
The [`Forms`](guide/forms-overview) page explains how to write
|
||||
two-way bindings with `NgModel`.
|
||||
|
@ -32,7 +32,7 @@ To do this:
|
||||
|
||||
1. Create a `typings.d.ts` file in your `src/` folder. This file is automatically included as global type definition.
|
||||
|
||||
2. Add the following code in `src/typings.d.ts`.
|
||||
2. Add the following code in `src/typings.d.ts`:
|
||||
|
||||
```
|
||||
declare module 'host' {
|
||||
@ -45,7 +45,7 @@ declare module 'host' {
|
||||
}
|
||||
```
|
||||
|
||||
3. In the component or file that uses the library, add the following code.
|
||||
3. In the component or file that uses the library, add the following code:
|
||||
|
||||
```
|
||||
import * as host from 'host';
|
||||
@ -129,7 +129,7 @@ interface JQuery {
|
||||
}
|
||||
```
|
||||
|
||||
If don't add the interface for the script-defined extension, your IDE shows an error:
|
||||
If you don't add the interface for the script-defined extension, your IDE shows an error:
|
||||
|
||||
```
|
||||
[TS][Error] Property 'myPlugin' does not exist on type 'JQuery'
|
||||
|
@ -53,6 +53,9 @@
|
||||
},
|
||||
"kyliau": {
|
||||
"name": "Keen Yee Liau",
|
||||
"twitter": "liauky",
|
||||
"website": "https://github.com/kyliau",
|
||||
"bio": "Keen works on language service and CLI. He also maintains Karma and Protractor.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "igorminar",
|
||||
"picture": "kyliau.jpg"
|
||||
|
@ -3,162 +3,5 @@
|
||||
</header>
|
||||
|
||||
<article class="events-container">
|
||||
<p>Where we'll be presenting:</p>
|
||||
<table class="is-full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Where we already presented:</p>
|
||||
<table class="is-full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- ng-vikings 2020 -->
|
||||
<tr>
|
||||
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
|
||||
<td>Oslo, Norway</td>
|
||||
<td>May 25-26 conference, 27 workshops, 2020</td>
|
||||
</tr>
|
||||
<!-- ng-conf 2020 -->
|
||||
<tr>
|
||||
<th><a href="https://ng-conf.org/" title="ng-conf">ng-conf</a></th>
|
||||
<td>Salt Lake City, Utah</td>
|
||||
<td>April 1-3, 2020</td>
|
||||
</tr>
|
||||
<!-- ngIndia 2020 -->
|
||||
<tr>
|
||||
<th><a href="https://www.ng-ind.com/" title="ngIndia">ngIndia</a></th>
|
||||
<td>Delhi, India</td>
|
||||
<td>Feb 29, 2020</td>
|
||||
</tr>
|
||||
<!-- ReactiveConf 2019 -->
|
||||
<tr>
|
||||
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
|
||||
<td>Prague, Czech Republic</td>
|
||||
<td>October 30 - November 1, 2019</td>
|
||||
</tr>
|
||||
<!-- NG Rome 2019-->
|
||||
<tr>
|
||||
<th>
|
||||
<a href="https://ngrome.io" title="NG Rome MMXIX - The Italian Angular Conference">NG Rome MMXIX</a>
|
||||
</th>
|
||||
<td>Rome, Italy</td>
|
||||
<td>Oct 6th workshops, 7th conference, 2019</td>
|
||||
</tr>
|
||||
<!-- AngularConnect 2019-->
|
||||
<tr>
|
||||
<th><a href="https://www.angularconnect.com/?utm_source=angular.io&utm_medium=referral"
|
||||
title="AngularConnect">AngularConnect</a></th>
|
||||
<td>London, UK</td>
|
||||
<td>September 19-20, 2019</td>
|
||||
</tr>
|
||||
<!-- NG-DE 2019-->
|
||||
<tr>
|
||||
<th><a href="https://ng-de.org/" title="NG-DE">NG-DE</a></th>
|
||||
<td>Berlin, Germany</td>
|
||||
<td>August 29th workshops, 30-31 conference, 2019</td>
|
||||
</tr>
|
||||
<!-- ngJapan-->
|
||||
<tr>
|
||||
<th><a href="https://ngjapan.org" title="ng-japan">ng-japan</a></th>
|
||||
<td>Tokyo, Japan</td>
|
||||
<td>July 13, 2019</td>
|
||||
</tr>
|
||||
<!-- ngVikings 2019-->
|
||||
<tr>
|
||||
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
|
||||
<td>Copenhagen, Denmark</td>
|
||||
<td>May 26 (workshops), 27-28 (conference), 2019</td>
|
||||
</tr>
|
||||
<!-- ng-conf 2019-->
|
||||
<tr>
|
||||
<th><a href="https://ng-conf.org/" title="ng-conf">ng-conf</a></th>
|
||||
<td>Salt Lake City, Utah</td>
|
||||
<td>May 1-3, 2019</td>
|
||||
</tr>
|
||||
<!-- ng-India 2019-->
|
||||
<tr>
|
||||
<th><a href="https://www.ng-ind.com/" title="ng-India">ng-India</a></th>
|
||||
<td>Gurgaon, India</td>
|
||||
<td>February 23, 2019</td>
|
||||
</tr>
|
||||
<!-- ngAtlanta 2019 -->
|
||||
<tr>
|
||||
<th><a href="https://ng-atl.org/" title="ngAtlanta">ngAtlanta</a></th>
|
||||
<td>Atlanta, Georgia</td>
|
||||
<td>January 9-12, 2019</td>
|
||||
</tr>
|
||||
<!-- AngularConnect-->
|
||||
<tr>
|
||||
<th>
|
||||
<a href="https://past.angularconnect.com/2018" title="AngularConnect">AngularConnect</a>
|
||||
</th>
|
||||
<td>London, United Kingdom</td>
|
||||
<td>November 5-7, 2018</td>
|
||||
</tr>
|
||||
<!-- ReactiveConf -->
|
||||
<tr>
|
||||
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
|
||||
<td>Prague, Czech Republic</td>
|
||||
<td>October 29-31, 2018</td>
|
||||
</tr>
|
||||
<!-- AngularMix -->
|
||||
<tr>
|
||||
<th><a href="https://angularmix.com/" title="AngularMix">AngularMix</a></th>
|
||||
<td>Orlando, Florida</td>
|
||||
<td>October 10-12, 2018</td>
|
||||
</tr>
|
||||
<!-- Angular Conf Australia-->
|
||||
<tr>
|
||||
<th>
|
||||
<a href="https://www.angularconf.com.au/" title="Angular Conf Australia">Angular Conf Australia</a>
|
||||
</th>
|
||||
<td>Melbourne, Australia</td>
|
||||
<td>Jun 22, 2018</td>
|
||||
</tr>
|
||||
<!-- ngJapan-->
|
||||
<tr>
|
||||
<th><a href="https://ngjapan.org/en.html" title="ng-japan">ng-japan</a></th>
|
||||
<td>Tokyo, Japan</td>
|
||||
<td>Jun 16, 2018</td>
|
||||
</tr>
|
||||
<!-- WeRDevs-->
|
||||
<tr>
|
||||
<th><a href="https://www.wearedevelopers.com/" title="WeAreDevs">WeAreDevelopers</a></th>
|
||||
<td>Vienna, Austria</td>
|
||||
<td>May 16-18, 2018</td>
|
||||
</tr>
|
||||
<!-- ngconf 2018-->
|
||||
<tr>
|
||||
<th><a href="https://www.ng-conf.org/" title="ng-conf">ng-conf</a></th>
|
||||
<td>Salt Lake City, Utah</td>
|
||||
<td>April 18-20, 2018</td>
|
||||
</tr>
|
||||
<!-- ngVikings-->
|
||||
<tr>
|
||||
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
|
||||
<td>Helsinki, Finland</td>
|
||||
<td>March 1-2, 2018</td>
|
||||
</tr>
|
||||
<!-- ngAtlanta-->
|
||||
<tr>
|
||||
<th><a href="http://ng-atl.org/" title="ngAtlanta">ngAtlanta</a></th>
|
||||
<td>Atlanta, Georgia</td>
|
||||
<td>January 30, 2018</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<aio-events></aio-events>
|
||||
</article>
|
||||
|
236
aio/content/marketing/events.json
Normal file
236
aio/content/marketing/events.json
Normal file
@ -0,0 +1,236 @@
|
||||
[
|
||||
{
|
||||
"name": "ng-china",
|
||||
"location": "Online",
|
||||
"linkUrl": "https://ng-china.org/",
|
||||
"date": {
|
||||
"start": "2020-11-21",
|
||||
"end": "2020-11-22"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EnterpriseNG",
|
||||
"location": "Online",
|
||||
"linkUrl": "https://www.ng-conf.org/",
|
||||
"date": {
|
||||
"start": "2020-11-19",
|
||||
"end": "2020-11-20"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ngrome",
|
||||
"location": "Online",
|
||||
"linkUrl": "https://ngrome.io/",
|
||||
"date": {
|
||||
"start": "2020-10-20",
|
||||
"end": "2020-10-20"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ngVikings",
|
||||
"location": "Oslo, Norway",
|
||||
"linkUrl": "https://ngvikings.org/",
|
||||
"date": {
|
||||
"start": "2020-05-25",
|
||||
"end": "2020-05-26"
|
||||
},
|
||||
"workshopsDate": {
|
||||
"start": "2020-05-27",
|
||||
"end": "2020-05-27"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ng-conf",
|
||||
"location": "Salt Lake City, Utah",
|
||||
"linkUrl": "https://ng-conf.org/",
|
||||
"date": {
|
||||
"start": "2020-04-01",
|
||||
"end": "2020-04-03"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ngIndia",
|
||||
"location": "Delhi, India",
|
||||
"linkUrl": "https://www.ng-ind.com/",
|
||||
"date": {
|
||||
"start": "2020-02-29",
|
||||
"end": "2020-02-29"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ReactiveConf",
|
||||
"location": "Prague, Czech Republic",
|
||||
"linkUrl": "https://reactiveconf.com/",
|
||||
"date": {
|
||||
"start": "2019-10-30",
|
||||
"end": "2019-11-01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "NG Rome MMXIX",
|
||||
"location": "Rome, Italy",
|
||||
"linkUrl": "https://ngrome.io",
|
||||
"tooltip": "NG Rome MMXIX - The Italian Angular Conference",
|
||||
"date": {
|
||||
"start": "2019-10-07",
|
||||
"end": "2019-10-07"
|
||||
},
|
||||
"workshopsDate": {
|
||||
"start": "2019-10-06",
|
||||
"end": "2019-10-06"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AngularConnect",
|
||||
"location": "London, UK",
|
||||
"linkUrl": "https://www.angularconnect.com/?utm_source=angular.io&utm_medium=referral",
|
||||
"date": {
|
||||
"start": "2019-09-19",
|
||||
"end": "2019-09-20"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "NG-DE",
|
||||
"location": "Berlin, Germany",
|
||||
"linkUrl": "https://ng-de.org/",
|
||||
"date": {
|
||||
"start": "2019-08-30",
|
||||
"end": "2019-08-31"
|
||||
},
|
||||
"workshopsDate": {
|
||||
"start": "2019-08-29",
|
||||
"end": "2019-08-29"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ng-japan",
|
||||
"location": "Tokyo, Japan",
|
||||
"linkUrl": "https://ngjapan.org/",
|
||||
"date": {
|
||||
"start": "2019-07-13",
|
||||
"end": "2019-07-13"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ngVikings",
|
||||
"location": "Copenhagen, Denmark",
|
||||
"linkUrl": "https://ngvikings.org/",
|
||||
"date": {
|
||||
"start": "2019-05-27",
|
||||
"end": "2019-05-28"
|
||||
},
|
||||
"workshopsDate": {
|
||||
"start": "2019-05-26",
|
||||
"end": "2019-05-26"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ng-conf",
|
||||
"location": "Salt Lake City, Utah",
|
||||
"linkUrl": "https://ng-conf.org/",
|
||||
"date": {
|
||||
"start": "2019-05-01",
|
||||
"end": "2019-05-03"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ng-India",
|
||||
"location": "Gurgaon, India",
|
||||
"linkUrl": "https://www.ng-ind.com/",
|
||||
"date": {
|
||||
"start": "2019-02-23",
|
||||
"end": "2019-02-23"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ngAtlanta",
|
||||
"location": "Atlanta, Georgia",
|
||||
"linkUrl": "https://ng-atl.org/",
|
||||
"date": {
|
||||
"start": "2019-01-09",
|
||||
"end": "2019-01-12"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AngularConnect",
|
||||
"location": "London, United Kingdom",
|
||||
"linkUrl": "https://past.angularconnect.com/2018",
|
||||
"date": {
|
||||
"start": "2018-11-05",
|
||||
"end": "2018-11-07"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ReactiveConf",
|
||||
"location": "Prague, Czech Republic",
|
||||
"linkUrl": "https://reactiveconf.com/",
|
||||
"date": {
|
||||
"start": "2018-10-29",
|
||||
"end": "2018-10-31"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AngularMix",
|
||||
"location": "Orlando, Florida",
|
||||
"linkUrl": "https://angularmix.com/",
|
||||
"date": {
|
||||
"start": "2018-10-10",
|
||||
"end": "2018-10-12"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Angular Conf Australia",
|
||||
"location": "Melbourne, Australia",
|
||||
"linkUrl": "https://www.angularconf.com.au/",
|
||||
"date": {
|
||||
"start": "2018-06-22",
|
||||
"end": "2018-06-22"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ng-japan",
|
||||
"location": "Tokyo, Japan",
|
||||
"linkUrl": "https://ngjapan.org/en.html",
|
||||
"date": {
|
||||
"start": "2018-06-16",
|
||||
"end": "2018-06-16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "WeAreDevelopers",
|
||||
"location": "Vienna, Austria",
|
||||
"linkUrl": "https://www.wearedevelopers.com/",
|
||||
"tooltip": "WeAreDevs",
|
||||
"date": {
|
||||
"start": "2018-05-16",
|
||||
"end": "2018-05-18"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ng-conf",
|
||||
"location": "Salt Lake City, Utah",
|
||||
"linkUrl": "https://ng-conf.org/",
|
||||
"date": {
|
||||
"start": "2018-04-18",
|
||||
"end": "2018-04-20"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ngVikings",
|
||||
"location": "Helsinki, Finland",
|
||||
"linkUrl": "https://ngvikings.org/",
|
||||
"date": {
|
||||
"start": "2018-03-01",
|
||||
"end": "2018-03-02"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ngAtlanta",
|
||||
"location": "Atlanta, Georgia",
|
||||
"linkUrl": "https://ng-atl.org/",
|
||||
"date": {
|
||||
"start": "2018-01-30",
|
||||
"end": "2018-01-30"
|
||||
}
|
||||
}
|
||||
]
|
@ -61,27 +61,27 @@
|
||||
"children": [
|
||||
{
|
||||
"url": "start",
|
||||
"title": "A Sample App",
|
||||
"title": "Getting started",
|
||||
"tooltip": "Take a look at Angular's component model, template syntax, and component communication."
|
||||
},
|
||||
{
|
||||
"url": "start/start-routing",
|
||||
"title": "In-app Navigation",
|
||||
"title": "Adding navigation",
|
||||
"tooltip": "Navigate among different page views using the browser's URL."
|
||||
},
|
||||
{
|
||||
"url": "start/start-data",
|
||||
"title": "Manage Data",
|
||||
"title": "Managing Data",
|
||||
"tooltip": "Use services and access external data via HTTP."
|
||||
},
|
||||
{
|
||||
"url": "start/start-forms",
|
||||
"title": "Forms for User Input",
|
||||
"title": "Using Forms for User Input",
|
||||
"tooltip": "Learn about fetching and managing data from users with forms."
|
||||
},
|
||||
{
|
||||
"url": "start/start-deployment",
|
||||
"title": "Deployment",
|
||||
"title": "Deploying an application",
|
||||
"tooltip": "Move to local development, or deploy your application to Firebase or your own server."
|
||||
}
|
||||
]
|
||||
@ -101,11 +101,6 @@
|
||||
"title": "Components",
|
||||
"tooltip": "Building dynamic views with data binding",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/displaying-data",
|
||||
"title": "Data binding",
|
||||
"tooltip": "Property binding helps show app data in the UI."
|
||||
},
|
||||
{
|
||||
"url": "guide/user-input",
|
||||
"title": "User Input",
|
||||
@ -542,27 +537,6 @@
|
||||
"title": "Tutorials",
|
||||
"tooltip": "End-to-end tutorials for learning Angular concepts and patterns.",
|
||||
"children": [
|
||||
{
|
||||
"title": "Routing",
|
||||
"tooltip": "End-to-end tutorials for learning about Angular's router.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/router-tutorial",
|
||||
"title": "Using Angular Routes in a Single-page Application",
|
||||
"tooltip": "A tutorial that covers many patterns associated with Angular routing."
|
||||
},
|
||||
{
|
||||
"url": "guide/router-tutorial-toh",
|
||||
"title": "Router tutorial: tour of heroes",
|
||||
"tooltip": "Explore how to use Angular's router. Based on the Tour of Heroes example."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "guide/forms",
|
||||
"title": "Building a Template-driven Form",
|
||||
"tooltip": "Create a template-driven form using directives and Angular template syntax."
|
||||
},
|
||||
{
|
||||
"title": "Tutorial: Tour of Heroes",
|
||||
"tooltip": "The Tour of Heroes app is used as a reference point in many Angular examples.",
|
||||
@ -609,6 +583,32 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Routing",
|
||||
"tooltip": "End-to-end tutorials for learning about Angular's router.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/router-tutorial",
|
||||
"title": "Using Angular Routes in a Single-page Application",
|
||||
"tooltip": "A tutorial that covers many patterns associated with Angular routing."
|
||||
},
|
||||
{
|
||||
"url": "guide/router-tutorial-toh",
|
||||
"title": "Router tutorial: tour of heroes",
|
||||
"tooltip": "Explore how to use Angular's router. Based on the Tour of Heroes example."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "guide/forms",
|
||||
"title": "Building a Template-driven Form",
|
||||
"tooltip": "Create a template-driven form using directives and Angular template syntax."
|
||||
},
|
||||
{
|
||||
"url": "guide/displaying-data",
|
||||
"title": "Data binding",
|
||||
"tooltip": "Property binding helps show app data in the UI."
|
||||
},
|
||||
{
|
||||
"url": "guide/web-worker",
|
||||
"title": "Web Workers",
|
||||
@ -954,6 +954,11 @@
|
||||
"url": "guide/styleguide",
|
||||
"title": "Coding Style Guide",
|
||||
"tooltip": "Guidelines for writing Angular code."
|
||||
},
|
||||
{
|
||||
"url": "guide/docs-style-guide",
|
||||
"title": "Documentation Style Guide",
|
||||
"tooltip": "Style guide for documentation authors."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Part 1: Getting started with a basic Angular app
|
||||
# Getting started with a basic Angular app
|
||||
|
||||
Welcome to Angular!
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Try it: Manage data
|
||||
# Managing data
|
||||
|
||||
At the end of [In-app Navigation](start/start-routing "Try it: In-app Navigation"), the online store application has a product catalog with two views: a product list and product details.
|
||||
Users can click on a product name from the list to see details in a new view, with a distinct URL, or route.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Try it: Deployment
|
||||
# Deploying an application
|
||||
|
||||
|
||||
To deploy your application, you have to compile it, and then host the JavaScript, CSS, and HTML on a web server. Built Angular applications are very portable and can live in any environment or served by any technology, such as Node, Java, .NET, PHP, and many others.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Try it: Use forms for user input
|
||||
# Using forms for user input
|
||||
|
||||
At the end of [Managing Data](start/start-data "Try it: Managing Data"), the online store application has a product catalog and a shopping cart.
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# In-app navigation
|
||||
# Adding navigation
|
||||
|
||||
At the end of [part 1](start "Get started with a basic Angular app"), the online store application has a basic product catalog.
|
||||
The app doesn't have any variable states or navigation.
|
||||
|
@ -23,7 +23,7 @@
|
||||
"build-local-with-viewengine": "yarn ~~build",
|
||||
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
||||
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js ef770f1cb",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 800ba9271",
|
||||
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
|
||||
"test": "yarn check-env && ng test",
|
||||
"pree2e": "yarn check-env && yarn update-webdriver",
|
||||
|
@ -40,6 +40,10 @@ export const ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES = [
|
||||
{
|
||||
selector: 'live-example',
|
||||
loadChildren: () => import('./live-example/live-example.module').then(m => m.LiveExampleModule)
|
||||
},
|
||||
{
|
||||
selector: 'aio-events',
|
||||
loadChildren: () => import('./events/events.module').then(m => m.EventsModule)
|
||||
}
|
||||
];
|
||||
|
||||
|
43
aio/src/app/custom-elements/events/events.component.html
Normal file
43
aio/src/app/custom-elements/events/events.component.html
Normal file
@ -0,0 +1,43 @@
|
||||
<p>Where we'll be presenting:</p>
|
||||
<table class="is-full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody >
|
||||
<tr *ngFor="let event of upcomingEvents">
|
||||
<th><a href="{{event.linkUrl}}" title="{{event.tooltip}}">{{event.name}}</a></th>
|
||||
<td>{{event.location}}</td>
|
||||
<td>
|
||||
<div>
|
||||
{{getEventDates(event)}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Where we already presented:</p>
|
||||
<table class="is-full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let event of pastEvents">
|
||||
<th><a href="{{event.linkUrl}}" title="{{event.tooltip}}">{{event.name}}</a></th>
|
||||
<td>{{event.location}}</td>
|
||||
<td>
|
||||
<div>
|
||||
{{getEventDates(event)}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
231
aio/src/app/custom-elements/events/events.component.spec.ts
Normal file
231
aio/src/app/custom-elements/events/events.component.spec.ts
Normal file
@ -0,0 +1,231 @@
|
||||
import { Injector } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Duration, Event, EventsComponent } from './events.component';
|
||||
import { EventsService } from './events.service';
|
||||
|
||||
describe('EventsComponent', () => {
|
||||
let component: EventsComponent;
|
||||
let injector: Injector;
|
||||
let eventsService: TestEventsService;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: EventsComponent, deps: [EventsService] } ,
|
||||
{ provide: EventsService, useClass: TestEventsService, deps: [] },
|
||||
]
|
||||
});
|
||||
eventsService = injector.get(EventsService) as unknown as TestEventsService;
|
||||
component = injector.get(EventsComponent) as unknown as EventsComponent;
|
||||
});
|
||||
|
||||
it('should have no pastEvents when first created', () => {
|
||||
expect(component.pastEvents).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should have no upcoming when first created', () => {
|
||||
expect(component.upcomingEvents).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('ngOnInit()', () => {
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(new Date(2020, 5, 15, 23, 59, 59));
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
afterEach(() => jasmine.clock().uninstall());
|
||||
|
||||
it('should separate past and upcoming events', () => {
|
||||
eventsService.events.next([
|
||||
createMockEvent(
|
||||
'Upcoming event 1',
|
||||
{start: '2020-06-16', end: '2020-06-17'},
|
||||
{start: '2020-06-18', end: '2020-06-18'}),
|
||||
createMockEvent(
|
||||
'Upcoming event 3',
|
||||
{start: '2222-01-01', end: '2222-01-02'}),
|
||||
createMockEvent(
|
||||
'Past event 2',
|
||||
{start: '2020-06-13', end: '2020-06-14'}),
|
||||
createMockEvent(
|
||||
'Upcoming event 2',
|
||||
{start: '2020-06-17', end: '2020-06-18'},
|
||||
{start: '2020-06-16', end: '2020-06-16'}),
|
||||
createMockEvent(
|
||||
'Past event 1',
|
||||
{start: '2020-05-30', end: '2020-05-31'}),
|
||||
createMockEvent(
|
||||
'Past event 3',
|
||||
{start: '2020-06-14', end: '2020-06-14'},
|
||||
{start: '2020-06-16', end: '2020-06-17'}),
|
||||
]);
|
||||
|
||||
expect(component.pastEvents.map(evt => evt.name)).toEqual(jasmine.arrayWithExactContents(
|
||||
['Past event 1', 'Past event 2', 'Past event 3']));
|
||||
|
||||
expect(component.upcomingEvents.map(evt => evt.name)).toEqual(jasmine.arrayWithExactContents(
|
||||
['Upcoming event 1', 'Upcoming event 2', 'Upcoming event 3']));
|
||||
});
|
||||
|
||||
it('should order past events in reverse chronological order (ignoring workshops dates)', () => {
|
||||
eventsService.events.next([
|
||||
createMockEvent(
|
||||
'Past event 2',
|
||||
{start: '1999-12-13', end: '1999-12-14'},
|
||||
{start: '1999-12-11', end: '1999-12-11'}),
|
||||
createMockEvent(
|
||||
'Past event 4',
|
||||
{start: '2020-01-16', end: '2020-01-17'},
|
||||
{start: '2020-01-14', end: '2020-01-15'}),
|
||||
createMockEvent(
|
||||
'Past event 3',
|
||||
{start: '2020-01-15', end: '2020-01-16'},
|
||||
{start: '2020-01-17', end: '2020-01-18'}),
|
||||
createMockEvent(
|
||||
'Past event 1',
|
||||
{start: '1999-12-12', end: '1999-12-15'}),
|
||||
]);
|
||||
|
||||
expect(component.pastEvents.map(evt => evt.name)).toEqual(
|
||||
['Past event 4', 'Past event 3', 'Past event 2', 'Past event 1']);
|
||||
});
|
||||
|
||||
it('should order upcoming events in chronological order (ignoring workshops dates)', () => {
|
||||
eventsService.events.next([
|
||||
createMockEvent(
|
||||
'Upcoming event 2',
|
||||
{start: '2020-12-13', end: '2020-12-14'},
|
||||
{start: '2020-12-11', end: '2020-12-11'}),
|
||||
createMockEvent(
|
||||
'Upcoming event 4',
|
||||
{start: '2021-01-16', end: '2021-01-17'},
|
||||
{start: '2021-01-14', end: '2021-01-15'}),
|
||||
createMockEvent(
|
||||
'Upcoming event 3',
|
||||
{start: '2021-01-15', end: '2021-01-16'},
|
||||
{start: '2021-01-17', end: '2021-01-18'}),
|
||||
createMockEvent(
|
||||
'Upcoming event 1',
|
||||
{start: '2020-12-12', end: '2020-12-15'}),
|
||||
]);
|
||||
|
||||
expect(component.upcomingEvents.map(evt => evt.name)).toEqual(
|
||||
['Upcoming event 1', 'Upcoming event 2', 'Upcoming event 3', 'Upcoming event 4']);
|
||||
});
|
||||
|
||||
it('should treat ongoing events as upcoming', () => {
|
||||
eventsService.events.next([
|
||||
createMockEvent(
|
||||
'Ongoing event 1',
|
||||
{start: '2020-06-14', end: '2020-06-16'}),
|
||||
createMockEvent(
|
||||
'Ongoing event 2',
|
||||
{start: '2020-06-14', end: '2020-06-15'},
|
||||
{start: '2020-06-13', end: '2020-06-13'}),
|
||||
]);
|
||||
|
||||
expect(component.pastEvents).toEqual([]);
|
||||
expect(component.upcomingEvents.map(evt => evt.name)).toEqual(jasmine.arrayWithExactContents(
|
||||
['Ongoing event 1', 'Ongoing event 2']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventDates()', () => {
|
||||
describe('(without workshops)', () => {
|
||||
it('should correctly format the main event date', () => {
|
||||
const testEvent = createMockEvent('Test', {start: '2020-06-20', end: '2020-06-20'});
|
||||
expect(component.getEventDates(testEvent)).toBe('June 20, 2020');
|
||||
});
|
||||
|
||||
it('should correctly format the main event date spanning mupliple days', () => {
|
||||
const testEvent = createMockEvent('Test', {start: '2019-09-19', end: '2019-09-21'});
|
||||
expect(component.getEventDates(testEvent)).toBe('September 19-21, 2019');
|
||||
});
|
||||
|
||||
it('should correctly format the main event date spanning mupliple months', () => {
|
||||
const testEvent = createMockEvent('Test', {start: '2019-10-30', end: '2019-11-01'});
|
||||
expect(component.getEventDates(testEvent)).toBe('October 30 - November 1, 2019');
|
||||
});
|
||||
});
|
||||
|
||||
describe('(with workshops)', () => {
|
||||
it('should correctly format event dates with workshops after main event', () => {
|
||||
const testEvent = createMockEvent(
|
||||
'Test',
|
||||
{start: '2020-07-25', end: '2020-07-26'},
|
||||
{start: '2020-07-27', end: '2020-07-27'});
|
||||
|
||||
expect(component.getEventDates(testEvent))
|
||||
.toBe('July 25-26 (conference), July 27 (workshops), 2020');
|
||||
});
|
||||
|
||||
it('should correctly format event dates with workshops before main event', () => {
|
||||
const testEvent = createMockEvent(
|
||||
'Test',
|
||||
{start: '2019-10-07', end: '2019-10-07'},
|
||||
{start: '2019-10-06', end: '2019-10-06'});
|
||||
|
||||
expect(component.getEventDates(testEvent))
|
||||
.toBe('October 6 (workshops), October 7 (conference), 2019');
|
||||
});
|
||||
|
||||
it('should correctly format event dates spanning multiple days', () => {
|
||||
const testEvent = createMockEvent(
|
||||
'Test',
|
||||
{start: '2019-08-30', end: '2019-08-31'},
|
||||
{start: '2019-08-28', end: '2019-08-29'});
|
||||
|
||||
expect(component.getEventDates(testEvent))
|
||||
.toBe('August 28-29 (workshops), August 30-31 (conference), 2019');
|
||||
});
|
||||
|
||||
it('should correctly format event dates with workshops on different month before the main event',
|
||||
() => {
|
||||
const testEvent = createMockEvent(
|
||||
'Test',
|
||||
{start: '2020-08-01', end: '2020-08-02'},
|
||||
{start: '2020-07-30', end: '2020-07-31'});
|
||||
|
||||
expect(component.getEventDates(testEvent))
|
||||
.toBe('July 30-31 (workshops), August 1-2 (conference), 2020');
|
||||
});
|
||||
|
||||
it('should correctly format event dates with workshops on different month after the main event',
|
||||
() => {
|
||||
const testEvent = createMockEvent(
|
||||
'Test',
|
||||
{start: '2020-07-30', end: '2020-07-31'},
|
||||
{start: '2020-08-01', end: '2020-08-02'});
|
||||
|
||||
expect(component.getEventDates(testEvent))
|
||||
.toBe('July 30-31 (conference), August 1-2 (workshops), 2020');
|
||||
});
|
||||
|
||||
it('should correctly format event dates spanning multiple months', () => {
|
||||
const testEvent = createMockEvent(
|
||||
'Test',
|
||||
{start: '2020-07-31', end: '2020-08-01'},
|
||||
{start: '2020-07-30', end: '2020-08-01'});
|
||||
|
||||
expect(component.getEventDates(testEvent))
|
||||
.toBe('July 30 - August 1 (workshops), July 31 - August 1 (conference), 2020');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helpers
|
||||
class TestEventsService {
|
||||
events = new Subject<Event[]>();
|
||||
}
|
||||
|
||||
function createMockEvent(name: string, date: Duration, workshopsDate?: Duration): Event {
|
||||
return {
|
||||
name,
|
||||
location: '',
|
||||
linkUrl: '',
|
||||
date,
|
||||
workshopsDate,
|
||||
};
|
||||
}
|
||||
});
|
102
aio/src/app/custom-elements/events/events.component.ts
Normal file
102
aio/src/app/custom-elements/events/events.component.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { EventsService } from './events.service';
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
const MONTHS = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
export type date = string; // of the format `YYYY-MM-DD`.
|
||||
export interface Duration {
|
||||
start: date;
|
||||
end: date;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
name: string;
|
||||
location: string;
|
||||
linkUrl: string;
|
||||
tooltip?: string;
|
||||
date: Duration;
|
||||
workshopsDate?: Duration;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aio-events',
|
||||
templateUrl: 'events.component.html'
|
||||
})
|
||||
export class EventsComponent implements OnInit {
|
||||
|
||||
pastEvents: Event[];
|
||||
upcomingEvents: Event[];
|
||||
|
||||
constructor(private eventsService: EventsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.eventsService.events.subscribe(events => {
|
||||
this.pastEvents = events
|
||||
.filter(event => new Date(event.date.end).getTime() < Date.now() - DAY)
|
||||
.sort((l: Event, r: Event) => isBefore(l.date, r.date) ? 1 : -1);
|
||||
|
||||
this.upcomingEvents = events
|
||||
.filter(event => new Date(event.date.end).getTime() >= Date.now() - DAY)
|
||||
.sort((l: Event, r: Event) => isBefore(l.date, r.date) ? -1 : 1);
|
||||
});
|
||||
}
|
||||
|
||||
getEventDates(event: Event) {
|
||||
let dateString;
|
||||
|
||||
// Check if there is a workshop
|
||||
if (event.workshopsDate) {
|
||||
const mainEventDateString = `${processDate(event.date)} (conference)`;
|
||||
const workshopsDateString = `${processDate(event.workshopsDate)} (workshops)`;
|
||||
const areWorkshopsBeforeEvent = isBefore(event.workshopsDate, event.date);
|
||||
|
||||
dateString = areWorkshopsBeforeEvent ?
|
||||
`${workshopsDateString}, ${mainEventDateString}` :
|
||||
`${mainEventDateString}, ${workshopsDateString}`;
|
||||
} else {
|
||||
// If no work shop date create conference date string
|
||||
dateString = processDate(event.date);
|
||||
}
|
||||
dateString = `${dateString}, ${new Date(event.date.end).getFullYear()}`;
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
function processDate(dates: Duration) {
|
||||
// Covert Date sting to date object for comparisons
|
||||
const startDate = new Date(dates.start);
|
||||
const endDate = new Date(dates.end);
|
||||
|
||||
// Create a date string in the start like January 31
|
||||
let processedDate = `${MONTHS[startDate.getMonth()]} ${startDate.getDate()}`;
|
||||
|
||||
// If they are in different months add the string '- February 2' Making the final string January 31 - February 2
|
||||
if (startDate.getMonth() !== endDate.getMonth()) {
|
||||
processedDate = `${processedDate} - ${MONTHS[endDate.getMonth()]} ${endDate.getDate()}`;
|
||||
} else if (startDate.getDate() !== endDate.getDate()) {
|
||||
// If not add - date eg it will make // January 30-31
|
||||
processedDate = `${processedDate}-${endDate.getDate()}`;
|
||||
}
|
||||
|
||||
return processedDate;
|
||||
}
|
||||
|
||||
function isBefore(duration1: Duration, duration2: Duration): boolean {
|
||||
return (duration1.start < duration2.start) ||
|
||||
(duration1.start === duration2.start && duration1.end < duration2.end);
|
||||
}
|
15
aio/src/app/custom-elements/events/events.module.ts
Normal file
15
aio/src/app/custom-elements/events/events.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { EventsComponent } from './events.component';
|
||||
import { EventsService } from './events.service';
|
||||
import { WithCustomElementComponent } from '../element-registry';
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule ],
|
||||
declarations: [ EventsComponent ],
|
||||
entryComponents: [ EventsComponent ],
|
||||
providers: [ EventsService]
|
||||
})
|
||||
export class EventsModule implements WithCustomElementComponent {
|
||||
customElementComponent: Type<any> = EventsComponent;
|
||||
}
|
56
aio/src/app/custom-elements/events/events.service.spec.ts
Normal file
56
aio/src/app/custom-elements/events/events.service.spec.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EventsService } from './events.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
|
||||
describe('EventsService', () => {
|
||||
|
||||
let injector: Injector;
|
||||
let eventsService: EventsService;
|
||||
let httpMock: HttpTestingController;
|
||||
let mockLogger: MockLogger;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
EventsService,
|
||||
{ provide: Logger, useClass: MockLogger }
|
||||
]
|
||||
});
|
||||
|
||||
eventsService = injector.get<EventsService>(EventsService);
|
||||
mockLogger = injector.get(Logger) as any;
|
||||
httpMock = injector.get(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
|
||||
it('should make a single connection to the server', () => {
|
||||
eventsService.events.subscribe();
|
||||
eventsService.events.subscribe();
|
||||
httpMock.expectOne('generated/events.json');
|
||||
expect().nothing(); // Prevent jasmine from complaining about no expectations.
|
||||
});
|
||||
|
||||
it('should handle a failed request for `events.json`', () => {
|
||||
const request = httpMock.expectOne('generated/events.json');
|
||||
request.error(new ErrorEvent('404'));
|
||||
expect(mockLogger.output.error).toEqual([
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/events\.json request failed:/);
|
||||
});
|
||||
|
||||
it('should return an empty array on a failed request for `events.json`', done => {
|
||||
const request = httpMock.expectOne('generated/events.json');
|
||||
request.error(new ErrorEvent('404'));
|
||||
eventsService.events.subscribe(results => {
|
||||
expect(results).toEqual([]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
32
aio/src/app/custom-elements/events/events.service.ts
Normal file
32
aio/src/app/custom-elements/events/events.service.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
import { ConnectableObservable, Observable, of } from 'rxjs';
|
||||
import { catchError, publishLast } from 'rxjs/operators';
|
||||
|
||||
import { Event } from './events.component';
|
||||
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
|
||||
const eventsPath = CONTENT_URL_PREFIX + 'events.json';
|
||||
|
||||
@Injectable()
|
||||
export class EventsService {
|
||||
events: Observable<Event[]>;
|
||||
|
||||
constructor(private http: HttpClient, private logger: Logger) {
|
||||
this.events = this.getEvents();
|
||||
}
|
||||
|
||||
private getEvents() {
|
||||
const events = this.http.get<any>(eventsPath).pipe(
|
||||
catchError(error => {
|
||||
this.logger.error(new Error(`${eventsPath} request failed: ${error.message}`));
|
||||
return of([]);
|
||||
}),
|
||||
publishLast()
|
||||
);
|
||||
(events as ConnectableObservable<Event[]>).connect();
|
||||
return events;
|
||||
}
|
||||
}
|
@ -25,7 +25,6 @@
|
||||
@import 'progress-bar';
|
||||
@import 'presskit';
|
||||
@import 'resources';
|
||||
@import 'scrollbar';
|
||||
@import 'search-results';
|
||||
@import 'select-menu';
|
||||
@import 'table';
|
||||
|
@ -1,27 +0,0 @@
|
||||
body::-webkit-scrollbar, mat-sidenav.sidenav::-webkit-scrollbar, .mat-sidenav-content::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track, mat-sidenav.sidenav::-webkit-scrollbar-track, .mat-sidenav-content::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb, mat-sidenav.sidenav::-webkit-scrollbar-thumb, .mat-sidenav-content::-webkit-scrollbar-thumb {
|
||||
background-color: $mediumgray;
|
||||
outline: 1px solid $darkgray;
|
||||
}
|
||||
|
||||
.search-results::-webkit-scrollbar, .toc-container::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.search-results::-webkit-scrollbar-track, .toc-container::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.search-results::-webkit-scrollbar-thumb, .toc-container::-webkit-scrollbar-thumb {
|
||||
background-color: $mediumgray;
|
||||
outline: 1px solid slategrey;
|
||||
}
|
@ -5,6 +5,7 @@ module.exports = function createSitemap() {
|
||||
'contributors-json',
|
||||
'navigation-json',
|
||||
'resources-json',
|
||||
'events-json'
|
||||
],
|
||||
ignoredPaths: [
|
||||
'file-not-found',
|
||||
|
@ -82,6 +82,11 @@ module.exports = new Package('angular-content', [basePackage, contentPackage])
|
||||
include: CONTENTS_PATH + '/marketing/resources.json',
|
||||
fileReader: 'jsonFileReader'
|
||||
},
|
||||
{
|
||||
basePath: CONTENTS_PATH,
|
||||
include: CONTENTS_PATH + '/marketing/events.json',
|
||||
fileReader: 'jsonFileReader'
|
||||
},
|
||||
]);
|
||||
|
||||
collectExamples.exampleFolders.push('examples');
|
||||
@ -110,7 +115,8 @@ module.exports = new Package('angular-content', [basePackage, contentPackage])
|
||||
{docTypes: ['navigation-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
|
||||
{docTypes: ['contributors-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
|
||||
{docTypes: ['announcements-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
|
||||
{docTypes: ['resources-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'}
|
||||
{docTypes: ['resources-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'},
|
||||
{docTypes: ['events-json'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'}
|
||||
]);
|
||||
})
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{%- macro renderHeritage(exportDoc) -%}
|
||||
{%- if exportDoc.extendsClauses.length %} extends {% for clause in exportDoc.extendsClauses -%}
|
||||
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
||||
{% if clause.doc.path %}<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% else %}{$ clause.text $}{% endif %}{% if not loop.last %}, {% endif -%}
|
||||
{% endfor %}{% endif %}
|
||||
{%- if exportDoc.implementsClauses.length %} implements {% for clause in exportDoc.implementsClauses -%}
|
||||
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
||||
|
@ -13,7 +13,7 @@ import * as webdriver from 'selenium-webdriver';
|
||||
declare var expect: any;
|
||||
|
||||
export function openBrowser(config: {
|
||||
url: string,
|
||||
url?: string,
|
||||
params?: {name: string, value: any}[],
|
||||
ignoreBrowserSynchronization?: boolean
|
||||
}) {
|
||||
|
@ -23,27 +23,34 @@ const globalOptions = {
|
||||
|
||||
const runner = createBenchpressRunner();
|
||||
|
||||
export async function runBenchmark(config: {
|
||||
export async function runBenchmark({
|
||||
id,
|
||||
url = '',
|
||||
params = [],
|
||||
ignoreBrowserSynchronization = true,
|
||||
microMetrics,
|
||||
work,
|
||||
prepare,
|
||||
setup,
|
||||
}: {
|
||||
id: string,
|
||||
url: string,
|
||||
params: {name: string, value: any}[],
|
||||
url?: string,
|
||||
params?: {name: string, value: any}[],
|
||||
ignoreBrowserSynchronization?: boolean,
|
||||
microMetrics?: {[key: string]: string},
|
||||
work?: () => void,
|
||||
prepare?: () => void,
|
||||
setup?: () => void
|
||||
work?: (() => void)|(() => Promise<unknown>),
|
||||
prepare?: (() => void)|(() => Promise<unknown>),
|
||||
setup?: (() => void)|(() => Promise<unknown>),
|
||||
}): Promise<any> {
|
||||
openBrowser(config);
|
||||
if (config.setup) {
|
||||
await config.setup();
|
||||
openBrowser({url, params, ignoreBrowserSynchronization});
|
||||
if (setup) {
|
||||
await setup();
|
||||
}
|
||||
const description: {[key: string]: any} = {};
|
||||
config.params.forEach((param) => description[param.name] = param.value);
|
||||
return runner.sample({
|
||||
id: config.id,
|
||||
execute: config.work,
|
||||
prepare: config.prepare,
|
||||
microMetrics: config.microMetrics,
|
||||
id,
|
||||
execute: work,
|
||||
prepare,
|
||||
microMetrics,
|
||||
providers: [{provide: Options.SAMPLE_DESCRIPTION, useValue: {}}]
|
||||
});
|
||||
}
|
||||
|
@ -2,25 +2,20 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "caretaker",
|
||||
srcs = [
|
||||
"cli.ts",
|
||||
],
|
||||
srcs = glob([
|
||||
"**/*.ts",
|
||||
]),
|
||||
module_name = "@angular/dev-infra-private/caretaker",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/caretaker/check",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/node-fetch",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//multimatch",
|
||||
"@npm//node-fetch",
|
||||
"@npm//typed-graphqlify",
|
||||
"@npm//yaml",
|
||||
"@npm//yargs",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "config",
|
||||
srcs = [
|
||||
"config.ts",
|
||||
],
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
],
|
||||
)
|
||||
|
@ -1,21 +0,0 @@
|
||||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "check",
|
||||
srcs = glob(["*.ts"]),
|
||||
module_name = "@angular/dev-infra-private/caretaker/service-statuses",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/caretaker:config",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/fs-extra",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/node-fetch",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//multimatch",
|
||||
"@npm//node-fetch",
|
||||
"@npm//typed-graphqlify",
|
||||
"@npm//yaml",
|
||||
"@npm//yargs",
|
||||
],
|
||||
)
|
@ -9,6 +9,7 @@
|
||||
import {GitClient} from '../../utils/git';
|
||||
import {getCaretakerConfig} from '../config';
|
||||
|
||||
import {printCiStatus} from './ci';
|
||||
import {printG3Comparison} from './g3';
|
||||
import {printGithubTasks} from './github';
|
||||
import {printServiceStatuses} from './services';
|
||||
@ -21,7 +22,9 @@ export async function checkServiceStatuses(githubToken: string) {
|
||||
/** The GitClient for interacting with git and Github. */
|
||||
const git = new GitClient(githubToken, config);
|
||||
|
||||
// TODO(josephperrott): Allow these checks to be loaded in parallel.
|
||||
await printServiceStatuses();
|
||||
await printGithubTasks(git, config.caretaker);
|
||||
await printG3Comparison(git);
|
||||
await printCiStatus(git);
|
||||
}
|
||||
|
59
dev-infra/caretaker/check/ci.ts
Normal file
59
dev-infra/caretaker/check/ci.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import {bold, green, info, red} from '../../utils/console';
|
||||
import {GitClient} from '../../utils/git';
|
||||
|
||||
|
||||
/** The results of checking the status of CI. */
|
||||
interface StatusCheckResult {
|
||||
status: 'success'|'failed'|'canceled'|'infrastructure_fail'|'timedout'|'failed'|'no_tests';
|
||||
timestamp: Date;
|
||||
buildUrl: string;
|
||||
}
|
||||
|
||||
/** Retrieve and log status of CI for the project. */
|
||||
export async function printCiStatus(git: GitClient) {
|
||||
info.group(bold(`CI`));
|
||||
// TODO(josephperrott): Expand list of branches checked to all active branches.
|
||||
await printStatus(git, 'master');
|
||||
info.groupEnd();
|
||||
info();
|
||||
}
|
||||
|
||||
/** Log the status of CI for a given branch to the console. */
|
||||
async function printStatus(git: GitClient, branch: string) {
|
||||
const result = await getStatusOfBranch(git, branch);
|
||||
const branchName = branch.padEnd(10);
|
||||
if (result === null) {
|
||||
info(`${branchName} was not found on CircleCI`);
|
||||
} else if (result.status === 'success') {
|
||||
info(`${branchName} ✅`);
|
||||
} else {
|
||||
info(`${branchName} ❌ (Ran at: ${result.timestamp.toLocaleString()})`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the CI status of a given branch from CircleCI. */
|
||||
async function getStatusOfBranch(git: GitClient, branch: string): Promise<StatusCheckResult|null> {
|
||||
const {owner, name} = git.remoteConfig;
|
||||
const url = `https://circleci.com/api/v1.1/project/gh/${owner}/${name}/tree/${
|
||||
branch}?limit=1&filter=completed&shallow=true`;
|
||||
const result = (await fetch(url).then(result => result.json()))?.[0];
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
status: result.outcome,
|
||||
timestamp: new Date(result.stop_time),
|
||||
buildUrl: result.build_url
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {existsSync, readFileSync} from 'fs-extra';
|
||||
import {existsSync, readFileSync} from 'fs';
|
||||
import * as multimatch from 'multimatch';
|
||||
import {join} from 'path';
|
||||
import {parse as parseYaml} from 'yaml';
|
||||
|
@ -13,15 +13,9 @@ import {GitClient} from '../../utils/git';
|
||||
import {CaretakerConfig} from '../config';
|
||||
|
||||
|
||||
interface GithubInfoQuery {
|
||||
[key: string]: {
|
||||
issueCount: number,
|
||||
};
|
||||
}
|
||||
|
||||
/** Retrieve the number of matching issues for each github query. */
|
||||
export async function printGithubTasks(git: GitClient, config: CaretakerConfig) {
|
||||
if (!config.githubQueries?.length) {
|
||||
export async function printGithubTasks(git: GitClient, config?: CaretakerConfig) {
|
||||
if (!config?.githubQueries?.length) {
|
||||
debug('No github queries defined in the configuration, skipping.');
|
||||
return;
|
||||
}
|
||||
|
@ -3,18 +3,10 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "commit-message",
|
||||
srcs = [
|
||||
"builder.ts",
|
||||
"cli.ts",
|
||||
"commit-message-draft.ts",
|
||||
"config.ts",
|
||||
"parse.ts",
|
||||
"restore-commit-message.ts",
|
||||
"validate.ts",
|
||||
"validate-file.ts",
|
||||
"validate-range.ts",
|
||||
"wizard.ts",
|
||||
],
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
exclude = ["**/*.spec.ts"],
|
||||
),
|
||||
module_name = "@angular/dev-infra-private/commit-message",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
@ -32,11 +24,7 @@ ts_library(
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"builder.spec.ts",
|
||||
"parse.spec.ts",
|
||||
"validate.spec.ts",
|
||||
],
|
||||
srcs = glob(["**/*.spec.ts"]),
|
||||
deps = [
|
||||
":commit-message",
|
||||
"//dev-infra/utils",
|
||||
|
@ -7,104 +7,19 @@
|
||||
*/
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {restoreCommitMessage} from './restore-commit-message';
|
||||
import {validateFile} from './validate-file';
|
||||
import {validateCommitRange} from './validate-range';
|
||||
import {runWizard} from './wizard';
|
||||
import {RestoreCommitMessageModule} from './restore-commit-message/cli';
|
||||
import {ValidateFileModule} from './validate-file/cli';
|
||||
import {ValidateRangeModule} from './validate-range/cli';
|
||||
import {WizardModule} from './wizard/cli';
|
||||
|
||||
/** Build the parser for the commit-message commands. */
|
||||
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
return localYargs.help()
|
||||
.strict()
|
||||
.command(
|
||||
'restore-commit-message-draft', false,
|
||||
args => {
|
||||
return args.option('file-env-variable', {
|
||||
type: 'string',
|
||||
array: true,
|
||||
conflicts: ['file'],
|
||||
required: true,
|
||||
description:
|
||||
'The key for the environment variable which holds the arguments for the\n' +
|
||||
'prepare-commit-msg hook as described here:\n' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
|
||||
coerce: arg => {
|
||||
const [file, source] = (process.env[arg] || '').split(' ');
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return [file, source];
|
||||
},
|
||||
});
|
||||
},
|
||||
args => {
|
||||
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
|
||||
})
|
||||
.command(
|
||||
'wizard <filePath> [source] [commitSha]', '', ((args: any) => {
|
||||
return args
|
||||
.positional(
|
||||
'filePath',
|
||||
{description: 'The file path to write the generated commit message into'})
|
||||
.positional('source', {
|
||||
choices: ['message', 'template', 'merge', 'squash', 'commit'],
|
||||
description: 'The source of the commit message as described here: ' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
|
||||
})
|
||||
.positional(
|
||||
'commitSha', {description: 'The commit sha if source is set to `commit`'});
|
||||
}),
|
||||
async (args: any) => {
|
||||
await runWizard(args);
|
||||
})
|
||||
.command(
|
||||
'pre-commit-validate', 'Validate the most recent commit message', {
|
||||
'file': {
|
||||
type: 'string',
|
||||
conflicts: ['file-env-variable'],
|
||||
description: 'The path of the commit message file.',
|
||||
},
|
||||
'file-env-variable': {
|
||||
type: 'string',
|
||||
conflicts: ['file'],
|
||||
description:
|
||||
'The key of the environment variable for the path of the commit message file.',
|
||||
coerce: arg => {
|
||||
const file = process.env[arg];
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return file;
|
||||
},
|
||||
}
|
||||
},
|
||||
args => {
|
||||
const file = args.file || args['file-env-variable'] || '.git/COMMIT_EDITMSG';
|
||||
validateFile(file);
|
||||
})
|
||||
.command(
|
||||
'validate-range', 'Validate a range of commit messages', {
|
||||
'range': {
|
||||
description: 'The range of commits to check, e.g. --range abc123..xyz456',
|
||||
demandOption: ' A range must be provided, e.g. --range abc123..xyz456',
|
||||
type: 'string',
|
||||
requiresArg: true,
|
||||
},
|
||||
},
|
||||
argv => {
|
||||
// If on CI, and not pull request number is provided, assume the branch
|
||||
// being run on is an upstream branch.
|
||||
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
|
||||
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
|
||||
info(`need to validate commit messages on CI runs on upstream branches.`);
|
||||
info();
|
||||
info(`Skipping check of provided commit range`);
|
||||
return;
|
||||
}
|
||||
validateCommitRange(argv.range);
|
||||
});
|
||||
.command(RestoreCommitMessageModule)
|
||||
.command(WizardModule)
|
||||
.command(ValidateFileModule)
|
||||
.command(ValidateRangeModule);
|
||||
}
|
||||
|
||||
if (require.main == module) {
|
||||
|
13
dev-infra/commit-message/commit-message-source.ts
Normal file
13
dev-infra/commit-message/commit-message-source.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* The source triggering the git commit message creation.
|
||||
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export type CommitMsgSource = 'message'|'template'|'merge'|'squash'|'commit';
|
@ -8,6 +8,7 @@
|
||||
|
||||
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
|
||||
|
||||
/** Configuration for commit-message comands. */
|
||||
export interface CommitMessageConfig {
|
||||
maxLineLength: number;
|
||||
minBodyLength: number;
|
||||
|
51
dev-infra/commit-message/restore-commit-message/cli.ts
Normal file
51
dev-infra/commit-message/restore-commit-message/cli.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {CommitMsgSource} from '../commit-message-source';
|
||||
|
||||
import {restoreCommitMessage} from './restore-commit-message';
|
||||
|
||||
export interface RestoreCommitMessageOptions {
|
||||
fileEnvVariable: string[];
|
||||
}
|
||||
|
||||
/** Builds the command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs.option('file-env-variable' as 'fileEnvVariable', {
|
||||
type: 'string',
|
||||
array: true,
|
||||
demandOption: true,
|
||||
description: 'The key for the environment variable which holds the arguments for the\n' +
|
||||
'prepare-commit-msg hook as described here:\n' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
|
||||
coerce: arg => {
|
||||
const [file, source] = (process.env[arg] || '').split(' ');
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return [file, source];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler({fileEnvVariable}: Arguments<RestoreCommitMessageOptions>) {
|
||||
restoreCommitMessage(fileEnvVariable[0], fileEnvVariable[1] as CommitMsgSource);
|
||||
}
|
||||
|
||||
/** yargs command module describing the command. */
|
||||
export const RestoreCommitMessageModule: CommandModule<{}, RestoreCommitMessageOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'restore-commit-message-draft',
|
||||
// Description: Restore a commit message draft if one has been saved from a failed commit attempt.
|
||||
// No describe is defiend to hide the command from the --help.
|
||||
describe: false,
|
||||
};
|
@ -8,9 +8,10 @@
|
||||
|
||||
import {writeFileSync} from 'fs';
|
||||
|
||||
import {debug, log} from '../utils/console';
|
||||
import {debug, log} from '../../utils/console';
|
||||
|
||||
import {loadCommitMessageDraft} from './commit-message-draft';
|
||||
import {loadCommitMessageDraft} from '../commit-message-draft';
|
||||
import {CommitMsgSource} from '../commit-message-source';
|
||||
|
||||
/**
|
||||
* Restore the commit message draft to the git to be used as the default commit message.
|
||||
@ -18,8 +19,7 @@ import {loadCommitMessageDraft} from './commit-message-draft';
|
||||
* The source provided may be one of the sources described in
|
||||
* https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export function restoreCommitMessage(
|
||||
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
|
||||
export function restoreCommitMessage(filePath: string, source?: CommitMsgSource) {
|
||||
if (!!source) {
|
||||
log('Skipping commit message restoration attempt');
|
||||
if (source === 'message') {
|
@ -1,30 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {readFileSync} from 'fs';
|
||||
import {resolve} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {deleteCommitMessageDraft, saveCommitMessageDraft} from './commit-message-draft';
|
||||
import {validateCommitMessage} from './validate';
|
||||
|
||||
/** Validate commit message at the provided file path. */
|
||||
export function validateFile(filePath: string) {
|
||||
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
|
||||
if (validateCommitMessage(commitMessage)) {
|
||||
info('√ Valid commit message');
|
||||
deleteCommitMessageDraft(filePath);
|
||||
return;
|
||||
}
|
||||
// On all invalid commit messages, the commit message should be saved as a draft to be
|
||||
// restored on the next commit attempt.
|
||||
saveCommitMessageDraft(filePath, commitMessage);
|
||||
// If the validation did not return true, exit as a failure.
|
||||
process.exit(1);
|
||||
}
|
62
dev-infra/commit-message/validate-file/cli.ts
Normal file
62
dev-infra/commit-message/validate-file/cli.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {getUserConfig} from '../../utils/config';
|
||||
|
||||
import {validateFile} from './validate-file';
|
||||
|
||||
|
||||
export interface ValidateFileOptions {
|
||||
file?: string;
|
||||
fileEnvVariable?: string;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
/** Builds the command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs
|
||||
.option('file', {
|
||||
type: 'string',
|
||||
conflicts: ['file-env-variable'],
|
||||
description: 'The path of the commit message file.',
|
||||
})
|
||||
.option('file-env-variable' as 'fileEnvVariable', {
|
||||
type: 'string',
|
||||
conflicts: ['file'],
|
||||
description: 'The key of the environment variable for the path of the commit message file.',
|
||||
coerce: (arg: string) => {
|
||||
const file = process.env[arg];
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return file;
|
||||
},
|
||||
})
|
||||
.option('error', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether invalid commit messages should be treated as failures rather than a warning',
|
||||
default: !!getUserConfig().commitMessage?.errorOnInvalidMessage || !!process.env['CI']
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler({error, file, fileEnvVariable}: Arguments<ValidateFileOptions>) {
|
||||
const filePath = file || fileEnvVariable || '.git/COMMIT_EDITMSG';
|
||||
validateFile(filePath, error);
|
||||
}
|
||||
|
||||
/** yargs command module describing the command. */
|
||||
export const ValidateFileModule: CommandModule<{}, ValidateFileOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'pre-commit-validate',
|
||||
describe: 'Validate the most recent commit message',
|
||||
};
|
47
dev-infra/commit-message/validate-file/validate-file.ts
Normal file
47
dev-infra/commit-message/validate-file/validate-file.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {readFileSync} from 'fs';
|
||||
import {resolve} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {error, green, info, log, red, yellow} from '../../utils/console';
|
||||
|
||||
import {deleteCommitMessageDraft, saveCommitMessageDraft} from '../commit-message-draft';
|
||||
import {printValidationErrors, validateCommitMessage} from '../validate';
|
||||
|
||||
/** Validate commit message at the provided file path. */
|
||||
export function validateFile(filePath: string, isErrorMode: boolean) {
|
||||
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
|
||||
const {valid, errors} = validateCommitMessage(commitMessage);
|
||||
if (valid) {
|
||||
info(`${green('√')} Valid commit message`);
|
||||
deleteCommitMessageDraft(filePath);
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
/** Function used to print to the console log. */
|
||||
let printFn = isErrorMode ? error : log;
|
||||
|
||||
printFn(`${isErrorMode ? red('✘') : yellow('!')} Invalid commit message`);
|
||||
printValidationErrors(errors, printFn);
|
||||
if (isErrorMode) {
|
||||
printFn(red('Aborting commit attempt due to invalid commit message.'));
|
||||
printFn(
|
||||
red('Commit message aborted as failure rather than warning due to local configuration.'));
|
||||
} else {
|
||||
printFn(yellow('Before this commit can be merged into the upstream repository, it must be'));
|
||||
printFn(yellow('amended to follow commit message guidelines.'));
|
||||
}
|
||||
|
||||
// On all invalid commit messages, the commit message should be saved as a draft to be
|
||||
// restored on the next commit attempt.
|
||||
saveCommitMessageDraft(filePath, commitMessage);
|
||||
// Set the correct exit code based on if invalid commit message is an error.
|
||||
process.exitCode = isErrorMode ? 1 : 0;
|
||||
}
|
50
dev-infra/commit-message/validate-range/cli.ts
Normal file
50
dev-infra/commit-message/validate-range/cli.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {info} from '../../utils/console';
|
||||
|
||||
import {validateCommitRange} from './validate-range';
|
||||
|
||||
|
||||
export interface ValidateRangeOptions {
|
||||
range: string;
|
||||
}
|
||||
|
||||
/** Builds the command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs.option('range', {
|
||||
description: 'The range of commits to check, e.g. --range abc123..xyz456',
|
||||
demandOption: ' A range must be provided, e.g. --range abc123..xyz456',
|
||||
type: 'string',
|
||||
requiresArg: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler({range}: Arguments<ValidateRangeOptions>) {
|
||||
// If on CI, and no pull request number is provided, assume the branch
|
||||
// being run on is an upstream branch.
|
||||
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
|
||||
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
|
||||
info(`need to validate commit messages on CI runs on upstream branches.`);
|
||||
info();
|
||||
info(`Skipping check of provided commit range`);
|
||||
return;
|
||||
}
|
||||
validateCommitRange(range);
|
||||
}
|
||||
|
||||
/** yargs command module describing the command. */
|
||||
export const ValidateRangeModule: CommandModule<{}, ValidateRangeOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'validate-range',
|
||||
describe: 'Validate a range of commit messages',
|
||||
};
|
@ -5,11 +5,11 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {info} from '../utils/console';
|
||||
import {exec} from '../utils/shelljs';
|
||||
import {error, info} from '../../utils/console';
|
||||
import {exec} from '../../utils/shelljs';
|
||||
|
||||
import {parseCommitMessage} from './parse';
|
||||
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
||||
import {parseCommitMessage} from '../parse';
|
||||
import {printValidationErrors, validateCommitMessage, ValidateCommitMessageOptions} from '../validate';
|
||||
|
||||
// Whether the provided commit is a fixup commit.
|
||||
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
||||
@ -19,11 +19,20 @@ const extractCommitHeader = (m: string) => parseCommitMessage(m).header;
|
||||
|
||||
/** Validate all commits in a provided git commit range. */
|
||||
export function validateCommitRange(range: string) {
|
||||
// A random value is used as a string to allow for a definite split point in the git log result.
|
||||
/**
|
||||
* A random value is used as a string to allow for a definite split point in the git log result.
|
||||
*/
|
||||
const randomValueSeparator = `${Math.random()}`;
|
||||
// Custom git log format that provides the commit header and body, separated as expected with
|
||||
// the custom separator as the trailing value.
|
||||
/**
|
||||
* Custom git log format that provides the commit header and body, separated as expected with the
|
||||
* custom separator as the trailing value.
|
||||
*/
|
||||
const gitLogFormat = `%s%n%n%b${randomValueSeparator}`;
|
||||
/**
|
||||
* A list of tuples containing a commit header string and the list of error messages for the
|
||||
* commit.
|
||||
*/
|
||||
const errors: [commitHeader: string, errors: string[]][] = [];
|
||||
|
||||
// Retrieve the commits in the provided range.
|
||||
const result = exec(`git log --reverse --format=${gitLogFormat} ${range}`);
|
||||
@ -45,12 +54,22 @@ export function validateCommitRange(range: string) {
|
||||
undefined :
|
||||
commits.slice(0, i).filter(isNonFixup).map(extractCommitHeader)
|
||||
};
|
||||
return validateCommitMessage(m, options);
|
||||
const {valid, errors: localErrors, commit} = validateCommitMessage(m, options);
|
||||
if (localErrors.length) {
|
||||
errors.push([commit.header, localErrors]);
|
||||
}
|
||||
return valid;
|
||||
});
|
||||
|
||||
if (allCommitsInRangeValid) {
|
||||
info('√ All commit messages in range valid.');
|
||||
} else {
|
||||
error('✘ Invalid commit message');
|
||||
errors.forEach(([header, validationErrors]) => {
|
||||
error.group(header);
|
||||
printValidationErrors(validationErrors);
|
||||
error.groupEnd();
|
||||
});
|
||||
// Exit with a non-zero exit code if invalid commit messages have
|
||||
// been discovered.
|
||||
process.exit(1);
|
@ -8,7 +8,7 @@
|
||||
|
||||
// Imports
|
||||
import * as validateConfig from './config';
|
||||
import {validateCommitMessage} from './validate';
|
||||
import {validateCommitMessage, ValidateCommitMessageResult} from './validate';
|
||||
|
||||
type CommitMessageConfig = validateConfig.CommitMessageConfig;
|
||||
|
||||
@ -31,44 +31,35 @@ const SCOPES = config.commitMessage.scopes.join(', ');
|
||||
const INVALID = false;
|
||||
const VALID = true;
|
||||
|
||||
function expectValidationResult(
|
||||
validationResult: ValidateCommitMessageResult, valid: boolean, errors: string[] = []) {
|
||||
expect(validationResult).toEqual(jasmine.objectContaining({valid, errors}));
|
||||
}
|
||||
|
||||
// TODO(josephperrott): Clean up tests to test script rather than for
|
||||
// specific commit messages we want to use.
|
||||
describe('validate-commit-message.js', () => {
|
||||
let lastError: string = '';
|
||||
|
||||
beforeEach(() => {
|
||||
lastError = '';
|
||||
|
||||
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
|
||||
spyOn(validateConfig, 'getCommitMessageConfig')
|
||||
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
|
||||
});
|
||||
|
||||
describe('validateMessage()', () => {
|
||||
it('should be valid', () => {
|
||||
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('fixup! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('squash! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('Revert: "fix(packaging): something"')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
expectValidationResult(validateCommitMessage('feat(packaging): something'), VALID);
|
||||
expectValidationResult(validateCommitMessage('fix(packaging): something'), VALID);
|
||||
expectValidationResult(validateCommitMessage('fixup! fix(packaging): something'), VALID);
|
||||
expectValidationResult(validateCommitMessage('squash! fix(packaging): something'), VALID);
|
||||
expectValidationResult(validateCommitMessage('Revert: "fix(packaging): something"'), VALID);
|
||||
});
|
||||
|
||||
it('should validate max length', () => {
|
||||
const msg =
|
||||
'fix(compiler): something super mega extra giga tera long, maybe even longer and longer and longer and longer and longer and longer...';
|
||||
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`The commit message header is longer than ${
|
||||
config.commitMessage.maxLineLength} characters`);
|
||||
expectValidationResult(validateCommitMessage(msg), INVALID, [
|
||||
`The commit message header is longer than ${config.commitMessage.maxLineLength} characters`
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip max length limit for URLs', () => {
|
||||
@ -77,49 +68,56 @@ describe('validate-commit-message.js', () => {
|
||||
'limit. For more details see the following super long URL:\n\n' +
|
||||
'https://github.com/angular/components/commit/e2ace018ddfad10608e0e32932c43dcfef4095d7#diff-9879d6db96fd29134fc802214163b95a';
|
||||
|
||||
expect(validateCommitMessage(msg)).toBe(VALID);
|
||||
expectValidationResult(validateCommitMessage(msg), VALID);
|
||||
});
|
||||
|
||||
it('should validate "<type>(<scope>): <subject>" format', () => {
|
||||
const msg = 'not correct format';
|
||||
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`The commit message header does not match the expected format.`);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(msg), INVALID,
|
||||
[`The commit message header does not match the expected format.`]);
|
||||
});
|
||||
|
||||
it('should fail when type is invalid', () => {
|
||||
const msg = 'weird(core): something';
|
||||
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`'weird' is not an allowed type.\n => TYPES: ${TYPES}`);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(msg), INVALID,
|
||||
[`'weird' is not an allowed type.\n => TYPES: ${TYPES}`]);
|
||||
});
|
||||
|
||||
it('should fail when scope is invalid', () => {
|
||||
const errorMessageFor = (scope: string, header: string) =>
|
||||
`'${scope}' is not an allowed scope.\n => SCOPES: ${SCOPES}`;
|
||||
|
||||
expect(validateCommitMessage('fix(Compiler): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('Compiler', 'fix(Compiler): something'));
|
||||
expectValidationResult(
|
||||
validateCommitMessage('fix(Compiler): something'), INVALID,
|
||||
[errorMessageFor('Compiler', 'fix(Compiler): something')]);
|
||||
|
||||
expect(validateCommitMessage('feat(bah): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('bah', 'feat(bah): something'));
|
||||
expectValidationResult(
|
||||
validateCommitMessage('feat(bah): something'), INVALID,
|
||||
[errorMessageFor('bah', 'feat(bah): something')]);
|
||||
|
||||
expect(validateCommitMessage('fix(webworker): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
|
||||
expectValidationResult(
|
||||
validateCommitMessage('fix(webworker): something'), INVALID,
|
||||
[errorMessageFor('webworker', 'fix(webworker): something')]);
|
||||
|
||||
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
|
||||
expectValidationResult(
|
||||
validateCommitMessage('refactor(security): something'), INVALID,
|
||||
[errorMessageFor('security', 'refactor(security): something')]);
|
||||
|
||||
expect(validateCommitMessage('refactor(docs): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('docs', 'refactor(docs): something'));
|
||||
expectValidationResult(
|
||||
validateCommitMessage('refactor(docs): something'), INVALID,
|
||||
[errorMessageFor('docs', 'refactor(docs): something')]);
|
||||
|
||||
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
|
||||
expectValidationResult(
|
||||
validateCommitMessage('feat(angular): something'), INVALID,
|
||||
[errorMessageFor('angular', 'feat(angular): something')]);
|
||||
});
|
||||
|
||||
it('should allow empty scope', () => {
|
||||
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
expectValidationResult(validateCommitMessage('build: blablabla'), VALID);
|
||||
});
|
||||
|
||||
// We do not want to allow WIP. It is OK to fail the PR build in this case to show that there is
|
||||
@ -127,30 +125,25 @@ describe('validate-commit-message.js', () => {
|
||||
it('should not allow "WIP: ..." syntax', () => {
|
||||
const msg = 'WIP: fix: something';
|
||||
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(msg), INVALID,
|
||||
[`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`]);
|
||||
});
|
||||
|
||||
describe('(revert)', () => {
|
||||
it('should allow valid "revert: ..." syntaxes', () => {
|
||||
expect(validateCommitMessage('revert: anything')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('Revert: "anything"')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('revert anything')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('rEvErT anything')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
expectValidationResult(validateCommitMessage('revert: anything'), VALID);
|
||||
expectValidationResult(validateCommitMessage('Revert: "anything"'), VALID);
|
||||
expectValidationResult(validateCommitMessage('revert anything'), VALID);
|
||||
expectValidationResult(validateCommitMessage('rEvErT anything'), VALID);
|
||||
});
|
||||
|
||||
it('should not allow "revert(scope): ..." syntax', () => {
|
||||
const msg = 'revert(compiler): reduce generated code payload size by 65%';
|
||||
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`'revert' is not an allowed type.\n => TYPES: ${TYPES}`);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(msg), INVALID,
|
||||
[`'revert' is not an allowed type.\n => TYPES: ${TYPES}`]);
|
||||
});
|
||||
|
||||
// https://github.com/angular/angular/issues/23479
|
||||
@ -158,28 +151,26 @@ describe('validate-commit-message.js', () => {
|
||||
const msg =
|
||||
'Revert "fix(compiler): Pretty print object instead of [Object object] (#22689)" (#23442)';
|
||||
|
||||
expect(validateCommitMessage(msg)).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
expectValidationResult(validateCommitMessage(msg), VALID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(squash)', () => {
|
||||
describe('without `disallowSquash`', () => {
|
||||
it('should return commits as valid', () => {
|
||||
expect(validateCommitMessage('squash! feat(core): add feature')).toBe(VALID);
|
||||
expect(validateCommitMessage('squash! fix: a bug')).toBe(VALID);
|
||||
expect(validateCommitMessage('squash! fix a typo')).toBe(VALID);
|
||||
expectValidationResult(validateCommitMessage('squash! feat(core): add feature'), VALID);
|
||||
expectValidationResult(validateCommitMessage('squash! fix: a bug'), VALID);
|
||||
expectValidationResult(validateCommitMessage('squash! fix a typo'), VALID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with `disallowSquash`', () => {
|
||||
it('should fail', () => {
|
||||
expect(validateCommitMessage('fix(core): something', {disallowSquash: true})).toBe(VALID);
|
||||
expect(validateCommitMessage('squash! fix(core): something', {
|
||||
disallowSquash: true
|
||||
})).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit must be manually squashed into the target commit');
|
||||
expectValidationResult(
|
||||
validateCommitMessage('fix(core): something', {disallowSquash: true}), VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage('squash! fix(core): something', {disallowSquash: true}),
|
||||
INVALID, ['The commit must be manually squashed into the target commit']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -187,9 +178,9 @@ describe('validate-commit-message.js', () => {
|
||||
describe('(fixup)', () => {
|
||||
describe('without `nonFixupCommitHeaders`', () => {
|
||||
it('should return commits as valid', () => {
|
||||
expect(validateCommitMessage('fixup! feat(core): add feature')).toBe(VALID);
|
||||
expect(validateCommitMessage('fixup! fix: a bug')).toBe(VALID);
|
||||
expect(validateCommitMessage('fixup! fixup! fix: a bug')).toBe(VALID);
|
||||
expectValidationResult(validateCommitMessage('fixup! feat(core): add feature'), VALID);
|
||||
expectValidationResult(validateCommitMessage('fixup! fix: a bug'), VALID);
|
||||
expectValidationResult(validateCommitMessage('fixup! fixup! fix: a bug'), VALID);
|
||||
});
|
||||
});
|
||||
|
||||
@ -197,36 +188,39 @@ describe('validate-commit-message.js', () => {
|
||||
it('should check that the fixup commit matches a non-fixup one', () => {
|
||||
const msg = 'fixup! foo';
|
||||
|
||||
expect(validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}))
|
||||
.toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}))
|
||||
.toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}))
|
||||
.toBe(VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}),
|
||||
VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}),
|
||||
VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}),
|
||||
VALID);
|
||||
|
||||
expect(validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}))
|
||||
.toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'Unable to find match for fixup commit among prior commits: \n' +
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}),
|
||||
INVALID,
|
||||
['Unable to find match for fixup commit among prior commits: \n' +
|
||||
' qux\n' +
|
||||
' quux\n' +
|
||||
' quuux');
|
||||
' quuux']);
|
||||
});
|
||||
|
||||
it('should fail if `nonFixupCommitHeaders` is empty', () => {
|
||||
expect(validateCommitMessage('refactor(core): make reactive', {
|
||||
disallowSquash: false,
|
||||
nonFixupCommitHeaders: []
|
||||
})).toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}))
|
||||
.toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
`Unable to find match for fixup commit among prior commits: -`);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
'refactor(core): make reactive',
|
||||
{disallowSquash: false, nonFixupCommitHeaders: []}),
|
||||
VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}),
|
||||
INVALID, [`Unable to find match for fixup commit among prior commits: -`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -246,24 +240,27 @@ describe('validate-commit-message.js', () => {
|
||||
});
|
||||
|
||||
it('should fail validation if the body is shorter than `minBodyLength`', () => {
|
||||
expect(validateCommitMessage(
|
||||
'fix(core): something\n\n Explanation of the motivation behind this change'))
|
||||
.toBe(VALID);
|
||||
expect(validateCommitMessage('fix(core): something\n\n too short')).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit message body does not meet the minimum length of 30 characters');
|
||||
expect(validateCommitMessage('fix(core): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit message body does not meet the minimum length of 30 characters');
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
'fix(core): something\n\n Explanation of the motivation behind this change'),
|
||||
VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage('fix(core): something\n\n too short'), INVALID,
|
||||
['The commit message body does not meet the minimum length of 30 characters']);
|
||||
expectValidationResult(validateCommitMessage('fix(core): something'), INVALID, [
|
||||
|
||||
'The commit message body does not meet the minimum length of 30 characters'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
|
||||
() => {
|
||||
expect(validateCommitMessage('docs: just fixing a typo')).toBe(VALID);
|
||||
expect(validateCommitMessage('docs(core): just fixing a typo')).toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
'docs(core): just fixing a typo\n\nThis was just a silly typo.'))
|
||||
.toBe(VALID);
|
||||
expectValidationResult(validateCommitMessage('docs: just fixing a typo'), VALID);
|
||||
expectValidationResult(validateCommitMessage('docs(core): just fixing a typo'), VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
'docs(core): just fixing a typo\n\nThis was just a silly typo.'),
|
||||
VALID);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -8,7 +8,7 @@
|
||||
import {error} from '../utils/console';
|
||||
|
||||
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
|
||||
import {parseCommitMessage} from './parse';
|
||||
import {parseCommitMessage, ParsedCommitMessage} from './parse';
|
||||
|
||||
/** Options for commit message validation. */
|
||||
export interface ValidateCommitMessageOptions {
|
||||
@ -16,27 +16,26 @@ export interface ValidateCommitMessageOptions {
|
||||
nonFixupCommitHeaders?: string[];
|
||||
}
|
||||
|
||||
/** The result of a commit message validation check. */
|
||||
export interface ValidateCommitMessageResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
commit: ParsedCommitMessage;
|
||||
}
|
||||
|
||||
/** Regex matching a URL for an entire commit body line. */
|
||||
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
|
||||
|
||||
/** Validate a commit message against using the local repo's config. */
|
||||
export function validateCommitMessage(
|
||||
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
|
||||
function printError(errorMessage: string) {
|
||||
error(
|
||||
`INVALID COMMIT MSG: \n` +
|
||||
`${'─'.repeat(40)}\n` +
|
||||
`${commitMsg}\n` +
|
||||
`${'─'.repeat(40)}\n` +
|
||||
`ERROR: \n` +
|
||||
` ${errorMessage}` +
|
||||
`\n\n` +
|
||||
`The expected format for a commit is: \n` +
|
||||
`<type>(<scope>): <subject>\n\n<body>`);
|
||||
}
|
||||
|
||||
commitMsg: string, options: ValidateCommitMessageOptions = {}): ValidateCommitMessageResult {
|
||||
const config = getCommitMessageConfig().commitMessage;
|
||||
const commit = parseCommitMessage(commitMsg);
|
||||
const errors: string[] = [];
|
||||
|
||||
/** Perform the validation checks against the parsed commit. */
|
||||
function validateCommitAndCollectErrors() {
|
||||
// TODO(josephperrott): Remove early return calls when commit message errors are found
|
||||
|
||||
////////////////////////////////////
|
||||
// Checking revert, squash, fixup //
|
||||
@ -51,7 +50,7 @@ export function validateCommitMessage(
|
||||
// the git history anyway, unless the options provided to not allow squash commits.
|
||||
if (commit.isSquash) {
|
||||
if (options.disallowSquash) {
|
||||
printError('The commit must be manually squashed into the target commit');
|
||||
errors.push('The commit must be manually squashed into the target commit');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -64,7 +63,7 @@ export function validateCommitMessage(
|
||||
// check.
|
||||
if (commit.isFixup) {
|
||||
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
|
||||
printError(
|
||||
errors.push(
|
||||
'Unable to find match for fixup commit among prior commits: ' +
|
||||
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
|
||||
return false;
|
||||
@ -77,19 +76,17 @@ export function validateCommitMessage(
|
||||
// Checking commit header //
|
||||
////////////////////////////
|
||||
if (commit.header.length > config.maxLineLength) {
|
||||
printError(`The commit message header is longer than ${config.maxLineLength} characters`);
|
||||
errors.push(`The commit message header is longer than ${config.maxLineLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!commit.type) {
|
||||
printError(`The commit message header does not match the expected format.`);
|
||||
errors.push(`The commit message header does not match the expected format.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (COMMIT_TYPES[commit.type] === undefined) {
|
||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
|
||||
errors.push(`'${commit.type}' is not an allowed type.\n => TYPES: ${
|
||||
Object.keys(COMMIT_TYPES).join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
@ -98,19 +95,19 @@ export function validateCommitMessage(
|
||||
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
|
||||
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
|
||||
errors.push(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
|
||||
commit.scope}' was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
|
||||
printError(
|
||||
errors.push(
|
||||
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (commit.scope && !config.scopes.includes(commit.scope)) {
|
||||
printError(
|
||||
errors.push(
|
||||
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
@ -126,7 +123,7 @@ export function validateCommitMessage(
|
||||
|
||||
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
|
||||
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||
printError(`The commit message body does not meet the minimum length of ${
|
||||
errors.push(`The commit message body does not meet the minimum length of ${
|
||||
config.minBodyLength} characters`);
|
||||
return false;
|
||||
}
|
||||
@ -139,10 +136,27 @@ export function validateCommitMessage(
|
||||
});
|
||||
|
||||
if (lineExceedsMaxLength) {
|
||||
printError(
|
||||
errors.push(
|
||||
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {valid: validateCommitAndCollectErrors(), errors, commit};
|
||||
}
|
||||
|
||||
|
||||
/** Print the error messages from the commit message validation to the console. */
|
||||
export function printValidationErrors(errors: string[], print = error) {
|
||||
print.group(`Error${errors.length === 1 ? '' : 's'}:`);
|
||||
errors.forEach(line => print(line));
|
||||
print.groupEnd();
|
||||
print();
|
||||
print('The expected format for a commit is: ');
|
||||
print('<type>(<scope>): <summary>');
|
||||
print();
|
||||
print('<body>');
|
||||
print();
|
||||
}
|
||||
|
54
dev-infra/commit-message/wizard/cli.ts
Normal file
54
dev-infra/commit-message/wizard/cli.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {CommitMsgSource} from '../commit-message-source';
|
||||
|
||||
import {runWizard} from './wizard';
|
||||
|
||||
|
||||
export interface WizardOptions {
|
||||
filePath: string;
|
||||
commitSha: string|undefined;
|
||||
source: CommitMsgSource|undefined;
|
||||
}
|
||||
|
||||
/** Builds the command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs
|
||||
.positional('filePath', {
|
||||
description: 'The file path to write the generated commit message into',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.positional('source', {
|
||||
choices: ['message', 'template', 'merge', 'squash', 'commit'] as const,
|
||||
description: 'The source of the commit message as described here: ' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
|
||||
})
|
||||
.positional('commitSha', {
|
||||
description: 'The commit sha if source is set to `commit`',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler(args: Arguments<WizardOptions>) {
|
||||
await runWizard(args);
|
||||
}
|
||||
|
||||
/** yargs command module describing the command. */
|
||||
export const WizardModule: CommandModule<{}, WizardOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'wizard <filePath> [source] [commitSha]',
|
||||
// Description: Run the wizard to build a base commit message before opening to complete.
|
||||
// No describe is defiend to hide the command from the --help.
|
||||
describe: false,
|
||||
};
|
@ -7,15 +7,12 @@
|
||||
*/
|
||||
import {writeFileSync} from 'fs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
import {getUserConfig} from '../../utils/config';
|
||||
import {debug, info} from '../../utils/console';
|
||||
|
||||
import {buildCommitMessage} from './builder';
|
||||
import {buildCommitMessage} from '../builder';
|
||||
import {CommitMsgSource} from '../commit-message-source';
|
||||
|
||||
/**
|
||||
* The source triggering the git commit message creation.
|
||||
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export type PrepareCommitMsgHookSource = 'message'|'template'|'merge'|'squash'|'commit';
|
||||
|
||||
/** The default commit message used if the wizard does not procude a commit message. */
|
||||
const defaultCommitMessage = `<type>(<scope>): <summary>
|
||||
@ -24,11 +21,16 @@ const defaultCommitMessage = `<type>(<scope>): <summary>
|
||||
# lines at 100 characters.>\n\n`;
|
||||
|
||||
export async function runWizard(
|
||||
args: {filePath: string, source?: PrepareCommitMsgHookSource, commitSha?: string}) {
|
||||
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
|
||||
args: {filePath: string, source?: CommitMsgSource, commitSha?: string}) {
|
||||
if (getUserConfig().commitMessage?.disableWizard) {
|
||||
debug('Skipping commit message wizard due to enabled `commitMessage.disableWizard` option in');
|
||||
debug('user config.');
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.source !== undefined) {
|
||||
info(`Skipping commit message wizard due because the commit was created via '${
|
||||
info(`Skipping commit message wizard because the commit was created via '${
|
||||
args.source}' source`);
|
||||
process.exitCode = 0;
|
||||
return;
|
@ -3,6 +3,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
ts_library(
|
||||
name = "common",
|
||||
srcs = glob(["*.ts"]),
|
||||
module_name = "@angular/dev-infra-private/pr/common",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
|
@ -33,6 +33,10 @@ import {MergeResult, MergeStatus, PullRequestMergeTask} from './task';
|
||||
export async function mergePullRequest(
|
||||
prNumber: number, githubToken: string, projectRoot: string = getRepoBaseDir(),
|
||||
config?: MergeConfigWithRemote) {
|
||||
// Set the environment variable to skip all git commit hooks triggered by husky. We are unable to
|
||||
// rely on `---no-verify` as some hooks still run, notably the `prepare-commit-msg` hook.
|
||||
process.env['HUSKY_SKIP_HOOKS'] = '1';
|
||||
|
||||
const api = await createPullRequestMergeTask(githubToken, projectRoot, config);
|
||||
|
||||
// Perform the merge. Force mode can be activated through a command line flag.
|
||||
|
@ -12,11 +12,13 @@
|
||||
"@angular/benchpress": "0.2.1",
|
||||
"@octokit/graphql": "<from-root>",
|
||||
"@octokit/types": "<from-root>",
|
||||
"@octokit/rest": "<from-root>",
|
||||
"brotli": "<from-root>",
|
||||
"chalk": "<from-root>",
|
||||
"cli-progress": "<from-root>",
|
||||
"glob": "<from-root>",
|
||||
"inquirer": "<from-root>",
|
||||
"inquirer-autocomplete-prompt": "<from-root>",
|
||||
"minimatch": "<from-root>",
|
||||
"multimatch": "<from-root>",
|
||||
"node-fetch": "<from-root>",
|
||||
@ -26,9 +28,7 @@
|
||||
"tslib": "<from-root>",
|
||||
"typed-graphqlify": "<from-root>",
|
||||
"yaml": "<from-root>",
|
||||
"yargs": "<from-root>"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"yargs": "<from-root>",
|
||||
"@bazel/buildifier": "<from-root>",
|
||||
"clang-format": "<from-root>",
|
||||
"protractor": "<from-root>",
|
||||
|
@ -112,7 +112,8 @@ export function main(
|
||||
if (fixedCircularDeps.length !== 0) {
|
||||
error(yellow(` Fixed circular dependencies that need to be removed from the golden:`));
|
||||
fixedCircularDeps.forEach(c => error(` • ${convertReferenceChainToString(c)}`));
|
||||
error();
|
||||
info(yellow(`\n Total: ${newCircularDeps.length} new cycle(s), ${
|
||||
fixedCircularDeps.length} fixed cycle(s). \n`));
|
||||
if (approveCommand) {
|
||||
info(yellow(` Please approve the new golden with: ${approveCommand}`));
|
||||
} else {
|
||||
|
@ -12,13 +12,11 @@ ts_library(
|
||||
"@npm//@octokit/graphql",
|
||||
"@npm//@octokit/rest",
|
||||
"@npm//@octokit/types",
|
||||
"@npm//@types/fs-extra",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//chalk",
|
||||
"@npm//fs-extra",
|
||||
"@npm//inquirer",
|
||||
"@npm//inquirer-autocomplete-prompt",
|
||||
"@npm//shelljs",
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {existsSync} from 'fs';
|
||||
import {dirname, join} from 'path';
|
||||
|
||||
import {error} from './console';
|
||||
import {debug, error} from './console';
|
||||
import {exec} from './shelljs';
|
||||
import {isTsNodeAvailable} from './ts-node';
|
||||
|
||||
@ -49,7 +49,16 @@ export type NgDevConfig<T = {}> = CommonConfig&T;
|
||||
const CONFIG_FILE_PATH = '.ng-dev/config';
|
||||
|
||||
/** The configuration for ng-dev. */
|
||||
let CONFIG: {}|null = null;
|
||||
let cachedConfig: NgDevConfig|null = null;
|
||||
|
||||
/**
|
||||
* The filename expected for local user config, without the file extension to allow a typescript,
|
||||
* javascript or json file to be used.
|
||||
*/
|
||||
const USER_CONFIG_FILE_PATH = '.ng-dev.user';
|
||||
|
||||
/** The local user configuration for ng-dev. */
|
||||
let userConfig: {[key: string]: any}|null = null;
|
||||
|
||||
/**
|
||||
* Get the configuration from the file system, returning the already loaded
|
||||
@ -57,15 +66,15 @@ let CONFIG: {}|null = null;
|
||||
*/
|
||||
export function getConfig(): NgDevConfig {
|
||||
// If the global config is not defined, load it from the file system.
|
||||
if (CONFIG === null) {
|
||||
if (cachedConfig === null) {
|
||||
// The full path to the configuration file.
|
||||
const configPath = join(getRepoBaseDir(), CONFIG_FILE_PATH);
|
||||
// Set the global config object.
|
||||
CONFIG = readConfigFile(configPath);
|
||||
// Read the configuration and validate it before caching it for the future.
|
||||
cachedConfig = validateCommonConfig(readConfigFile(configPath));
|
||||
}
|
||||
// Return a clone of the global config to ensure that a new instance of the config is returned
|
||||
// each time, preventing unexpected effects of modifications to the config object.
|
||||
return validateCommonConfig({...CONFIG});
|
||||
// Return a clone of the cached global config to ensure that a new instance of the config
|
||||
// is returned each time, preventing unexpected effects of modifications to the config object.
|
||||
return {...cachedConfig};
|
||||
}
|
||||
|
||||
/** Validate the common configuration has been met for the ng-dev command. */
|
||||
@ -86,8 +95,11 @@ function validateCommonConfig(config: Partial<NgDevConfig>) {
|
||||
return config as NgDevConfig;
|
||||
}
|
||||
|
||||
/** Resolves and reads the specified configuration file. */
|
||||
function readConfigFile(configPath: string): object {
|
||||
/**
|
||||
* Resolves and reads the specified configuration file, optionally returning an empty object if the
|
||||
* configuration file cannot be read.
|
||||
*/
|
||||
function readConfigFile(configPath: string, returnEmptyObjectOnError = false): object {
|
||||
// If the the `.ts` extension has not been set up already, and a TypeScript based
|
||||
// version of the given configuration seems to exist, set up `ts-node` if available.
|
||||
if (require.extensions['.ts'] === undefined && existsSync(`${configPath}.ts`) &&
|
||||
@ -103,7 +115,12 @@ function readConfigFile(configPath: string): object {
|
||||
try {
|
||||
return require(configPath);
|
||||
} catch (e) {
|
||||
error('Could not read configuration file.');
|
||||
if (returnEmptyObjectOnError) {
|
||||
debug(`Could not read configuration file at ${configPath}, returning empty object instead.`);
|
||||
debug(e);
|
||||
return {};
|
||||
}
|
||||
error(`Could not read configuration file at ${configPath}.`);
|
||||
error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
@ -135,3 +152,23 @@ export function getRepoBaseDir() {
|
||||
}
|
||||
return baseRepoDir.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local user configuration from the file system, returning the already loaded copy if it is
|
||||
* defined.
|
||||
*
|
||||
* @returns The user configuration object, or an empty object if no user configuration file is
|
||||
* present. The object is an untyped object as there are no required user configurations.
|
||||
*/
|
||||
export function getUserConfig() {
|
||||
// If the global config is not defined, load it from the file system.
|
||||
if (userConfig === null) {
|
||||
// The full path to the configuration file.
|
||||
const configPath = join(getRepoBaseDir(), USER_CONFIG_FILE_PATH);
|
||||
// Set the global config object.
|
||||
userConfig = readConfigFile(configPath, true);
|
||||
}
|
||||
// Return a clone of the user config to ensure that a new instance of the config is returned
|
||||
// each time, preventing unexpected effects of modifications to the config object.
|
||||
return {...userConfig};
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {writeFileSync} from 'fs-extra';
|
||||
import {writeFileSync} from 'fs';
|
||||
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
|
||||
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
||||
import {join} from 'path';
|
||||
@ -196,6 +196,9 @@ export function captureLogOutputForCommand(argv: Arguments) {
|
||||
/** Path to the log file location. */
|
||||
const logFilePath = join(getRepoBaseDir(), '.ng-dev.log');
|
||||
|
||||
// Strip ANSI escape codes from log outputs.
|
||||
LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, '');
|
||||
|
||||
writeFileSync(logFilePath, LOGGED_TEXT);
|
||||
|
||||
// For failure codes greater than 1, the new logged lines should be written to a specific log
|
||||
|
@ -4,71 +4,23 @@ Caretaker is responsible for merging PRs into the individual branches and intern
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Draining the queue of PRs ready to be merged. (PRs with [`PR action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22PR+action%3A+merge%22) label)
|
||||
- Draining the queue of PRs ready to be merged. (PRs with [`action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22action%3A+merge%22) label)
|
||||
- Assigning [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
|
||||
|
||||
## Merging the PR
|
||||
|
||||
A PR needs to have `PR action: merge` and `PR target: *` labels to be considered
|
||||
ready to merge. Merging is performed by running `merge-pr` with a PR number to merge.
|
||||
A PR needs to have `action: merge` and `target: *` labels to be considered
|
||||
ready to merge. Merging is performed by running `ng-dev pr merge` with a PR number to merge.
|
||||
|
||||
The tooling automatically verifies the given PR is ready for merge. If the PR passes the tests, the
|
||||
tool will automatically merge it based on the applied target label.
|
||||
|
||||
To merge a PR run:
|
||||
|
||||
```
|
||||
$ ./scripts/github/merge-pr 1234
|
||||
$ yarn ng-dev pr merge <pr number>
|
||||
```
|
||||
|
||||
The `merge-pr` script will:
|
||||
- Ensure that all appropriate labels are on the PR.
|
||||
- Fetches the latest PR code from the `angular/angular` repo.
|
||||
- It will `cherry-pick` all of the SHAs from the PR into the current corresponding branches `master` and or `?.?.x` (patch).
|
||||
- It will rewrite commit history by automatically adding `Close #1234` and `(#1234)` into the commit message.
|
||||
|
||||
NOTE: The `merge-pr` will land the PR on `master` and or `?.?.x` (patch) as described by `PR target: *` label.
|
||||
|
||||
### Recovering from failed `merge-pr` due to conflicts
|
||||
|
||||
When running `merge-pr` the script will output the commands which it is about to run.
|
||||
|
||||
```
|
||||
$ ./scripts/github/merge-pr 1234
|
||||
======================
|
||||
GitHub Merge PR Steps
|
||||
======================
|
||||
git cherry-pick angular/pr/1234~1..angular/pr/1234
|
||||
git filter-branch -f --msg-filter "/home/misko/angular/scripts/github/utils/github.closes 1234" HEAD~1..HEAD
|
||||
```
|
||||
|
||||
If the `cherry-pick` command fails than resolve conflicts and use `git cherry-pick --continue` once ready. After the `cherry-pick` is done cut&paste and run the `filter-branch` command to properly rewrite the messages
|
||||
|
||||
## Cherry-picking PRs into patch branch
|
||||
|
||||
In addition to merging PRs into the master branch, many PRs need to be also merged into a patch branch.
|
||||
Follow these steps to get patch branch up to date.
|
||||
|
||||
1. Check out the most recent patch branch: `git checkout 4.3.x`
|
||||
2. Get a list of PRs merged into master: `git log master --oneline -n10`
|
||||
3. For each PR number in the commit message run: `./scripts/github/merge-pr 1234`
|
||||
- The PR will only merge if the `PR target:` matches the branch.
|
||||
|
||||
Once all of the PRs are in patch branch, push the all branches and tags to github using `push-upstream` script.
|
||||
|
||||
|
||||
## Pushing merged PRs into github
|
||||
|
||||
Use `push-upstream` script to push all of the branch and tags to github.
|
||||
|
||||
```
|
||||
$ ./scripts/github/push-upstream
|
||||
git push git@github.com:angular/angular.git master:master 4.3.x:4.3.x
|
||||
Counting objects: 25, done.
|
||||
Delta compression using up to 6 threads.
|
||||
Compressing objects: 100% (17/17), done.
|
||||
Writing objects: 100% (25/25), 2.22 KiB | 284.00 KiB/s, done.
|
||||
Total 25 (delta 22), reused 8 (delta 7)
|
||||
remote: Resolving deltas: 100% (22/22), completed with 18 local objects.
|
||||
To github.com:angular/angular.git
|
||||
079d884b6..d1c4a94bb master -> master
|
||||
git push --tags -f git@github.com:angular/angular.git patch_sync:patch_sync
|
||||
Everything up-to-date
|
||||
```
|
||||
The `ng-dev pr merge` tool will automatically restore to the previous git state when a merge fails.
|
||||
|
@ -12,7 +12,7 @@ Change approvals in our monorepo are managed via [PullApprove](https://docs.pull
|
||||
# Merging
|
||||
|
||||
Once a change has all of the required approvals, either the last approver or the PR author (if PR author has the project collaborator status)
|
||||
should mark the PR with the `PR action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
|
||||
should mark the PR with the `action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
|
||||
This signals to the caretaker that the PR should be merged. See [merge instructions](CARETAKER.md).
|
||||
|
||||
# Who is the Caretaker?
|
||||
|
@ -154,9 +154,7 @@ available as a long-term distribution mechanism, but they are guaranteed to be a
|
||||
time of the build.
|
||||
|
||||
You can access the artifacts for a specific CI run by going to the workflow page, clicking on the
|
||||
`publish_packages_as_artifacts` job and then switching to the "Artifacts" tab.
|
||||
(If you happen to know the build number of the job, the URL will be something like:
|
||||
`https://circleci.com/gh/angular/angular/<build-number>#artifacts`)
|
||||
`publish_packages_as_artifacts` job and then switching to the "ARTIFACTS" tab.
|
||||
|
||||
#### Archives for each Package
|
||||
On the "Artifacts" tab, there is a list of links to compressed archives for Angular packages. The
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Triage Process and GitHub Labels for Angular
|
||||
|
||||
This document describes how the Angular team uses labels and milestones to triage issues on github.
|
||||
This document describes how the Angular team uses labels and milestones to triage issues on GitHub.
|
||||
The basic idea of the process is that caretaker only assigns a component (`comp: *`) label.
|
||||
The owner of the component is then responsible for the secondary / component-level triage.
|
||||
|
||||
@ -125,32 +125,32 @@ Triaging PRs is the same as triaging issues, except that the labels `frequency:
|
||||
|
||||
PRs also have additional label categories that should be used to signal their state.
|
||||
|
||||
Every triaged PR must have a `PR action` label assigned to it:
|
||||
Every triaged PR must have a `action: *` label assigned to it:
|
||||
|
||||
* `PR action: discuss`: Discussion is needed, to be led by the author.
|
||||
* `action: discuss`: Discussion is needed, to be led by the author.
|
||||
* _**Who adds it:** Typically the PR author._
|
||||
* _**Who removes it:** Whoever added it._
|
||||
* `PR action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
|
||||
* `action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
|
||||
* _**Who adds it:** Any team member. The caretaker can use it to differentiate PRs pending review from merge-ready PRs._
|
||||
* _**Who removes it:** Whoever added it or the reviewer adding the last missing review._
|
||||
* `PR action: cleanup`: More work is needed from the author.
|
||||
* `action: cleanup`: More work is needed from the author.
|
||||
* _**Who adds it:** The reviewer requesting changes to the PR._
|
||||
* _**Who removes it:** Either the author (after implementing the requested changes) or the reviewer (after confirming the requested changes have been implemented)._
|
||||
* `PR action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
|
||||
* `action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
|
||||
* _**Who adds it:** Typically the PR author._
|
||||
* _**Who removes it:** Whoever added it._
|
||||
|
||||
|
||||
In addition, PRs can have the following states:
|
||||
|
||||
* `PR state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
|
||||
* `state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
|
||||
* _**Who adds it:** The PR author._
|
||||
* _**Who removes it:** Whoever added it._
|
||||
* `PR state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
|
||||
* `state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
|
||||
* _**Who adds it:** Any team member._
|
||||
* _**Who removes it:** Any team member._
|
||||
|
||||
When a PR is ready for review, a review should be requested using the Reviewers interface in Github.
|
||||
When a PR is ready for review, a review should be requested using the Reviewers interface in GitHub.
|
||||
|
||||
|
||||
## PR Target
|
||||
@ -160,15 +160,29 @@ In our git workflow, we merge changes either to the `master` branch, the active
|
||||
The decision about the target must be done by the PR author and/or reviewer.
|
||||
This decision is then honored when the PR is being merged by the caretaker.
|
||||
|
||||
To communicate the target we use the following labels:
|
||||
To communicate the target we use GitHub labels and only one target label may be applied to a PR.
|
||||
|
||||
* `PR target: master & patch`: the PR should me merged into the master branch and cherry-picked into the most recent patch branch. All PRs with fixes, docs and refactorings should use this target.
|
||||
* `PR target: master-only`: the PR should be merged only into the `master` branch. All PRs with new features, API changes or high-risk changes should use this target.
|
||||
* `PR target: patch-only`: the PR should be merged only into the most recent patch branch (e.g. 5.0.x). This target is useful if a `master & patch` PR can't be cleanly cherry-picked into the stable branch and a new PR is needed.
|
||||
* `PR target: LTS-only`: the PR should be merged only into the active LTS branch(es). Only security and critical fixes are allowed in these branches. Always send a new PR targeting just the LTS branch and request review approval from @IgorMinar.
|
||||
* `PR target: TBD`: the target is yet to be determined.
|
||||
Targeting an active release train:
|
||||
|
||||
If a PR is missing the `PR target: *` label, or if the label is set to "TBD" when the PR is sent to the caretaker, the caretaker should reject the PR and request the appropriate target label to be applied before the PR is merged.
|
||||
* `target: major`: Any breaking change
|
||||
* `target: minor`: Any new feature
|
||||
* `target: patch`: Bug fixes, refactorings, documentation changes, etc. that pose no or very low risk of adversely
|
||||
affecting existing applications.
|
||||
|
||||
Special Cases:
|
||||
* `target: rc`: A critical fix for an active release-train while it is in a feature freeze or RC phase
|
||||
* `target: lts`: A criticial fix for a specific release-train that is still within the long term support phase
|
||||
|
||||
|
||||
Notes:
|
||||
- To land a change only in a patch/RC branch, without landing it in any other active release-train branch (such
|
||||
as `master`), the patch/RC branch can be targeted in the GitHub UI with the appropriate
|
||||
`target: patch`/`target: rc` label.
|
||||
- `target: lts` PRs must target the specific LTS branch they would need to merge into in the GitHub UI, in
|
||||
cases which a change is desired in multiple LTS branches, individual PRs for each LTS branch must be created
|
||||
|
||||
|
||||
If a PR is missing the `target:*` label, it will be marked as pending by the angular robot status checks.
|
||||
|
||||
|
||||
## PR Approvals
|
||||
@ -182,7 +196,7 @@ In any case, the reviewer should actually look through the code and provide feed
|
||||
|
||||
Note that approved state does not mean a PR is ready to be merged.
|
||||
For example, a reviewer might approve the PR but request a minor tweak that doesn't need further review, e.g., a rebase or small uncontroversial change.
|
||||
Only the `PR action: merge` label means that the PR is ready for merging.
|
||||
Only the `action: merge` label means that the PR is ready for merging.
|
||||
|
||||
|
||||
## Special Labels
|
||||
@ -201,7 +215,7 @@ Only issues with `cla:yes` should be merged into master.
|
||||
|
||||
Applying this label to a PR makes the angular.io preview available regardless of the author. [More info](../aio/aio-builds-setup/docs/overview--security-model.md)
|
||||
|
||||
### `PR action: merge-assistance`
|
||||
### `action: merge-assistance`
|
||||
* _**Who adds it:** Any team member._
|
||||
* _**Who removes it:** Any team member._
|
||||
|
||||
@ -211,7 +225,7 @@ The comment should be formatted like this: `merge-assistance: <explain what kind
|
||||
|
||||
For example, the PR owner might not be a Googler and needs help to run g3sync; or one of the checks is failing due to external causes and the PR should still be merged.
|
||||
|
||||
### `PR action: rerun CI at HEAD`
|
||||
### `action: rerun CI at HEAD`
|
||||
* _**Who adds it:** Any team member._
|
||||
* _**Who removes it:** The Angular Bot, once it triggers the CI rerun._
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user