fix(docs-infra): remove part aio infra (#54929)

Remove parts of the aio infra

PR Close #54929
This commit is contained in:
Joey Perrott 2024-03-18 18:22:08 +00:00 committed by Jessica Janiuk
parent 199150849b
commit ade024407d
375 changed files with 188 additions and 41313 deletions

View file

@ -56,11 +56,6 @@ build --enable_runfiles
build:release --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=release"
build:release --stamp
# Building AIO against local Angular deps requires stamping
# versions in Angular packages due to CLI version checks.
build:aio_local_deps --stamp
build:aio_local_deps --workspace_status_command="yarn -s --cwd aio local-workspace-status"
# Snapshots should also be stamped with version control information.
build:snapshot-build --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=snapshot"
build:snapshot-build --stamp
@ -74,14 +69,6 @@ build:snapshot-build --stamp
##########################################################
build --flag_alias=aio_build_config=//aio:flag_aio_build_config
####################################
# AIO first party dep substitution #
# Turn on with #
# --config=aio_local_deps #
####################################
build:aio_local_deps --//aio:flag_aio_local_deps
###############################
# Output #
###############################
@ -118,10 +105,6 @@ common:remote --jobs=200
build:remote --google_default_credentials
# Limit the number of test jobs for on an AIO local deps build. The example tests running
# concurrently pushes the circleci executor RAM usage to its limits.
test:aio_local_deps --jobs=24
# Force remote exeuctions to consider the entire run as linux
build:remote --cpu=k8
build:remote --host_cpu=k8

View file

@ -29,7 +29,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel build --config=aio_local_deps //adev:build
run: yarn bazel build //adev:build
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@5774b71c01a55c4c998f858ee37d3b77ae704c31
with:
workflow-artifact-name: 'adev-preview'

View file

@ -5,6 +5,7 @@ on:
branches:
- main
- '[0-9]+.[0-9]+.x'
- 'remove-aio-stuff'
pull_request:
types: [opened, synchronize, reopened]
@ -101,7 +102,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel test --config=aio_local_deps //adev:test
run: yarn bazel test //adev:test
publish-snapshots:
if: github.event_name == 'push'
@ -261,7 +262,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel build --config=aio_local_deps //adev:build
run: yarn bazel build //adev:build
- name: Move generated project files into deployment directory
run: cp -r dist/bin/adev/build adev/build
- name: Setup firebase context

View file

@ -2,7 +2,6 @@ workspace(
name = "angular",
managed_directories = {
"@npm": ["node_modules"],
"@aio_npm": ["aio/node_modules"],
},
)
@ -109,30 +108,6 @@ yarn_install(
yarn_lock = "//:yarn.lock",
)
yarn_install(
name = "aio_npm",
# Note that we add the postinstall scripts here so that the dependencies are re-installed
# when the postinstall patches are modified.
data = [
YARN_LABEL,
"//:.yarnrc",
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
"//aio:tools/cli-patches/bazel-architect-output.patch",
"//aio:tools/cli-patches/patch.js",
],
# Currently disabled due to:
# 1. Missing Windows support currently.
# 2. Incompatibilites with the `ts_library` rule.
exports_directories_only = False,
manual_build_file_contents = npm_package_archives(),
package_json = "//aio:package.json",
# We prefer to symlink the `node_modules` to only maintain a single install.
# See https://github.com/angular/dev-infra/pull/446#issuecomment-1059820287 for details.
symlink_node_modules = True,
yarn = YARN_LABEL,
yarn_lock = "//aio:yarn.lock",
)
yarn_install(
name = "aio_example_deps",
# Rename the default js_library target from "node_modules" as this obscures the

View file

@ -1,218 +0,0 @@
{
"root": true,
"overrides": [
{
"files": ["*.ts"],
"parserOptions": {
"project": [
"tsconfig.json",
"tools/firebase-test-utils/tsconfig.json",
"tests/e2e/tsconfig.json"
],
"createDefaultProgram": true
},
"extends": ["plugin:@angular-eslint/template/process-inline-templates"],
"plugins": [
"@typescript-eslint",
"@angular-eslint",
"eslint-plugin-jsdoc",
"eslint-plugin-import",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow"
],
"rules": {
"@angular-eslint/component-class-suffix": "error",
"@angular-eslint/component-selector": [
"error",
{
"prefix": "aio",
"style": "kebab-case",
"type": "element"
}
],
"@angular-eslint/contextual-lifecycle": "error",
"@angular-eslint/directive-class-suffix": "error",
"@angular-eslint/directive-selector": [
"error",
{
"prefix": "aio",
"style": "camelCase",
"type": "attribute"
}
],
"@angular-eslint/no-conflicting-lifecycle": "error",
"@angular-eslint/no-host-metadata-property": "off",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-native": "error",
"@angular-eslint/no-output-on-prefix": "error",
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/no-outputs-metadata-property": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": "error",
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/dot-notation": "error",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": [
"error",
{
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@typescript-eslint/member-ordering": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": [
"error",
{
"ignoreParameters": true
}
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-shadow": ["error"],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"@typescript-eslint/semi": ["error", "always"],
"@typescript-eslint/triple-slash-reference": [
"error",
{
"lib": "always",
"path": "always",
"types": "prefer-import"
}
],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"arrow-parens": "off",
"comma-dangle": "off",
"complexity": "off",
"constructor-super": "error",
"curly": "error",
"eol-last": "error",
"eqeqeq": ["error", "smart"],
"guard-for-in": "error",
"id-denylist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
],
"id-match": "error",
"import/no-deprecated": "warn",
"indent": "off",
"jsdoc/check-alignment": "error",
"jsdoc/no-types": "error",
"max-classes-per-file": "off",
"max-len": ["error", 120],
"new-parens": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-cond-assign": "error",
"no-console": [
"error",
{
"allow": ["log", "warn", "error"]
}
],
"no-debugger": "error",
"no-empty": "off",
"no-empty-function": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-invalid-this": "off",
"no-multiple-empty-lines": "off",
"no-new-wrappers": "error",
"no-restricted-imports": [
"error",
{
"message": "Please import directly from 'rxjs' instead",
"name": "rxjs/Rx"
}
],
"no-restricted-syntax": [
"error",
{
"message": "Don't keep jasmine focus methods.",
"selector": "CallExpression[callee.name=/^(fdescribe|fit)$/]"
}
],
"no-shadow": "off",
"no-tabs": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-unsafe-finally": "error",
"no-unused-expressions": "off",
"no-unused-labels": "error",
"no-use-before-define": "off",
"no-var": "error",
"object-shorthand": "error",
"one-var": ["error", "never"],
"prefer-arrow/prefer-arrow-functions": "off",
"prefer-const": "error",
"quote-props": ["error", "as-needed"],
"quotes": "off",
"radix": "error",
"sort-keys": "off",
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"asyncArrow": "always",
"named": "never"
}
],
"use-isnan": "error",
"valid-typeof": "off"
}
},
{
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"],
"rules": {
"@angular-eslint/template/alt-text": "error",
"@angular-eslint/template/elements-content": "error",
"@angular-eslint/template/label-has-associated-control": "error",
"@angular-eslint/template/table-scope": "error",
"@angular-eslint/template/valid-aria": "error",
"@angular-eslint/template/click-events-have-key-events": "error",
"@angular-eslint/template/eqeqeq": "off",
"@angular-eslint/template/mouse-events-have-key-events": "error",
"@angular-eslint/template/no-autofocus": "error",
"@angular-eslint/template/no-distracting-elements": "error",
"@angular-eslint/template/no-positive-tabindex": "error"
}
}
]
}

View file

@ -1 +0,0 @@
engine-strict = true

View file

@ -1,11 +1,11 @@
load("@aio_npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin", "npm_package_bin")
load("//tools:defaults.bzl", "nodejs_binary")
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
load(":local_packages_util.bzl", "link_local_packages", "substitute_local_package_deps")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag")
load("//aio/scripts:local_server_test.bzl", "local_server_test")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load(":aio_targets.bzl", "aio_test")
load("@bazel_skylib//lib:collections.bzl", "collections")
@ -91,7 +91,7 @@ npm_package_bin(
tags = [
"requires-network",
],
tool = "@aio_npm//dgeni/bin:dgeni",
tool = "@npm//dgeni/bin:dgeni",
toolchains = [
"@npm//@angular/build-tooling/bazel/git-toolchain:current_git_toolchain",
],
@ -148,29 +148,29 @@ APPLICATION_DEPS = [
":stackblitz",
":example-zips",
"//aio/src/generated:ngsw-config",
"@aio_npm//@angular-devkit/build-angular",
"@aio_npm//@angular-eslint/builder",
"@aio_npm//@angular/animations",
"@aio_npm//@angular/cdk",
"@aio_npm//@angular/cli",
"@aio_npm//@angular/common",
"@aio_npm//@angular/compiler",
"@aio_npm//@angular/compiler-cli",
"@aio_npm//@angular/core",
"@aio_npm//@angular/elements",
"@aio_npm//@angular/forms",
"@aio_npm//@angular/material",
"@aio_npm//@angular/platform-browser",
"@aio_npm//@angular/platform-browser-dynamic",
"@aio_npm//@angular/router",
"@aio_npm//@angular/service-worker",
"@aio_npm//@types/lunr",
"@aio_npm//@types/trusted-types",
"@aio_npm//lunr",
"@aio_npm//rxjs",
"@aio_npm//safevalues",
"@aio_npm//tslib",
"@aio_npm//zone.js",
"@npm//@angular-devkit/build-angular",
"@npm//@angular-eslint/builder",
"@npm//@angular/animations",
"@npm//@angular/cdk",
"@npm//@angular/cli",
"@npm//@angular/common",
"@npm//@angular/compiler",
"@npm//@angular/compiler-cli",
"@npm//@angular/core",
"@npm//@angular/elements",
"@npm//@angular/forms",
"@npm//@angular/material",
"@npm//@angular/platform-browser",
"@npm//@angular/platform-browser-dynamic",
"@npm//@angular/router",
"@npm//@angular/service-worker",
"@npm//@types/lunr",
"@npm//@types/trusted-types",
"@npm//lunr",
"@npm//rxjs",
"@npm//safevalues",
"@npm//tslib",
"@npm//zone.js",
]
# All sources, specs, and config files required to test the docs app
@ -183,16 +183,16 @@ TEST_FILES = APPLICATION_FILES + [
# Dependencies required to test the docs app
TEST_DEPS = APPLICATION_DEPS + [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@aio_npm//@types/jasmine",
"@aio_npm//@types/node",
"@aio_npm//assert",
"@aio_npm//jasmine",
"@aio_npm//jasmine-core",
"@aio_npm//karma-chrome-launcher",
"@aio_npm//karma-coverage",
"@aio_npm//karma-jasmine",
"@aio_npm//karma-jasmine-html-reporter",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//assert",
"@npm//jasmine",
"@npm//jasmine-core",
"@npm//karma-chrome-launcher",
"@npm//karma-coverage",
"@npm//karma-jasmine",
"@npm//karma-jasmine-html-reporter",
"//aio/tools:windows-chromium-path",
]
@ -202,12 +202,12 @@ E2E_FILES = APPLICATION_FILES + glob(["tests/e2e/**"])
# Dependencies required to run the e2e tests
E2E_DEPS = APPLICATION_DEPS + [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@aio_npm//@types/jasmine",
"@aio_npm//@types/node",
"@aio_npm//jasmine-spec-reporter",
"@aio_npm//protractor",
"@aio_npm//ts-node",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//jasmine-spec-reporter",
"@npm//protractor",
"@npm//ts-node",
"//aio/tools:windows-chromium-path",
]
@ -322,7 +322,7 @@ architect_test(
# (presumably after AIO migration to Bazel)
flaky = True,
toolchains = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
],
)

View file

@ -1,114 +0,0 @@
# Angular documentation project (https://angular.io)
Everything in this folder is part of the documentation project. This includes:
* the web site for displaying the documentation.
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
* the tooling for setting up examples for development; and generating live-example and zip files from the examples.
<a name="developer-tasks"></a>
## Developer tasks
We use [Yarn](https://yarnpkg.com) to manage the dependencies and development tasks. Behind the scenes, [Bazel](https://bazel.build/) is used to build targets and run tests.
You should run all these tasks from the `angular/aio` folder.
Here are the most important tasks you might need to use:
* `yarn` - install all the dependencies.
* `yarn build` - create a development build of the application.
* `yarn build-prod` - create a production build of the application.
* `yarn build-local` - same as `build`, but uses locally built Angular packages from source code rather than from npm.
* `yarn start` - run a development web server that watches, rebuilds, and reloads the page when there are changes to the source code or docs.
* `yarn start-local` - same as `start`, but uses local Angular packages.
* `yarn test` - run all the unit tests for the doc-viewer once.
* `yarn test-local` - similar to `test`, but tests against locally built Angular packages.
* `yarn test-and-watch` - watch all the source files for the doc-viewer, and run all the unit tests when any change.
* `yarn e2e` - run all the e2e tests for the doc-viewer.
* `yarn e2e-local` - similar to `e2e`, but tests against locally built Angular packages.
* `yarn lint` - check that the doc-viewer code follows our style rules.
* `yarn docs-watch` - similar to `start`, but only watches for docs changes and uses a faster, low-fidelity build ideal for quick editing.
* `yarn docs-test` - run the unit tests for the doc generation code.
* `yarn docs-lint` - check that the doc gen code follows our style rules.
* `yarn create-example` - create a new example directory containing initial source files.
* `yarn example-playground <exampleName>` - set up a playground to manually test an example combined with its boilerplate files
- `--local` - link locally build Angular packages as deps
- `--watch` - update the playground when sources change
* `yarn example-e2e` - run all e2e tests for examples. Available options:
- `--local`: run e2e tests against locally built Angular packages.
- `--filter=foo`: limit e2e tests to those containing the word "foo".
- `--exclude=bar`: exclude e2e tests containing the word "bar".
> **Note for Windows users**
>
> The underlying Bazel build requires creating [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) (see [here](./tools/examples/README.md#symlinked-node_modules) for details). On Windows, this requires to either have [Developer Mode enabled](https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10) (supported on Windows 10 or newer) or run the setup commands as administrator.
## Using ServiceWorker locally
Running `yarn start` (even when explicitly targeting production mode) does not set up the
ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then
serve the files with `yarn http-server ../dist/bin/aio/build -p 4200`.
## Guide to authoring
There are two types of content in the documentation:
* **API docs**: descriptions of all that make up the Angular platform, such as the modules, classes, interfaces or decorators.
API docs are generated directly from the source code.
The source code is contained in TypeScript files, located in the `angular/packages` folder.
Each API item may have a preceding comment, which contains JSDoc style tags and content.
The content is written in markdown. To generate docs, each package's files need to be explicitly included in the [packages/BUILD.bazel](../packages/BUILD.bazel) file under the `files_for_docgen` target.
* **Other content**: guides, tutorials, and other marketing material.
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
* **Code examples**: code examples need to be testable to ensure their accuracy.
Also, our examples have a specific look and feel and allow the user to copy the source code. For larger
examples they are rendered in a tabbed interface (e.g. template, HTML, and TypeScript on separate
tabs). Additionally, some are live examples, which provide links where the code can be edited, executed, and/or downloaded. For details on working with code examples, please read the [Code snippets](https://angular.io/guide/docs-style-guide#code-snippets), [Source code markup](https://angular.io/guide/docs-style-guide#source-code-markup), and [Live examples](https://angular.io/guide/docs-style-guide#live-examples) pages of the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
The [Authors Style Guide](https://angular.io/guide/docs-style-guide) prescribes guidelines for
writing guide pages, explains how to use the documentation classes and components, and how to markup sample source code to produce code snippets.
### Generating the complete docs
Running the `yarn build` or `yarn start` tasks will automatically generate the docs. This will process all the source files (API and other),
extracting the documentation and generating JSON files that can be consumed by the doc-viewer.
### Partial doc generation for editors
Full doc generation can take up to one minute. That's too slow for efficient document creation and editing.
You can make small changes in a smart editor that displays formatted markdown:
>In VS Code, _Cmd-K, V_ opens markdown preview in side pane; _Cmd-B_ toggles left sidebar
You also want to see those changes displayed properly in the doc viewer
with a quick, edit/view cycle time.
For this purpose, use the `yarn docs-watch` task, which watches for changes to source files and only
re-processes the files necessary to generate the docs that are related to the file that has changed.
Since this task takes shortcuts, it is much faster (often less than 1 second) but it won't produce full
fidelity content. For example, links to other docs and code examples may not render correctly. This is
most particularly noticed in links to other docs and in the embedded examples, which may not always render
correctly.
The general setup is as follows:
* Open a terminal, ensure the dependencies are installed, then start the doc-viewer:
```bash
yarn docs-watch
```
* A browser will open automatically at https://localhost:4200/. Navigate to the document on which you want to work.
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
be regenerated, the app will rebuild and the page will reload.
* If you get a build error complaining about examples or any other odd behavior, be sure to consult
the [Authors Style Guide](https://angular.io/guide/docs-style-guide).

View file

@ -1,42 +0,0 @@
# Update the angular.io app
The dependencies of the angular.io app (including Angular, Angular Material and Angular CLI) are automatically updated using [Renovate](https://renovatebot.com/).
However, it is useful to periodically also manually update the app to more closely match (in file layout, configs, etc.) what a new Angular CLI app would look like.
This is typically only needed once for each new major Angular version.
Since angular.io is an Angular CLI app, we can take advantage of `ng update` to apply migrations.
Follow these steps to align the angular.io app with new CLI apps.
> **Note:**
> The following steps assume that the related Angular dependencies have already been updated in [aio/package.json](./package.json) (for example, automatically by Renovate).
> **Note:**
> All commands shown below are expected to be executed from inside the [aio/](./) directory (unless specified otherwise).
- Determine (for example, by examining git history) what is the last versions for which this process was performed.
These will be referred to as `<FROM_VERSION_*>`.
If you can't determine these, use arbitrary versions, such as the previous major version.
- Run the following commands to automatically apply any available migrations to the project:
```sh
# Ensure dependencies are installed.
yarn install
# Migrate project to new versions.
yarn ng update @angular/cli --allow-dirty --migrate-only --from=<FROM_VERSION_CLI>
yarn ng update @angular/core --allow-dirty --migrate-only --from=<FROM_VERSION_ANGULAR>
yarn ng update @angular/material --allow-dirty --migrate-only --from=<FROM_VERSION_MATERIAL>
```
> **Note:**
> Depending on the number of changes generated from each `ng update` command, it might make sense to create a separate commit for each update.
- Inspect [package.json](./package.json) to determine what is the current version of Angular CLI (i.e. `@angular/cli`) used in the app.
This will be referred to as `<TO_VERSION_CLI>`.
- Use the [angular-cli-diff](https://github.com/cexbrayat/angular-cli-diff) repository to discover more changes (which are not automatically applied via `ng update` migrations) between Angular CLI apps of different versions.
Visit https://github.com/cexbrayat/angular-cli-diff/compare/<FROM_VERSION_CLI>...<TO_VERSION_CLI>, inspect the changes between the two versions and apply the ones that make sense to the angular.io source code.
- Commit all changes and [submit a pull request](../CONTRIBUTING.md#submit-pr).

View file

@ -1,4 +1,4 @@
load("@aio_npm//@angular-devkit/architect-cli:index.bzl", "architect_test")
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect_test")
def aio_test(name, data, args, **kwargs):
architect_test(
@ -10,7 +10,7 @@ def aio_test(name, data, args, **kwargs):
"CHROME_BIN": "../$(CHROMIUM)",
},
toolchains = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
],
**kwargs
)

View file

@ -1,264 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "yarn",
"warnings": {
"typescriptMismatch": false
},
"analytics": false,
"cache": {
// Disable build caching as the cache folder will just be dropped
// when run under Bazel sandboxed execution.
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"site": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
},
"@schematics/angular:component": {
"inlineStyle": true,
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "aio",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": ["zone.js"],
"ngswConfigPath": "src/generated/ngsw-config.json",
"tsConfig": "tsconfig.app.json",
"webWorkerTsConfig": "tsconfig.worker.json",
"optimization": {
"fonts": {
"inline": false
},
"scripts": true,
"styles": {
"inlineCritical": false,
"minify": true
}
},
"outputHashing": "all",
"sourceMap": true,
"namedChunks": true,
"assets": [
// Architect on Windows has difficulty service assets files within a
// Bazel runfiles symlink forest. Using "followSymlinks" seems to
// fix this. Note that the assets below don't need this workaround as
// symlinked tree artifacts pointing to the output tree don't appear
// to have the same issue.
{
"followSymlinks": true,
"input": "src/assets",
"glob": "**/*",
"output": "/assets/"
},
{
"followSymlinks": true,
"input": "src/",
"glob": "**/pwa-manifest.json",
"output": "/"
},
{
"followSymlinks": true,
"input": "src/",
"glob": "**/google385281288605d160.html",
"output": "/"
},
{
"input": "stackblitz/generated",
"output": "generated",
"glob": "**"
},
{
"input": "example-zips/generated",
"output": "generated",
"glob": "**"
},
{
"input": "dgeni/generated",
"output": "generated",
"glob": "**"
},
{
"input": "dgeni-fast/generated",
"output": "generated",
"glob": "**"
}
],
"styles": [
"src/styles/main.scss",
{
"inject": false,
"input": "src/styles/custom-themes/dark-theme.scss",
"bundleName": "dark-theme"
},
{
"inject": false,
"input": "src/styles/custom-themes/light-theme.scss",
"bundleName": "light-theme"
}
],
"scripts": [],
"budgets": [
{
"type": "initial",
"maximumWarning": "850kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
},
"configurations": {
"next": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.next.ts"
}
],
"serviceWorker": true
},
"rc": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.rc.ts"
}
],
"serviceWorker": true
},
"stable": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.stable.ts"
}
],
"serviceWorker": true
},
"archive": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.archive.ts"
}
],
"serviceWorker": true
},
"ci": {
"progress": false
},
"development": {
"buildOptimizer": false,
"optimization": false,
"outputHashing": "none",
"vendorChunk": true,
"extractLicenses": false
}
},
"defaultConfiguration": "stable"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"headers": {
// Keep in sync with the `firebase.json` file.
// Note: Unlike in `firebase.json` we omit the `report-uri` for development.
"Content-Security-Policy": "require-trusted-types-for 'script'; trusted-types angular angular#bundler angular#unsafe-bypass aio#analytics google#safe goog#html"
}
},
"configurations": {
"next": {
"browserTarget": "site:build:next"
},
"rc": {
"browserTarget": "site:build:rc"
},
"stable": {
"browserTarget": "site:build:stable"
},
"archive": {
"browserTarget": "site:build:archive"
},
"ci": {
"browserTarget": "site:build:ci"
},
"development": {
"browserTarget": "site:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "site:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"webWorkerTsConfig": "tsconfig.worker.json",
"karmaConfig": "karma.conf.js",
"assets": ["src/assets", "src/pwa-manifest.json", "src/google385281288605d160.html"],
"styles": [
"src/styles/main.scss",
{
"inject": false,
"input": "src/styles/custom-themes/dark-theme.scss",
"bundleName": "dark-theme"
},
{
"inject": false,
"input": "src/styles/custom-themes/light-theme.scss",
"bundleName": "light-theme"
}
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/!(generated)/**/*.ts",
"src/!(generated)/**/*.html",
"tests/**/*.ts"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "tests/e2e/protractor.conf.js",
"devServerTarget": "site:serve"
},
"configurations": {
"ci": {
"devServerTarget": "site:serve:ci"
}
}
}
}
}
}
}

View file

@ -206,7 +206,7 @@ def docs_example(name, test = True, test_tags = [], test_exec_properties = {}, f
data = [
":%s" % name,
YARN_LABEL,
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
"//aio/tools/examples:run-example-e2e",
"//aio/tools:windows-chromium-path",
# We install the whole node modules for runtime deps of e2e tests
@ -229,7 +229,7 @@ def docs_example(name, test = True, test_tags = [], test_exec_properties = {}, f
"CHROMEDRIVER_BIN": "$(CHROMEDRIVER)",
},
toolchains = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
],
exec_properties = test_exec_properties,
flaky = flaky,

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,5 +1,5 @@
load("//aio/content/examples:examples.bzl", "docs_example")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
package(default_visibility = ["//visibility:public"])

View file

@ -1,11 +0,0 @@
{
"rules": {
".read": false,
".write": false,
"events": {
".read": true,
".write": false
}
}
}

View file

@ -1,287 +0,0 @@
{
// Docs on Firebase hosting configuration: https://firebase.google.com/docs/hosting/full-config
"hosting": {
"target": "aio",
"public": "dist",
"cleanUrls": true,
//////////////////////////////////////////////////////////////////////////////////////////////
// ADDING REDIRECTS
//
// In order to permanently redirect a URL to a new address, add an new entry in the `redirects`
// list:
// ```json
// {"type": 301, "source": "/old/url", "destination": "/new/url"}
// ```
//
// This will cause the server to redirect any clients that visit `/old/url` to `/new/url`. At
// the same time, the ServiceWorker will be automatically configured to let requests for
// `/old/url` pass through to the server (instead of serving them locally with the cached
// `index.html`).
//
// In addition, you should also add a corresponding testcase in `URLS_TO_REDIRECT.txt` (inside
// `tests/deployment/shared/`) to ensure that the new redirect behaves as expected.
//
// NOTE 1:
// On clients that still have an old ServiceWorker version installed the next time they visit a
// recently redirected URL, the ServiceWorker will not yet know to let the request pass through
// to the server (and get redirected). In such cases, those clients will get a "Page not found"
// error, as the app tries to load `generated/docs/old/url.json` (which no longer exists). Such
// errors would typically go away after reloading the page (since the ServiceWorker will have
// updated to the latest version in the background).
//
// This temporary error is usually not a concern, because it only affects clients with an old
// ServiceWorker version at the time of the first visit and it goes away on reload. If, however,
// it is important to avoid this error for a specific redirect, you can add an additional
// redirect entry for the requested JSON file:
// ```json
// {"type": 302, "source": "/generated/docs/old/url.json", "destination": "/generated/docs/new/url.json"}
// ```
//
// In order to avoid bloating the redirects config, it is recommended to remove this additional
// entry after some time has passed, at which point the large majority of existing clients will
// have switched to an updated ServiceWorker version.
//
// NOTE 2:
// If you decide to add the additional entry described above, keep in mind that the JSON file
// URL is derived from the page URL by prepending `/generated/docs`, replacing any upper-case
// letters with the corresponding lower-case ones followed by an underscore (`_`) and appending
// `.json`.
//
// For example, the page URL `/api/forms/FormBuilder` would result in requesting the JSON file
// `/generated/docs/api/forms/f_ormb_uilder.json`.
//////////////////////////////////////////////////////////////////////////////////////////////
"redirects": [
// A random bad indexed page that used `api/api`
{"type": 301, "source": "/api/api/:rest*", "destination": "/api/:rest*"},
// Guide renames/removals
{"type": 301, "source": "/docs/*/latest/cli-quickstart.html", "destination": "/start"},
{"type": 301, "source": "/docs/*/latest/glossary.html", "destination": "/guide/glossary"},
{"type": 301, "source": "/docs/*/latest/quickstart.html", "destination": "/start"},
{"type": 301, "source": "/docs/*/latest/guide/server-communication.html", "destination": "/guide/understanding-communicating-with-http"},
{"type": 301, "source": "/docs/*/latest/guide/style-guide.html", "destination": "/guide/styleguide"},
{"type": 301, "source": "/guide/bazel", "destination": "https://github.com/angular/angular/blob/main/packages/bazel/docs/BAZEL_SCHEMATICS.md"},
{"type": 301, "source": "/guide", "destination": "/docs"},
{"type": 301, "source": "/guide/cli-quickstart", "destination": "/start"},
{"type": 301, "source": "/guide/i18n", "destination": "/guide/i18n-overview"},
{"type": 301, "source": "/guide/service-worker-getstart", "destination": "/guide/service-worker-getting-started"},
{"type": 301, "source": "/guide/service-worker-comm", "destination": "/guide/service-worker-communications"},
{"type": 301, "source": "/guide/service-worker-configref", "destination": "/guide/service-worker-config"},
{"type": 301, "source": "/guide/webpack", "destination": "https://v5.angular.io/guide/webpack"},
{"type": 301, "source": "/guide/http", "destination": "/guide/understanding-communicating-with-http"},
{"type": 301, "source": "/guide/setup", "destination": "/guide/setup-local"},
{"type": 301, "source": "/guide/setup-systemjs-anatomy", "destination": "/guide/file-structure"},
{"type": 301, "source": "/guide/change-log", "destination": "https://github.com/angular/angular/blob/main/CHANGELOG.md"},
{"type": 301, "source": "/guide/quickstart", "destination": "/start"},
{"type": 301, "source": "/getting-started", "destination": "/start"},
{"type": 301, "source": "/getting-started/:rest*", "destination": "/start/:rest*"},
{"type": 301, "source": "/guide/displaying-data", "destination": "/start#template-syntax"},
{"type": 301, "source": "/guide/ivy", "destination": "https://v12.angular.io/guide/ivy"},
{"type": 301, "source": "/guide/updating-to-version-10", "destination": "https://v10.angular.io/guide/updating-to-version-10"},
{"type": 301, "source": "/guide/updating-to-version-11", "destination": "https://v11.angular.io/guide/updating-to-version-11"},
{"type": 301, "source": "/guide/updating-to-version-12", "destination": "https://v12.angular.io/guide/updating-to-version-12"},
{"type": 301, "source": "/guide/updating-to-version-13", "destination": "https://v13.angular.io/guide/update-to-latest-version"},
{"type": 301, "source": "/guide/set-document-title", "destination": "/guide/router#setting-the-page-title"},
// Update Angular guide refactor to name individual update topics.
// This redirects the generic link to the current version's topic.
// The destination value must be updated with each new major version.
// When changing this value, be sure to update or add the corresponding
// test in angular/aio/tests/deployment/shared/URLS_TO_REDIRECT.txt
{"type": 301, "source": "/guide/update-to-latest-version", "destination": "/guide/update-to-version-17"},
// Renaming of Getting Started topics
{"type": 301, "source": "/start/data", "destination": "/start/start-data"},
{"type": 301, "source": "/start/deployment", "destination": "/start/start-deployment"},
{"type": 301, "source": "/start/forms", "destination": "/start/start-forms"},
{"type": 301, "source": "/start/routing", "destination": "/start/start-routing"},
// Tutorials moved to subdirectories
{"type": 301, "source": "/tutorial/index", "destination": "/tutorial/tour-of-heroes/index"},
{"type": 301, "source": "/tutorial/toh-pt0", "destination": "/tutorial/tour-of-heroes/toh-pt0"},
{"type": 301, "source": "/tutorial/toh-pt1", "destination": "/tutorial/tour-of-heroes/toh-pt1"},
{"type": 301, "source": "/tutorial/toh-pt2", "destination": "/tutorial/tour-of-heroes/toh-pt2"},
{"type": 301, "source": "/tutorial/toh-pt3", "destination": "/tutorial/tour-of-heroes/toh-pt3"},
{"type": 301, "source": "/tutorial/toh-pt4", "destination": "/tutorial/tour-of-heroes/toh-pt4"},
{"type": 301, "source": "/tutorial/toh-pt5", "destination": "/tutorial/tour-of-heroes/toh-pt5"},
{"type": 301, "source": "/tutorial/toh-pt6", "destination": "/tutorial/tour-of-heroes/toh-pt6"},
// some top level guide pages on old site were moved below the guide folder
{"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"},
{"type": 301, "source": "/docs/styleguide", "destination": "/guide/styleguide"},
// news is now blog
{"type": 301, "source": "/news*", "destination": "https://blog.angular.io/"},
// cookbook guides were moved (and sometime renamed or removed)
{"type": 301, "source": "/docs/*/latest/cookbook", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/cookbook/", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/cookbook/index.html", "destination": "/docs"},
{"type": 301, "source": "/**/cookbook/ts-to-js*", "destination": "https://v2.angular.io/docs/ts/latest/cookbook/ts-to-js.html"},
{"type": 301, "source": "/docs/*/latest/cookbook/a1-a2-quick-reference.html", "destination": "/guide/ajs-quick-reference"},
{"type": 301, "source": "/docs/*/latest/cookbook/component-communication.html", "destination": "/guide/component-interaction"},
{"type": 301, "source": "/docs/*/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"},
{"type": 301, "source": "/docs/*/latest/cookbook/:cookbook.html", "destination": "/guide/:cookbook"},
// Forms related code was moved from the `common` to `forms` package (and NgFor was renamed to NgForOf)
{"type": 301, "source": "/**/NgFor-*", "destination": "/api/common/NgForOf"},
{"type": 301, "source": "/**/api/common/index/MaxLengthValidator-*", "destination": "/api/forms/MaxLengthValidator"},
{"type": 301, "source": "/**/api/common/ControlGroup*", "destination": "/api/forms/FormGroup"},
{"type": 301, "source": "/**/api/common/Control*", "destination": "/api/forms/FormControl"},
{"type": 301, "source": "/**/api/common/SelectControlValueAccessor-*", "destination": "/api/forms/SelectControlValueAccessor"},
{"type": 301, "source": "/**/api/common/NgModel", "destination": "/api/forms/NgModel"},
// `@angular/http` package was removed, and new `HttpClient` APIs are available under `@angular/common/http` package
{"type": 301, "source": "/api/http", "destination": "/guide/deprecations#http"},
{"type": 301, "source": "/api/http/**", "destination": "/guide/deprecations#http"},
// Animations moves, renames and removals
{"type": 301, "source": "/api/animate/:rest*", "destination": "/api/animations/:rest*"},
// AnimationStateDeclarationMetadata was removed
{"type": 301, "source": "/**/AnimationStateDeclarationMetadata*", "destination": "/api/animations"},
// `AnimationDriver` was moved to the `animations/browser` package
{"type": 301, "source": "/api/platform-browser/AnimationDriver", "destination": "/api/animations/browser/AnimationDriver"},
// The `testing` package was renamed to `core/testing`
{"type": 301, "source": "/api/testing/:api-*", "destination": "/api/core/testing/:api"},
// CORE_DIRECTIVES & PLATFORM_PIPES were removed and are now in the CommonModule
{"type": 301, "source": "/**/CORE_DIRECTIVES*", "destination": "/api/common/CommonModule"},
{"type": 301, "source": "/**/PLATFORM_PIPES*", "destination": "/api/common/CommonModule"},
// DirectiveMetadata is now covered by the Directive decorator
{"type": 301, "source": "/**/DirectiveMetadata-*", "destination": "/api/core/Directive"},
// OptionalMetadata is now covered by the Optional decorator
{"type": 301, "source": "/**/OptionalMetadata-*", "destination": "/api/core/Optional"},
// HTTP_PROVIDERS was removed and is now provided in HttpModule
{"type": 301, "source": "/**/HTTP_PROVIDERS*", "destination": "/api/http/HttpModule"},
// URLs that use the old scheme of adding the type to the end (e.g. `SomeClass-class`)
// (Exclude disambiguated URLs that might be suffixed with `-\d+` (e.g. `SomeClass-1`))
{"type": 301, "source": "/api/:package/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/:package/:api"},
{"type": 301, "source": "/api/:package/testing/index/:api", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/api/:package/testing/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/api/upgrade/:package/index/:api", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/api/upgrade/:package/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/upgrade/:package/:api"},
// URLs that use the old scheme before we moved the docs to the angular/angular repo
{"type": 301, "source": "/docs/*/latest", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/api/", "destination": "/api"},
{"type": 301, "source": "/docs/*/latest/api/testing/:api-*", "destination": "/api/core/testing/:api"},
{"type": 301, "source": "/docs/*/latest/api/:package/:api-*", "destination": "/api/:package/:api"},
{"type": 301, "source": "/docs/*/latest/api/:package/index/:api-*", "destination": "/api/:package/:api"},
{"type": 301, "source": "/docs/*/latest/api/:package/testing", "destination": "/api/:package/testing"},
{"type": 301, "source": "/docs/*/latest/api/:package/testing/index/:api-*", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/docs/*/latest/api/platform-browser/animations/index/:api-*", "destination": "/api/platform-browser/animations/:api"},
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/:api-*", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/index/:api-*", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/docs/*/latest/glossary", "destination": "/guide/glossary"},
{"type": 301, "source": "/docs/*/latest/guide/", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/guide/lifecycle-hooks", "destination": "/guide/lifecycle-hooks"},
{"type": 301, "source": "/docs/*/latest/:rest*", "destination": "/:rest*"},
{"type": 301, "source": "/docs/latest/:rest*", "destination": "/:rest*"},
{"type": 301, "source": "/docs/styleguide*", "destination": "/guide/styleguide"},
{"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"},
{"type": 301, "source": "/guide/ngmodule", "destination": "/guide/ngmodules"},
{"type": 301, "source": "/guide/learning-angular*", "destination": "/start"},
{"type": 301, "source": "/testing", "destination": "/guide/testing"},
{"type": 301, "source": "/testing/**", "destination": "/guide/testing"},
// Work around Firebase hosting bug with `/index.html?<query>` leading to infinite redirects.
// See https://github.com/angular/angular/issues/42518#issuecomment-858545483 for details.
{"type": 301, "source": "/index.html:query*", "destination": "/:query*"},
// Strip off the `.html` extension, because Firebase will not do this automatically any more
// (unless the new URL points to an existing file, which is not necessarily the case here).
{"type": 301, "source": "/:topLevelFile.html", "destination": "/:topLevelFile"},
{"type": 301, "source": "/:somePath*/:file.html", "destination": "/:somePath*/:file"},
// The below paths are referenced in users projects generated by the CLI
{"type": 301, "source": "/config/tsconfig", "destination": "/guide/typescript-configuration"},
{"type": 301, "source": "/config/solution-tsconfig", "destination": "https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig"},
{"type": 301, "source": "/strict", "destination": "/guide/strict-mode"},
// Use discoverable link for Angular DevTools and redirect to the guide page
{"type": 301, "source": "/devtools", "destination": "/guide/devtools"},
// Use discoverable link for Angular Package Format and redirect to the guide page
{"type": 301, "source": "/apf", "destination": "/guide/angular-package-format"},
// Temporarily serve the guide at `/devtools` as well as `/guide/devtools` until users have
// their SW updated to a version that knows about the `/devtools` redirect.
{"type": 302, "source": "/generated/docs/devtools.json", "destination": "/generated/docs/guide/devtools.json"},
// Analytics pages that have been deleted in v15
{"type": 301, "source": "/cli/usage-analytics-gathering", "destination": "/cli/analytics"},
{"type": 301, "source": "/analytics", "destination": "/cli/analytics"},
// Deprecated error messages
{"type": 301, "source": "/errors/NG6999", "destination": "https://v13.angular.io/errors/NG6999"},
// Universal has been renamed to ssr
{"type": 301, "source": "/guide/universal", "destination": "/guide/ssr"}
],
"rewrites": [
{
"source": "**/!(*.*)",
"destination": "/index.html"
}
],
"headers": [
{
// All URLs
"source": "**",
"headers": [
// Report Trusted Types violations
{
"key": "Content-Security-Policy-Report-Only",
// The following Trusted Types policies are allowed:
// - angular: Angular's main internal policy. Defined in the `@angular/core` package.
// - angular#bundler: Used by Angular's bundler. Defined in the `webpack` package, enabled by `@angular-devkit/build-angular`.
// - angular#unsafe-bypass: For bypassSecurityTrust* usage. Defined in the `@angular/core` package.
// - aio#analytics: For the Legacy Google Analytics snippet. Defined in `index.html`.
// - google#safe: Used by the safevalues library. Defined in the `safevalues` package.
// - goog#html: Used by the Global Site Tag (`gtag.js`).
// csp.withgoogle.com is Google's CSP report collecting
// infrastructure.
// NOTE: Keep in sync with the header in `angular.json`.
"value": "require-trusted-types-for 'script'; trusted-types angular angular#bundler angular#unsafe-bypass aio#analytics google#safe goog#html; report-uri https://csp.withgoogle.com/csp/angular.io"
},
{
"key": "X-Frame-Options",
"value": "DENY"
}
]
},
{
// All paths (URLs without a file extension).
"source": "**/!(*.*)",
"headers": [
{"key": "Cache-Control", "value": "no-cache"},
{"key": "Link", "value": "</generated/navigation.json>;rel=preload;as=fetch,</generated/docs/index.json>;rel=preload;as=fetch"}
]
},
{
// Images, fonts, (non-hashed) CSS/JS files.
"source": "**/*.@(gif|jpg|jpeg|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|woff2)",
"headers": [
{"key": "Cache-Control", "value": "max-age=86400"} // 1 day
]
},
{
// Hashed CSS/JS files...
"source": "**/*.+([0-9a-f]).@(css|js)",
"headers": [
{"key": "Cache-Control", "value": "max-age=2592000"} // 30 days
]
}
]
},
"database": {
"rules": "database.rules.json"
}
}

View file

@ -1,74 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const {getAdjustedChromeBinPathForWindows} = require('./tools/windows-chromium-path');
process.env.CHROME_BIN = getAdjustedChromeBinPathForWindows();
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma'),
{'reporter:jasmine-seed': ['type', JasmineSeedReporter]},
],
proxies: {
'/dummy/image': 'src/assets/images/logos/angular/angular.png',
},
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
random: true,
seed: '',
},
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/site'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
],
},
reporters: ['progress', 'kjhtml', 'jasmine-seed'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
// See /integration/README.md#browser-tests for more info on these args
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
},
},
browsers: ['ChromeHeadlessNoSandbox'],
browserNoActivityTimeout: 60000,
singleRun: false,
restartOnFileChange: true,
});
};
// Helpers
function JasmineSeedReporter(baseReporterDecorator) {
baseReporterDecorator(this);
this.onBrowserComplete = (browser, result) => {
const seed = result.order && result.order.random && result.order.seed;
if (seed) this.write(`${browser}: Randomized with seed ${seed}.\n`);
};
this.onRunComplete = () => undefined;
}

View file

@ -43,7 +43,7 @@ def link_local_packages(all_aio_deps):
# Special case deps that must be testonly
testonly_deps = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
]
# Stamp a corresponding target for each AIO dep that filters out transitive

View file

@ -1,150 +0,0 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app-shell",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/index.html",
"/pwa-manifest.json",
"/assets/images/favicons/favicon.ico",
"/assets/js/*.js",
"/*.css",
"/*.js"
],
"urls": [
"https://fonts.googleapis.com/**",
"https://fonts.gstatic.com/s/**"
]
}
},
{
"name": "assets-eager",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/**",
"/generated/images/marketing/**",
"!/assets/images/favicons/**",
"!/**/_unused/**"
]
}
},
{
"name": "assets-lazy",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/favicons/**",
"!/**/_unused/**"
]
}
},
{
"name": "docs-index",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/generated/*.json",
"/generated/docs/*.json",
"/generated/docs/api/api-list.json",
"/generated/docs/app/search-data.json"
]
}
},
{
"name": "docs-lazy",
"installMode": "lazy",
"updateMode": "lazy",
"resources": {
"files": [
"/generated/docs/**/*.json",
"/generated/images/**",
"!/**/_unused/**"
]
}
}
],
"navigationUrls": [
"/**",
"!/**/*.*",
"!/**/*__*",
"!/**/*__*/**",
"!/**/stackblitz/{0,1}",
"!/**/AnimationStateDeclarationMetadata*",
"!/**/CORE_DIRECTIVES*",
"!/**/DirectiveMetadata-*",
"!/**/HTTP_PROVIDERS*",
"!/**/NgFor-*",
"!/**/OptionalMetadata-*",
"!/**/PLATFORM_PIPES*",
"!/**/api/common/Control*",
"!/**/api/common/ControlGroup*",
"!/**/api/common/NgModel/{0,1}",
"!/**/api/common/SelectControlValueAccessor-*",
"!/**/api/common/index/MaxLengthValidator-*",
"!/**/cookbook/ts-to-js*",
"!/analytics/{0,1}",
"!/apf/{0,1}",
"!/api/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
"!/api/*/testing/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
"!/api/*/testing/index/*",
"!/api/animate/**",
"!/api/api/**",
"!/api/http/**",
"!/api/http/{0,1}",
"!/api/platform-browser/AnimationDriver/{0,1}",
"!/api/testing/*-*",
"!/api/upgrade/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
"!/api/upgrade/*/index/*",
"!/cli/usage-analytics-gathering/{0,1}",
"!/config/solution-tsconfig/{0,1}",
"!/config/tsconfig/{0,1}",
"!/devtools/{0,1}",
"!/docs/*/latest/**",
"!/docs/*/latest/{0,1}",
"!/docs/latest/**",
"!/docs/styleguide*",
"!/docs/styleguide/{0,1}",
"!/getting-started/**",
"!/getting-started/{0,1}",
"!/guide/bazel/{0,1}",
"!/guide/change-log/{0,1}",
"!/guide/cli-quickstart/{0,1}",
"!/guide/displaying-data/{0,1}",
"!/guide/i18n/{0,1}",
"!/guide/ivy/{0,1}",
"!/guide/learning-angular*",
"!/guide/metadata/{0,1}",
"!/guide/ngmodule/{0,1}",
"!/guide/quickstart/{0,1}",
"!/guide/service-worker-comm/{0,1}",
"!/guide/service-worker-configref/{0,1}",
"!/guide/service-worker-getstart/{0,1}",
"!/guide/set-document-title/{0,1}",
"!/guide/setup-systemjs-anatomy/{0,1}",
"!/guide/setup/{0,1}",
"!/guide/update-to-latest-version/{0,1}",
"!/guide/updating-to-version-10/{0,1}",
"!/guide/updating-to-version-11/{0,1}",
"!/guide/updating-to-version-12/{0,1}",
"!/guide/updating-to-version-13/{0,1}",
"!/guide/webpack/{0,1}",
"!/guide/{0,1}",
"!/news*",
"!/start/data/{0,1}",
"!/start/deployment/{0,1}",
"!/start/forms/{0,1}",
"!/start/routing/{0,1}",
"!/strict/{0,1}",
"!/styleguide/{0,1}",
"!/testing/**",
"!/testing/{0,1}"
]
}

View file

@ -1,81 +0,0 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app-shell",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/index.html",
"/pwa-manifest.json",
"/assets/images/favicons/favicon.ico",
"/assets/js/*.js",
"/*.css",
"/*.js"
],
"urls": [
"https://fonts.googleapis.com/**",
"https://fonts.gstatic.com/s/**"
]
}
},
{
"name": "assets-eager",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/**",
"/generated/images/marketing/**",
"!/assets/images/favicons/**",
"!/**/_unused/**"
]
}
},
{
"name": "assets-lazy",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/favicons/**",
"!/**/_unused/**"
]
}
},
{
"name": "docs-index",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/generated/*.json",
"/generated/docs/*.json",
"/generated/docs/api/api-list.json",
"/generated/docs/app/search-data.json"
]
}
},
{
"name": "docs-lazy",
"installMode": "lazy",
"updateMode": "lazy",
"resources": {
"files": [
"/generated/docs/**/*.json",
"/generated/images/**",
"!/**/_unused/**"
]
}
}
],
"navigationUrls": [
"/**",
"!/**/*.*",
"!/**/*__*",
"!/**/*__*/**",
"!/**/stackblitz/{0,1}"
]
}

View file

@ -1,171 +0,0 @@
{
"name": "angular.io",
"version": "0.0.0",
"main": "index.js",
"repository": "git@github.com:angular/angular.git",
"author": "Angular",
"license": "MIT",
"scripts": {
"postinstall": "node tools/cli-patches/patch.js && patch-package --patch-dir ../tools/npm-patches/",
"prebuild": "yarn setup",
"build": "bazel build //aio:build",
"build-prod": "yarn build --config=release",
"build-local": "yarn build --config=aio_local_deps",
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint && yarn security-lint",
"test": "bazel test //aio:test --test_output=streamed --test_tag_filters=-broken",
"test-and-watch": "ibazel test //aio:test-and-watch --test_output=streamed --test_tag_filters=-broken",
"test-local": "yarn test --config=aio_local_deps",
"test:ci": "bazel test //aio/... --test_tag_filters=-broken",
"test-local:ci": "yarn test:ci --config=aio_local_deps",
"e2e": "bazel test //aio:e2e",
"e2e-local": "yarn e2e --config=aio_local_deps",
"setup": "yarn --cwd .. install && yarn install --frozen-lockfile && yarn ~~check-env",
"set-opensearch-url": "node --eval \"const sh = require('shelljs'); sh.set('-e'); sh.sed('-i', /PLACEHOLDER_URL/g, process.argv[1], '../dist/bin/aio/build/assets/opensearch.xml');\"",
"smoke-tests": "protractor tests/deployment/e2e/protractor.conf.js --suite smoke --baseUrl",
"test-a11y-score": "bazel run //aio/scripts:test-aio-a11y",
"test-a11y-score-localhost": "bazel test //aio:test-a11y-score-localhost",
"test-pwa-score": "sh -c 'bazel run //aio/scripts:audit-web-app ${0} all:0,pwa:${1}'",
"test-pwa-score-localhost": "bazel test //aio:test-pwa-score-localhost",
"test-production-url": "bazel test //aio/tests/deployment/e2e",
"example-e2e": "node --experimental-import-meta-resolve tools/examples/run-filtered-example-e2es.mjs",
"example-list-overrides": "bazel run //aio/tools/examples:example-boilerplate list-overrides",
"example-lint": "eslint content/examples",
"example-playground": "node ./tools/examples/create-example-playground-wrapper.mjs",
"deploy-production": "node ./scripts/deploy-to-firebase/index.mjs",
"check-env": "yarn ~~check-env",
"payload-size": "scripts/payload.sh",
"predocs": "node scripts/contributors/validate-data && bazel build -- //aio:stackblitz //aio:example-zips",
"docs-watch": "bazel run //aio:docs-watch --config=release",
"docs": "bazel build //aio:dgeni --config=release",
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
"docs-test": "bazel test //aio/tools/transforms/...",
"redirects-test": "bazel test //aio/tests/deployment/unit:test",
"firebase-utils-test": "bazel test //aio/tools/firebase-test-utils:test",
"tools-lint": "eslint --ext=.mjs scripts/deploy-to-firebase && eslint tools/firebase-test-utils",
"tools-test": "bazel test -- //aio/tools/... //aio/scripts/deploy-to-firebase:test",
"start": "ibazel run //aio:serve --config=release",
"start-local": "yarn start --config=aio_local_deps",
"boilerplate:test": "bazel test //aio/tools/examples:example-boilerplate-test",
"create-example": "bazel run //aio/tools/examples:create-example",
"security-lint": "tsec -p tsconfig.app.json",
"~~audit-web-app": "node scripts/audit-web-app.mjs",
"~~check-env": "node scripts/check-environment",
"~~light-server": "light-server --bind=localhost --historyindex=/index.html --no-reload",
"local-workspace-status": "node scripts/local-workspace-status.mjs"
},
"//engines-comment": "If applicable, also update /package.json and /aio/tools/examples/shared/package.json",
"engines": {
"node": ">=16.14.0",
"yarn": ">=1.22.4 <2",
"npm": "Please use yarn instead of NPM to install dependencies"
},
"private": true,
"dependencies": {
"@angular/animations": "17.3.0-rc.0",
"@angular/cdk": "17.3.0-rc.0",
"@angular/common": "17.3.0-rc.0",
"@angular/compiler": "17.3.0-rc.0",
"@angular/core": "17.3.0-rc.0",
"@angular/elements": "17.3.0-rc.0",
"@angular/forms": "17.3.0-rc.0",
"@angular/material": "17.3.0-rc.0",
"@angular/platform-browser": "17.3.0-rc.0",
"@angular/platform-browser-dynamic": "17.3.0-rc.0",
"@angular/router": "17.3.0-rc.0",
"@angular/service-worker": "17.3.0-rc.0",
"rxjs": "~7.8.0",
"safevalues": "^0.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/architect-cli": "0.1702.0-rc.0",
"@angular-devkit/build-angular": "17.2.0-rc.0",
"@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/template-parser": "^17.0.0",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#7c4cf003cb4ac849986beaa243d7e85a893612f2",
"@angular/cli": "17.2.0-rc.0",
"@angular/compiler-cli": "17.2.0-rc.1",
"@bazel/bazelisk": "^1.7.5",
"@bazel/buildozer": "^6.0.0",
"@bazel/ibazel": "^0.16.2",
"@bazel/jasmine": "^5.4.1",
"@bazel/runfiles": "5.8.1",
"@bazel/typescript": "5.8.1",
"@types/jasmine": "~5.1.0",
"@types/lunr": "^2.3.3",
"@types/node": "^12.7.9",
"@types/trusted-types": "^2.0.2",
"@typescript-eslint/eslint-plugin": "7.4.0",
"@typescript-eslint/parser": "7.4.0",
"archiver": "^7.0.0",
"assert": "^2.0.0",
"canonical-path": "1.0.0",
"chalk": "^4.1.0",
"cjson": "^0.5.0",
"cross-spawn": "^7.0.3",
"css-selector-parser": "^3.0.0",
"dgeni": "^0.4.14",
"dgeni-packages": "^0.30.0",
"entities": "^4.0.0",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jasmine": "4.1.3",
"eslint-plugin-jsdoc": "48.2.1",
"eslint-plugin-prefer-arrow": "1.2.3",
"find-free-port": "^2.0.0",
"firebase-tools": "^13.0.0",
"fs-extra": "^11.0.0",
"get-port": "^7.0.0",
"globby": "^14.0.0",
"hast-util-has-property": "^1.0.4",
"hast-util-is-element": "^1.1.0",
"hast-util-to-string": "^1.0.4",
"html": "^1.0.0",
"ignore": "^5.1.8",
"image-size": "^1.0.0",
"jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"jasmine-spec-reporter": "~7.0.0",
"jsdom": "^24.0.0",
"json-schema-traverse": "^1.0.0",
"json5": "^2.2.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"light-server": "^2.9.1",
"lighthouse": "^11.0.0",
"lighthouse-logger": "^2.0.0",
"lodash": "^4.17.21",
"lunr": "^2.3.9",
"npm-run-all": "^4.1.5",
"patch-package": "^7.0.0",
"protractor": "~7.0.0",
"puppeteer-core": "22.6.1",
"rehype-slug": "^4.0.1",
"remark": "^12.0.0",
"remark-html": "^13.0.0",
"rimraf": "^5.0.0",
"semver": "^7.3.5",
"shelljs": "^0.8.5",
"source-map-support": "0.5.21",
"stemmer": "^2.0.0",
"tree-kill": "^1.1.0",
"ts-node": "^10.8.1",
"tsec": "^0.2.2",
"tslint": "6.1.3",
"typescript": "~5.0.2",
"uglify-js": "^3.13.3",
"unist-util-filter": "^2.0.3",
"unist-util-source": "^3.0.0",
"unist-util-visit": "^2.0.3",
"unist-util-visit-parents": "^3.1.1",
"watchr": "^3.0.1",
"xregexp": "^5.0.2",
"yargs": "^17.0.0"
}
}

View file

@ -1,5 +1,5 @@
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("@aio_npm//@angular/build-tooling/bazel:filter_outputs.bzl", "filter_first_output")
load("@npm//@angular/build-tooling/bazel:filter_outputs.bzl", "filter_first_output")
load("//tools:defaults.bzl", "nodejs_binary", "nodejs_test")
package(default_visibility = ["//visibility:public"])
@ -13,8 +13,8 @@ nodejs_binary(
data = [
"//aio:firebase.json",
"//aio:ngsw-config.template.json",
"@aio_npm//canonical-path",
"@aio_npm//json5",
"@npm//canonical-path",
"@npm//json5",
],
entry_point = "build-ngsw-config.js",
)
@ -26,7 +26,7 @@ js_library(
],
deps = [
"//aio/tools/transforms/authors-package:watchdocs",
"@aio_npm//@angular-devkit/architect-cli",
"@npm//@angular-devkit/architect-cli",
],
)
@ -40,17 +40,17 @@ nodejs_binary(
testonly = True,
data = [
"//aio/tools:windows-chromium-path",
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
"@aio_npm//lighthouse",
"@aio_npm//lighthouse-logger",
"@aio_npm//puppeteer-core",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//lighthouse",
"@npm//lighthouse-logger",
"@npm//puppeteer-core",
],
entry_point = "audit-web-app.mjs",
env = {
"CHROME_BIN": "$(CHROMIUM)",
},
toolchains = [
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
],
)
@ -85,7 +85,7 @@ nodejs_test(
data = [
":audit-web-app",
":audit-web-app-script",
"@aio_npm//shelljs",
"@npm//shelljs",
],
entry_point = "test-aio-a11y.mjs",
env = {

View file

@ -1,5 +1,5 @@
load("@aio_npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
DEPLOY_TO_FIREBASE_SOURCES = glob(
["**/*.mjs"],
@ -7,7 +7,7 @@ DEPLOY_TO_FIREBASE_SOURCES = glob(
)
DEPLOY_TO_FIREBASE_DEPS = [
"@aio_npm//shelljs",
"@npm//shelljs",
"//aio:build",
"//:package.json",
]

View file

@ -16,16 +16,16 @@ def local_server_test(name, entry_point, serve_target, data = [], args = [], **k
name = name,
testonly = True,
args = [
"$(rootpath @aio_npm//light-server/bin:light-server)",
"$(rootpath @npm//light-server/bin:light-server)",
"$(rootpath %s)" % serve_target,
"$(rootpath %s)" % entry_point,
] + args,
data = [
"//aio/scripts:run-with-local-server.mjs",
"@aio_npm//get-port",
"@aio_npm//shelljs",
"@aio_npm//tree-kill",
"@aio_npm//light-server/bin:light-server",
"@npm//get-port",
"@npm//shelljs",
"@npm//tree-kill",
"@npm//light-server/bin:light-server",
serve_target,
entry_point,
] + data,

View file

@ -1,11 +0,0 @@
{
"ban-reviewed-conversions": [
"src/app/custom-elements/code/code.component.ts",
"src/app/custom-elements/code/pretty-printer.service.ts",
"src/app/documents/document.service.ts",
"src/app/shared/security.ts"
],
"ban-worker-calls": [
"src/app/search/search.service.ts"
]
}

View file

@ -1,52 +0,0 @@
<!--
This content replaces the `<body>` content of `index.html` to generate our custom `404.html` page.
The content must visually and structurally resemble the resulting HTML of the main app for not
found pages (e.g. https://angular.io/not/exist.ing).
-->
<style>
.mat-toolbar-row{display:flex;box-sizing:border-box;padding:0 16px;width:100%;flex-direction:row;align-items:center;white-space:nowrap}
.mat-toolbar-row{height:64px}
@media (max-width:600px){.mat-toolbar-row{height:56px}}
.nav-link.home{margin-left:24px}
</style>
<aio-shell class="mode-stable page-file-not-found folder-file-not-found view- aio-notification-hide">
<mat-toolbar class="app-toolbar no-print mat-toolbar mat-primary">
<mat-toolbar-row class="mat-toolbar-row">
<a class="nav-link home" href="/">
<img alt="Home" height="40" src="assets/images/logos/angular/logo-nav@2x.png" title="Home" width="150">
</a>
</mat-toolbar-row>
</mat-toolbar>
<mat-sidenav-container class="sidenav-container mat-drawer-container mat-sidenav-container" role="main">
<mat-sidenav-content class="mat-drawer-content mat-sidenav-content">
<section class="sidenav-content" role="article">
<aio-doc-viewer>
<div class="content">
<div class="nf-container l-flex-wrap flex-center">
<img src="assets/images/support/angular-404.svg" width="300" height="300" alt="not found"/>
<div class="nf-response l-flex-wrap center">
<h1 class="no-toc" id="page-not-found">Resource Not Found</h1>
<p>We're sorry. The resource you are looking for cannot be found.</p>
</div>
</div>
</div>
</aio-doc-viewer>
</section>
</mat-sidenav-content>
</mat-sidenav-container>
<footer class="no-print">
<aio-footer>
<p>
Powered by Google ©2010-2022.
Code licensed under an <a href="license" title="License text">MIT-style License</a>.
Documentation licensed under <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>.
</p>
</aio-footer>
</footer>
</aio-shell>

View file

@ -1,87 +0,0 @@
<div id="top-of-page"></div>
<aio-cookies-popup></aio-cookies-popup>
<a class="skip-to-content-link" href="#main-content">Skip to main content</a>
<div *ngIf="isFetching" class="progress-bar-container">
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
</div>
<header>
<mat-toolbar #appToolbar color="primary" class="app-toolbar no-print" [class.transitioning]="isTransitioning">
<mat-toolbar-row class="notification-container">
<aio-notification notificationId="state-of-js-2023" expirationDate="2023-12-12" [dismissOnContentClick]="true" (dismissed)="notificationDismissed()">
<a href="https://survey.devographics.com/survey/state-of-js/2023?source=angular_homepage">
<mat-icon class="icon" svgIcon="insert_comment" aria-label="Announcement"></mat-icon>
<span class="message">Share your experience with Angular in <b>The State of JavaScript</b></span>
<span class="action-button">Go to survey</span>
</a>
</aio-notification>
</mat-toolbar-row>
<mat-toolbar-row>
<button mat-button class="hamburger" [class.no-animations]="disableAnimations" (click)="sidenav.toggle()" title="Docs menu">
<mat-icon svgIcon="menu"></mat-icon>
</button>
<a class="nav-link home" href="/" [ngSwitch]="showTopMenu">
<img *ngSwitchCase="true" src="assets/images/logos/angular/logo-nav@2x.png" width="150" height="40" title="Home" alt="Home">
<img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
</a>
<aio-top-menu *ngIf="showTopMenu" [nodes]="topMenuNodes" [currentNode]="currentNodes.TopBar"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event, true)"></aio-search-box>
<aio-theme-toggle #themeToggle></aio-theme-toggle>
<div #externalIcons class="toolbar-external-icons-container">
<a mat-icon-button href="https://twitter.com/angular" title="Twitter" aria-label="Angular on Twitter">
<mat-icon svgIcon="logos:twitter"></mat-icon>
</a>
<a mat-icon-button href="https://github.com/angular/angular" title="GitHub" aria-label="Angular on GitHub">
<mat-icon svgIcon="logos:github"></mat-icon>
</a>
<a mat-icon-button href="https://youtube.com/angular" title="YouTube" aria-label="Angular on YouTube">
<mat-icon svgIcon="logos:youtube"></mat-icon>
</a>
</div>
</mat-toolbar-row>
</mat-toolbar>
</header>
<aio-search-results *ngIf="showSearchResults"
#searchResultsView
[searchResults]="searchResults | async"
(resultSelected)="hideSearchResults()"
(closeButtonClick)="hideSearchResults()">
</aio-search-results>
<mat-sidenav-container class="sidenav-container" [class.no-animations]="disableAnimations" [class.has-floating-toc]="hasFloatingToc">
<mat-sidenav [class.collapsed]="!dockSideNav" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()">
<aio-nav-menu *ngIf="!showTopMenu" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes.TopBarNarrow" [isWide]="dockSideNav" navLabel="primary"></aio-nav-menu>
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes.SideNav" [isWide]="dockSideNav" navLabel="guides and docs"></aio-nav-menu>
<div class="doc-version">
<aio-nav-menu [nodes]="docVersions" [isWide]="true" [currentNode]="currentDocsVersionNode" navLabel="docs versions"></aio-nav-menu>
</div>
</mat-sidenav>
<section class="sidenav-content-container">
<main class="sidenav-content" [id]="pageId">
<div id="main-content" tabindex="-1"></div>
<aio-mode-banner *ngIf="versionInfo" [mode]="deployment.mode" [version]="versionInfo"></aio-mode-banner>
<aio-doc-viewer [class.no-animations]="disableAnimations" [doc]="currentDocument" (docReady)="onDocReady()" (docRemoved)="onDocRemoved()" (docInserted)="onDocInserted()" (docRendered)="onDocRendered()">
</aio-doc-viewer>
<aio-dt *ngIf="dtOn" [(doc)]="currentDocument"></aio-dt>
</main>
<div *ngIf="hasFloatingToc" class="toc-container no-print" [style.max-height.px]="tocMaxHeight" (wheel)="restrainScrolling($event)">
<aio-lazy-ce selector="aio-toc"></aio-lazy-ce>
</div>
</section>
<footer class="no-print">
<aio-footer [nodes]="footerNodes" [versionInfo]="versionInfo"></aio-footer>
</footer>
</mat-sidenav-container>

File diff suppressed because it is too large Load diff

View file

@ -1,480 +0,0 @@
import {
Component,
ElementRef,
HostBinding,
HostListener,
OnInit,
QueryList,
ViewChild,
ViewChildren,
} from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { DocumentContents, DocumentService } from 'app/documents/document.service';
import { NotificationComponent } from 'app/layout/notification/notification.component';
import { CurrentNodes, NavigationNode, NavigationService, VersionInfo } from 'app/navigation/navigation.service';
import { SearchResults } from 'app/search/interfaces';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchService } from 'app/search/search.service';
import { Deployment } from 'app/shared/deployment.service';
import { LocationService } from 'app/shared/location.service';
import { ScrollService } from 'app/shared/scroll.service';
import { TocService } from 'app/shared/toc.service';
import { SwUpdatesService } from 'app/sw-updates/sw-updates.service';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { first, map } from 'rxjs/operators';
const sideNavView = 'SideNav';
export const showTopMenuWidth = 1150;
export const dockSideNavWidth = 992;
export const showFloatingTocWidth = 800;
@Component({
selector: 'aio-shell',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
static reducedMotion = window.matchMedia('(prefers-reduced-motion)').matches;
// Disable all Angular animations if the user prefers reduced motion or for the initial render.
@HostBinding('@.disabled')
get disableAnimations(): boolean { return AppComponent.reducedMotion || this.isStarting; }
currentDocument: DocumentContents;
currentDocVersion: NavigationNode;
currentNodes: CurrentNodes = {};
currentPath: string;
docVersions: NavigationNode[];
dtOn = false;
footerNodes: NavigationNode[];
/**
* An HTML friendly identifier for the currently displayed page.
* This is computed from the `currentDocument.id` by replacing `/` with `-`
*/
pageId: string;
/**
* An HTML friendly identifier for the "folder" of the currently displayed page.
* This is computed by taking everything up to the first `/` in the `currentDocument.id`
*/
folderId: string;
/**
* These CSS classes are computed from the current state of the application
* (e.g. what document is being viewed) to allow for fine grain control over
* the styling of individual pages.
* You will get three classes:
*
* * `page-...`: computed from the current document id (e.g. events, guide-security, tutorial-toh-pt2)
* * `folder-...`: computed from the top level folder for an id (e.g. guide, tutorial, etc)
* * `view-...`: computed from the navigation view (e.g. SideNav, TopBar, etc)
*/
@HostBinding('class')
hostClasses = '';
private isStarting = true;
isTransitioning = true;
isFetching = false;
showTopMenu = false;
dockSideNav = false;
private isFetchingTimeout: any;
private isSideNavDoc = false;
sideNavNodes: NavigationNode[];
topMenuNodes: NavigationNode[];
topMenuNarrowNodes: NavigationNode[];
hasFloatingToc = false;
private showFloatingToc = new BehaviorSubject(false);
tocMaxHeight: string;
private tocMaxHeightOffset = 0;
currentDocsVersionNode?: NavigationNode;
versionInfo: VersionInfo | undefined;
get isOpened() { return this.dockSideNav && this.isSideNavDoc; }
get mode() { return this.isOpened ? 'side' : 'over'; }
// Search related properties
showSearchResults = false;
searchResults: Observable<SearchResults>;
@ViewChildren('searchBox, searchResultsView', { read: ElementRef })
searchElements: QueryList<ElementRef>;
@ViewChild(SearchBoxComponent, { static: true })
searchBox: SearchBoxComponent;
@ViewChild('searchResultsView', { read: ElementRef })
searchResultsView: ElementRef;
@ViewChild(MatSidenav, { static: true })
sidenav: MatSidenav;
@ViewChild(NotificationComponent, { static: true })
notification: NotificationComponent;
notificationAnimating = false;
@ViewChild('appToolbar', { read: ElementRef }) toolbar: ElementRef;
@ViewChildren('themeToggle, externalIcons', { read: ElementRef }) toolbarIcons: QueryList<ElementRef>;
constructor(
public deployment: Deployment,
private documentService: DocumentService,
private hostElement: ElementRef,
private locationService: LocationService,
private navigationService: NavigationService,
private scrollService: ScrollService,
private searchService: SearchService,
private swUpdatesService: SwUpdatesService,
private tocService: TocService
) { }
ngOnInit() {
// Do not initialize the search on browsers that lack web worker support
if ('Worker' in window) {
// Delay initialization by up to 2 seconds
this.searchService.initWorker(2000);
}
this.onResize(window.innerWidth);
/* No need to unsubscribe because this root component never dies */
this.documentService.currentDocument.subscribe(doc => this.currentDocument = doc);
this.locationService.currentPath.subscribe(path => {
if (path === this.currentPath) {
// scroll only if on same page (most likely a change to the hash)
this.scrollService.scroll();
} else {
// don't scroll; leave that to `onDocRendered`
this.currentPath = path;
// Start progress bar if doc not rendered within brief time
clearTimeout(this.isFetchingTimeout);
this.isFetchingTimeout = setTimeout(() => this.isFetching = true, 200);
}
});
this.navigationService.currentNodes.subscribe(currentNodes => {
this.currentNodes = currentNodes;
// Redirect to docs if we are in archive mode and are not hitting a docs page
// (i.e. we have arrived at a marketing page)
if (this.deployment.mode === 'archive' && !currentNodes[sideNavView]) {
this.locationService.replace('docs');
}
});
// Compute the version picker list from the current version and the versions in the navigation map
combineLatest([
this.navigationService.versionInfo,
this.navigationService.navigationViews.pipe(map(views => views.docVersions)),
this.locationService.currentUrl,
]).subscribe(([versionInfo, versions, currentUrl]) => {
// TODO(pbd): consider whether we can lookup the stable and next versions from the internet
const computedVersions: NavigationNode[] = [
{ title: 'next', url: 'https://next.angular.io/' },
{ title: 'rc', url: 'https://rc.angular.io/' },
{ title: 'stable', url: 'https://angular.io/' },
];
if (this.deployment.mode === 'archive') {
computedVersions.push({ title: `v${versionInfo.major}` });
}
const allDocsVersionNodes = [...computedVersions, ...versions].map(version => ({
...version,
// Update the urls so that they point to the same page the user is currently at
url: `${version.url}${(version.url?.endsWith('/') ? '' : '/' )}${currentUrl}`,
}));
// Find the current version - either title matches the current deployment mode
// or its title matches the major version of the current version info
this.currentDocsVersionNode = allDocsVersionNodes.find(
version => version.title === this.deployment.mode || version.title === `v${versionInfo.major}`
);
this.docVersions = [
{
title: 'Docs Versions',
children : allDocsVersionNodes
}
];
});
this.navigationService.navigationViews.subscribe(views => {
this.footerNodes = views.Footer || [];
this.sideNavNodes = views.SideNav || [];
this.topMenuNodes = views.TopBar || [];
this.topMenuNarrowNodes = views.TopBarNarrow || this.topMenuNodes;
});
this.navigationService.versionInfo.subscribe(vi => this.versionInfo = vi);
const hasNonEmptyToc = this.tocService.tocList.pipe(map(tocList => tocList.length > 0));
combineLatest([hasNonEmptyToc, this.showFloatingToc])
.subscribe(([hasToc, showFloatingToc]) => this.hasFloatingToc = hasToc && showFloatingToc);
// Generally, we want to delay updating the shell (e.g. host classes, sidenav state) for the new
// document, until after the leaving document has been removed (to avoid having the styles for
// the new document applied prematurely).
// For the first document, though, (when we know there is no previous document), we want to
// ensure the styles are applied as soon as possible to avoid flicker.
combineLatest([
this.documentService.currentDocument, // ...needed to determine host classes
this.navigationService.currentNodes, // ...needed to determine `sidenav` state
]).pipe(first())
.subscribe(() => this.updateShell());
// Start listening for SW version update events.
this.swUpdatesService.enable();
}
onDocReady() {
// About to transition to new view.
this.isTransitioning = true;
// Stop fetching timeout (which, when render is fast, means progress bar never shown)
clearTimeout(this.isFetchingTimeout);
// If progress bar has been shown, keep it for at least 500ms (to avoid flashing).
setTimeout(() => this.isFetching = false, 500);
}
onDocRemoved() {
this.scrollService.removeStoredScrollInfo();
}
onDocInserted() {
// Update the shell (host classes, sidenav state) to match the new document.
// This may be called as a result of actions initiated by view updates.
// In order to avoid errors (e.g. `ExpressionChangedAfterItHasBeenChecked`), updating the view
// (e.g. sidenav, host classes) needs to happen asynchronously.
setTimeout(() => this.updateShell());
// Scroll the good position depending on the context
this.scrollService.scrollAfterRender(500);
}
onDocRendered() {
if (this.isStarting) {
// In order to ensure that the initial sidenav-content left margin
// adjustment happens without animation, we need to ensure that
// `isStarting` remains `true` until the margin change is triggered.
// (Apparently, this happens with a slight delay.)
setTimeout(() => this.isStarting = false, 100);
}
this.isTransitioning = false;
}
@HostListener('window:resize', ['$event.target.innerWidth'])
onResize(width: number) {
this.showTopMenu = width >= showTopMenuWidth;
this.dockSideNav = width >= dockSideNavWidth;
this.showFloatingToc.next(width > showFloatingTocWidth);
if (this.showTopMenu && !this.isSideNavDoc) {
// If this is a non-sidenav doc and the screen is wide enough so that we can display menu
// items in the top-bar, ensure the sidenav is closed.
// (This condition can only be met when the resize event changes the value of `showTopMenu`
// from `false` to `true` while on a non-sidenav doc.)
this.sidenav.toggle(false);
}
}
@HostListener('focusin', ['$event.target'])
onFocus(eventTarget: HTMLElement) {
// Implement a focus trap starting at the input search and ending after the search results
if (this.showSearchResults) {
const insideFocusLoop = [
...this.toolbarIcons,
...this.searchElements
].some(element => element.nativeElement.contains(eventTarget));
const insideToolbar = this.toolbar.nativeElement.contains(eventTarget);
if (!insideFocusLoop) {
if (!insideToolbar) {
// the user is focusing forward at the last search result element,
// loop it back to the search input
this.focusSearchBox();
} else {
// the user is focusing backward from the search input,
// loop it back to the results' close button
const closeBtn: HTMLButtonElement =
this.searchResultsView.nativeElement.querySelector('button.close-button');
closeBtn.focus();
}
}
}
}
@HostListener('click', ['$event.target', '$event.button', '$event.ctrlKey', '$event.metaKey', '$event.altKey'])
onClick(eventTarget: HTMLElement, button: number, ctrlKey: boolean, metaKey: boolean, altKey: boolean): boolean {
// Hide the search results if we clicked outside both the "search box" and the "search results"
if (
this.showSearchResults &&
!this.searchElements.some(element => element.nativeElement.contains(eventTarget))
) {
this.hideSearchResults();
}
// Show developer source view if the footer is clicked while holding the meta and alt keys
if (eventTarget.tagName === 'FOOTER' && metaKey && altKey) {
this.dtOn = !this.dtOn;
return false;
}
// Deal with anchor clicks; climb DOM tree until anchor found (or null)
let target: HTMLElement | null = eventTarget;
while (target && !(target instanceof HTMLAnchorElement)) {
target = target.parentElement;
}
if (target instanceof HTMLAnchorElement) {
return this.locationService.handleAnchorClick(target, button, ctrlKey, metaKey);
}
// Allow the click to pass through
return true;
}
setPageId(id: string) {
// Special case the home page
this.pageId = (id === 'index') ? 'home' : id.replace('/', '-');
}
setFolderId(id: string) {
// Special case the home page
this.folderId = (id === 'index') ? 'home' : id.split('/', 1)[0];
}
notificationDismissed() {
this.notificationAnimating = true;
// this should be kept in sync with the animation durations in:
// - aio/src/styles/2-modules/_notification.scss
// - aio/src/app/layout/notification/notification.component.ts
setTimeout(() => this.notificationAnimating = false, 250);
this.updateHostClasses();
}
updateHostClasses() {
const mode = `mode-${this.deployment.mode}`;
const sideNavOpen = `sidenav-${this.sidenav.opened ? 'open' : 'closed'}`;
const pageClass = `page-${this.pageId}`;
const folderClass = `folder-${this.folderId}`;
const viewClasses = Object.keys(this.currentNodes).map(view => `view-${view}`).join(' ');
const notificationClass = `aio-notification-${this.notification.showNotification}`;
const notificationAnimatingClass = this.notificationAnimating ? 'aio-notification-animating' : '';
this.hostClasses = [
mode,
sideNavOpen,
pageClass,
folderClass,
viewClasses,
notificationClass,
notificationAnimatingClass
].join(' ');
}
updateShell() {
// Update the SideNav state (if necessary).
this.updateSideNav();
// Update the host classes.
this.setPageId(this.currentDocument.id);
this.setFolderId(this.currentDocument.id);
this.updateHostClasses();
}
updateSideNav() {
// Preserve current sidenav open state by default.
let openSideNav = this.sidenav.opened;
const isSideNavDoc = !!this.currentNodes[sideNavView];
if (this.isSideNavDoc !== isSideNavDoc) {
// View type changed. Is it now a sidenav view (e.g, guide or tutorial)?
// Open if changed to a sidenav doc; close if changed to a marketing doc.
openSideNav = this.isSideNavDoc = isSideNavDoc;
}
// May be open or closed when wide; always closed when narrow.
this.sidenav.toggle(this.dockSideNav && openSideNav);
}
// Dynamically change height of table of contents container
@HostListener('window:scroll')
onScroll() {
if (!this.tocMaxHeightOffset) {
// Must wait until `mat-toolbar` is measurable.
const el = this.hostElement.nativeElement as Element;
const headerEl = el.querySelector('.app-toolbar');
const footerEl = el.querySelector('footer');
if (headerEl && footerEl) {
this.tocMaxHeightOffset =
headerEl.clientHeight +
footerEl.clientHeight +
24; // fudge margin
}
}
this.tocMaxHeight = (document.body.scrollHeight - window.scrollY - this.tocMaxHeightOffset).toFixed(2);
}
// Restrain scrolling inside an element, when the cursor is over it
restrainScrolling(evt: WheelEvent) {
const elem = evt.currentTarget as Element;
const scrollTop = elem.scrollTop;
if (evt.deltaY < 0) {
// Trying to scroll up: Prevent scrolling if already at the top.
if (scrollTop < 1) {
evt.preventDefault();
}
} else {
// Trying to scroll down: Prevent scrolling if already at the bottom.
const maxScrollTop = elem.scrollHeight - elem.clientHeight;
if (maxScrollTop - scrollTop < 1) {
evt.preventDefault();
}
}
}
// Search related methods and handlers
hideSearchResults() {
this.showSearchResults = false;
const oldSearch = this.locationService.search();
if (oldSearch.search !== undefined) {
this.locationService.setSearch('', { ...oldSearch, search: undefined });
}
}
focusSearchBox() {
if (this.searchBox) {
this.searchBox.focus();
}
}
doSearch(query: string, fromFocus = false) {
if (this.showSearchResults && fromFocus) {
// the results where already being displayed so there is no
// need to perform the search until the input actually changes
return;
}
this.searchResults = this.searchService.search(query);
this.showSearchResults = !!query;
}
@HostListener('document:keyup', ['$event.key', '$event.which'])
onKeyUp(key: string, keyCode: number) {
// forward slash "/"
if (key === '/' || keyCode === 191) {
this.focusSearchBox();
}
if (key === 'Escape' || keyCode === 27) {
// escape key
if (this.showSearchResults) {
this.hideSearchResults();
this.focusSearchBox();
}
}
}
}

View file

@ -1,194 +0,0 @@
import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatIconRegistry } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { svg } from 'app/shared/security';
import { trustedResourceUrl, unwrapResourceUrl } from 'safevalues';
import { AppComponent } from 'app/app.component';
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
import { Deployment } from 'app/shared/deployment.service';
import { CookiesPopupComponent } from 'app/layout/cookies-popup/cookies-popup.component';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component';
import { AnalyticsService } from 'app/shared/analytics.service';
import { Logger } from 'app/shared/logger.service';
import { LocationService } from 'app/shared/location.service';
import { STORAGE_PROVIDERS } from 'app/shared/storage.service';
import { NavigationService } from 'app/navigation/navigation.service';
import { DocumentService } from 'app/documents/document.service';
import { SearchService } from 'app/search/search.service';
import { TopMenuComponent } from 'app/layout/top-menu/top-menu.component';
import { FooterComponent } from 'app/layout/footer/footer.component';
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
import { ReportingErrorHandler } from 'app/shared/reporting-error-handler';
import { ScrollService } from 'app/shared/scroll.service';
import { ScrollSpyService } from 'app/shared/scroll-spy.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { NotificationComponent } from 'app/layout/notification/notification.component';
import { TocService } from 'app/shared/toc.service';
import { CurrentDateToken, currentDateProvider } from 'app/shared/current-date';
import { WindowToken, windowProvider } from 'app/shared/window';
import { CustomElementsModule } from 'app/custom-elements/custom-elements.module';
import { SharedModule } from 'app/shared/shared.module';
import { ThemeToggleComponent } from 'app/shared/theme-picker/theme-toggle.component';
import { environment } from '../environments/environment';
// These are the hardcoded inline svg sources to be used by the `<mat-icon>` component.
/* eslint-disable max-len */
export const svgIconProviders = [
{
provide: SVG_ICONS,
useValue: {
name: 'close',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
name: 'insert_comment',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
name: 'keyboard_arrow_right',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
name: 'menu',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
</svg>`,
},
multi: true,
},
// Namespace: logos
{
provide: SVG_ICONS,
useValue: {
namespace: 'logos',
name: 'github',
svgSource:
svg`<svg focusable="false" viewBox="0 0 51.8 50.4" xmlns="http://www.w3.org/2000/svg">
<path d="M25.9,0.2C11.8,0.2,0.3,11.7,0.3,25.8c0,11.3,7.3,20.9,17.5,24.3c1.3,0.2,1.7-0.6,1.7-1.2c0-0.6,0-2.6,0-4.8
c-7.1,1.5-8.6-3-8.6-3c-1.2-3-2.8-3.7-2.8-3.7c-2.3-1.6,0.2-1.6,0.2-1.6c2.6,0.2,3.9,2.6,3.9,2.6c2.3,3.9,6,2.8,7.5,2.1
c0.2-1.7,0.9-2.8,1.6-3.4c-5.7-0.6-11.7-2.8-11.7-12.7c0-2.8,1-5.1,2.6-6.9c-0.3-0.7-1.1-3.3,0.3-6.8c0,0,2.1-0.7,7,2.6
c2-0.6,4.2-0.9,6.4-0.9c2.2,0,4.4,0.3,6.4,0.9c4.9-3.3,7-2.6,7-2.6c1.4,3.5,0.5,6.1,0.3,6.8c1.6,1.8,2.6,4.1,2.6,6.9
c0,9.8-6,12-11.7,12.6c0.9,0.8,1.7,2.4,1.7,4.7c0,3.4,0,6.2,0,7c0,0.7,0.5,1.5,1.8,1.2c10.2-3.4,17.5-13,17.5-24.3
C51.5,11.7,40.1,0.2,25.9,0.2z" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
namespace: 'logos',
name: 'twitter',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>`,
},
multi: true,
},
{
provide: SVG_ICONS,
useValue: {
namespace: 'logos',
name: 'youtube',
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M21.58 7.19c-.23-.86-.91-1.54-1.77-1.77C18.25 5 12 5 12 5s-6.25 0-7.81.42c-.86.23-1.54.91-1.77 1.77
C2 8.75 2 12 2 12s0 3.25.42 4.81c.23.86.91 1.54 1.77 1.77C5.75 19 12 19 12 19s6.25 0 7.81-.42
c.86-.23 1.54-.91 1.77-1.77C22 15.25 22 12 22 12s0-3.25-.42-4.81zM10 15V9l5.2 3-5.2 3z" />
</svg>`,
},
multi: true,
},
];
/* eslint-enable max-len */
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule.withConfig({disableAnimations: AppComponent.reducedMotion}),
CustomElementsModule,
HttpClientModule,
MatButtonModule,
MatProgressBarModule,
MatSidenavModule,
MatToolbarModule,
SharedModule,
ServiceWorkerModule.register(
// Make sure service worker is loaded with a TrustedScriptURL
unwrapResourceUrl(trustedResourceUrl`/ngsw-worker.js`) as string,
{enabled: environment.production}),
],
declarations: [
AppComponent,
CookiesPopupComponent,
DocViewerComponent,
DtComponent,
FooterComponent,
ModeBannerComponent,
NavMenuComponent,
NavItemComponent,
SearchBoxComponent,
NotificationComponent,
TopMenuComponent,
ThemeToggleComponent,
],
providers: [
AnalyticsService,
Deployment,
DocumentService,
{ provide: ErrorHandler, useClass: ReportingErrorHandler },
Logger,
Location,
{ provide: LocationStrategy, useClass: PathLocationStrategy },
LocationService,
{ provide: MatIconRegistry, useClass: CustomIconRegistry },
NavigationService,
ScrollService,
ScrollSpyService,
SearchService,
STORAGE_PROVIDERS,
svgIconProviders,
TocService,
{ provide: CurrentDateToken, useFactory: currentDateProvider },
{ provide: WindowToken, useFactory: windowProvider },
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View file

@ -1,116 +0,0 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { AnnouncementBarComponent } from './announcement-bar.component';
const today = new Date();
const lastWeek = changeDays(today, -7);
const yesterday = changeDays(today, -1);
const tomorrow = changeDays(today, 1);
const nextWeek = changeDays(today, 7);
describe('AnnouncementBarComponent', () => {
let element: HTMLElement;
let fixture: ComponentFixture<AnnouncementBarComponent>;
let component: AnnouncementBarComponent;
let httpMock: HttpTestingController;
let mockLogger: MockLogger;
beforeEach(() => {
const injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [AnnouncementBarComponent],
providers: [{ provide: Logger, useClass: MockLogger }]
});
httpMock = injector.inject(HttpTestingController);
mockLogger = injector.inject(Logger) as any;
fixture = TestBed.createComponent(AnnouncementBarComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});
it('should have no announcement when first created', () => {
expect(component.announcement).toBeUndefined();
});
describe('ngOnInit', () => {
it('should make a single request to the server', () => {
component.ngOnInit();
httpMock.expectOne('generated/announcements.json');
expect().nothing(); // Prevent jasmine from complaining about no expectations.
});
it('should set the announcement to the first "live" one in the list loaded from `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.flush([
{ startDate: lastWeek, endDate: yesterday, message: 'Test Announcement 0' },
{ startDate: tomorrow, endDate: nextWeek, message: 'Test Announcement 1' },
{ startDate: yesterday, endDate: tomorrow, message: 'Test Announcement 2' },
{ startDate: yesterday, endDate: tomorrow, message: 'Test Announcement 3' }
]);
expect(component.announcement.message).toEqual('Test Announcement 2');
});
it('should set the announcement to `undefined` if there are no announcements in `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.flush([]);
expect(component.announcement).toBeUndefined();
});
it('should handle invalid data in `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.flush('some random response');
expect(component.announcement).toBeUndefined();
expect(mockLogger.output.error).toEqual([
[jasmine.any(Error)]
]);
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json contains invalid data:/);
});
it('should handle a failed request for `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.error(new ProgressEvent('404'));
expect(component.announcement).toBeUndefined();
expect(mockLogger.output.error).toEqual([
[jasmine.any(Error)]
]);
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json request failed:/);
});
});
describe('rendering', () => {
beforeEach(() => {
component.announcement = {
imageUrl: 'dummy/image',
linkUrl: 'link/to/website',
message: 'this is an <b>important</b> message',
endDate: '2018-03-01',
startDate: '2018-02-01'
};
fixture.detectChanges();
});
it('should display the message as HTML', () => {
expect(element.innerHTML).toContain('this is an <b>important</b> message');
});
it('should display an image', () => {
expect(element.querySelector('img')?.src).toContain('dummy/image');
});
it('should display a link', () => {
expect(element.querySelector('a')?.href).toContain('link/to/website');
});
});
});
function changeDays(initial: Date, days: number) {
return (new Date(initial.valueOf()).setDate(initial.getDate() + days));
}

View file

@ -1,85 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, map } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
const announcementsPath = CONTENT_URL_PREFIX + 'announcements.json';
export interface Announcement {
imageUrl: string;
message: string;
linkUrl: string;
startDate: string;
endDate: string;
}
/**
* Display the latest live announcement. This is used on the homepage.
*
* The data for the announcements is kept in `aio/content/marketing/announcements.json`.
*
* The format for that data file looks like:
*
* ```
* [
* {
* "startDate": "2018-02-01",
* "endDate": "2018-03-01",
* "message": "This is an <b>important</b> announcement",
* "imageUrl": "url/to/image",
* "linkUrl": "url/to/website"
* },
* ...
* ]
* ```
*
* Only one announcement will be shown at any time. This is determined as the first "live"
* announcement in the file, where "live" means that its start date is before today, and its
* end date is after today.
*
* **Security Note:**
* The `message` field can contain unsanitized HTML but this field should only updated by
* verified members of the Angular team.
*/
@Component({
selector: 'aio-announcement-bar',
template: `
<div class="homepage-container" *ngIf="announcement">
<div class="announcement-bar">
<img [src]="announcement.imageUrl" alt="">
<p [innerHTML]="announcement.message"></p>
<a class="button" [href]="announcement.linkUrl">Learn More</a>
</div>
</div>`
})
export class AnnouncementBarComponent implements OnInit {
announcement: Announcement;
constructor(private http: HttpClient, private logger: Logger) {}
ngOnInit() {
this.http.get<Announcement[]>(announcementsPath)
.pipe(
catchError(error => {
this.logger.error(new Error(`${announcementsPath} request failed: ${error.message}`));
return [];
}),
map(announcements => this.findCurrentAnnouncement(announcements)),
catchError(error => {
this.logger.error(new Error(`${announcementsPath} contains invalid data: ${error.message}`));
return [];
}),
)
.subscribe(announcement => this.announcement = announcement);
}
/**
* Get the first date in the list that is "live" now
*/
private findCurrentAnnouncement(announcements: Announcement[]) {
return announcements
.filter(announcement => new Date(announcement.startDate).valueOf() < Date.now())
.filter(announcement => new Date(announcement.endDate).valueOf() > Date.now())
[0];
}
}

View file

@ -1,14 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { SharedModule } from '../../shared/shared.module';
import { AnnouncementBarComponent } from './announcement-bar.component';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, SharedModule, HttpClientModule ],
declarations: [ AnnouncementBarComponent ]
})
export class AnnouncementBarModule implements WithCustomElementComponent {
customElementComponent: Type<any> = AnnouncementBarComponent;
}

View file

@ -1,37 +0,0 @@
<div class="l-flex-wrap api-filter">
<aio-select (change)="setType($event.option)"
[options]="types"
[selected]="type"
[showSymbol]="true"
label="Type:">
</aio-select>
<aio-select (change)="setStatus($event.option)"
[options]="statuses"
[selected]="status"
[disabled]="type.value === 'package'"
label="Status:">
</aio-select>
<div class="form-search">
<input #filter placeholder="Filter" (input)="setQuery($any($event.target).value)" aria-label="Filter Search">
<em class="material-icons">search</em>
</div>
</div>
<article class="api-list-container">
<div *ngFor="let section of filteredSections | async" >
<h2 *ngIf="section.items"><a [href]="section.path" [class.deprecated-api-item]="section.deprecated">{{section.title}}</a></h2>
<ul class="api-list" *ngIf="section.items && section.items.length">
<ng-container *ngFor="let item of section.items">
<li class="api-item">
<a [href]="item.path" [class.deprecated-api-item]="item.stability === 'deprecated'">
<span class="symbol {{item.docType}}"></span>
{{item.title}}
</a>
</li>
</ng-container>
</ul>
</div>
</article>

View file

@ -1,317 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BehaviorSubject } from 'rxjs';
import { ApiListComponent } from './api-list.component';
import { ApiItem, ApiSection, ApiService } from './api.service';
import { LocationService } from 'app/shared/location.service';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { ApiListModule } from './api-list.module';
describe('ApiListComponent', () => {
let component: ApiListComponent;
let fixture: ComponentFixture<ApiListComponent>;
let sections: ApiSection[];
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ ApiListModule ],
providers: [
{ provide: ApiService, useClass: TestApiService },
{ provide: Logger, useClass: MockLogger },
{ provide: LocationService, useClass: TestLocationService }
]
});
fixture = TestBed.createComponent(ApiListComponent);
component = fixture.componentInstance;
sections = getApiSections();
});
/**
* Expectation Utility: Assert that filteredSections has the expected result for this test.
*
* @param itemTest - return true if the item passes the match test
*
* Subscribes to `filteredSections` and performs expectation within subscription callback.
*/
function expectFilteredResult(label: string, itemTest: (item: ApiItem) => boolean) {
component.filteredSections.subscribe(filtered => {
filtered = filtered.filter(section => section.items);
expect(filtered.length).withContext('expected something').toBeGreaterThan(0);
expect(filtered.every(section => section.items?.every(itemTest))).withContext(label).toBe(true);
});
}
describe('#filteredSections', () => {
beforeEach(() => {
fixture.detectChanges();
});
it('should return all complete sections when no criteria', () => {
let filtered: ApiSection[]|undefined;
component.filteredSections.subscribe(f => filtered = f);
expect(filtered).toEqual(sections);
});
it('item.show should be true for all queried items', () => {
component.setQuery('class');
expectFilteredResult('query: class', item => /class/.test(item.name));
});
it('items should be an array for every item in section when query matches section name', () => {
component.setQuery('core');
component.filteredSections.subscribe(filtered => {
filtered = filtered.filter(section => Array.isArray(section.items));
expect(filtered.length).withContext('only one section').toBe(1);
expect(filtered[0].name).toBe('core');
expect(filtered[0].items).toEqual(sections.find(section => section.name === 'core')?.items as ApiItem[]);
});
});
describe('section.items', () => {
it('should null if there are no matching items and the section itself does not match', () => {
component.setQuery('core');
component.filteredSections.subscribe(filtered => {
const commonSection = filtered.find(section => section.name === 'common');
expect(commonSection?.items).toBe(null);
});
});
it('should be visible if they have the selected stability status', () => {
component.setStatus({value: 'stable', title: 'Stable'});
expectFilteredResult('status: stable', item => item.stability === 'stable');
});
it('should be visible if they have the selected security status', () => {
component.setStatus({value: 'security-risk', title: 'Security Risk'});
expectFilteredResult('status: security-risk', item => item.securityRisk);
});
it('should be visible if they have the selected developer preview status', () => {
component.setStatus({value: 'developer-preview', title: 'Developer Preview'});
expectFilteredResult('status: developer-preview', item => item.developerPreview);
});
it('should be visible if they match the selected API type', () => {
component.setType({value: 'class', title: 'Class'});
expectFilteredResult('type: class', item => item.docType === 'class');
});
});
it('should have no sections and no items visible when there is no match', () => {
component.setQuery('fizbuzz');
component.filteredSections.subscribe(filtered => {
expect(filtered.some(section => !!section.items)).toBeFalsy();
});
});
});
describe('initial criteria from location', () => {
let locationService: TestLocationService;
beforeEach(() => {
locationService = fixture.componentRef.injector.get<any>(LocationService);
});
function expectOneItem(name: string, section: string, type: string, stability: string) {
fixture.detectChanges();
component.filteredSections.subscribe(filtered => {
filtered = filtered.filter(s => s.items);
expect(filtered.length).withContext('sections').toBe(1);
expect(filtered[0].name).withContext('section name').toBe(section);
const items = filtered[0].items as ApiItem[];
expect(items.length).withContext('items').toBe(1);
const item = items[0];
const badItem = 'Wrong item: ' + JSON.stringify(item, null, 2);
expect(item.docType).withContext(badItem).toBe(type);
expect(item.stability).withContext(badItem).toBe(stability);
expect(item.name).withContext(badItem).toBe(name);
});
}
it('should filter as expected for ?query', () => {
locationService.query = {query: '_3'};
expectOneItem('class_3', 'core', 'class', 'experimental');
});
it('should filter as expected for ?status', () => {
locationService.query = {status: 'deprecated'};
expectOneItem('function_1', 'core', 'function', 'deprecated');
});
it('should filter as expected when status is security-risk', () => {
locationService.query = {status: 'security-risk'};
fixture.detectChanges();
expectFilteredResult('security-risk', item => item.securityRisk);
});
it('should filter as expected when status is developer-preview', () => {
locationService.query = {status: 'developer-preview'};
fixture.detectChanges();
expectFilteredResult('developer-preview', item => item.developerPreview);
});
it('should filter as expected for ?type', () => {
locationService.query = {type: 'pipe'};
expectOneItem('pipe_1', 'common', 'pipe', 'stable');
});
it('should filter as expected for ?query&status&type', () => {
locationService.query = {
query: 's_1',
status: 'experimental',
type: 'class'
};
fixture.detectChanges();
expectOneItem('class_1', 'common', 'class', 'experimental');
});
it('should ignore case for ?query&status&type', () => {
locationService.query = {
query: 'S_1',
status: 'ExperiMental',
type: 'CLASS'
};
fixture.detectChanges();
expectOneItem('class_1', 'common', 'class', 'experimental');
});
});
describe('location path after criteria change', () => {
let locationService: TestLocationService;
beforeEach(() => {
locationService = fixture.componentRef.injector.get<any>(LocationService);
});
it('should have query', () => {
component.setQuery('foo');
// `setSearch` 2nd param is a query/search params object
const search = locationService.setSearch.calls.mostRecent().args[1];
expect(search.query).toBe('foo');
});
it('should keep last of multiple query settings (in lowercase)', () => {
component.setQuery('foo');
component.setQuery('fooBar');
const search = locationService.setSearch.calls.mostRecent().args[1];
expect(search.query).toBe('foobar');
});
it('should have query, status, and type', () => {
component.setQuery('foo');
component.setStatus({value: 'stable', title: 'Stable'});
component.setType({value: 'class', title: 'Class'});
const search = locationService.setSearch.calls.mostRecent().args[1];
expect(search.query).toBe('foo');
expect(search.status).toBe('stable');
expect(search.type).toBe('class');
});
});
});
////// Helpers ////////
class TestLocationService {
query: {[index: string]: string } = {};
setSearch = jasmine.createSpy('setSearch');
search() { return this.query; }
}
class TestApiService {
sectionsSubject = new BehaviorSubject(getApiSections());
sections = this.sectionsSubject.asObservable();
}
const apiSections: ApiSection[] = [
{
name: 'common',
title: 'common',
path: 'api/common',
deprecated: false,
items: [
{
name: 'class_1',
title: 'Class 1',
path: 'api/common/class_1',
docType: 'class',
stability: 'experimental',
securityRisk: false,
developerPreview: false
},
{
name: 'class_2',
title: 'Class 2',
path: 'api/common/class_2',
docType: 'class',
stability: 'stable',
securityRisk: false,
developerPreview: false
},
{
name: 'directive_1',
title: 'Directive 1',
path: 'api/common/directive_1',
docType: 'directive',
stability: 'stable',
securityRisk: true,
developerPreview: false
},
{
name: 'pipe_1',
title: 'Pipe 1',
path: 'api/common/pipe_1',
docType: 'pipe',
stability: 'stable',
securityRisk: true,
developerPreview: false
},
],
},
{
name: 'core',
title: 'core',
path: 'api/core',
deprecated: false,
items: [
{
name: 'class_3',
title: 'Class 3',
path: 'api/core/class_3',
docType: 'class',
stability: 'experimental',
securityRisk: false,
developerPreview: false
},
{
name: 'function_1',
title: 'Function 1',
path: 'api/core/function 1',
docType: 'function',
stability: 'deprecated',
securityRisk: true,
developerPreview: false
},
{
name: 'const_1',
title: 'Const 1',
path: 'api/core/const_1',
docType: 'const',
stability: 'stable',
securityRisk: false,
developerPreview: true
},
],
},
];
function getApiSections() { return apiSections; }

View file

@ -1,173 +0,0 @@
/*
* API List & Filter Component
*
* A page that displays a formatted list of the public Angular API entities.
* Clicking on a list item triggers navigation to the corresponding API entity document.
* Can add/remove API entity links based on filter settings.
*/
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { combineLatest, Observable, ReplaySubject } from 'rxjs';
import { LocationService } from 'app/shared/location.service';
import { ApiItem, ApiSection, ApiService } from './api.service';
import { Option } from 'app/shared/select/select.component';
import { map } from 'rxjs/operators';
class SearchCriteria {
query = '';
status = 'all';
type = 'all';
}
@Component({
selector: 'aio-api-list',
templateUrl: './api-list.component.html',
})
export class ApiListComponent implements OnInit {
filteredSections: Observable<ApiSection[]>;
showStatusMenu = false;
showTypeMenu = false;
private criteriaSubject = new ReplaySubject<SearchCriteria>(1);
private searchCriteria = new SearchCriteria();
status: Option;
type: Option;
// API types
types: Option[] = [
{ value: 'all', title: 'All' },
{ value: 'class', title: 'Class' },
{ value: 'const', title: 'Const'},
{ value: 'decorator', title: 'Decorator' },
{ value: 'directive', title: 'Directive' },
{ value: 'element', title: 'Element'},
{ value: 'block', title: 'Block'},
{ value: 'enum', title: 'Enum' },
{ value: 'function', title: 'Function' },
{ value: 'interface', title: 'Interface' },
{ value: 'package', title: 'Package'},
{ value: 'pipe', title: 'Pipe'},
{ value: 'ngmodule', title: 'NgModule'},
{ value: 'type-alias', title: 'Type alias' },
];
statuses: Option[] = [
{ value: 'all', title: 'All' },
{ value: 'stable', title: 'Stable'},
{ value: 'developer-preview', title: 'Developer Preview'},
{ value: 'deprecated', title: 'Deprecated' },
{ value: 'security-risk', title: 'Security Risk' }
];
@ViewChild('filter', { static: true }) queryEl: ElementRef;
constructor(
private apiService: ApiService,
private locationService: LocationService) { }
ngOnInit() {
this.filteredSections =
combineLatest([
this.apiService.sections,
this.criteriaSubject,
]).pipe(
map( results => ({ sections: results[0], criteria: results[1]})),
map( results => (
results.sections
.map(section => ({ ...section, items: this.filterSection(section, results.criteria) }))
))
);
this.initializeSearchCriteria();
}
// TODO: may need to debounce as the original did
// although there shouldn't be any perf consequences if we don't
setQuery(query: string) {
this.setSearchCriteria({query: (query || '').toLowerCase().trim() });
}
setStatus(status: Option) {
this.toggleStatusMenu();
this.status = status;
this.setSearchCriteria({status: status.value});
}
setType(type: Option) {
this.toggleTypeMenu();
this.type = type;
this.setSearchCriteria({type: type.value});
}
toggleStatusMenu() {
this.showStatusMenu = !this.showStatusMenu;
}
toggleTypeMenu() {
this.showTypeMenu = !this.showTypeMenu;
}
//////// Private //////////
private filterSection(section: ApiSection, { query, status, type }: SearchCriteria) {
const sectionNameMatches = !query || section.name.indexOf(query) !== -1;
const matchesQuery = (item: ApiItem) =>
sectionNameMatches || item.name.indexOf(query) !== -1;
const matchesStatus = (item: ApiItem) =>
status === 'all' ||
status === item.stability ||
(status === 'security-risk' && item.securityRisk) ||
(status === 'developer-preview' && item.developerPreview);
const matchesType = (item: ApiItem) =>
type === 'all' || type === item.docType;
const items: ApiItem[] = (section.items || []).filter(item =>
matchesType(item) && matchesStatus(item) && matchesQuery(item));
// If there are no items we still return an empty array if the section name matches and the type is 'package'
return items.length ? items : (sectionNameMatches && type === 'package') ? [] : null;
}
// Get initial search criteria from URL search params
private initializeSearchCriteria() {
const {query, status, type} = this.locationService.search();
const q = (query || '').toLowerCase();
// Hack: can't bind to query because input cursor always forced to end-of-line.
this.queryEl.nativeElement.value = q;
this.status = this.statuses.find(x => x.value === status) || this.statuses[0];
this.type = this.types.find(x => x.value === type) || this.types[0];
this.searchCriteria = {
query: q,
status: this.status.value,
type: this.type.value
};
this.criteriaSubject.next(this.searchCriteria);
}
private setLocationSearch() {
const {query, status, type} = this.searchCriteria;
const params = {
query: query ? query : undefined,
status: status !== 'all' ? status : undefined,
type: type !== 'all' ? type : undefined
};
this.locationService.setSearch('API Search', params);
}
private setSearchCriteria(criteria: Partial<SearchCriteria>) {
this.criteriaSubject.next(Object.assign(this.searchCriteria, criteria));
this.setLocationSearch();
}
}

View file

@ -1,16 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { SharedModule } from '../../shared/shared.module';
import { ApiListComponent } from './api-list.component';
import { ApiService } from './api.service';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, SharedModule, HttpClientModule ],
declarations: [ ApiListComponent ],
providers: [ ApiService ]
})
export class ApiListModule implements WithCustomElementComponent {
customElementComponent: Type<any> = ApiListComponent;
}

View file

@ -1,125 +0,0 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';
import { ApiService } from './api.service';
describe('ApiService', () => {
let injector: Injector;
let service: ApiService;
let httpMock: HttpTestingController;
beforeEach(() => {
injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
ApiService,
{ provide: Logger, useClass: TestLogger }
]
});
service = injector.get<ApiService>(ApiService);
httpMock = injector.get(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('should not immediately connect to the server', () => {
httpMock.expectNone({});
expect().nothing(); // Prevent jasmine from complaining about no expectations.
});
it('subscribers should be completed/unsubscribed when service destroyed', () => {
let completed = false;
service.sections.subscribe({complete: () => completed = true});
service.ngOnDestroy();
expect(completed).toBe(true);
// Stop `httpMock.verify()` from complaining.
httpMock.expectOne({});
});
describe('#sections', () => {
it('first subscriber should fetch sections', done => {
const data = [
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
];
service.sections.subscribe(sections => {
expect(sections).toEqual(data);
done();
});
httpMock.expectOne({}).flush(data);
});
it('second subscriber should get previous sections and NOT trigger refetch', done => {
const data = [
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
];
let subscriptions = 0;
service.sections.subscribe(sections => {
subscriptions++;
expect(sections).toEqual(data);
});
service.sections.subscribe(sections => {
subscriptions++;
expect(sections).toEqual(data);
expect(subscriptions).toBe(2);
done();
});
httpMock.expectOne({}).flush(data);
});
});
describe('#fetchSections', () => {
it('should connect to the server w/ expected URL', () => {
service.fetchSections();
httpMock.expectOne('generated/docs/api/api-list.json');
expect().nothing(); // Prevent jasmine from complaining about no expectations.
});
it('should refresh the #sections observable w/ new content on second call', () => {
let call = 0;
let data = [
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
];
service.sections.subscribe(sections => {
// called twice during this test
// (1) during subscribe
// (2) after refresh
expect(sections).withContext('call ' + call++).toEqual(data);
});
httpMock.expectOne({}).flush(data);
// refresh/refetch
data = [{name: 'c', title: 'C', path: '', items: [], deprecated: false}];
service.fetchSections();
httpMock.expectOne({}).flush(data);
expect(call).withContext('should be called twice').toBe(2);
});
});
});
class TestLogger {
log = jasmine.createSpy('log');
error = jasmine.createSpy('error');
}

View file

@ -1,90 +0,0 @@
import { Injectable, OnDestroy } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { ReplaySubject, Subject } from 'rxjs';
import { takeUntil, tap } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service';
import { DOC_CONTENT_URL_PREFIX } from 'app/documents/document.service';
export interface ApiItem {
name: string;
title: string;
path: string;
docType: string;
stability: string;
securityRisk: boolean;
developerPreview: boolean;
}
export interface ApiSection {
path: string;
name: string;
title: string;
deprecated: boolean;
items: ApiItem[]|null;
}
@Injectable()
export class ApiService implements OnDestroy {
private apiBase = DOC_CONTENT_URL_PREFIX + 'api/';
private apiListJsonDefault = 'api-list.json';
private firstTime = true;
private onDestroy = new Subject<void>();
private sectionsSubject = new ReplaySubject<ApiSection[]>(1);
private _sections = this.sectionsSubject.pipe(takeUntil(this.onDestroy));
/**
* Return a cached observable of API sections from a JSON file.
* API sections is an array of Angular top modules and metadata about their API documents (items).
*/
get sections() {
if (this.firstTime) {
this.firstTime = false;
this.fetchSections(); // TODO: get URL for fetchSections by configuration?
// makes sectionsSubject hot; subscribe ensures stays alive (always refCount > 0);
this._sections.subscribe(sections => this.logger.log(`ApiService got API ${sections.length} section(s)`));
}
return this._sections.pipe(tap(sections => {
sections.forEach(section => {
section.deprecated = !!section.items &&
section.items.every(item => item.stability === 'deprecated');
});
}));
}
constructor(private http: HttpClient, private logger: Logger) { }
ngOnDestroy() {
this.onDestroy.next();
}
/**
* Fetch API sections from a JSON file.
* API sections is an array of Angular top modules and metadata about their API documents (items).
* Updates `sections` observable
*
* @param [src] - Name of the api list JSON file
*/
fetchSections(src?: string) {
// TODO: get URL by configuration?
const url = this.apiBase + (src || this.apiListJsonDefault);
this.http.get<ApiSection[]>(url)
.pipe(
takeUntil(this.onDestroy),
tap(() => this.logger.log(`Got API sections from ${url}`)),
)
.subscribe({
next: sections => this.sectionsSubject.next(sections),
error: (err: HttpErrorResponse) => {
// TODO: handle error
this.logger.error(err);
throw err; // rethrow for now.
},
});
}
}

View file

@ -1,106 +0,0 @@
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CodeExampleComponent } from './code-example.component';
import { CodeExampleModule } from './code-example.module';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { PrettyPrinter } from './pretty-printer.service';
describe('CodeExampleComponent', () => {
let hostComponent: HostComponent;
let codeExampleComponent: CodeExampleComponent;
let fixture: ComponentFixture<HostComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ CodeExampleModule ],
declarations: [
HostComponent,
],
providers: [
{ provide: Logger, useClass: MockLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
]
});
fixture = TestBed.createComponent(HostComponent);
fixture.detectChanges();
hostComponent = fixture.componentInstance;
codeExampleComponent = hostComponent.codeExampleComponent;
});
it('should be able to capture the code snippet provided in content', () => {
expect(codeExampleComponent.aioCode.code.toString().trim()).toBe('const foo = "bar";');
});
it('should clean-up the projected code snippet once captured', () => {
expect(codeExampleComponent.content.nativeElement.innerHTML).toBe('');
});
it('should change aio-code classes based on header presence', () => {
expect(codeExampleComponent.header).toBe('Great Example');
expect(fixture.nativeElement.querySelector('header')).toBeTruthy();
const aioCodeEl = fixture.nativeElement.querySelector('aio-code');
expect(aioCodeEl).toHaveClass('headed-code');
expect(aioCodeEl).not.toHaveClass('simple-code');
codeExampleComponent.header = '';
fixture.detectChanges();
expect(codeExampleComponent.header).toBe('');
expect(fixture.nativeElement.querySelector('header')).toBeFalsy();
expect(aioCodeEl).not.toHaveClass('headed-code');
expect(aioCodeEl).toHaveClass('simple-code');
});
it('should set avoidFile class if path has .avoid.', () => {
const codeExampleComponentElement: HTMLElement =
fixture.nativeElement.querySelector('code-example');
expect(codeExampleComponent.path).toBe('code-path');
expect(codeExampleComponentElement.className.indexOf('avoidFile') === -1).toBe(true);
codeExampleComponent.path = 'code-path.avoid.';
fixture.detectChanges();
expect(codeExampleComponentElement.className.indexOf('avoidFile') === -1).toBe(false);
});
it('should coerce hidecopy', () => {
expect(codeExampleComponent.hidecopy).toBe(false);
hostComponent.hidecopy = true;
fixture.detectChanges();
expect(codeExampleComponent.hidecopy).toBe(true);
hostComponent.hidecopy = 'false';
fixture.detectChanges();
expect(codeExampleComponent.hidecopy).toBe(false);
hostComponent.hidecopy = 'true';
fixture.detectChanges();
expect(codeExampleComponent.hidecopy).toBe(true);
});
});
@Component({
selector: 'aio-host-comp',
template: `
<code-example [header]="header" [path]="path" [hidecopy]="hidecopy">
{{code}}
</code-example>
`
})
class HostComponent {
code = 'const foo = "bar";';
header = 'Great Example';
path = 'code-path';
hidecopy: boolean | string = false;
@ViewChild(CodeExampleComponent, {static: true}) codeExampleComponent: CodeExampleComponent;
}

View file

@ -1,86 +0,0 @@
/* eslint-disable @angular-eslint/component-selector */
import { Component, HostBinding, ElementRef, ViewChild, Input, AfterViewInit } from '@angular/core';
import { fromInnerHTML } from 'app/shared/security';
import { CodeComponent } from './code.component';
/**
* An embeddable code block that displays nicely formatted code.
* Example usage:
*
* ```
* <code-example language="ts" linenums="2" class="special" header="Do Stuff">
* // a code block
* console.log('do stuff');
* </code-example>
* ```
*/
@Component({
selector: 'code-example',
template: `
<!-- Content projection is used to get the content HTML provided to this component -->
<div #content style="display: none"><ng-content></ng-content></div>
<header *ngIf="header">{{header}}</header>
<aio-code [class.headed-code]="!!this.header"
[class.simple-code]="!this.header"
[language]="language"
[linenums]="linenums"
[path]="path"
[region]="region"
[hideCopy]="hidecopy"
[header]="header">
</aio-code>
`,
})
export class CodeExampleComponent implements AfterViewInit {
classes: { 'headed-code': boolean, 'simple-code': boolean };
@Input() language: string;
@Input() linenums: string;
@Input() region: string;
@Input() header: string;
@Input()
set path(path: string) {
this._path = path;
this.isAvoid = this.path.indexOf('.avoid.') !== -1;
}
get path(): string { return this._path; }
private _path = '';
@Input()
set hidecopy(hidecopy: boolean) {
// Coerce the boolean value.
this._hidecopy = hidecopy != null && `${hidecopy}` !== 'false';
}
get hidecopy(): boolean { return this._hidecopy; }
private _hidecopy: boolean;
/* eslint-disable-next-line @angular-eslint/no-input-rename */
@Input('hide-copy')
set hyphenatedHideCopy(hidecopy: boolean) {
this.hidecopy = hidecopy;
}
/* eslint-disable-next-line @angular-eslint/no-input-rename */
@Input('hideCopy')
set capitalizedHideCopy(hidecopy: boolean) {
this.hidecopy = hidecopy;
}
@HostBinding('class.avoidFile') isAvoid = false;
@ViewChild('content', { static: true }) content: ElementRef<HTMLDivElement>;
@ViewChild(CodeComponent, { static: true }) aioCode: CodeComponent;
ngAfterViewInit() {
const contentElem = this.content.nativeElement;
this.aioCode.code = fromInnerHTML(contentElem);
contentElem.textContent = ''; // Remove DOM nodes that are no longer needed.
}
}

View file

@ -1,14 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CodeExampleComponent } from './code-example.component';
import { CodeModule } from './code.module';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, CodeModule ],
declarations: [ CodeExampleComponent ],
exports: [ CodeExampleComponent ]
})
export class CodeExampleModule implements WithCustomElementComponent {
customElementComponent: Type<any> = CodeExampleComponent;
}

View file

@ -1,104 +0,0 @@
import { Component, ViewChild, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CodeTabsComponent } from './code-tabs.component';
import { CodeTabsModule } from './code-tabs.module';
import { PrettyPrinter } from './pretty-printer.service';
describe('CodeTabsComponent', () => {
let fixture: ComponentFixture<HostComponent>;
let hostComponent: HostComponent;
let codeTabsComponent: CodeTabsComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ HostComponent ],
imports: [ CodeTabsModule, NoopAnimationsModule ],
schemas: [ NO_ERRORS_SCHEMA ],
providers: [
{ provide: Logger, useClass: MockLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
]
});
fixture = TestBed.createComponent(HostComponent);
fixture.detectChanges();
hostComponent = fixture.componentInstance;
codeTabsComponent = hostComponent.codeTabsComponent;
});
it('should get correct tab info', () => {
const tabs = codeTabsComponent.tabs;
expect(tabs.length).toBe(2);
// First code pane expectations
expect(tabs[0].class).toBe('class-A');
expect(tabs[0].language).toBe('language-A');
expect(tabs[0].linenums).toBe('linenums-A');
expect(tabs[0].path).toBe('path-A');
expect(tabs[0].region).toBe('region-A');
expect(tabs[0].header).toBe('header-A');
expect(tabs[0].code.toString().trim()).toBe('Code example 1');
// Second code pane expectations
expect(tabs[1].class).toBe('class-B');
expect(tabs[1].language).toBe('language-B');
expect(tabs[1].linenums).withContext('Default linenums should have been used')
.toBe('default-linenums');
expect(tabs[1].path).toBe('path-B');
expect(tabs[1].region).toBe('region-B');
expect(tabs[1].header).toBe('header-B');
expect(tabs[1].code.toString().trim()).toBe('Code example 2');
});
it('should create the right number of tabs with the right labels and classes', () => {
const matTabs = fixture.nativeElement.querySelectorAll('.mdc-tab__text-label');
expect(matTabs.length).toBe(2);
expect(matTabs[0].textContent.trim()).toBe('header-A');
expect(matTabs[0].querySelector('.class-A')).toBeTruthy();
expect(matTabs[1].textContent.trim()).toBe('header-B');
expect(matTabs[1].querySelector('.class-B')).toBeTruthy();
});
it('should show the first tab with the right code', () => {
const codeContent = fixture.nativeElement.querySelector('aio-code').textContent;
expect(codeContent.indexOf('Code example 1') !== -1).toBeTruthy();
});
it('should clean-up the projected tabs content once captured', () => {
expect(codeTabsComponent.content.nativeElement.innerHTML).toBe('');
});
});
@Component({
selector: 'aio-host-comp',
template: `
<code-tabs linenums="default-linenums">
<code-pane class="class-A"
language="language-A"
linenums="linenums-A"
path="path-A"
region="region-A"
header="header-A">
Code example 1
</code-pane>
<code-pane class="class-B"
language="language-B"
path="path-B"
region="region-B"
header="header-B">
Code example 2
</code-pane>
</code-tabs>
`
})
class HostComponent {
@ViewChild(CodeTabsComponent, {static: true}) codeTabsComponent: CodeTabsComponent;
}

View file

@ -1,89 +0,0 @@
/* eslint-disable @angular-eslint/component-selector */
import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { fromInnerHTML } from 'app/shared/security';
import { CodeComponent } from './code.component';
export interface TabInfo {
class: string;
code: TrustedHTML;
path: string;
region: string;
header?: string;
language?: string;
linenums?: string;
}
/**
* Renders a set of tab group of code snippets.
*
* The innerHTML of the `<code-tabs>` component should contain `<code-pane>` elements.
* Each `<code-pane>` has the same interface as the embedded `<code-example>` component.
* The optional `linenums` attribute is the default `linenums` for each code pane.
*/
@Component({
selector: 'code-tabs',
template: `
<!-- Use content projection so that the provided HTML's code-panes can be split into tabs -->
<div #content style="display: none"><ng-content></ng-content></div>
<mat-card>
<mat-tab-group class="code-tab-group" [disableRipple]="true">
<mat-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
<ng-template mat-tab-label>
<span class="{{ tab.class }}">{{ tab.header }}</span>
</ng-template>
<aio-code class="{{ tab.class }}"
[language]="tab.language"
[linenums]="tab.linenums"
[path]="tab.path"
[region]="tab.region"
[header]="tab.header">
</aio-code>
</mat-tab>
</mat-tab-group>
</mat-card>
`,
})
export class CodeTabsComponent implements OnInit, AfterViewInit {
tabs: TabInfo[];
@Input() linenums: string | undefined;
@ViewChild('content', { static: true }) content: ElementRef<HTMLDivElement>;
@ViewChildren(CodeComponent) codeComponents: QueryList<CodeComponent>;
ngOnInit() {
this.tabs = [];
const contentElem = this.content.nativeElement;
const codeExamples = Array.from(contentElem.querySelectorAll('code-pane'));
// Remove DOM nodes that are no longer needed.
contentElem.textContent = '';
for (const tabContent of codeExamples) {
this.tabs.push(this.getTabInfo(tabContent));
}
}
ngAfterViewInit() {
this.codeComponents.toArray().forEach((codeComponent, i) => {
codeComponent.code = this.tabs[i].code;
});
}
/** Gets the extracted TabInfo data from the provided code-pane element. */
private getTabInfo(tabContent: Element): TabInfo {
return {
class: tabContent.getAttribute('class') || '',
code: fromInnerHTML(tabContent),
path: tabContent.getAttribute('path') || '',
region: tabContent.getAttribute('region') || '',
header: tabContent.getAttribute('header') || undefined,
language: tabContent.getAttribute('language') || undefined,
linenums: tabContent.getAttribute('linenums') || this.linenums,
};
}
}

View file

@ -1,16 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CodeTabsComponent } from './code-tabs.component';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { CodeModule } from './code.module';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, MatCardModule, MatTabsModule, CodeModule ],
declarations: [ CodeTabsComponent ],
exports: [ CodeTabsComponent ]
})
export class CodeTabsModule implements WithCustomElementComponent {
customElementComponent: Type<any> = CodeTabsComponent;
}

View file

@ -1,324 +0,0 @@
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatSnackBar } from '@angular/material/snack-bar';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Clipboard } from '@angular/cdk/clipboard';
import { CodeComponent } from './code.component';
import { CodeModule } from './code.module';
import { Logger } from 'app/shared/logger.service';
import { htmlEscape } from 'safevalues';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { PrettyPrinter } from './pretty-printer.service';
const oneLineCode = 'const foo = "bar";';
const smallMultiLineCode = `<hero-details>
<h2>Bah Dah Bing</h2>
<hero-team>
<h3>NYC Team</h3>
</hero-team>
</hero-details>`;
const bigMultiLineCode = `${smallMultiLineCode}\n${smallMultiLineCode}\n${smallMultiLineCode}`;
describe('CodeComponent', () => {
let hostComponent: HostComponent;
let fixture: ComponentFixture<HostComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ NoopAnimationsModule, CodeModule ],
declarations: [ HostComponent ],
providers: [
{ provide: Logger, useClass: TestLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
]
});
fixture = TestBed.createComponent(HostComponent);
hostComponent = fixture.componentInstance;
fixture.detectChanges();
});
describe('pretty printing', () => {
const getFormattedCode = () => fixture.nativeElement.querySelector('code').innerHTML;
it('should format a one-line code sample without linenums by default', () => {
hostComponent.setCode(oneLineCode);
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${oneLineCode}`);
});
it('should add line numbers to one-line code sample when linenums is `true`', () => {
hostComponent.setCode(oneLineCode);
hostComponent.linenums = true;
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${oneLineCode}`);
});
it('should add line numbers to one-line code sample when linenums is `\'true\'`', () => {
hostComponent.setCode(oneLineCode);
hostComponent.linenums = 'true';
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${oneLineCode}`);
});
it('should format a small multi-line code sample without linenums by default', () => {
hostComponent.setCode(smallMultiLineCode);
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${htmlEscape(smallMultiLineCode)}`);
});
it('should add line numbers to a small multi-line code sample when linenums is `true`', () => {
hostComponent.setCode(smallMultiLineCode);
hostComponent.linenums = true;
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${htmlEscape(smallMultiLineCode)}`);
});
it('should add line numbers to a small multi-line code sample when linenums is `\'true\'`', () => {
hostComponent.setCode(smallMultiLineCode);
hostComponent.linenums = 'true';
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${htmlEscape(smallMultiLineCode)}`);
});
it('should format a big multi-line code without linenums by default', () => {
hostComponent.setCode(bigMultiLineCode);
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${htmlEscape(bigMultiLineCode)}`);
});
it('should add line numbers to a big multi-line code sample when linenums is `true`', () => {
hostComponent.setCode(bigMultiLineCode);
hostComponent.linenums = true;
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${htmlEscape(bigMultiLineCode)}`);
});
it('should add line numbers to a big multi-line code sample when linenums is `\'true\'`', () => {
hostComponent.setCode(bigMultiLineCode);
hostComponent.linenums = 'true';
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${htmlEscape(bigMultiLineCode)}`);
});
it('should skip prettify if language is `\'none\'`', () => {
hostComponent.setCode(bigMultiLineCode);
hostComponent.language = 'none';
fixture.detectChanges();
expect(getFormattedCode()).toBe(htmlEscape(bigMultiLineCode).toString());
});
});
describe('whitespace handling', () => {
it('should remove common indentation from the code before rendering', () => {
hostComponent.linenums = false;
fixture.detectChanges();
hostComponent.setCode(`
abc
let x = text.split('\\n');
ghi
jkl
`);
const codeContent = fixture.nativeElement.querySelector('code').textContent;
expect(codeContent).toEqual(
'Formatted code (language: auto, linenums: false): abc\n let x = text.split(\'\\n\');\nghi\n\njkl');
});
it('should trim whitespace from the code before rendering', () => {
hostComponent.linenums = false;
fixture.detectChanges();
hostComponent.setCode('\n\n\n' + smallMultiLineCode + '\n\n\n');
const codeContent = fixture.nativeElement.querySelector('code').textContent;
expect(codeContent).toEqual(codeContent.trim());
});
it('should trim whitespace from code before computing whether to format linenums', () => {
hostComponent.setCode('\n\n\n' + oneLineCode + '\n\n\n');
// `<li>`s are a tell-tale for line numbers
const lis = fixture.nativeElement.querySelectorAll('li');
expect(lis.length).withContext('should be no linenums').toBe(0);
});
});
describe('error message', () => {
function getErrorMessage() {
const missing: HTMLElement = fixture.nativeElement.querySelector('.code-missing');
return missing ? missing.textContent : null;
}
it('should not display "code-missing" class when there is some code', () => {
expect(getErrorMessage()).withContext('should not have element with "code-missing" class').toBeNull();
});
it('should display error message when there is no code (after trimming)', () => {
hostComponent.setCode(' \n ');
expect(getErrorMessage()).toContain('missing');
});
it('should show path and region in missing-code error message', () => {
hostComponent.path = 'fizz/buzz/foo.html';
hostComponent.region = 'something';
fixture.detectChanges();
hostComponent.setCode(' \n ');
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html#something$/);
});
it('should show path only in missing-code error message when no region', () => {
hostComponent.path = 'fizz/buzz/foo.html';
fixture.detectChanges();
hostComponent.setCode(' \n ');
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html$/);
});
it('should show simple missing-code error message when no path/region', () => {
hostComponent.setCode(' \n ');
expect(getErrorMessage()).toMatch(/missing.$/);
});
});
describe('copy button', () => {
function getButton() {
const btnDe = fixture.debugElement.query(By.css('button'));
return btnDe ? btnDe.nativeElement : null;
}
it('should be hidden if the `hideCopy` input is true', () => {
hostComponent.hideCopy = true;
fixture.detectChanges();
expect(getButton()).toBe(null);
});
it('should have title', () => {
expect(getButton().title).toBe('Copy code snippet');
});
it('should have no aria-label by default', () => {
expect(getButton().getAttribute('aria-label')).toBe('');
});
it('should have aria-label explaining what is being copied when header passed in', () => {
hostComponent.header = 'a/b/c/foo.ts';
fixture.detectChanges();
expect(getButton().getAttribute('aria-label')).toContain(hostComponent.header);
});
it('should call copier service when clicked', () => {
const clipboard = TestBed.inject(Clipboard);
const spy = spyOn(clipboard, 'copy');
expect(spy.calls.count()).withContext('before click').toBe(0);
getButton().click();
expect(spy.calls.count()).withContext('after click').toBe(1);
});
it('should copy code text when clicked', () => {
const clipboard = TestBed.inject(Clipboard);
const spy = spyOn(clipboard, 'copy');
getButton().click();
expect(spy.calls.argsFor(0)[0]).withContext('after click').toBe(oneLineCode);
});
it('should preserve newlines in the copied code', () => {
const clipboard = TestBed.inject(Clipboard);
const spy = spyOn(clipboard, 'copy');
const expectedCode = smallMultiLineCode.trim();
let actualCode;
hostComponent.setCode(smallMultiLineCode);
[false, true, 42].forEach(linenums => {
hostComponent.linenums = linenums;
fixture.detectChanges();
getButton().click();
actualCode = spy.calls.mostRecent().args[0];
expect(actualCode).withContext(`when linenums=${linenums}`).toBe(expectedCode);
expect(actualCode.match(/\r?\n/g)?.length).toBe(5);
spy.calls.reset();
});
});
it('should display a message when copy succeeds', () => {
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
const clipboard = TestBed.inject(Clipboard);
spyOn(snackBar, 'open');
spyOn(clipboard, 'copy').and.returnValue(true);
getButton().click();
expect(snackBar.open).toHaveBeenCalledWith('Code Copied', '', { duration: 800 });
});
it('should display an error when copy fails', () => {
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
const clipboard = TestBed.inject(Clipboard);
const logger = TestBed.inject(Logger) as unknown as TestLogger;
spyOn(snackBar, 'open');
spyOn(clipboard, 'copy').and.returnValue(false);
getButton().click();
expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 });
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(jasmine.any(Error));
expect(logger.error.calls.mostRecent().args[0].message).toMatch(/^ERROR copying code to clipboard:/);
});
});
});
//// Test helpers ////
@Component({
selector: 'aio-host-comp',
template: `
<aio-code [language]="language"
[linenums]="linenums" [path]="path" [region]="region"
[hideCopy]="hideCopy" [header]="header"></aio-code>
`
})
class HostComponent implements AfterViewInit {
hideCopy: boolean;
language: string;
linenums: boolean | number | string;
path: string;
region: string;
header: string;
@ViewChild(CodeComponent, {static: false}) codeComponent: CodeComponent;
ngAfterViewInit() {
this.setCode(oneLineCode);
}
/** Changes the displayed code on the code component. */
setCode(code: string) {
this.codeComponent.code = htmlEscape(code);
}
}
class TestLogger {
log = jasmine.createSpy('log');
error = jasmine.createSpy('error');
}

View file

@ -1,204 +0,0 @@
import { Component, ElementRef, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core';
import { Clipboard } from '@angular/cdk/clipboard';
import { Logger } from 'app/shared/logger.service';
import { PrettyPrinter } from './pretty-printer.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { unwrapHtml } from 'safevalues';
import { htmlSafeByReview } from 'safevalues/restricted/reviewed';
import { fromOuterHTML } from 'app/shared/security';
/**
* Formatted Code Block
*
* Pretty renders a code block, used in the docs and API reference by the code-example and
* code-tabs embedded components.
* It includes a "copy" button that will send the content to the clipboard when clicked
*
* Example usage:
*
* ```
* <aio-code
* [language]="ts"
* [linenums]="true"
* [path]="router/src/app/app.module.ts"
* [region]="animations-module">
* </aio-code>
* ```
*
*
* Renders code provided through the `updateCode` method.
*/
@Component({
selector: 'aio-code',
template: `
<pre class="prettyprint lang-{{language}}">
<button *ngIf="!hideCopy" class="material-icons copy-button no-print"
title="Copy code snippet"
[attr.aria-label]="ariaLabel"
(click)="doCopy()">
<span aria-hidden="true">content_copy</span>
</button>
<code class="animated fadeIn" #codeContainer></code>
</pre>
`
})
export class CodeComponent implements OnChanges {
ariaLabel = '';
/** The code to be copied when clicking the copy button, this should not be HTML encoded */
private codeText: string;
/** Code that should be formatted with current inputs and displayed in the view. */
set code(code: TrustedHTML) {
this._code = code;
if (!this._code.toString().trim()) {
this.showMissingCodeMessage();
} else {
this.formatDisplayedCode();
}
}
get code(): TrustedHTML {
return this._code;
}
_code: TrustedHTML;
/** Whether the copy button should be shown. */
@Input() hideCopy: boolean;
/** Language to render the code (e.g. javascript, typescript). */
@Input() language: string | undefined;
/**
* Whether to display line numbers:
* - If false: hide
* - If true: show
* - If number: show but start at that number
*/
@Input() linenums: boolean | number | string | undefined;
/** Path to the source of the code. */
@Input() path: string;
/** Region of the source of the code being displayed. */
@Input() region: string;
/** Optional header to be displayed above the code. */
@Input()
set header(header: string | undefined) {
this._header = header;
this.ariaLabel = this.header ? `Copy code snippet from ${this.header}` : '';
}
get header(): string|undefined { return this._header; }
private _header: string | undefined;
@Output() codeFormatted = new EventEmitter<void>();
/** The element in the template that will display the formatted code. */
@ViewChild('codeContainer', { static: true }) codeContainer: ElementRef;
constructor(
private snackbar: MatSnackBar,
private pretty: PrettyPrinter,
private clipboard: Clipboard,
private logger: Logger) {}
ngOnChanges() {
// If some inputs have changed and there is code displayed, update the view with the latest
// formatted code.
if (this.code) {
this.formatDisplayedCode();
}
}
private formatDisplayedCode() {
const linenums = this.getLinenums();
const leftAlignedCode = leftAlign(this.code);
this.setCodeHtml(leftAlignedCode); // start with unformatted code
this.codeText = this.getCodeText(); // store the unformatted code as text (for copying)
const skipPrettify = of(undefined);
const prettifyCode = this.pretty
.formatCode(leftAlignedCode, this.language, linenums)
.pipe(tap(formattedCode => this.setCodeHtml(formattedCode)));
if (linenums !== false && this.language === 'none') {
this.logger.warn("Using 'linenums' with 'language: none' is currently not supported.");
}
((this.language === 'none' ? skipPrettify : prettifyCode) as Observable<unknown>)
.subscribe({
next: () => this.codeFormatted.emit(),
error: () => { /* ignore failure to format */ },
});
}
/** Sets the message showing that the code could not be found. */
private showMissingCodeMessage() {
const src = this.path ? this.path + (this.region ? '#' + this.region : '') : '';
const msg = `The code sample is missing${src ? ` for\n${src}` : '.'}`;
const el = document.createElement('p');
el.className = 'code-missing';
el.textContent = msg;
this.setCodeHtml(fromOuterHTML(el));
}
/** Sets the innerHTML of the code container to the provided code string. */
private setCodeHtml(formattedCode: TrustedHTML) {
// **Security:** Code example content is provided by docs authors and as such its considered to
// be safe for innerHTML purposes.
this.codeContainer.nativeElement.innerHTML = unwrapHtml(formattedCode);
}
/** Gets the textContent of the displayed code element. */
private getCodeText() {
// `prettify` may remove newlines, e.g. when `linenums` are on. Retrieve the content of the
// container as text, before prettifying it.
// We take the textContent because we don't want it to be HTML encoded.
return this.codeContainer.nativeElement.textContent;
}
/** Copies the code snippet to the user's clipboard. */
doCopy() {
const code = this.codeText;
const successfullyCopied = this.clipboard.copy(code);
if (successfullyCopied) {
this.logger.log('Copied code to clipboard:', code);
this.snackbar.open('Code Copied', '', { duration: 800 });
} else {
this.logger.error(new Error(`ERROR copying code to clipboard: "${code}"`));
this.snackbar.open('Copy failed. Please try again!', '', { duration: 800 });
}
}
/** Gets the calculated value of linenums (boolean/number). */
getLinenums() {
const linenums =
typeof this.linenums === 'boolean' ? this.linenums :
this.linenums === 'true' ? true :
this.linenums === 'false' ? false :
typeof this.linenums === 'string' ? parseInt(this.linenums, 10) :
this.linenums;
return (linenums != null) && !isNaN(linenums as number) && linenums;
}
}
function leftAlign(text: TrustedHTML): TrustedHTML {
let indent = Number.MAX_VALUE;
const lines = text.toString().split('\n');
lines.forEach(line => {
const lineIndent = line.search(/\S/);
if (lineIndent !== -1) {
indent = Math.min(lineIndent, indent);
}
});
return htmlSafeByReview(
lines.map(line => line.slice(indent)).join('\n').trim(),
'safe manipulation of existing trusted HTML');
}

View file

@ -1,13 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CodeComponent } from './code.component';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { PrettyPrinter } from './pretty-printer.service';
@NgModule({
imports: [ CommonModule, MatSnackBarModule ],
declarations: [ CodeComponent ],
exports: [ CodeComponent ],
providers: [ PrettyPrinter ]
})
export class CodeModule { }

View file

@ -1,65 +0,0 @@
import { Injectable } from '@angular/core';
import { htmlSafeByReview } from 'safevalues/restricted/reviewed';
import { from, Observable } from 'rxjs';
import { first, map, share } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service';
type PrettyPrintOne = (code: TrustedHTML, language?: string, linenums?: number|boolean) => string;
/**
* Wrapper around the prettify.js library
*/
@Injectable()
export class PrettyPrinter {
private prettyPrintOne: Observable<PrettyPrintOne>;
constructor(private logger: Logger) {
this.prettyPrintOne = from(this.getPrettyPrintOne()).pipe(share());
}
private getPrettyPrintOne(): Promise<PrettyPrintOne> {
const ppo = (window as any).prettyPrintOne;
return ppo ? Promise.resolve(ppo) :
// `prettyPrintOne` is not on `window`, which means `prettify.js` has not been loaded yet.
// Import it; ad a side-effect it will add `prettyPrintOne` on `window`.
import('assets/js/prettify.js' as any)
.then(
() => (window as any).prettyPrintOne,
err => {
const msg = `Cannot get prettify.js from server: ${err.message}`;
this.logger.error(new Error(msg));
// return a pretty print fn that always fails.
return () => { throw new Error(msg); };
});
}
/**
* Format code snippet as HTML.
*
* @param code - the code snippet to format; should already be HTML encoded
* @param [language] - The language of the code to render (could be javascript, html, typescript, etc)
* @param [linenums] - Whether to display line numbers:
* - false: don't display
* - true: do display
* - number: do display but start at the given number
* @returns Observable<string> - Observable of formatted code
*/
formatCode(code: TrustedHTML, language?: string, linenums?: number|boolean) {
return this.prettyPrintOne.pipe(
map(ppo => {
try {
return htmlSafeByReview(
ppo(code, language, linenums), 'prettify.js modifies already trusted HTML inline');
} catch (err) {
const msg = `Could not format code that begins '${code.toString().slice(0, 50)}...'.`;
console.error(msg, err);
throw new Error(msg);
}
}),
first(), // complete immediately
);
}
}

View file

@ -1,109 +0,0 @@
import { Injector } from '@angular/core';
import { of } from 'rxjs';
import { ContributorGroup } from './contributors.model';
import { ContributorListComponent } from './contributor-list.component';
import { ContributorService } from './contributor.service';
import { LocationService } from 'app/shared/location.service';
// Testing the component class behaviors, independent of its template
// Let e2e tests verify how it displays.
describe('ContributorListComponent', () => {
let component: ContributorListComponent;
let injector: Injector;
let contributorService: TestContributorService;
let locationService: TestLocationService;
let contributorGroups: ContributorGroup[];
beforeEach(() => {
injector = Injector.create({
providers: [
{provide: ContributorListComponent, deps: [ContributorService, LocationService] },
{provide: ContributorService, useClass: TestContributorService, deps: [] },
{provide: LocationService, useClass: TestLocationService, deps: [] }
]
});
locationService = injector.get(LocationService) as unknown as TestLocationService;
contributorService = injector.get(ContributorService) as unknown as TestContributorService;
contributorGroups = contributorService.testContributors;
});
it('should select the first group when no query string', () => {
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[0]);
});
it('should select the first group when query string w/o "group" property', () => {
locationService.searchResult = { foo: 'GDE' };
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[0]);
});
it('should select the first group when query group not found', () => {
locationService.searchResult = { group: 'foo' };
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[0]);
});
it('should select the GDE group when query group is "GDE"', () => {
locationService.searchResult = { group: 'GDE' };
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[1]);
});
it('should select the GDE group when query group is "gde" (case insensitive)', () => {
locationService.searchResult = { group: 'gde' };
component = getComponent();
expect(component.selectedGroup).toBe(contributorGroups[1]);
});
it('should set the query to the "GDE" group when user selects "GDE"', () => {
component = getComponent();
component.selectGroup('GDE');
expect(locationService.searchResult.group).toBe('GDE');
});
it('should set the query to the first group when user selects unknown name', () => {
component = getComponent();
component.selectGroup('GDE'); // a legit group that isn't the first
component.selectGroup('foo'); // not a legit group name
expect(locationService.searchResult.group).toBe('Angular');
});
//// Test Helpers ////
function getComponent(): ContributorListComponent {
const comp = injector.get(ContributorListComponent);
comp.ngOnInit();
return comp;
}
interface SearchResult {
[index: string]: string;
}
class TestLocationService {
searchResult: SearchResult = {};
search = jasmine.createSpy('search').and.callFake(() => this.searchResult);
setSearch = jasmine.createSpy('setSearch')
.and.callFake((_label: string, result: SearchResult) => {
this.searchResult = result;
});
}
class TestContributorService {
testContributors = getTestData();
contributors = of(this.testContributors);
}
function getTestData(): ContributorGroup[] {
return [
// Not interested in the contributors data in these tests
{ name: 'Angular', order: 0, contributors: [] },
{ name: 'GDE', order: 1, contributors: [] },
];
}
});

View file

@ -1,48 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ContributorGroup } from './contributors.model';
import { ContributorService } from './contributor.service';
import { LocationService } from 'app/shared/location.service';
@Component({
selector: 'aio-contributor-list',
template: `
<div class="flex-center group-buttons">
<button *ngFor="let name of groupNames"
class="button mat-button filter-button"
[class.selected]="name === selectedGroup.name"
(click)="selectGroup(name)">{{name}}</button>
</div>
<section *ngIf="selectedGroup" class="grid-fluid">
<div class="contributor-group">
<aio-contributor *ngFor="let person of selectedGroup.contributors"
[person]="person"></aio-contributor>
</div>
</section>
`,
})
export class ContributorListComponent implements OnInit {
private groups: ContributorGroup[];
groupNames: string[];
selectedGroup: ContributorGroup;
constructor(
private contributorService: ContributorService,
private locationService: LocationService) { }
ngOnInit() {
const groupName = this.locationService.search().group || '';
// no need to unsubscribe because `contributors` completes
this.contributorService.contributors
.subscribe(grps => {
this.groups = grps;
this.groupNames = grps.map(g => g.name);
this.selectGroup(groupName);
});
}
selectGroup(name: string) {
name = name.toLowerCase();
this.selectedGroup = this.groups.find(g => g.name.toLowerCase() === name) || this.groups[0];
this.locationService.setSearch('', {group: this.selectedGroup.name});
}
}

View file

@ -1,16 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { ContributorListComponent } from './contributor-list.component';
import { ContributorService } from './contributor.service';
import { ContributorComponent } from './contributor.component';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule, MatIconModule ],
declarations: [ ContributorListComponent, ContributorComponent ],
providers: [ ContributorService ]
})
export class ContributorListModule implements WithCustomElementComponent {
customElementComponent: Type<any> = ContributorListComponent;
}

View file

@ -1,35 +0,0 @@
import { Component, Input } from '@angular/core';
import { Contributor } from './contributors.model';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
@Component({
selector: 'aio-contributor',
template: `
<section class="contributor-card" [class.no-image]="!person.picture"
attr.aria-labelledby="{{person.name}}-section-heading">
<div *ngIf="person.picture" class="contributor-image-wrapper">
<img [src]="pictureBase+person.picture" alt="{{person.name}}" class="contributor-image">
</div>
<h3 id="{{person.name}}-section-heading" class="contributor-title">{{person.name}}</h3>
<p class="contributor-bio">{{person.bio}}</p>
<div class="contributor-social-links">
<a *ngIf="person.twitter" mat-icon-button class="info-item icon contributor-social"
attr.aria-label="twitter of {{person.name}}"
href="https://twitter.com/{{person.twitter}}"
target="_blank" (click)="$event.stopPropagation()">
<mat-icon svgIcon="logos:twitter"></mat-icon>
</a>
<a *ngIf="person.website" mat-icon-button class="info-item icon"
attr.aria-label="website of {{person.name}}"
href="{{person.website}}" target="_blank" (click)="$event.stopPropagation()">
<mat-icon class="link-icon">link</mat-icon>
</a>
</div>
</section>
`
})
export class ContributorComponent {
@Input() person: Contributor;
pictureBase = CONTENT_URL_PREFIX + 'images/bios/';
}

View file

@ -1,133 +0,0 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ContributorService } from './contributor.service';
import { ContributorGroup } from './contributors.model';
describe('ContributorService', () => {
let injector: Injector;
let contribService: ContributorService;
let httpMock: HttpTestingController;
beforeEach(() => {
injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
ContributorService
]
});
contribService = injector.get<ContributorService>(ContributorService);
httpMock = injector.get(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('should make a single connection to the server', () => {
const req = httpMock.expectOne({});
expect(req.request.url).toBe('generated/contributors.json');
});
describe('#contributors', () => {
let contribs: ContributorGroup[];
let testData: any;
beforeEach(() => {
testData = getTestContribs();
httpMock.expectOne({}).flush(testData);
contribService.contributors.subscribe(results => contribs = results);
});
it('contributors observable should complete', () => {
let completed = false;
contribService.contributors.subscribe({complete: () => completed = true});
expect(completed).withContext('observable completed').toBe(true);
});
it('should reshape the contributor json to expected result', () => {
const groupNames = contribs.map(g => g.name).join(',');
expect(groupNames).toEqual('Angular,Collaborators,GDE');
});
it('should have expected "GDE" contribs in order', () => {
const gde = contribs[2];
const actualAngularNames = gde.contributors.map(l => l.name).join(',');
const expectedAngularNames = [testData.gkalpak, testData.kapunahelewong].map(l => l.name).join(',');
expect(actualAngularNames).toEqual(expectedAngularNames);
});
it('should support including a contributor in multiple groups', () => {
const contributor = testData.gkalpak;
const matchedGroups = contribs
.filter(group => group.contributors.includes(contributor))
.map(group => group.name);
expect(matchedGroups).toEqual(['Collaborators', 'GDE']);
});
});
it('should do WHAT(?) if the request fails');
});
function getTestContribs() {
return {
kapunahelewong: {
name: 'Kapunahele Wong',
picture: 'kapunahelewong.jpg',
website: 'https://github.com/kapunahelewong',
twitter: 'kapunahele',
bio: 'Kapunahele is a front-end developer and contributor to angular.io',
groups: ['GDE']
},
misko: {
name: 'Miško Hevery',
picture: 'misko.jpg',
twitter: 'mhevery',
website: 'http://misko.hevery.com',
bio: 'Miško Hevery is the creator of AngularJS framework.',
groups: ['Angular']
},
igor: {
name: 'Igor Minar',
picture: 'igor-minar.jpg',
twitter: 'IgorMinar',
website: 'https://google.com/+IgorMinar',
bio: 'Igor is a software engineer at Angular.',
groups: ['Angular']
},
kara: {
name: 'Kara Erickson',
picture: 'kara-erickson.jpg',
twitter: 'karaforthewin',
website: 'https://github.com/kara',
bio: 'Kara is a software engineer on the Angular team at Angular and a co-organizer of the Angular-SF Meetup. ',
groups: ['Angular']
},
jeffcross: {
name: 'Jeff Cross',
picture: 'jeff-cross.jpg',
twitter: 'jeffbcross',
website: 'https://twitter.com/jeffbcross',
bio: 'Jeff was one of the earliest core team members on AngularJS.',
groups: ['Collaborators']
},
naomi: {
name: 'Naomi Black',
picture: 'naomi.jpg',
twitter: 'naomitraveller',
website: 'http://google.com/+NaomiBlack',
bio: 'Naomi is Angular\'s TPM generalist and jack-of-all-trades.',
groups: ['Angular']
},
gkalpak: {
name: 'George Kalpakas',
picture: 'gkalpak.jpg',
twitter: 'gkalpakas',
bio: 'George wrote this test, so he gets to have his name included here.',
groups: ['GDE', 'Collaborators'],
}
};
}

View file

@ -1,69 +0,0 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {AsyncSubject, connectable, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {Contributor, ContributorGroup} from './contributors.model';
// TODO(andrewjs): Look into changing this so that we don't import the service just to get the const
import {CONTENT_URL_PREFIX} from 'app/documents/document.service';
const contributorsPath = CONTENT_URL_PREFIX + 'contributors.json';
const knownGroups = ['Angular', 'Collaborators', 'GDE'];
@Injectable()
export class ContributorService {
contributors: Observable<ContributorGroup[]>;
constructor(private http: HttpClient) {
this.contributors = this.getContributors();
}
private getContributors() {
const contributors = this.http.get<{[key: string]: Contributor}>(contributorsPath).pipe(
// Create group map
map((contribs) => {
const contribMap: {[name: string]: Contributor[]} = {};
Object.keys(contribs).forEach((key) => {
const contributor = contribs[key];
contributor.groups.forEach((group) => {
const contribGroup = contribMap[group] || (contribMap[group] = []);
contribGroup.push(contributor);
});
});
return contribMap;
}),
// Flatten group map into sorted group array of sorted contributors
map((cmap) =>
Object.keys(cmap)
.map((key) => {
const order = knownGroups.indexOf(key);
return {
name: key,
order: order === -1 ? knownGroups.length : order,
contributors: cmap[key].sort(compareContributors),
};
})
.sort(compareGroups)
)
);
const connectableContributors = connectable(contributors, {
connector: () => new AsyncSubject(),
resetOnDisconnect: false,
});
connectableContributors.connect();
return connectableContributors;
}
}
function compareContributors(l: Contributor, r: Contributor) {
return l.name.toUpperCase() > r.name.toUpperCase() ? 1 : -1;
}
function compareGroups(l: ContributorGroup, r: ContributorGroup) {
return l.order === r.order ? (l.name > r.name ? 1 : -1) : l.order > r.order ? 1 : -1;
}

View file

@ -1,15 +0,0 @@
export interface ContributorGroup {
name: string;
order: number;
contributors: Contributor[];
}
export interface Contributor {
groups: string[];
name: string;
picture?: string;
website?: string;
twitter?: string;
bio?: string;
isFlipped?: boolean;
}

View file

@ -1,24 +0,0 @@
import { NgModule } from '@angular/core';
import { ROUTES} from '@angular/router';
import { ElementsLoader } from './elements-loader';
import {
ELEMENT_MODULE_LOAD_CALLBACKS,
ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES,
ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN
} from './element-registry';
import { LazyCustomElementComponent } from './lazy-custom-element.component';
@NgModule({
declarations: [ LazyCustomElementComponent ],
exports: [ LazyCustomElementComponent ],
providers: [
ElementsLoader,
{ provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, useValue: ELEMENT_MODULE_LOAD_CALLBACKS },
// Providing these routes as a signal to the build system that these modules should be
// registered as lazy-loadable.
// TODO(andrewjs): Provide first-class support for providing this.
{ provide: ROUTES, useValue: ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES, multi: true },
],
})
export class CustomElementsModule { }

View file

@ -1,47 +0,0 @@
import { VERSION } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { environment } from 'environments/environment';
import { DistTagComponent } from './dist-tag.component';
describe('DistTagComponent', () => {
let actualMode: string;
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [DistTagComponent] });
actualMode = environment.mode;
});
afterEach(() => {
(environment.mode as any) = actualMode;
});
describe('rendering', () => {
it('should display nothing (for stable versions)', () => {
environment.mode = 'stable';
const fixture = TestBed.createComponent(DistTagComponent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('');
});
it('should display the current major version (for archive versions)', () => {
environment.mode = 'archive';
const fixture = TestBed.createComponent(DistTagComponent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('@' + VERSION.major);
});
it('should display "@next" (for rc versions)', () => {
environment.mode = 'rc';
const fixture = TestBed.createComponent(DistTagComponent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('@next');
});
it('should display "@next" (for next versions)', () => {
environment.mode = 'next';
const fixture = TestBed.createComponent(DistTagComponent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('@next');
});
});
});

View file

@ -1,27 +0,0 @@
import { Component, VERSION } from '@angular/core';
import { environment } from 'environments/environment';
/**
* Display the dist-tag of Angular for installing from npm at the point these docs are generated.
*/
@Component({
selector: 'aio-angular-dist-tag',
template: '{{tag}}',
})
export class DistTagComponent {
tag: string;
constructor() {
switch (environment.mode) {
case 'stable':
this.tag = '';
break;
case 'next':
case 'rc':
this.tag = '@next';
break;
default:
this.tag = `@${VERSION.major}`;
}
}
}

View file

@ -1,11 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { WithCustomElementComponent } from '../element-registry';
import { DistTagComponent } from './dist-tag.component';
@NgModule({
declarations: [ DistTagComponent ],
})
export class DistTagModule implements WithCustomElementComponent {
customElementComponent: Type<any> = DistTagComponent;
}

View file

@ -1,71 +0,0 @@
import { InjectionToken, Type } from '@angular/core';
import { LoadChildrenCallback } from '@angular/router';
// Modules containing custom elements must be set up as lazy-loaded routes (loadChildren)
// TODO(andrewjs): This is a hack, Angular should have first-class support for preparing a module
// that contains custom elements.
export const ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES = [
{
selector: 'aio-announcement-bar',
loadChildren: () => import('./announcement-bar/announcement-bar.module').then(m => m.AnnouncementBarModule)
},
{
selector: 'aio-api-list',
loadChildren: () => import('./api/api-list.module').then(m => m.ApiListModule)
},
{
selector: 'aio-contributor-list',
loadChildren: () => import('./contributor/contributor-list.module').then(m => m.ContributorListModule)
},
{
selector: 'aio-file-not-found-search',
loadChildren: () => import('./search/file-not-found-search.module').then(m => m.FileNotFoundSearchModule)
},
{
selector: 'aio-angular-dist-tag',
loadChildren: () => import('./dist-tag/dist-tag.module').then(m => m.DistTagModule)
},
{
selector: 'aio-resource-list',
loadChildren: () => import('./resource/resource-list.module').then(m => m.ResourceListModule)
},
{
selector: 'aio-toc',
loadChildren: () => import('./toc/toc.module').then(m => m.TocModule)
},
{
selector: 'code-example',
loadChildren: () => import('./code/code-example.module').then(m => m.CodeExampleModule)
},
{
selector: 'code-tabs',
loadChildren: () => import('./code/code-tabs.module').then(m => m.CodeTabsModule)
},
{
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)
}
];
/**
* Interface expected to be implemented by all modules that declare a component that can be used as
* a custom element.
*/
export interface WithCustomElementComponent {
customElementComponent: Type<any>;
}
/** Injection token to provide the element path modules. */
export const ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN = new InjectionToken<
Map<string, LoadChildrenCallback>
>('aio/elements-map');
/** Map of possible custom element selectors to their lazy-loadable module paths. */
export const ELEMENT_MODULE_LOAD_CALLBACKS = new Map<string, LoadChildrenCallback>();
ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES.forEach(route => {
ELEMENT_MODULE_LOAD_CALLBACKS.set(route.selector, route.loadChildren);
});

View file

@ -1,255 +0,0 @@
import { Component, NgModule, Type } from '@angular/core';
import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing';
import { ElementsLoader } from './elements-loader';
import { ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, WithCustomElementComponent } from './element-registry';
interface Deferred {
resolve(): void;
reject(err: any): void;
}
describe('ElementsLoader', () => {
let elementsLoader: ElementsLoader;
beforeEach(() => {
const injector = TestBed.configureTestingModule({
providers: [
ElementsLoader,
{
provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN,
useValue: new Map<string, () => Promise<Type<WithCustomElementComponent>>>([
['element-a-selector', async () => createFakeCustomElementModule('element-a-module')],
['element-b-selector', async () => createFakeCustomElementModule('element-b-module')],
['element-c-selector', async () => createFakeCustomElementModule('element-c-module')],
]),
},
]
});
elementsLoader = injector.inject(ElementsLoader);
});
describe('loadContainedCustomElements()', () => {
let loadCustomElementSpy: jasmine.Spy;
beforeEach(() => loadCustomElementSpy = spyOn(elementsLoader, 'loadCustomElement'));
it('should attempt to load and register all contained elements', fakeAsync(() => {
expect(loadCustomElementSpy).not.toHaveBeenCalled();
const hostEl = document.createElement('div');
hostEl.innerHTML = `
<element-a-selector></element-a-selector>
<element-b-selector></element-b-selector>
`;
elementsLoader.loadContainedCustomElements(hostEl);
flushMicrotasks();
expect(loadCustomElementSpy).toHaveBeenCalledTimes(2);
expect(loadCustomElementSpy).toHaveBeenCalledWith('element-a-selector');
expect(loadCustomElementSpy).toHaveBeenCalledWith('element-b-selector');
}));
it('should attempt to load and register only contained elements', fakeAsync(() => {
expect(loadCustomElementSpy).not.toHaveBeenCalled();
const hostEl = document.createElement('div');
hostEl.innerHTML = `
<element-b-selector></element-b-selector>
`;
elementsLoader.loadContainedCustomElements(hostEl);
flushMicrotasks();
expect(loadCustomElementSpy).toHaveBeenCalledTimes(1);
expect(loadCustomElementSpy).toHaveBeenCalledWith('element-b-selector');
}));
it('should wait for all contained elements to load and register', fakeAsync(() => {
const deferreds = returnPromisesFromSpy(loadCustomElementSpy);
const hostEl = document.createElement('div');
hostEl.innerHTML = `
<element-a-selector></element-a-selector>
<element-b-selector></element-b-selector>
`;
const log: any[] = [];
elementsLoader.loadContainedCustomElements(hostEl).subscribe({
next: v => log.push(`emitted: ${v}`),
error: e => log.push(`errored: ${e}`),
complete: () => log.push('completed'),
});
flushMicrotasks();
expect(log).toEqual([]);
deferreds[0].resolve();
flushMicrotasks();
expect(log).toEqual([]);
deferreds[1].resolve();
flushMicrotasks();
expect(log).toEqual(['emitted: undefined', 'completed']);
}));
it('should fail if any of the contained elements fails to load and register', fakeAsync(() => {
const deferreds = returnPromisesFromSpy(loadCustomElementSpy);
const hostEl = document.createElement('div');
hostEl.innerHTML = `
<element-a-selector></element-a-selector>
<element-b-selector></element-b-selector>
`;
const log: any[] = [];
elementsLoader.loadContainedCustomElements(hostEl).subscribe({
next: v => log.push(`emitted: ${v}`),
error: e => log.push(`errored: ${e}`),
complete: () => log.push('completed'),
});
flushMicrotasks();
expect(log).toEqual([]);
deferreds[0].resolve();
flushMicrotasks();
expect(log).toEqual([]);
deferreds[1].reject('foo');
flushMicrotasks();
expect(log).toEqual(['errored: foo']);
}));
});
describe('loadCustomElement()', () => {
let definedSpy: jasmine.Spy;
let whenDefinedSpy: jasmine.Spy;
let whenDefinedDeferreds: Deferred[];
beforeEach(() => {
// `loadCustomElement()` uses the `window.customElements` API. Provide mocks for these tests.
definedSpy = spyOn(window.customElements, 'define');
whenDefinedSpy = spyOn(window.customElements, 'whenDefined');
whenDefinedDeferreds = returnPromisesFromSpy(whenDefinedSpy);
});
it('should be able to load and register an element', fakeAsync(() => {
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).toHaveBeenCalledTimes(1);
expect(definedSpy).toHaveBeenCalledWith('element-a-selector', jasmine.any(Function));
// Verify the right component was loaded/registered.
const Ctor = definedSpy.calls.argsFor(0)[1];
expect(Ctor.observedAttributes).toEqual(['element-a-module']);
}));
it('should wait until the element is defined', fakeAsync(() => {
let state = 'pending';
elementsLoader.loadCustomElement('element-b-selector').then(() => state = 'resolved');
flushMicrotasks();
expect(state).toBe('pending');
expect(whenDefinedSpy).toHaveBeenCalledTimes(1);
expect(whenDefinedSpy).toHaveBeenCalledWith('element-b-selector');
whenDefinedDeferreds[0].resolve();
flushMicrotasks();
expect(state).toBe('resolved');
}));
it('should not load and register the same element more than once', fakeAsync(() => {
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).toHaveBeenCalledTimes(1);
definedSpy.calls.reset();
// While loading/registering is still in progress:
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).not.toHaveBeenCalled();
definedSpy.calls.reset();
whenDefinedDeferreds[0].resolve();
// Once loading/registering is already completed:
let state = 'pending';
elementsLoader.loadCustomElement('element-a-selector').then(() => state = 'resolved');
flushMicrotasks();
expect(state).toBe('resolved');
expect(definedSpy).not.toHaveBeenCalled();
}));
it('should fail if defining the custom element fails', fakeAsync(() => {
let state = 'pending';
elementsLoader.loadCustomElement('element-b-selector').catch(e => state = `rejected: ${e}`);
flushMicrotasks();
expect(state).toBe('pending');
whenDefinedDeferreds[0].reject('foo');
flushMicrotasks();
expect(state).toBe('rejected: foo');
}));
it('should be able to load and register an element again if previous attempt failed',
fakeAsync(() => {
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).toHaveBeenCalledTimes(1);
definedSpy.calls.reset();
// While loading/registering is still in progress:
elementsLoader.loadCustomElement('element-a-selector').catch(() => undefined);
flushMicrotasks();
expect(definedSpy).not.toHaveBeenCalled();
whenDefinedDeferreds[0].reject('foo');
flushMicrotasks();
expect(definedSpy).not.toHaveBeenCalled();
// Once loading/registering has already failed:
elementsLoader.loadCustomElement('element-a-selector');
flushMicrotasks();
expect(definedSpy).toHaveBeenCalledTimes(1);
})
);
});
});
// TEST CLASSES/HELPERS
class FakeCustomElementModule implements WithCustomElementComponent {
static readonly modulePath: string;
customElementComponent: Type<any>;
}
function createFakeComponent(inputName: string): Type<any> {
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
@Component({inputs: [inputName]})
class FakeComponent {}
return FakeComponent;
}
function createFakeCustomElementModule(modulePath: string): typeof FakeCustomElementModule {
@NgModule({})
class FakeModule extends FakeCustomElementModule {
static override readonly modulePath = modulePath;
override customElementComponent = createFakeComponent(modulePath);
}
return FakeModule;
}
function returnPromisesFromSpy(spy: jasmine.Spy): Deferred[] {
const deferreds: Deferred[] = [];
spy.and.callFake(() => new Promise<void>((resolve, reject) => deferreds.push({resolve, reject})));
return deferreds;
}

View file

@ -1,78 +0,0 @@
import { createNgModule, Inject, Injectable, NgModuleRef, Type } from '@angular/core';
import { ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, WithCustomElementComponent } from './element-registry';
import { from, Observable, of } from 'rxjs';
import { createCustomElement } from '@angular/elements';
import { LoadChildrenCallback } from '@angular/router';
@Injectable()
export class ElementsLoader {
/** Map of unregistered custom elements and their respective module paths to load. */
private elementsToLoad: Map<string, LoadChildrenCallback>;
/** Map of custom elements that are in the process of being loaded and registered. */
private elementsLoading = new Map<string, Promise<void>>();
constructor(private moduleRef: NgModuleRef<any>,
@Inject(ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN) elementModulePaths: Map<string, LoadChildrenCallback>) {
this.elementsToLoad = new Map(elementModulePaths);
}
/**
* Queries the provided element for any custom elements that have not yet been registered with
* the browser. Custom elements that are registered will be removed from the list of unregistered
* elements so that they will not be queried in subsequent calls.
*/
loadContainedCustomElements(element: HTMLElement): Observable<void> {
const unregisteredSelectors = Array.from(this.elementsToLoad.keys())
.filter(s => element.querySelector(s));
if (!unregisteredSelectors.length) { return of(undefined); }
// Returns observable that completes when all discovered elements have been registered.
const allRegistered = Promise.all(unregisteredSelectors.map(s => this.loadCustomElement(s)));
return from(allRegistered.then(() => undefined));
}
/** Loads and registers the custom element defined on the `WithCustomElement` module factory. */
loadCustomElement(selector: string): Promise<void> {
if (this.elementsLoading.has(selector)) {
// The custom element is in the process of being loaded and registered.
return this.elementsLoading.get(selector) as Promise<void>;
}
if (this.elementsToLoad.has(selector)) {
// Load and register the custom element (for the first time).
const modulePathLoader = this.elementsToLoad.get(selector) as LoadChildrenCallback;
const loadedAndRegistered =
(modulePathLoader() as Promise<Type<WithCustomElementComponent>>)
.then(elementModule => {
const elementModuleRef = createNgModule(elementModule, this.moduleRef.injector);
const injector = elementModuleRef.injector;
const CustomElementComponent = elementModuleRef.instance.customElementComponent;
const CustomElement = createCustomElement(CustomElementComponent, {injector});
customElements.define(selector, CustomElement);
return customElements.whenDefined(selector);
})
.then(() => {
// The custom element has been successfully loaded and registered.
// Remove from `elementsLoading` and `elementsToLoad`.
this.elementsLoading.delete(selector);
this.elementsToLoad.delete(selector);
})
.catch(err => {
// The custom element has failed to load and register.
// Remove from `elementsLoading`.
// (Do not remove from `elementsToLoad` in case it was a temporary error.)
this.elementsLoading.delete(selector);
return Promise.reject(err);
});
this.elementsLoading.set(selector, loadedAndRegistered);
return loadedAndRegistered;
}
// The custom element has already been loaded and registered.
return Promise.resolve();
}
}

View file

@ -1,41 +0,0 @@
<h2>Where we'll be presenting:</h2>
<ng-container [ngSwitch]="!!upcomingEvents.length">
<div *ngSwitchCase="false">
<p>We don't have any upcoming speaking engagements at the moment.</p>
<p>
Until something comes up, make sure you check our <a href="https://www.youtube.com/angular">YouTube channel</a>
and follow us on <a href="https://twitter.com/angular">social media</a>.
</p>
<p>
If you want us to be part of your event reach out on <a href="mailto:devrel@angular.io">devrel&#64;angular.io</a>!
</p>
</div>
<ng-container *ngSwitchDefault>
<ng-container *ngTemplateOutlet="eventsTable; context: {$implicit: upcomingEvents}"></ng-container>
</ng-container>
</ng-container>
<br />
<h2>Where we already presented:</h2>
<ng-container *ngTemplateOutlet="eventsTable; context: {$implicit: pastEvents}"></ng-container>
<ng-template #eventsTable let-events>
<table class="is-full-width">
<thead>
<tr>
<th>Event</th>
<th>Start date</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let event of events">
<th [ngSwitch]="!!event.linkUrl">
<span *ngSwitchCase="false">{{ event.name }}</span>
<a *ngSwitchDefault href="{{ event.linkUrl }}">{{ event.name }}</a>
</th>
<td>{{ event.date.start }}</td>
</tr>
</tbody>
</table>
</ng-template>

View file

@ -1,104 +0,0 @@
import { Injector } from '@angular/core';
import { Subject } from 'rxjs';
import { AngularEvent, 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.length).toEqual(0);
});
it('should have no upcoming when first created', () => {
expect(component.upcomingEvents.length).toEqual(0);
});
describe('ngOnInit()', () => {
beforeEach(() => {
jasmine.clock().install();
// End of day on June 15
jasmine.clock().mockDate(new Date(Date.parse('2020-06-16') - 1));
component.ngOnInit();
});
afterEach(() => jasmine.clock().uninstall());
it('should separate past and upcoming events', () => {
eventsService.events.next([
createMockEvent('Upcoming event 1', {start: '2020-06-16'}),
createMockEvent('Upcoming event 3', {start: '2222-01-01'}),
createMockEvent('Past event 2', {start: '2020-06-13'}),
createMockEvent('Upcoming event 2', {start: '2020-06-17'}),
createMockEvent('Past event 1', {start: '2020-05-30'}),
createMockEvent('Past event 3', {start: '2020-06-14'}),
]);
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', () => {
eventsService.events.next([
createMockEvent('Past event 2', {start: '1999-12-13'}),
createMockEvent('Past event 4', {start: '2020-01-16'}),
createMockEvent('Past event 3', {start: '2020-01-15'}),
createMockEvent('Past event 1', {start: '1999-12-12'}),
]);
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', () => {
eventsService.events.next([
createMockEvent('Upcoming event 2', {start: '2020-12-13'}),
createMockEvent('Upcoming event 4', {start: '2021-01-16'}),
createMockEvent('Upcoming event 3', {start: '2021-01-15'}),
createMockEvent('Upcoming event 1', {start: '2020-12-12'}),
]);
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-15'}),
]);
expect(component.pastEvents).toEqual([]);
expect(component.upcomingEvents.map(evt => evt.name)).toEqual(jasmine.arrayWithExactContents(
['Ongoing event 1']));
});
});
// Helpers
class TestEventsService {
events = new Subject<AngularEvent[]>();
}
function createMockEvent(name: string, date: AngularEvent['date']): AngularEvent {
return {
name,
linkUrl: '',
date,
};
}
});

View file

@ -1,41 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { EventsService } from './events.service';
const DAY = 24 * 60 * 60 * 1000;
export interface AngularEvent {
name: string;
linkUrl?: string;
date: {
start: `${number}-${number}-${number}`; // Date string in the format: `YYYY-MM-DD`
};
}
@Component({
selector: 'aio-events',
templateUrl: 'events.component.html'
})
export class EventsComponent implements OnInit {
pastEvents: AngularEvent[] = [];
upcomingEvents: AngularEvent[] = [];
constructor(private eventsService: EventsService) { }
ngOnInit() {
this.eventsService.events.subscribe(events => {
this.pastEvents = events
.filter(event => isInThePast(event))
.sort((l: AngularEvent, r: AngularEvent) => (l.date.start < r.date.start) ? 1 : -1);
this.upcomingEvents = events
.filter(event => !isInThePast(event))
.sort((l: AngularEvent, r: AngularEvent) => (l.date.start < r.date.start) ? -1 : 1);
});
}
}
function isInThePast(event: AngularEvent): boolean {
return new Date(event.date.start).getTime() < Date.now() - DAY;
}

View file

@ -1,14 +0,0 @@
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 ],
providers: [ EventsService ],
})
export class EventsModule implements WithCustomElementComponent {
customElementComponent: Type<any> = EventsComponent;
}

View file

@ -1,56 +0,0 @@
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 ProgressEvent('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 ProgressEvent('404'));
eventsService.events.subscribe(results => {
expect(results).toEqual([]);
done();
});
});
});

View file

@ -1,35 +0,0 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {AsyncSubject, connectable, Observable, of} from 'rxjs';
import {catchError} from 'rxjs/operators';
import {AngularEvent} 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<AngularEvent[]>;
constructor(private http: HttpClient, private logger: Logger) {
this.events = this.getEvents();
}
private getEvents() {
const eventsSource = this.http.get<any>(eventsPath).pipe(
catchError((error) => {
this.logger.error(new Error(`${eventsPath} request failed: ${error.message}`));
return of([]);
})
);
const events = connectable(eventsSource, {
connector: () => new AsyncSubject(),
resetOnDisconnect: false,
});
events.connect();
return events;
}
}

View file

@ -1,67 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { LazyCustomElementComponent } from './lazy-custom-element.component';
import { ElementsLoader } from './elements-loader';
describe('LazyCustomElementComponent', () => {
let mockElementsLoader: jasmine.SpyObj<ElementsLoader>;
let mockLogger: MockLogger;
let fixture: ComponentFixture<LazyCustomElementComponent>;
beforeEach(() => {
mockElementsLoader = jasmine.createSpyObj<ElementsLoader>('ElementsLoader', [
'loadContainedCustomElements',
'loadCustomElement',
]);
const injector = TestBed.configureTestingModule({
declarations: [ LazyCustomElementComponent ],
providers: [
{ provide: ElementsLoader, useValue: mockElementsLoader },
{ provide: Logger, useClass: MockLogger },
],
});
mockLogger = injector.inject(Logger) as any;
fixture = TestBed.createComponent(LazyCustomElementComponent);
});
it('should set the HTML content based on the selector', () => {
const elem = fixture.nativeElement;
expect(elem.innerHTML).toBe('');
fixture.componentInstance.selector = 'foo-bar';
fixture.detectChanges();
expect(elem.innerHTML).toBe('<foo-bar></foo-bar>');
});
it('should load the specified custom element', () => {
expect(mockElementsLoader.loadCustomElement).not.toHaveBeenCalled();
fixture.componentInstance.selector = 'foo-bar';
fixture.detectChanges();
expect(mockElementsLoader.loadCustomElement).toHaveBeenCalledWith('foo-bar');
});
it('should log an error (and abort) if the selector is empty', () => {
fixture.detectChanges();
expect(mockElementsLoader.loadCustomElement).not.toHaveBeenCalled();
expect(mockLogger.output.error).toEqual([[jasmine.any(Error)]]);
expect(mockLogger.output.error[0][0].message).toBe('Invalid selector for \'aio-lazy-ce\': ');
});
it('should log an error (and abort) if the selector is invalid', () => {
fixture.componentInstance.selector = 'foo-bar><script></script><foo-bar';
fixture.detectChanges();
expect(mockElementsLoader.loadCustomElement).not.toHaveBeenCalled();
expect(mockLogger.output.error).toEqual([[jasmine.any(Error)]]);
expect(mockLogger.output.error[0][0].message).toBe(
'Invalid selector for \'aio-lazy-ce\': foo-bar><script></script><foo-bar');
});
});

View file

@ -1,28 +0,0 @@
import { Component, ElementRef, Input, OnInit } from '@angular/core';
import { Logger } from 'app/shared/logger.service';
import { ElementsLoader } from './elements-loader';
@Component({
selector: 'aio-lazy-ce',
template: '',
})
export class LazyCustomElementComponent implements OnInit {
@Input() selector = '';
constructor(
private elementRef: ElementRef,
private elementsLoader: ElementsLoader,
private logger: Logger,
) {}
ngOnInit() {
if (!this.selector || /[^\w-]/.test(this.selector)) {
this.logger.error(new Error(`Invalid selector for 'aio-lazy-ce': ${this.selector}`));
return;
}
this.elementRef.nativeElement.textContent = '';
this.elementRef.nativeElement.appendChild(document.createElement(this.selector));
this.elementsLoader.loadCustomElement(this.selector);
}
}

View file

@ -1,22 +0,0 @@
<!-- Content projection is used to get the content HTML provided to the component. -->
<span #content style="display: none"><ng-content></ng-content></span>
<span [ngSwitch]="mode">
<span *ngSwitchCase="'embedded'">
<div title="{{title}}">
<aio-embedded-stackblitz [src]="stackblitz"></aio-embedded-stackblitz>
</div>
<p *ngIf="enableDownload">
You can also <a [href]="zip" download title="Download example">download this example</a>.
</p>
</span>
<span *ngSwitchCase="'downloadOnly'">
<a [href]="zip" download title="{{title}}">{{title}}</a>
</span>
<span *ngSwitchDefault>
<a [href]="stackblitz" target="_blank" title="{{title}}">{{title}}</a>
<span *ngIf="enableDownload">
/ <a [href]="zip" download title="Download example">download example</a>
</span>
</span>
</span>

View file

@ -1,230 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component, DebugElement } from '@angular/core';
import { Location } from '@angular/common';
import { LiveExampleComponent, EmbeddedStackblitzComponent } from './live-example.component';
const defaultTestPath = '/test';
describe('LiveExampleComponent', () => {
let liveExampleDe: DebugElement;
let liveExampleComponent: LiveExampleComponent;
let fixture: ComponentFixture<HostComponent>;
let testPath: string;
//////// test helpers ////////
@Component({
selector: 'aio-host-comp',
template: '<live-example></live-example>'
})
class HostComponent { }
class TestLocation {
path() { return testPath; }
}
function getAnchors() {
return liveExampleDe.queryAll(By.css('a')).map(de => de.nativeElement as HTMLAnchorElement);
}
function getHrefs() { return getAnchors().map(a => a.href); }
function setHostTemplate(template: string) {
TestBed.overrideComponent(HostComponent, {set: {template}});
}
function testComponent(testFn: () => void) {
fixture = TestBed.createComponent(HostComponent);
liveExampleDe = fixture.debugElement.children[0];
liveExampleComponent = liveExampleDe.componentInstance;
// Trigger `ngAfterContentInit()`.
fixture.detectChanges();
testFn();
}
//////// tests ////////
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ HostComponent, LiveExampleComponent, EmbeddedStackblitzComponent ],
providers: [
{ provide: Location, useClass: TestLocation }
]
})
// Disable the <iframe> within the EmbeddedStackblitzComponent
.overrideComponent(EmbeddedStackblitzComponent, {set: {template: 'NO IFRAME'}});
testPath = defaultTestPath;
});
describe('when not embedded', () => {
function getLiveExampleAnchor() { return getAnchors()[0]; }
it('should create LiveExampleComponent', () => {
testComponent(() => {
expect(liveExampleComponent).withContext('LiveExampleComponent').toBeTruthy();
});
});
it('should have expected stackblitz & download hrefs', () => {
testPath = '/tutorial/tour-of-heroes/toh-pt1';
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/toh-pt1/stackblitz.html');
expect(hrefs[1]).toContain('/toh-pt1/toh-pt1.zip');
});
});
it('should have expected stackblitz & download hrefs even when path has # frag', () => {
testPath = '/tutorial/tour-of-heroes/toh-pt1#somewhere';
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/toh-pt1/stackblitz.html');
expect(hrefs[1]).toContain('/toh-pt1/toh-pt1.zip');
});
});
it('should have expected stackblitz & download hrefs even when path has ? params', () => {
testPath = '/tutorial/tour-of-heroes/toh-pt1?foo=1&bar="bar"';
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/toh-pt1/stackblitz.html');
expect(hrefs[1]).toContain('/toh-pt1/toh-pt1.zip');
});
});
it('should have expected stackblitz & download hrefs when has example directory (name)', () => {
testPath = '/guide/somewhere';
setHostTemplate('<live-example name="toh-pt1"></live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/toh-pt1/stackblitz.html');
expect(hrefs[1]).toContain('/toh-pt1/toh-pt1.zip');
});
});
it('should have expected stackblitz & download hrefs when has `stackblitz`', () => {
testPath = '/testing';
setHostTemplate('<live-example stackblitz="app-specs"></live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/testing/app-specs.stackblitz.html');
expect(hrefs[1]).toContain('/testing/app-specs.testing.zip');
});
});
it('should have expected stackblitz & download hrefs when has `name` & `stackblitz`', () => {
testPath = '/guide/somewhere';
setHostTemplate('<live-example name="testing" stackblitz="app-specs"></live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/testing/app-specs.stackblitz.html');
expect(hrefs[1]).toContain('/testing/app-specs.testing.zip');
});
});
it('should be embedded style by default', () => {
setHostTemplate('<live-example></live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain(defaultTestPath + '/stackblitz.html');
});
});
it('should not have a download link when `noDownload` attr present', () => {
setHostTemplate('<live-example noDownload></live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs.length).withContext('only the stackblitz live-example anchor').toBe(1);
expect(hrefs[0]).toContain('stackblitz.html');
});
});
it('should only have a download link when `downloadOnly` attr present', () => {
setHostTemplate('<live-example downloadOnly>download this</live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs.length).withContext('only the zip anchor').toBe(1);
expect(hrefs[0]).toContain('.zip'); });
});
it('should have default title when no title attribute or content', () => {
setHostTemplate('<live-example></live-example>');
testComponent(() => {
const expectedTitle = 'live example';
const anchor = getLiveExampleAnchor();
expect(anchor.textContent).withContext('anchor content').toBe(expectedTitle);
expect(anchor.getAttribute('title')).withContext('title').toBe(expectedTitle);
});
});
it('should add title when set `title` attribute', () => {
const expectedTitle = 'Great Example';
setHostTemplate(`<live-example title="${expectedTitle}"></live-example>`);
testComponent(() => {
const anchor = getLiveExampleAnchor();
expect(anchor.textContent).withContext('anchor content').toBe(expectedTitle);
expect(anchor.getAttribute('title')).withContext('title').toBe(expectedTitle);
});
});
it('should add title from <live-example> body', () => {
const expectedTitle = 'The Greatest Example';
setHostTemplate(`<live-example title="ignore this title">${expectedTitle}</live-example>`);
testComponent(() => {
const anchor = getLiveExampleAnchor();
expect(anchor.textContent).withContext('anchor content').toBe(expectedTitle);
expect(anchor.getAttribute('title')).withContext('title').toBe(expectedTitle);
});
});
it('should not duplicate the exampleDir on a zip when there is a / on the name', () => {
setHostTemplate('<live-example name="testing/ts"></live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/testing/ts/stackblitz.html');
expect(hrefs[1]).toContain('/testing/ts/testing.zip');
});
});
});
describe('when embedded', () => {
function getDownloadAnchor() {
const anchor = liveExampleDe.query(By.css('p > a'));
return anchor && anchor.nativeElement as HTMLAnchorElement;
}
function getEmbeddedStackblitzComponent() {
const compDe = liveExampleDe.query(By.directive(EmbeddedStackblitzComponent));
return compDe && compDe.componentInstance as EmbeddedStackblitzComponent;
}
it('should have hidden, embedded stackblitz', () => {
setHostTemplate('<live-example embedded></live-example>');
testComponent(() => {
expect(liveExampleComponent.mode).withContext('component is embedded').toBe('embedded');
expect(getEmbeddedStackblitzComponent()).withContext('EmbeddedStackblitzComponent').toBeTruthy();
});
});
it('should have download paragraph with expected anchor href', () => {
testPath = '/tutorial/tour-of-heroes/toh-pt1';
setHostTemplate('<live-example embedded></live-example>');
testComponent(() => {
expect(getDownloadAnchor().href).toContain('/toh-pt1/toh-pt1.zip');
});
});
it('should not have download paragraph when has `nodownload`', () => {
testPath = '/tutorial/tour-of-heroes/toh-pt1';
setHostTemplate('<live-example embedded nodownload></live-example>');
testComponent(() => {
expect(getDownloadAnchor()).toBeNull();
});
});
});
});

View file

@ -1,157 +0,0 @@
/* eslint-disable @angular-eslint/component-selector */
import { AfterContentInit, AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Location } from '@angular/common';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
import { AttrMap, boolFromValue, getAttrs, getAttrValue } from 'app/shared/attribute-utils';
const LIVE_EXAMPLE_BASE = CONTENT_URL_PREFIX + 'live-examples/';
const ZIP_BASE = CONTENT_URL_PREFIX + 'zips/';
/**
* Angular.io Live Example Embedded Component
*
* Renders a link to a live/host example of the doc page.
*
* All attributes and the text content are optional
*
* Usage:
* <live-example
* [name="..."] // name of the example directory
* [stackblitz="...""] // name of the stackblitz file (becomes part of zip file name as well)
* [embedded] // embed the stackblitz in the doc page, else display in new browser tab (default)
* [noDownload] // no downloadable zip option
* [downloadOnly] // just the zip
* [title="..."]> // text for live example link and tooltip
* text // higher precedence way to specify text for live example link and tooltip
* </live-example>
* Example:
* <p>Run <live-example>Try the live example</live-example></p>.
* // ~/resources/live-examples/{page}/stackblitz.json
*
* <p>Run <live-example name="toh-pt1">this example</live-example></p>.
* // ~/resources/live-examples/toh-pt1/stackblitz.json
*
* // Link to the default stackblitz in the toh-pt1 sample
* // The title overrides default ("live example") with "Tour of Heroes - Part 1"
* <p>Run <live-example name="toh-pt1" title="Tour of Heroes - Part 1"></live-example></p>.
* // ~/resources/live-examples/toh-pt1/stackblitz.json
*
* <p>Run <live-example stackblitz="minimal"></live-example></p>.
* // ~/resources/live-examples/{page}/minimal.stackblitz.json
*
* // Embed the current page's default stackblitz
* // Text within tag is "live example"
* // No title (no tooltip)
* <live-example embedded title=""></live-example>
* // ~/resources/live-examples/{page}/stackblitz.json
*
* // Displays within the document page as an embedded style stackblitz editor
* <live-example name="toh-pt1" embedded stackblitz="minimal">Tour of Heroes - Part 1</live-example>
* // ~/resources/live-examples/toh-pt1/minimal.stackblitz.json
*/
@Component({
selector: 'live-example',
templateUrl: 'live-example.component.html'
})
export class LiveExampleComponent implements AfterContentInit {
readonly mode: 'default' | 'embedded' | 'downloadOnly';
readonly enableDownload: boolean;
readonly stackblitz: string;
readonly zip: string;
title: string;
@ViewChild('content', { static: true })
private content: ElementRef;
constructor(elementRef: ElementRef, location: Location) {
const attrs = getAttrs(elementRef);
const exampleDir = this.getExampleDir(attrs, location.path(false));
const stackblitzName = this.getStackblitzName(attrs);
this.mode = this.getMode(attrs);
this.enableDownload = this.getEnableDownload(attrs);
this.stackblitz = this.getStackblitz(exampleDir, stackblitzName, this.mode === 'embedded');
this.zip = this.getZip(exampleDir, stackblitzName);
this.title = this.getTitle(attrs);
}
ngAfterContentInit() {
// Angular will sanitize this title when displayed, so it should be plain text.
const textContent = this.content.nativeElement.textContent.trim();
if (textContent) {
this.title = textContent;
}
}
private getEnableDownload(attrs: AttrMap) {
const downloadDisabled = boolFromValue(getAttrValue(attrs, 'noDownload'));
return !downloadDisabled;
}
private getExampleDir(attrs: AttrMap, path: string) {
let exampleDir = getAttrValue(attrs, 'name');
if (!exampleDir) {
// Take the last path segment, excluding query params and hash fragment.
const match = path.match(/[^/?#]+(?=\/?(?:\?|#|$))/);
exampleDir = match ? match[0] : 'index';
}
return exampleDir.trim();
}
private getMode(this: LiveExampleComponent, attrs: AttrMap): typeof this.mode {
const downloadOnly = boolFromValue(getAttrValue(attrs, 'downloadOnly'));
const isEmbedded = boolFromValue(getAttrValue(attrs, 'embedded'));
return downloadOnly ? 'downloadOnly'
: isEmbedded ? 'embedded' :
'default';
}
private getStackblitz(exampleDir: string, stackblitzName: string, isEmbedded: boolean) {
const urlQuery = isEmbedded ? '?ctl=1' : '';
return `${LIVE_EXAMPLE_BASE}${exampleDir}/${stackblitzName}stackblitz.html${urlQuery}`;
}
private getStackblitzName(attrs: AttrMap) {
const attrValue = (getAttrValue(attrs, 'stackblitz') || '').trim();
return attrValue && `${attrValue}.`;
}
private getTitle(attrs: AttrMap) {
return (getAttrValue(attrs, 'title') || 'live example').trim();
}
private getZip(exampleDir: string, stackblitzName: string) {
const zipName = exampleDir.split('/')[0];
return `${ZIP_BASE}${exampleDir}/${stackblitzName}${zipName}.zip`;
}
}
///// EmbeddedStackblitzComponent ///
/**
* Hides the <iframe> so we can test LiveExampleComponent without actually triggering
* a call to stackblitz to load the iframe
*/
@Component({
selector: 'aio-embedded-stackblitz',
template: '<iframe #iframe style="border: 0" width="100%" height="100%" title="live example"></iframe>',
styles: [ 'iframe { min-height: 400px; }' ]
})
export class EmbeddedStackblitzComponent implements AfterViewInit {
@Input() src: string;
@ViewChild('iframe', { static: true }) iframe: ElementRef;
ngAfterViewInit() {
// DEVELOPMENT TESTING ONLY
// this.src = 'https://angular.io/resources/live-examples/quickstart/ts/stackblitz.json';
if (this.iframe) {
// security: the `src` is always authored by the documentation team
// and is considered to be safe
this.iframe.nativeElement.src = this.src;
}
}
}

View file

@ -1,12 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EmbeddedStackblitzComponent, LiveExampleComponent } from './live-example.component';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule ],
declarations: [ LiveExampleComponent, EmbeddedStackblitzComponent ]
})
export class LiveExampleModule implements WithCustomElementComponent {
customElementComponent: Type<any> = LiveExampleComponent;
}

View file

@ -1,21 +0,0 @@
<div class="center-layout">
<div class="flex-center group-buttons">
<button *ngFor="let category of categories"
class="button mat-button filter-button"
[class.selected]="category.id == selectedCategory.id"
(click)="selectCategory(category.id)">{{category.title}}</button>
</div>
<div class="showcase">
<div *ngFor="let subCategory of selectedCategory?.subCategories">
<h2 class="subcategory-title" id="{{subCategory.id}}">{{subCategory.title}}</h2>
<div *ngFor="let resource of subCategory.resources">
<div class="resource-item">
<a class="resource-row-link" rel="noopener" target="_blank" [href]="resource.url">
<h3 class="resource-name">{{resource.title}}</h3>
<p class="resource-description">{{resource.desc || 'No Description'}}</p>
</a>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,119 +0,0 @@
import { Injector } from '@angular/core';
import { of } from 'rxjs';
import { ResourceListComponent } from './resource-list.component';
import { ResourceService } from './resource.service';
import { LocationService } from 'app/shared/location.service';
import { Category } from './resource.model';
// Testing the component class behaviors, independent of its template
// Let e2e tests verify how it displays.
describe('ResourceListComponent', () => {
let component: ResourceListComponent;
let injector: Injector;
let resourceService: TestResourceService;
let locationService: TestLocationService;
let categories: Category[];
beforeEach(() => {
injector = Injector.create({
providers: [
{provide: ResourceListComponent, deps: [ResourceService, LocationService] },
{provide: ResourceService, useClass: TestResourceService, deps: [] },
{provide: LocationService, useClass: TestLocationService, deps: [] }
]
});
locationService = injector.get(LocationService) as unknown as TestLocationService;
resourceService = injector.get(ResourceService) as unknown as TestResourceService;
categories = resourceService.testCategories;
});
it('should select the first category when no query string', () => {
component = getComponent();
expect(component.selectedCategory).toBe(categories[0]);
});
it('should select the first category when query string w/o "category" property', () => {
locationService.searchResult = { foo: 'development' };
component = getComponent();
expect(component.selectedCategory).toBe(categories[0]);
});
it('should select the first category when query category not found', () => {
locationService.searchResult = { category: 'foo' };
component = getComponent();
expect(component.selectedCategory).toBe(categories[0]);
});
it('should select the education category when query category is "education"', () => {
locationService.searchResult = { category: 'education' };
component = getComponent();
expect(component.selectedCategory).toBe(categories[1]);
});
it('should select the education category when query category is "EDUCATION" (case insensitive)', () => {
locationService.searchResult = { category: 'EDUCATION' };
component = getComponent();
expect(component.selectedCategory).toBe(categories[1]);
});
it('should set the query to the "education" category when user selects "education"', () => {
component = getComponent();
component.selectCategory('education');
expect(locationService.searchResult.category).toBe('education');
});
it('should set the query to the first category when user selects unknown name', () => {
component = getComponent();
component.selectCategory('education'); // a legit group that isn't the first
component.selectCategory('foo'); // not a legit group name
expect(locationService.searchResult.category).toBe('development');
});
//// Test Helpers ////
function getComponent(): ResourceListComponent {
const comp = injector.get(ResourceListComponent);
comp.ngOnInit();
return comp;
}
class TestResourceService {
testCategories = getTestData();
categories = of(this.testCategories);
}
interface SearchResult {
[index: string]: string;
}
class TestLocationService {
searchResult: SearchResult = {};
search = jasmine.createSpy('search').and.callFake(() => this.searchResult);
setSearch = jasmine.createSpy('setSearch')
.and.callFake((_label: string, result: SearchResult) => {
this.searchResult = result;
});
}
function getTestData(): Category[] {
return [
// Not interested in the sub-categories data in these tests
{
id: 'development',
title: 'Development',
order: 0,
subCategories: []
},
{
id: 'education',
title: 'Education',
order: 1,
subCategories: []
},
];
}
});

View file

@ -1,37 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Category } from './resource.model';
import { ResourceService } from './resource.service';
import { LocationService } from 'app/shared/location.service';
@Component({
selector: 'aio-resource-list',
templateUrl: 'resource-list.component.html'
})
export class ResourceListComponent implements OnInit {
categories: Category[];
selectedCategory: Category;
constructor(
private resourceService: ResourceService,
private locationService: LocationService) {
}
ngOnInit() {
const category = this.locationService.search().category || '';
// Not using async pipe because cats appear twice in template
// No need to unsubscribe because categories observable completes.
this.resourceService.categories.subscribe(cats => {
this.categories = cats;
this.selectCategory(category);
});
}
selectCategory(id: string) {
id = id.toLowerCase();
this.selectedCategory =
this.categories.find(category => category.id.toLowerCase() === id) || this.categories[0];
this.locationService.setSearch('', {category: this.selectedCategory.id});
}
}

View file

@ -1,14 +0,0 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ResourceListComponent } from './resource-list.component';
import { ResourceService } from './resource.service';
import { WithCustomElementComponent } from '../element-registry';
@NgModule({
imports: [ CommonModule ],
declarations: [ ResourceListComponent ],
providers: [ ResourceService ]
})
export class ResourceListModule implements WithCustomElementComponent {
customElementComponent: Type<any> = ResourceListComponent;
}

View file

@ -1,22 +0,0 @@
export interface Category {
id: string; // "education"
title: string; // "Education"
order: number; // 2
subCategories: SubCategory[];
}
export interface SubCategory {
id: string; // "books"
title: string; // "Books"
order: number; // 1
resources: Resource[];
}
export interface Resource {
category: string; // "Education"
subCategory: string; // "Books"
id: string; // "-KLI8vJ0ZkvWhqPembZ7"
desc: string; // "This books shows all the steps necessary for the development of SPA"
title: string; // "Practical Angular 2",
url: string; // "https://leanpub.com/practical-angular-2"
}

View file

@ -1,146 +0,0 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ResourceService } from './resource.service';
import { Category } from './resource.model';
describe('ResourceService', () => {
let injector: Injector;
let resourceService: ResourceService;
let httpMock: HttpTestingController;
beforeEach(() => {
injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
ResourceService
]
});
resourceService = injector.get(ResourceService);
httpMock = injector.get(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('should make a single connection to the server', () => {
const req = httpMock.expectOne({});
expect(req.request.url).toBe('generated/resources.json');
});
describe('#categories', () => {
let categories: Category[];
let testData: any;
beforeEach(() => {
testData = getTestResources();
httpMock.expectOne({}).flush(testData);
resourceService.categories.subscribe(results => categories = results);
});
it('categories observable should complete', () => {
let completed = false;
resourceService.categories.subscribe({complete: () => completed = true});
expect(completed).withContext('observable completed').toBe(true);
});
it('should reshape contributors.json to sorted category array', () => {
const actualIds = categories.map(c => c.id).join(',');
expect(actualIds).toBe('cat-1,cat-3');
});
it('should convert ids to canonical form', () => {
// canonical form is lowercase with dashes for spaces
const cat = categories[1];
const sub = cat.subCategories[0];
const res = sub.resources[0];
expect(cat.id).withContext('category id').toBe('cat-3');
expect(sub.id).withContext('subcat id').toBe('cat3-subcat2');
expect(res.id).withContext('resources id').toBe('cat3-subcat2-res1');
});
it('resource knows its category and sub-category titles', () => {
const cat = categories[1];
const sub = cat.subCategories[0];
const res = sub.resources[0];
expect(res.category).withContext('category title').toBe(cat.title);
expect(res.subCategory).withContext('subcategory title').toBe(sub.title);
});
it('should have expected SubCategories of "Cat 3"', () => {
const actualIds = categories[1].subCategories.map(s => s.id).join(',');
expect(actualIds).toBe('cat3-subcat2,cat3-subcat1');
});
it('should have expected sorted resources of "Cat 1:SubCat1"', () => {
const actualIds = categories[0].subCategories[0].resources.map(r => r.id).join(',');
expect(actualIds).toBe('a-a-a,s-s-s,z-z-z');
});
});
it('should do WHAT(?) if the request fails');
});
function getTestResources() {
return {
'Cat 3': {
order: 3,
subCategories: {
'Cat3 SubCat1': {
order: 2,
resources: {
'Cat3 SubCat1 Res1': {
desc: 'Meetup in Barcelona, Spain. ',
title: 'Angular Beers',
url: 'http://www.meetup.com/AngularJS-Beers/',
},
'Cat3 SubCat1 Res2': {
desc: 'Angular Camps in Barcelona, Spain.',
title: 'Angular Camp',
url: 'http://angularcamp.org/',
},
},
},
'Cat3 SubCat2': {
order: 1,
resources: {
'Cat3 SubCat2 Res1': {
desc: 'A community index of components and libraries',
title: 'Catalog of Angular Components & Libraries',
url: 'https://a/b/c',
},
},
},
},
},
'Cat 1': {
order: 1,
subCategories: {
'Cat1 SubCat1': {
order: 1,
resources: {
'S S S': {
desc: 'SSS',
title: 'Sssss',
url: 'http://s/s/s',
},
'A A A': {
desc: 'AAA',
title: 'Aaaa',
url: 'http://a/a/a',
},
'Z Z Z': {
desc: 'ZZZ',
title: 'Zzzzz',
url: 'http://z/z/z',
},
},
},
},
},
};
}

View file

@ -1,81 +0,0 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {AsyncSubject, connectable, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {Category, Resource, SubCategory} from './resource.model';
import {CONTENT_URL_PREFIX} from 'app/documents/document.service';
const resourcesPath = CONTENT_URL_PREFIX + 'resources.json';
@Injectable()
export class ResourceService {
categories: Observable<Category[]>;
constructor(private http: HttpClient) {
this.categories = this.getCategories();
}
private getCategories(): Observable<Category[]> {
const categoriesSrc = this.http.get<any>(resourcesPath).pipe(map((data) => mkCategories(data)));
const categories = connectable(categoriesSrc, {
connector: () => new AsyncSubject(),
resetOnDisconnect: false,
});
categories.connect();
return categories;
}
}
// Extract sorted Category[] from resource JSON data
function mkCategories(categoryJson: any): Category[] {
return Object.keys(categoryJson).map(catKey => {
const cat = categoryJson[catKey];
return {
id: makeId(catKey),
title: catKey,
order: cat.order,
subCategories: mkSubCategories(cat.subCategories, catKey)
} as Category;
})
.sort(compareCats);
}
// Extract sorted SubCategory[] from JSON category data
function mkSubCategories(subCategoryJson: any, catKey: string): SubCategory[] {
return Object.keys(subCategoryJson).map(subKey => {
const sub = subCategoryJson[subKey];
return {
id: makeId(subKey),
title: subKey,
order: sub.order,
resources: mkResources(sub.resources, subKey, catKey)
} as SubCategory;
})
.sort(compareCats);
}
// Extract sorted Resource[] from JSON subcategory data
function mkResources(resourceJson: any, subKey: string, catKey: string): Resource[] {
return Object.keys(resourceJson).map(resKey => {
const res = resourceJson[resKey];
res.category = catKey;
res.subCategory = subKey;
res.id = makeId(resKey);
return res as Resource;
})
.sort(compareTitles);
}
function compareCats(l: Category | SubCategory, r: Category | SubCategory) {
return l.order === r.order ? compareTitles(l, r) : l.order > r.order ? 1 : -1;
}
function compareTitles(l: {title: string}, r: {title: string}) {
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
}
function makeId(title: string) {
return title.toLowerCase().replace(/\s+/g, '-');
}

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