mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
394 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 710a24a7d0 | |||
| 0fad36eff2 | |||
| 291deac663 | |||
| f01b7183d2 | |||
| ac9cd6108f | |||
| 7f1c55755d | |||
| fafcb0d23f | |||
| 492ad4698a | |||
| 13bf5b7007 | |||
| ef974647b0 | |||
| db1a8ebdb4 | |||
| 6a13fd573d | |||
| 6cb3ade035 | |||
| 6c68f4ca71 | |||
| 4b81784317 | |||
| 6df5f0e0f5 | |||
| f6a9292943 | |||
| cfcce8c880 | |||
| cf0a0f3fab | |||
| 31a047f186 | |||
| f671f86ac2 | |||
| b07b39de78 | |||
| 221226e0a1 | |||
| 6bcdaec268 | |||
| f3b8a35933 | |||
| e480a9fd63 | |||
| 31a1575334 | |||
| baeb671334 | |||
| f763d2666f | |||
| 49dca36880 | |||
| 2580eccc2f | |||
| 98376f2c09 | |||
| f5307bf119 | |||
| 151a544277 | |||
| 4e200bf13b | |||
| 5de7575be8 | |||
| a523801b25 | |||
| 290e26a2ab | |||
| 95e4806ce3 | |||
| cc8dbb61c4 | |||
| 64d9e658a4 | |||
| b393e3727e | |||
| 78f817c358 | |||
| 60f979e01f | |||
| eb44643bdf | |||
| 561cb1d8da | |||
| 031a1a8c8e | |||
| 24c6b0eafb | |||
| a2a5646253 | |||
| d033540d0f | |||
| 8b6fd8dd18 | |||
| c8ff0be38f | |||
| a00beec2e0 | |||
| 76aba9ef0e | |||
| a55c8bb0e0 | |||
| 3831942771 | |||
| 30757bd4d5 | |||
| f5872c9921 | |||
| 7c066a4af4 | |||
| 6a1d4ed667 | |||
| e46162d3e4 | |||
| 0fbdb63bbc | |||
| e1f84a31dc | |||
| aa2d815648 | |||
| 54a7b33245 | |||
| 38aab6aa9d | |||
| 12ffb25e94 | |||
| cb5d9a480e | |||
| 825718f2b1 | |||
| 0b7e7fd3a5 | |||
| 22ff9cacce | |||
| 6b164dc83a | |||
| c01af4302f | |||
| 01efb1b807 | |||
| b16fc2610a | |||
| cfb42ee2c8 | |||
| 47756d81e5 | |||
| 90018f9327 | |||
| 7b60b5196f | |||
| 35e998effb | |||
| 158b1984bf | |||
| 27bf371e32 | |||
| 56ec37dcbc | |||
| d5fd614836 | |||
| e2f3ae51df | |||
| f61519ab90 | |||
| b651648db6 | |||
| a548c0333e | |||
| 7a95cccf50 | |||
| 44c48a4835 | |||
| 2cea80c6e2 | |||
| 7d6752a806 | |||
| 354340d643 | |||
| e3950b4e1f | |||
| 881bb4d719 | |||
| 6c7d2b6fe3 | |||
| 96d554d8e7 | |||
| e994305d70 | |||
| a87d50639b | |||
| 7e5d71de3e | |||
| bd8ac477c3 | |||
| 1ce0f7b07a | |||
| a08232793d | |||
| 8c3d41b5b3 | |||
| 387051b078 | |||
| d6fec2fc75 | |||
| 4626691198 | |||
| 5c6f3f8ec0 | |||
| 70d30c28e0 | |||
| 6240613477 | |||
| 227de98201 | |||
| a46c8cbf4e | |||
| 1d31a0c129 | |||
| bd0f811405 | |||
| f729215f0a | |||
| bd275b1b26 | |||
| 181d5755ee | |||
| e1c0493135 | |||
| 4f125c5f9a | |||
| c30dde257b | |||
| 1cc87b26cb | |||
| da429c59b6 | |||
| fc6b2f2669 | |||
| fecd141dfa | |||
| 6b83328577 | |||
| d4c20aca71 | |||
| a86661ff24 | |||
| 590c61889f | |||
| b44f2f9e47 | |||
| a48d65cdf4 | |||
| 42899c5aa0 | |||
| 2367b038fb | |||
| d8eab6f807 | |||
| dab522f011 | |||
| 8400533555 | |||
| 43acd44cf8 | |||
| cab7e003b3 | |||
| 2c3daebc5d | |||
| 91dbcb5eab | |||
| fa9bca23f7 | |||
| 51d73ed957 | |||
| 9c2be715a3 | |||
| c267f54bc3 | |||
| 83067b3ef2 | |||
| 09e905ad67 | |||
| 57404d4723 | |||
| 1c92609719 | |||
| e5bd6e9ccc | |||
| 5a816b8739 | |||
| c0d7bac111 | |||
| 3afc9d0cdb | |||
| de136105aa | |||
| 95accb07b4 | |||
| a2ba5482c3 | |||
| 842b2cceb6 | |||
| 6988a0070e | |||
| 236b448364 | |||
| 1f9ff84e28 | |||
| 7ff5152547 | |||
| 80e7a0f8fa | |||
| 739c0d27ec | |||
| a8e57a1fab | |||
| 32f417f77f | |||
| c99e5cf168 | |||
| 2ce2ad1c42 | |||
| 0d88e25311 | |||
| 79b21662fd | |||
| f9b226f9dc | |||
| 6d39551f53 | |||
| 5dd4bd6cf5 | |||
| 6d4271c340 | |||
| 0717c08b7d | |||
| 2837342852 | |||
| 1841d7903b | |||
| 90af00a07c | |||
| e06a25b6c3 | |||
| 8358dcd7e3 | |||
| cc13d6f1cd | |||
| 0754122232 | |||
| bf3281a6a5 | |||
| 356da9c9f6 | |||
| 4e4c678180 | |||
| 0f37b12a17 | |||
| 7c950794ea | |||
| c9a3708e55 | |||
| ddef3ac9a4 | |||
| 935c1816fd | |||
| 2e12e277c5 | |||
| 3bd85fb7b0 | |||
| af46256043 | |||
| 34a59967d7 | |||
| 792026bc7d | |||
| d2fca84bc4 | |||
| 1f5039bbd6 | |||
| 696f003553 | |||
| e5c3b25496 | |||
| b9ea2d6900 | |||
| abb4a9bad3 | |||
| d517a75c9b | |||
| 96fd73dc5d | |||
| fdd82857b1 | |||
| ddd9df68bb | |||
| 59b6ec6be8 | |||
| 3f456821b5 | |||
| 2aaddd3f64 | |||
| 6c58252521 | |||
| d8280ace7d | |||
| b2c38399f9 | |||
| 65ccaa4169 | |||
| 8156b3d4ec | |||
| 59a3475d77 | |||
| 6e82ae5871 | |||
| de3454a507 | |||
| 0451e7eabc | |||
| e355f05b9a | |||
| 595009ecf4 | |||
| 1d7d53caa3 | |||
| 6a11e9e8b4 | |||
| 877ba3e3c1 | |||
| 3861a73135 | |||
| e74b578bd9 | |||
| b14f0dc5d6 | |||
| 4461cefa4f | |||
| ce54ead2b5 | |||
| f6c2b39812 | |||
| 8e17dd719d | |||
| 57b9798cf6 | |||
| 2579c7121e | |||
| d7397fb29b | |||
| e40e55d902 | |||
| ffd0b4c30b | |||
| 5dd2822523 | |||
| 7e8a214474 | |||
| e92eda9a31 | |||
| 499c22251d | |||
| becbcdcff0 | |||
| 384d7aacd0 | |||
| 612986bcd3 | |||
| da1b71e0a5 | |||
| 6a01d62b9d | |||
| befd197933 | |||
| 9376e7f05e | |||
| 576338a9f5 | |||
| 8580542153 | |||
| 57630c3769 | |||
| f1d42cef87 | |||
| 48816e8c22 | |||
| d3cc0e998f | |||
| 13b16b905f | |||
| 40f75364d4 | |||
| 2b0698def0 | |||
| 6858149eb1 | |||
| a803c89a21 | |||
| 957effa404 | |||
| ea6d75291f | |||
| 403d217a59 | |||
| 1acff00468 | |||
| 4c878f90d2 | |||
| 938007df9d | |||
| cd903cc9f4 | |||
| 9b32d9148e | |||
| 0536b61bb8 | |||
| df68ad4731 | |||
| 6d0362cb44 | |||
| 1660f667e0 | |||
| 1dd8558f82 | |||
| c9b1ddff4d | |||
| 54bc384661 | |||
| bd9e91ecf7 | |||
| 4e00851c04 | |||
| 54fed68bbf | |||
| 114a2f8153 | |||
| 7002cfc811 | |||
| 0d23baa740 | |||
| 3c08f91a01 | |||
| 20993f7a87 | |||
| 6ade8c9c8c | |||
| e4df686ec6 | |||
| 1fa5020739 | |||
| d2df9fb59b | |||
| fe82292583 | |||
| 439e83a974 | |||
| 4ade911e66 | |||
| d2f8412322 | |||
| 00dee8384c | |||
| ceb7f1eaf0 | |||
| f069b3a71a | |||
| ec1e652f8a | |||
| a87586538a | |||
| 6a3a7b0d13 | |||
| 0c1cf7ac6b | |||
| 394c0fb719 | |||
| 6795cccbbb | |||
| 96ad3bf065 | |||
| 3f7d081112 | |||
| 2560115793 | |||
| 9060cebc02 | |||
| db81193ce6 | |||
| af6a75a837 | |||
| f206d1041f | |||
| f5aed23924 | |||
| 8473675db2 | |||
| 0c66e2424c | |||
| 9d565cd6d6 | |||
| 182d574fa3 | |||
| 569f57daa9 | |||
| dfc77e5f58 | |||
| 6005fe2d70 | |||
| 1dc14d9853 | |||
| 129b5d8bde | |||
| 7a064e372a | |||
| 81c315ec6e | |||
| 4dce8766f8 | |||
| 0b4c013aac | |||
| 958346107f | |||
| a207421ada | |||
| 7cc9f193fe | |||
| 30f993a356 | |||
| aaf00092c8 | |||
| 44f08baf63 | |||
| b4c1ffbfa1 | |||
| 4567a090c9 | |||
| fc3cde6cff | |||
| 1a8a5cf68f | |||
| 866d0f422d | |||
| b00dd23846 | |||
| 6fbe371b2c | |||
| edd060efbf | |||
| 1091239d2d | |||
| dc6ca34d4c | |||
| 33bd523be3 | |||
| 08553046bf | |||
| 603f30cab0 | |||
| eccbf36dbd | |||
| 419855629d | |||
| 37768da011 | |||
| 1e941f6f84 | |||
| 920f072824 | |||
| 37d627dbd4 | |||
| 1032c1e1a5 | |||
| 8e4a7ab52b | |||
| 3fec271fa3 | |||
| f9ef762bc7 | |||
| e87ac245e0 | |||
| 39a6f62551 | |||
| 96267922fe | |||
| ba37440cbf | |||
| 5d3f66d1a6 | |||
| 36993f3459 | |||
| d811d82a12 | |||
| c6e4cf12f0 | |||
| a318e6ac7c | |||
| c9cde3ab10 | |||
| 35fd10a9fd | |||
| e760fd7865 | |||
| 4e135947e6 | |||
| 6fe4f44b1d | |||
| 920820a1a8 | |||
| 6735f5f634 | |||
| 2d197f0c65 | |||
| e5720edb46 | |||
| eaf735da58 | |||
| bd1e78d746 | |||
| 81a287a79a | |||
| 2fa5e702de | |||
| 65c2f02ccb | |||
| a6be2e221f | |||
| 6ae68f39b9 | |||
| 450f360bd7 | |||
| c1dc86d186 | |||
| a58301f980 | |||
| b2ba678e73 | |||
| 2d98726ec9 | |||
| 030a68b3df | |||
| 6fd863d6e1 | |||
| ba9fc2419e | |||
| e90f5e5f22 | |||
| 8e5d54a3d5 | |||
| 54f7578d55 | |||
| a6fcf75423 | |||
| 99e7629159 | |||
| 6f19117a6a | |||
| ffe9b1fcc2 | |||
| 0792424df2 | |||
| 5c70148d20 | |||
| 65b460448e | |||
| 4cd1b3d38b | |||
| 4e9d6878a5 | |||
| 2bf088e7bb | |||
| 2e5ee1c198 | |||
| 371e26f6b3 | |||
| d50d3b85bd | |||
| 073ebfe09e | |||
| 926db6dd36 |
+4
-1
@@ -16,7 +16,6 @@ integration/cli-hello-world/node_modules
|
||||
integration/cli-hello-world-ivy-i18n/node_modules
|
||||
integration/cli-hello-world-lazy/node_modules
|
||||
integration/cli-hello-world-mocha/node_modules
|
||||
integration/cli-signal-inputs/node_modules
|
||||
integration/defer/node_modules
|
||||
integration/dynamic-compiler/node_modules
|
||||
integration/forms/node_modules
|
||||
@@ -39,3 +38,7 @@ integration/typings_test_ts49/node_modules
|
||||
integration/typings_test_ts50/node_modules
|
||||
integration/typings_test_ts51/node_modules
|
||||
integration/typings_test_ts52/node_modules
|
||||
|
||||
|
||||
# Temporarily ignore all of adev
|
||||
adev/
|
||||
@@ -1,5 +1,5 @@
|
||||
# Enable debugging tests with --config=debug
|
||||
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results --spawn_strategy=local
|
||||
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results
|
||||
|
||||
# Do not attempt to de-flake locally.
|
||||
# On CI we might set this to `3` to run with deflaking.
|
||||
@@ -56,6 +56,11 @@ 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
|
||||
@@ -69,6 +74,14 @@ 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 #
|
||||
###############################
|
||||
@@ -105,6 +118,10 @@ 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
|
||||
@@ -160,9 +177,6 @@ test:saucelabs --flaky_test_attempts=1
|
||||
|
||||
# --ng_perf will ask the Ivy compiler to produce performance results for each build.
|
||||
build --flag_alias=ng_perf=//packages/compiler-cli:ng_perf
|
||||
# --adev_fast will run adev build/serve in a faster mode, skipping things like prerendering
|
||||
# for local development.
|
||||
build --flag_alias=fast_adev=//adev:fast_build_mode
|
||||
|
||||
####################################################
|
||||
# User bazel configuration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Image metadata and config.
|
||||
# Ideally, the Node.js version should match what we use on CI.
|
||||
# See `executors > default-executor` in `.circleci/config.yml`.
|
||||
FROM cimg/node:18.13.0@sha256:45d0ee279f522c4562a464df71108104cc9b2187205333ffd0707d4f05589731
|
||||
FROM cimg/node:16.13-browsers@sha256:6c7a82701665dda03e92d682360fa2e4358e07dab8d1b6a7e48f7d36e3d98117
|
||||
|
||||
|
||||
LABEL name="Angular dev environment" \
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"devondcarew.bazel-code",
|
||||
"gkalpak.aio-docs-utils",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"xaver.clang-format",
|
||||
// The following extensions are useful when working on angular.io (i.e. inside the `aio/` directory).
|
||||
//"angular.ng-template",
|
||||
//"dbaeumer.vscode-eslint",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: 'Docs or angular.dev Bug Report'
|
||||
name: 'Docs or angular.io Bug Report'
|
||||
description: Report an issue in Angular's documentation or angular.io application
|
||||
|
||||
body:
|
||||
|
||||
@@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4
|
||||
- uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3
|
||||
with:
|
||||
path: |
|
||||
./node_modules/
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# This workflow builds the previews for pull requests when a certain label is applied.
|
||||
# The actual deployment happens as part of a dedicated second workflow to avoid security
|
||||
# issues where the building would otherwise occur in an authorized context where secrets
|
||||
# could be leaked. More details can be found here:
|
||||
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Build adev for preview deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
adev-build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'adev: preview') ||
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build //adev:build
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/adev/build/browser'
|
||||
@@ -1,50 +0,0 @@
|
||||
# This workflow runs whenever the ADEV build workflow has completed. Deployment happens
|
||||
# as part of a dedicated second workflow to avoid security issues where the building would
|
||||
# otherwise occur in an authorized context where secrets could be leaked.
|
||||
#
|
||||
# More details can be found here:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Deploying adev preview to Firebase
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build adev for preview deployment']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
# Needed in order to be able to comment on the pull request.
|
||||
pull-requests: write
|
||||
# Needed in order to checkout the repository
|
||||
contents: read
|
||||
# Needed in order to retrieve the artifacts from the previous job
|
||||
actions: read
|
||||
|
||||
env:
|
||||
PREVIEW_PROJECT: ng-dev-previews
|
||||
PREVIEW_SITE: ng-dev-previews-fw
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
|
||||
with:
|
||||
token: '${{secrets.GITHUB_TOKEN}}'
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: ./
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
firebase-config-dir: './adev'
|
||||
firebase-public-dir: './adev/build/browser'
|
||||
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
|
||||
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
|
||||
@@ -0,0 +1,42 @@
|
||||
# This workflow builds the AIO previews for pull requests when a certain label is applied.
|
||||
# The actual deployment happens as part of a dedicated second workflow to avoid security
|
||||
# issues where the building would otherwise occur in an authorized context where secrets
|
||||
# could be leaked. More details can be found here:
|
||||
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Build AIO app for preview deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
aio-build:
|
||||
runs-on: ubuntu-latest
|
||||
# We only want to build and deploy the AIO app if the `aio: preview` label has been
|
||||
# added, or if the label is already applied and new changes have been made in the PR.
|
||||
if: |
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'aio: preview') ||
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'aio: preview'))
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
# Build the web package. Note: We run Bazel from a low-resource Github action container,
|
||||
# so we manually need to instruct Bazel to run more actions concurrently as by default
|
||||
# the number of concurrent actions is determined based on the host resources.
|
||||
- run: bazel build //aio:build --jobs=32 --announce_rc --verbose_failures
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
workflow-artifact-name: 'aio'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/aio/build'
|
||||
@@ -0,0 +1,44 @@
|
||||
# This workflow runs whenever the AIO build workflow has completed. Deployment happens
|
||||
# as part of a dedicated second workflow to avoid security issues where the building would
|
||||
# otherwise occur in an authorized context where secrets could be leaked.
|
||||
#
|
||||
# More details can be found here:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Deploying AIO preview to Firebase
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build AIO app for preview deployment']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
# Needed in order to be able to comment on the pull request.
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
PREVIEW_PROJECT: ng-dev-previews
|
||||
PREVIEW_SITE: ng-dev-previews-fw
|
||||
|
||||
jobs:
|
||||
aio-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: aio/
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
npx -y firebase-tools@latest target:clear --project ${{env.PREVIEW_PROJECT}} hosting aio
|
||||
npx -y firebase-tools@latest target:apply --project ${{env.PREVIEW_PROJECT}} hosting aio ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'aio'
|
||||
firebase-config-dir: './aio/'
|
||||
firebase-public-dir: './aio/dist'
|
||||
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
|
||||
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
|
||||
@@ -13,9 +13,9 @@ jobs:
|
||||
assistant_to_the_branch_manager:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
exit: true
|
||||
|
||||
# Indicate that the benchmark command was received.
|
||||
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
|
||||
with:
|
||||
comment-id: ${{github.event.comment.id}}
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- uses: alessbell/pull-request-comment-branch@aad01d65d6982b8eacabed5e9a684cd8ceb98da6 # v1.1
|
||||
id: comment-branch
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
# Specify repository as the PR branch might be from a fork.
|
||||
repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}}
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
id: benchmark
|
||||
name: Running benchmark
|
||||
|
||||
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
|
||||
with:
|
||||
issue-number: ${{github.event.issue.number}}
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
saucelabs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
env:
|
||||
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }}
|
||||
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
# Checking out the pull request commit is intended here as we need to run the changed code tests.
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Starting Saucelabs tunnel service
|
||||
run: ./tools/saucelabs/sauce-service.sh run &
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
|
||||
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
|
||||
# test in all supported browsers on Saucelabs anyway until this job can be removed.
|
||||
- name: Preparing Bazel-generated fixtures required in legacy tests
|
||||
run: |
|
||||
yarn bazel build //packages/core/test:downleveled_es5_fixture //packages/common/locales
|
||||
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
|
||||
mkdir -p dist/legacy-test-out/core/test/reflection/
|
||||
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
|
||||
dist/legacy-test-out/core/test/reflection/es5_downleveled_inheritance_fixture.js
|
||||
# Locale files are needed for i18n tests running within Saucelabs. These are added
|
||||
# directly as sources so that the TypeScript compilation of `/packages/tsconfig.json`
|
||||
# can succeed. Note that the base locale and currencies files are checked-in, so
|
||||
# we do not need to re-generate those through Bazel.
|
||||
mkdir -p packages/common/locales/extra
|
||||
cp dist/bin/packages/common/locales/*.ts packages/common/locales
|
||||
cp dist/bin/packages/common/locales/extra/*.ts packages/common/locales/extra
|
||||
- name: Build bundle of tests to run on Saucelabs
|
||||
run: node tools/legacy-saucelabs/build-saucelabs-test-bundle.mjs
|
||||
- name: Wait and confirm Saucelabs tunnel has connected
|
||||
run: ./tools/saucelabs/sauce-service.sh ready-wait
|
||||
timeout-minutes: 3
|
||||
- name: Running tests on Saucelabs.
|
||||
run: KARMA_WEB_TEST_MODE=SL_REQUIRED yarn karma start ./karma-js.conf.js --single-run
|
||||
- name: Stop Saucelabs tunnel service
|
||||
run: ./tools/saucelabs/sauce-service.sh stop
|
||||
+103
-109
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
- 'remove-aio-stuff'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
@@ -24,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -36,6 +35,8 @@ jobs:
|
||||
run: yarn install --frozen-lockfile --cwd aio
|
||||
- name: Check code lint
|
||||
run: yarn -s tslint
|
||||
- name: Check code lint in aio
|
||||
run: yarn -s --cwd aio lint
|
||||
- name: Check for circular dependencies
|
||||
run: yarn -s ts-circular-deps:check
|
||||
- name: Validate pull approve configuration
|
||||
@@ -59,13 +60,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Run unit tests
|
||||
@@ -77,34 +78,103 @@ jobs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile --network-timeout 100000
|
||||
- name: Run CI tests for framework
|
||||
run: yarn test:ci
|
||||
|
||||
adev:
|
||||
aio:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules for aio
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Run AIO tests with upstream packages
|
||||
run: yarn --cwd aio test:ci
|
||||
- name: Check generated bundle sizes
|
||||
run: yarn --cwd aio payload-size
|
||||
|
||||
aio-local:
|
||||
runs-on:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev in fast mode to ensure it continues to work
|
||||
run: yarn bazel build //adev:build --fast_adev
|
||||
- name: Run tests
|
||||
run: yarn bazel test //adev:test
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Run AIO tests with local packages
|
||||
run: yarn --cwd aio test-local:ci
|
||||
- name: Check generated bundle sizes
|
||||
run: yarn --cwd aio payload-size aio-local
|
||||
|
||||
aio-deploy:
|
||||
needs: [aio]
|
||||
if: needs.aio.result == 'success' && github.event_name == 'push'
|
||||
runs-on:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules for aio
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Set the stable branch environment variable
|
||||
id: latest_version
|
||||
run: |
|
||||
echo "CI_STABLE_BRANCH=$(npm info @angular/core dist-tags.latest | sed -r 's/^\s*([0-9]+\.[0-9]+)\.[0-9]+.*$/\1.x/')" >> $GITHUB_OUTPUT
|
||||
echo "CI_BRANCH=$(echo ${{ github.event.ref }} | cut -d '/' -f 3)" >> $GITHUB_OUTPUT
|
||||
- name: Deploy aio to production
|
||||
env:
|
||||
CI_AIO_MIN_PWA_SCORE: '95'
|
||||
CI_COMMIT: ${{ github.event.after }}
|
||||
CI_REPO_NAME: 'angular'
|
||||
CI_PULL_REQUEST: 'false'
|
||||
CI_REPO_OWNER: 'angular'
|
||||
CI_BUILD_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
CI_STABLE_BRANCH: ${{ steps.latest_version.outputs.CI_STABLE_BRANCH }}
|
||||
CI_BRANCH: ${{ steps.latest_version.outputs.CI_BRANCH }}
|
||||
CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN: ${{ secrets.AIO_DEPLOY_TOKEN }}
|
||||
CI_SECRET_PAYLOAD_FIREBASE_TOKEN: ${{ secrets.AIO_PAYLOAD_TOKEN }}
|
||||
run: yarn --cwd aio deploy-production
|
||||
- name: Notify about failed deployment
|
||||
if: ${{ failure() }}
|
||||
uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v=v1.24.0
|
||||
with:
|
||||
channel-id: 'C07DT5M6V,CKV1F72BG'
|
||||
slack-message: 'Deploy to aio job failed for ${{ github.base_ref }} branch failed on build ${{ github.event.after }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.ANGULAR_ROBOT_SLACK_TOKEN }}
|
||||
|
||||
publish-snapshots:
|
||||
if: github.event_name == 'push'
|
||||
@@ -112,13 +182,13 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
|
||||
@@ -130,7 +200,7 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -138,9 +208,9 @@ jobs:
|
||||
./packages/zone.js/node_modules
|
||||
./packages/zone.js/test/typings/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: |
|
||||
@@ -148,26 +218,28 @@ jobs:
|
||||
//packages/zone.js/bundles:zone.umd.js \
|
||||
//packages/zone.js:npm_package \
|
||||
//packages/zone.js/test/closure:closure_js \
|
||||
//packages/zone.js:zone_externs
|
||||
|
||||
- run: |
|
||||
rm -Rf packages/zone.js/build
|
||||
rm -Rf packages/zone.js/test/extra/*.umd.js
|
||||
|
||||
mkdir -p packages/zone.js/build/
|
||||
mkdir -p packages/zone.js/build/test/
|
||||
mkdir -p packages/zone.js/test/
|
||||
|
||||
cp dist/bin/packages/zone.js/bundles/zone.umd.js packages/zone.js/build/zone.umd.js
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-mix.umd.js ./packages/zone.js/test/extra/
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-patch-electron.umd.js ./packages/zone.js/test/extra/
|
||||
cp dist/bin/packages/zone.js/test/closure/zone.closure.mjs ./packages/zone.js/build/test/zone.closure.mjs
|
||||
cp dist/bin/packages/zone.js/zone_externs.js ./packages/zone.js/build/zone_externs.js
|
||||
|
||||
# Install
|
||||
- run: yarn --cwd packages/zone.js install --frozen-lockfile --non-interactive
|
||||
# Run zone.js tools tests
|
||||
- run: yarn --cwd packages/zone.js promisetest
|
||||
- run: yarn --cwd packages/zone.js promisefinallytest
|
||||
- run: yarn --cwd packages/zone.js jest:test
|
||||
- run: yarn --cwd packages/zone.js jest:nodetest
|
||||
- run: yarn --cwd packages/zone.js electrontest
|
||||
- run: yarn --cwd packages/zone.js closuretest
|
||||
- run: yarn --cwd packages/zone.js/test/typings install --frozen-lockfile --non-interactive
|
||||
- run: yarn --cwd packages/zone.js/test/typings test
|
||||
|
||||
@@ -179,7 +251,7 @@ jobs:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
# Checking out the pull request commit is intended here as we need to run the changed code tests.
|
||||
@@ -187,94 +259,16 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Set up Sauce Tunnel Daemon
|
||||
run: yarn bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
|
||||
env:
|
||||
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }}
|
||||
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
- name: Run all saucelabs bazel tests
|
||||
run: |
|
||||
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
|
||||
yarn bazel test --config=saucelabs --jobs=$JOBS ${TESTS}
|
||||
|
||||
saucelabs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
env:
|
||||
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- name: Starting Saucelabs tunnel service
|
||||
run: ./tools/saucelabs/sauce-service.sh run &
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
|
||||
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
|
||||
# test in all supported browsers on Saucelabs anyway until this job can be removed.
|
||||
- name: Preparing Bazel-generated fixtures required in legacy tests
|
||||
run: |
|
||||
yarn bazel build //packages/core/test:downleveled_es5_fixture //packages/common/locales
|
||||
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
|
||||
mkdir -p dist/legacy-test-out/core/test/reflection/
|
||||
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
|
||||
dist/legacy-test-out/core/test/reflection/es5_downleveled_inheritance_fixture.js
|
||||
# Locale files are needed for i18n tests running within Saucelabs. These are added
|
||||
# directly as sources so that the TypeScript compilation of `/packages/tsconfig.json`
|
||||
# can succeed. Note that the base locale and currencies files are checked-in, so
|
||||
# we do not need to re-generate those through Bazel.
|
||||
mkdir -p packages/common/locales/extra
|
||||
cp dist/bin/packages/common/locales/*.ts packages/common/locales
|
||||
cp dist/bin/packages/common/locales/extra/*.ts packages/common/locales/extra
|
||||
- name: Build bundle of tests to run on Saucelabs
|
||||
run: node tools/legacy-saucelabs/build-saucelabs-test-bundle.mjs
|
||||
- name: Wait and confirm Saucelabs tunnel has connected
|
||||
run: ./tools/saucelabs/sauce-service.sh ready-wait
|
||||
timeout-minutes: 3
|
||||
- name: Running tests on Saucelabs.
|
||||
run: KARMA_WEB_TEST_MODE=SL_REQUIRED yarn karma start ./karma-js.conf.js --single-run
|
||||
- name: Stop Saucelabs tunnel service
|
||||
run: ./tools/saucelabs/sauce-service.sh stop
|
||||
|
||||
adev-deploy:
|
||||
needs: [adev]
|
||||
if: needs.adev.result == 'success' && github.event_name == 'push' && github.ref_name == 'main'
|
||||
env:
|
||||
DEPLOY_PROJECT: angular-dev-site
|
||||
DEPLOY_SITE: angular-dev-site
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
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
|
||||
run: |
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{ env.DEPLOY_PROJECT }} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{ env.DEPLOY_PROJECT }} hosting angular-docs ${{ env.DEPLOY_SITE }}
|
||||
- name: Deploy to firebase
|
||||
uses: FirebaseExtended/action-hosting-deploy@120e124148ab7016bec2374e5050f15051255ba2 # v0.7.1
|
||||
with:
|
||||
entryPoint: './adev'
|
||||
firebaseServiceAccount: ${{ secrets.ANGULAR_DEV_SITE_DEPLOY }}
|
||||
projectId: ${{ env.DEPLOY_PROJECT }}
|
||||
channelId: live
|
||||
|
||||
@@ -12,14 +12,14 @@ jobs:
|
||||
labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
post_approval_changes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Feature request triage bot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 14:00 every day
|
||||
- cron: '0 14 * * *'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
feature_triage:
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/feature-request@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
@@ -13,8 +13,8 @@ jobs:
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
run-tests-guide-url: http://go/angular-g3sync-start
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
status:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -22,15 +22,15 @@ jobs:
|
||||
version: ['https://next.angular.io/', 'https://angular.io/']
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules in aio
|
||||
run: yarn install --frozen-lockfile --cwd aio
|
||||
- name: Run basic e2e and deployment config tests.
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
|
||||
@@ -25,12 +25,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 'Checkout code'
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
|
||||
uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
uses: github/codeql-action/upload-sarif@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
# Setting `persist-credentials: false` prevents the github-action account from being the
|
||||
# account that is attempted to be used for authentication, instead the remote is set to
|
||||
@@ -28,11 +28,11 @@ jobs:
|
||||
# This is needed as otherwise the PR creation will fail with `shallow update not allowed` when the forked branch is not in sync.
|
||||
fetch-depth: 0
|
||||
- name: Generate CLI help
|
||||
run: node adev/scripts/update-cli-help/index.mjs
|
||||
run: node aio/scripts/update-cli-help/index.mjs
|
||||
env:
|
||||
ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN: ${{ secrets.ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN }}
|
||||
- name: Create a PR (if necessary)
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@e6e98d32d7d4d350ecf4ec10ad9c7e75198daabd
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
branch-prefix: update-cli-help
|
||||
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Update the `events.json` file that powers the [AIO events page](https://angular.io/events) (if
|
||||
# necessary) and create a pull request.
|
||||
#
|
||||
# For more details on the overall process, see
|
||||
# [aio/scripts/generate-events/README.md](../../aio/scripts/generate-events/README.md).
|
||||
|
||||
name: Update AIO events
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
schedule:
|
||||
# Run every day at 15:00.
|
||||
- cron: '0 15 * * *'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_events:
|
||||
name: Update `events.json` (if necessary)
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
# Setting `persist-credentials: false` prevents the github-action account from being the
|
||||
# account that is attempted to be used for authentication, instead the remote is set to
|
||||
# an authenticated URL.
|
||||
persist-credentials: false
|
||||
# This is needed as otherwise the PR creation will fail with `shallow update not allowed` when the forked branch is not in sync.
|
||||
fetch-depth: 0
|
||||
- name: Generate `events.json`
|
||||
run: node aio/scripts/generate-events/index.mjs --ignore-invalid-dates
|
||||
- name: Create a PR (if necessary)
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
branch-prefix: docs-update-events
|
||||
pr-title: 'docs: update events'
|
||||
pr-description: |
|
||||
Generated `events.json` with the latest events retrieved from the Firebase DB.
|
||||
pr-labels: |
|
||||
action: review
|
||||
area: docs
|
||||
target: patch
|
||||
angular-robot-token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
|
||||
@@ -57,7 +57,3 @@ baseline.json
|
||||
# Husky
|
||||
.husky/_
|
||||
aio/content/examples/.DS_Store
|
||||
|
||||
|
||||
# Ignore cache created with the Angular CLI.
|
||||
.angular/
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set +e
|
||||
|
||||
yarn -s ng-dev commit-message pre-commit-validate --file $1 2>/dev/null
|
||||
|
||||
+4
-1
@@ -1,8 +1,11 @@
|
||||
set +e
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set +e
|
||||
yarn -s ng-dev format staged 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "WARNING: failed to run file formatting (ng-dev format staged)"
|
||||
fi
|
||||
|
||||
|
||||
exit 0;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set +e
|
||||
|
||||
yarn -s ng-dev commit-message restore-commit-message-draft $1 $2 2>/dev/null
|
||||
|
||||
+15
-8
@@ -5,25 +5,32 @@ import {FormatConfig} from '@angular/ng-dev';
|
||||
*/
|
||||
export const format: FormatConfig = {
|
||||
'prettier': {
|
||||
'matchers': ['**/*.{yaml,yml}'],
|
||||
},
|
||||
'clang-format': {
|
||||
'matchers': [
|
||||
'**/*.{yaml,yml}',
|
||||
'**/*.{js,ts}',
|
||||
|
||||
// Do not format d.ts files as they are generated
|
||||
'!**/*.d.ts',
|
||||
// TODO: burn down format failures and remove aio and integration exceptions.
|
||||
'!aio/**',
|
||||
'!integration/**',
|
||||
// Both third_party and .yarn are directories containing copied code which should
|
||||
// not be modified.
|
||||
'!third_party/**',
|
||||
'!.yarn/**',
|
||||
// Do not format d.ts files as they are generated
|
||||
'!**/*.d.ts',
|
||||
// Do not format generated ng-dev script
|
||||
'!dev-infra/ng-dev.js',
|
||||
'!dev-infra/build-worker.js',
|
||||
// Do not format compliance test-cases since they must match generated code
|
||||
'!packages/compiler-cli/test/compliance/test_cases/**/*.js',
|
||||
// Do not format the locale files which are checked-in for Google3, but generated using
|
||||
// the `generate-locales-tool` from `packages/common/locales`.
|
||||
'!packages/core/src/i18n/locale_en.ts',
|
||||
'!packages/common/locales/closure-locale.ts',
|
||||
'!packages/common/src/i18n/currencies.ts',
|
||||
// Test cases contain non valid code.
|
||||
'!packages/compiler-cli/test/compliance/test_cases/**/*.{js,ts}',
|
||||
// Ignore AIO as its pending removal.
|
||||
'!aio/**/*.{js,ts}',
|
||||
// Temporarily disable formatting for adev
|
||||
'!adev/**',
|
||||
],
|
||||
},
|
||||
'buildifier': true,
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
{
|
||||
"syncedFilePatterns": [
|
||||
"LICENSE",
|
||||
"modules/benchmarks/**",
|
||||
"packages/**"
|
||||
],
|
||||
"separateFilePatterns": [
|
||||
"packages/core/primitives/**"
|
||||
],
|
||||
"syncedFilePatterns": ["LICENSE", "modules/benchmarks/**", "packages/**"],
|
||||
"alwaysExternalFilePatterns": [
|
||||
"packages/*",
|
||||
"packages/bazel/*",
|
||||
@@ -29,6 +22,7 @@
|
||||
"packages/docs/**",
|
||||
"packages/elements/schematics/**",
|
||||
"packages/examples/**",
|
||||
"packages/language-service/**",
|
||||
"packages/localize/**",
|
||||
"packages/private/**",
|
||||
"packages/service-worker/**",
|
||||
@@ -37,6 +31,8 @@
|
||||
"packages/http/**",
|
||||
"**/.gitignore",
|
||||
"**/.gitkeep",
|
||||
"**/yarn.lock",
|
||||
"**/package.json",
|
||||
"**/third_party/**",
|
||||
"**/tsconfig-build.json",
|
||||
"**/tsconfig-tsec.json",
|
||||
|
||||
@@ -17,9 +17,4 @@ export const pullRequest: PullRequestConfig = {
|
||||
// the `bazel` package is not considered part of the public API so that features
|
||||
// can land in patch branches.
|
||||
targetLabelExemptScopes: ['dev-infra', 'docs-infra', 'bazel'],
|
||||
// enables specific validations during the pull request merge process
|
||||
validators: {
|
||||
assertEnforceTested: true,
|
||||
assertIsolatedSeparateFiles: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"tabs": false,
|
||||
"embeddedLanguageFormatting": "off",
|
||||
"singleQuote": true,
|
||||
"semicolon": true,
|
||||
"quoteProps": "preserve",
|
||||
|
||||
+46
-102
@@ -140,7 +140,7 @@ groups:
|
||||
- alxhub
|
||||
- crisbeto
|
||||
- ~jelbourn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
|
||||
# =========================================================
|
||||
# Framework: Compiler
|
||||
@@ -166,9 +166,8 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- JoostK
|
||||
- dylhunn
|
||||
|
||||
# =========================================================
|
||||
# Framework: Migrations
|
||||
@@ -183,7 +182,7 @@ groups:
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- AndrewKushnir
|
||||
|
||||
# =========================================================
|
||||
@@ -193,7 +192,7 @@ groups:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files.exclude("packages/core/schematics/*").exclude('packages/core/primitives/*'), [
|
||||
contains_any_globs(files.exclude("packages/core/schematics/*"), [
|
||||
'packages/core/**/{*,.*}',
|
||||
'packages/examples/core/**/{*,.*}',
|
||||
'packages/platform-browser/**/{*,.*}',
|
||||
@@ -322,9 +321,7 @@ groups:
|
||||
'aio/content/guide/providers.md',
|
||||
'aio/content/examples/providers/**/{*,.*}',
|
||||
'aio/content/images/guide/providers/**/{*,.*}',
|
||||
'aio/content/guide/signal-queries.md',
|
||||
'aio/content/guide/singleton-services.md',
|
||||
'aio/content/guide/signal-inputs.md',
|
||||
'aio/content/guide/sharing-ngmodules.md',
|
||||
'aio/content/guide/standalone-components.md',
|
||||
'aio/content/guide/standalone-migration.md',
|
||||
@@ -347,7 +344,6 @@ groups:
|
||||
'aio/content/guide/defer.md',
|
||||
'aio/content/guide/signals.md',
|
||||
'aio/content/guide/control_flow.md',
|
||||
'aio/content/guide/model-inputs.md',
|
||||
'aio/content/examples/injection-token/**/{*,.*}',
|
||||
])
|
||||
reviewers:
|
||||
@@ -356,9 +352,8 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
@@ -378,7 +373,7 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
@@ -415,7 +410,7 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
@@ -438,7 +433,7 @@ groups:
|
||||
- andrewseguin
|
||||
- atscott
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
@@ -536,7 +531,7 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
@@ -615,7 +610,7 @@ groups:
|
||||
reviewers:
|
||||
users:
|
||||
- alxhub
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
|
||||
# =========================================================
|
||||
# Framework: Testing
|
||||
@@ -644,7 +639,7 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
@@ -663,7 +658,7 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
@@ -682,7 +677,7 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
@@ -778,7 +773,7 @@ groups:
|
||||
- atscott
|
||||
- crisbeto
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
@@ -813,7 +808,7 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- dylhunn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- josephperrott
|
||||
- pkozlowski-opensource
|
||||
|
||||
@@ -867,7 +862,7 @@ groups:
|
||||
reviewers:
|
||||
users:
|
||||
- AndrewKushnir
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
|
||||
# =========================================================
|
||||
# Docs: Marketing
|
||||
@@ -939,14 +934,13 @@ groups:
|
||||
'aio/content/guide/update-to-version-14.md',
|
||||
'aio/content/guide/update-to-version-15.md',
|
||||
'aio/content/guide/update-to-version-16.md',
|
||||
'aio/content/guide/update-to-version-17.md',
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- alxhub
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- jelbourn
|
||||
|
||||
# =========================================================
|
||||
@@ -1075,18 +1069,9 @@ groups:
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- twerske
|
||||
- josephperrott
|
||||
- jelbourn
|
||||
- bencodezen
|
||||
- thePunderWoman
|
||||
- dylhunn
|
||||
- AndrewKushnir
|
||||
- alxhub
|
||||
- crisbeto
|
||||
- devversion
|
||||
- atscott
|
||||
- pkozlowski-opensource
|
||||
- ~JeanMeche
|
||||
|
||||
# =========================================================
|
||||
# Docs-infra
|
||||
@@ -1144,7 +1129,7 @@ groups:
|
||||
reviewers:
|
||||
users:
|
||||
- AndrewKushnir
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- josephperrott
|
||||
|
||||
# =========================================================
|
||||
@@ -1165,7 +1150,6 @@ groups:
|
||||
- josephperrott
|
||||
- mgechev
|
||||
- twerske
|
||||
- ~JeanMeche
|
||||
|
||||
# =========================================================
|
||||
# Dev-infra
|
||||
@@ -1243,7 +1227,7 @@ groups:
|
||||
- atscott
|
||||
- dylhunn
|
||||
- ~jelbourn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
reviews:
|
||||
request: 3 # Request reviews from 3 people
|
||||
@@ -1269,7 +1253,7 @@ groups:
|
||||
- atscott
|
||||
- dylhunn
|
||||
- ~jelbourn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
reviews:
|
||||
request: 2 # Request reviews from 2 people
|
||||
@@ -1295,7 +1279,7 @@ groups:
|
||||
- atscott
|
||||
- dylhunn
|
||||
- ~jelbourn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
####################################################################################
|
||||
@@ -1319,69 +1303,9 @@ groups:
|
||||
- andrewseguin
|
||||
- dgp1130
|
||||
- ~jelbourn
|
||||
- thePunderWoman
|
||||
- jessicajaniuk
|
||||
- josephperrott
|
||||
|
||||
# =========================================================
|
||||
# Primitives
|
||||
# =========================================================
|
||||
|
||||
# Angular team required reviews
|
||||
primitives:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'packages/core/primitives/**/{*,.*}',
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- pkozlowski-opensource # Pawel Kozlowski
|
||||
- alxhub # Alex Rickabaugh
|
||||
- thePunderWoman # Jessica Janiuk
|
||||
- dylhunn # Dylan Hunn
|
||||
- AndrewKushnir # Andrew Kushnir
|
||||
- atscott # Andrew Scott
|
||||
reviews:
|
||||
required: 1
|
||||
reviewed_for: optional
|
||||
labels:
|
||||
pending: 'requires: TGP'
|
||||
approved: 'requires: TGP'
|
||||
rejected: 'requires: TGP'
|
||||
|
||||
# External team required reviews
|
||||
primitives-shared:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'packages/core/primitives/**/{*,.*}',
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- csmick # Cameron Smick
|
||||
- lannka # Hongfei Ding
|
||||
- eduhmc # Eduardo Huerta-Mercado
|
||||
- ehlemur # Edward Lesmes
|
||||
- ellenyuan # Ellen Yuan
|
||||
- jatraman # Jatin Ramanathan
|
||||
- jesse-good # Jesse Costello-Good
|
||||
- mturco # Matt Turco
|
||||
- neonstalwart # Ben Hockey
|
||||
- nicholasyu-google # Nicholas Yu
|
||||
- emspishak # Eric Spishak-Thomas
|
||||
- iteriani # Thomas Nguyen
|
||||
- tbondwilkinson # Tom Wilkinson
|
||||
- rahatarmanahmed # Rahat Ahmed
|
||||
reviews:
|
||||
required: 1
|
||||
reviewed_for: optional
|
||||
labels:
|
||||
pending: 'requires: TGP'
|
||||
approved: 'requires: TGP'
|
||||
rejected: 'requires: TGP'
|
||||
|
||||
####################################################################################
|
||||
# Override managed result groups
|
||||
#
|
||||
@@ -1458,6 +1382,26 @@ groups:
|
||||
author_value: 0 # The author of the PR cannot provide an approval for themself
|
||||
reviewed_for: ignored # All reviews apply to this group whether noted via Reviewed-for or not
|
||||
reviewers:
|
||||
teams:
|
||||
# Any member of the team can provide a review to perform the minimum review required
|
||||
- team
|
||||
users:
|
||||
- alan-agius4 # Alan Agius
|
||||
- AleksanderBodurri # Aleksander Bodurri
|
||||
- alxhub # Alex Rickabaugh
|
||||
- AndrewKushnir # Andrew Kushnir
|
||||
- andrewseguin # Andrew Seguin
|
||||
- atscott # Andrew Scott
|
||||
- clydin # Charles Lyding
|
||||
- crisbeto # Kristiyan Kostadinov
|
||||
- devversion # Paul Gschwendtner
|
||||
- dgp1130 # Doug Parker
|
||||
- dylhunn # Dylan Hunn
|
||||
- jelbourn # Jeremy Elbourn
|
||||
- jessicajaniuk # Jessica Janiuk
|
||||
- JiaLiPassion # Jia Li
|
||||
- JoostK # Joost Koehoorn
|
||||
- josephperrott # Joey Perrott
|
||||
- MarkTechson # Mark Thompson (Techson)
|
||||
- mgechev # Minko Gechev
|
||||
- mmalerba # Miles Malerba
|
||||
- pkozlowski-opensource # Pawel Kozlowski
|
||||
- twerske # Emma Twersky
|
||||
- zarend # Zach Arend
|
||||
|
||||
Vendored
+6
@@ -5,6 +5,12 @@
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"BazelBuild.vscode-bazel",
|
||||
"gkalpak.aio-docs-utils",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"xaver.clang-format",
|
||||
// The following extensions are useful when working on angular.io (i.e. inside the `aio/` directory).
|
||||
//"angular.ng-template",
|
||||
//"dbaeumer.vscode-eslint",
|
||||
//"errata-ai.vale-server",
|
||||
],
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -1,10 +1,16 @@
|
||||
{
|
||||
// Format js and ts files on save with `clang-format.executable`
|
||||
// If `clang-format.executable` is not being used, these two settings should be removed otherwise it will break existing formatting.
|
||||
// You can instead run `yarn gulp format` to manually format your code.
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
// Please install https://marketplace.visualstudio.com/items?itemName=xaver.clang-format to take advantage of `clang-format` in VSCode.
|
||||
// (See https://clang.llvm.org/docs/ClangFormat.html for more info `clang-format`.)
|
||||
"clang-format.executable": "${workspaceRoot}/node_modules/.bin/clang-format",
|
||||
// Exclude third party modules and build artifacts from the editor watchers/searches.
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
|
||||
@@ -13,7 +13,6 @@ exports_files([
|
||||
"scripts/ci/payload-size.sh",
|
||||
"scripts/ci/payload-size.js",
|
||||
"package.json",
|
||||
"angular.json",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
|
||||
+23
-980
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -207,7 +207,7 @@ To ensure consistency throughout the source code, keep these rules in mind as yo
|
||||
* All public API methods **must be documented**.
|
||||
* We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at **100 characters**.
|
||||
|
||||
An automated formatter is available, see [DEVELOPER.md](docs/DEVELOPER.md#formatting-your-source-code).
|
||||
An automated formatter is available, see [DEVELOPER.md](docs/DEVELOPER.md#clang-format).
|
||||
|
||||
|
||||
## <a name="commit"></a> Commit Message Format
|
||||
@@ -389,6 +389,7 @@ The following documents can help you sort out issues with GitHub accounts and mu
|
||||
|
||||
|
||||
|
||||
[angular-group]: https://groups.google.com/forum/#!forum/angular
|
||||
[coc]: https://github.com/angular/code-of-conduct/blob/main/CODE_OF_CONDUCT.md
|
||||
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
|
||||
[corporate-cla]: https://cla.developers.google.com/about/google-corporate
|
||||
@@ -397,3 +398,7 @@ The following documents can help you sort out issues with GitHub accounts and mu
|
||||
[discord]: https://discord.gg/angular
|
||||
[individual-cla]: https://cla.developers.google.com/about/google-individual
|
||||
[js-style-guide]: https://google.github.io/styleguide/jsguide.html
|
||||
[jsfiddle]: https://jsfiddle.net/
|
||||
[plunker]: https://plnkr.co/edit
|
||||
[runnable]: https://runnable.com/
|
||||
[stackoverflow]: https://stackoverflow.com/questions/tagged/angular
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
|
||||
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center">Angular - The modern web developer's platform</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="adev/src/assets/images/press-kit/angular_icon_gradient.gif" alt="angular-logo" width="120px" height="120px"/>
|
||||
<img src="aio/src/assets/images/logos/angular/angular_renaissance.png" alt="angular-logo" width="120px" height="120px"/>
|
||||
<br>
|
||||
<em>Angular is a development platform for building mobile and desktop web applications
|
||||
<br> using TypeScript/JavaScript and other languages.</em>
|
||||
@@ -61,10 +61,6 @@ Get started with Angular, learn the fundamentals and explore advanced topics on
|
||||
- [Lazy Loading][lazyloading]
|
||||
- [Animations][animations]
|
||||
|
||||
### Local Development
|
||||
|
||||
To contribute to the Angular Docs, check out the [Angular.dev README](adev/README.md)
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
@@ -152,8 +148,8 @@ Join the conversation and help the community.
|
||||
[angularmaterial]: https://material.angular.io/
|
||||
[cli]: https://angular.dev/tools/cli
|
||||
[architecture]: https://angular.dev/essentials
|
||||
[componentstemplates]: https://angular.dev/tutorials/learn-angular/1-components-in-angular
|
||||
[forms]: https://angular.dev/tutorials/learn-angular/15-forms
|
||||
[componentstemplates]: https://angular.dev/tutorials/learn-angular/components-in-angular
|
||||
[forms]: https://angular.dev/tutorials/learn-angular/forms
|
||||
[api]: https://angular.dev/api
|
||||
[angularelements]: https://angular.dev/guide/elements
|
||||
[ssr]: https://angular.dev/guide/ssr
|
||||
|
||||
@@ -2,12 +2,27 @@ workspace(
|
||||
name = "angular",
|
||||
managed_directories = {
|
||||
"@npm": ["node_modules"],
|
||||
"@aio_npm": ["aio/node_modules"],
|
||||
},
|
||||
)
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
load("//:yarn.bzl", "YARN_LABEL")
|
||||
|
||||
# Add a patch fix for rules_webtesting v0.3.5 required for enabling runfiles on Windows.
|
||||
# TODO: Remove the http_archive for this transitive dependency when a release is cut
|
||||
# for https://github.com/bazelbuild/rules_webtesting/commit/581b1557e382f93419da6a03b91a45c2ac9a9ec8
|
||||
# and the version is updated in rules_nodejs.
|
||||
http_archive(
|
||||
name = "io_bazel_rules_webtesting",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//:tools/bazel-repo-patches/rules_webtesting__windows_runfiles_fix.patch",
|
||||
],
|
||||
sha256 = "e9abb7658b6a129740c0b3ef6f5a2370864e102a5ba5ffca2cea565829ed825a",
|
||||
urls = ["https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.5/rules_webtesting.tar.gz"],
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
patches = [
|
||||
@@ -49,17 +64,7 @@ load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains")
|
||||
|
||||
nodejs_register_toolchains(
|
||||
name = "nodejs",
|
||||
node_repositories = {
|
||||
"18.20.0-darwin_arm64": ("node-v18.20.0-darwin-arm64.tar.gz", "node-v18.20.0-darwin-arm64", "10066ad4dd9e03ea5c4c45ef8775420ff37b860de09bbdf87b97e0c07b1ea036"),
|
||||
"18.20.0-darwin_amd64": ("node-v18.20.0-darwin-x64.tar.gz", "node-v18.20.0-darwin-x64", "062ba71618e88e06321de5caa038843c350aababa2d315f3ca7b8551f8e66c1c"),
|
||||
"18.20.0-linux_arm64": ("node-v18.20.0-linux-arm64.tar.xz", "node-v18.20.0-linux-arm64", "afe51da9ffb38ac1e3a20d9a06efd403ced4bf8831ab554a02a088dd8392fd79"),
|
||||
"18.20.0-linux_ppc64le": ("node-v18.20.0-linux-ppc64le.tar.xz", "node-v18.20.0-linux-ppc64le", "9e652bbf53a3e37285b11dfb9d6a9bb8b02010c3b50e5c8229d4cc10e72681da"),
|
||||
"18.20.0-linux_s390x": ("node-v18.20.0-linux-s390x.tar.xz", "node-v18.20.0-linux-s390x", "a6c2a5796b9d9e9bf21da62ec89ff30b41a8108880b9eaa3c912f1ce795a7743"),
|
||||
"18.20.0-linux_amd64": ("node-v18.20.0-linux-x64.tar.xz", "node-v18.20.0-linux-x64", "03eea148e56785babb27930b05ed6bf311aaa3bc573c0399dd63cad2fe5713c7"),
|
||||
"18.20.0-windows_amd64": ("node-v18.20.0-win-x64.zip", "node-v18.20.0-win-x64", "1c0aab05cc6836a8f5148cca345b92ebc948a4a2013f18d117b7ade6ff05aca6"),
|
||||
},
|
||||
# We need at least Node 18.17 due to some transitive dependencies.
|
||||
node_version = "18.20.0",
|
||||
node_version = "18.13.0",
|
||||
)
|
||||
|
||||
# Download npm dependencies.
|
||||
@@ -75,7 +80,7 @@ yarn_install(
|
||||
"//:.yarnrc",
|
||||
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
|
||||
"//tools:postinstall-patches.js",
|
||||
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-56d0b1f597b9e4b5d99f289f6904a220802164c1.patch",
|
||||
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-b2cec2dcba358f9dc7111800a3d65738f57abe8f.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+concatjs+5.8.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+esbuild+5.7.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+protractor+5.7.1.patch",
|
||||
@@ -94,6 +99,30 @@ 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
|
||||
@@ -167,10 +196,10 @@ cldr_xml_data_repository(
|
||||
# sass rules
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
sha256 = "845061fad5cb42a353ac394fe6450ec9f21d337acf5b98011ef29ddcb33e3671",
|
||||
strip_prefix = "rules_sass-9f8be59f5175d0dd3438d7da994b2782299e0c2c",
|
||||
sha256 = "7a90567717c3417a23ff4b21963a698a6e9aa669ac9e682121ea33f218a2816a",
|
||||
strip_prefix = "rules_sass-425ecafa8268bc013d684216363fe75bc802323e",
|
||||
urls = [
|
||||
"https://github.com/bazelbuild/rules_sass/archive/9f8be59f5175d0dd3438d7da994b2782299e0c2c.zip",
|
||||
"https://github.com/bazelbuild/rules_sass/archive/425ecafa8268bc013d684216363fe75bc802323e.zip",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
/src/assets/tutorials
|
||||
/src/assets/previews
|
||||
/TEMP
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# Ignore ng-dev logs and user-config
|
||||
.ng-dev.err*
|
||||
.ng-dev.log
|
||||
.ng-dev.user*
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
# Yarn v2+. Zero installs disabled since we are not using PnP.
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
!.yarn/yarn.js
|
||||
|
||||
|
||||
# Firebase
|
||||
.firebase
|
||||
*-debug.log
|
||||
.runtimeconfig.json
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
// Default state for all rules
|
||||
"default": true,
|
||||
|
||||
// MD013/line-length - Line length
|
||||
"MD013": {
|
||||
// Number of characters
|
||||
"line_length": 750,
|
||||
// Include code blocks
|
||||
"code_blocks": false,
|
||||
// Include tables
|
||||
"tables": false
|
||||
},
|
||||
|
||||
// MD034/bare-URL-used - Do not lint bare url - we display links in config code that does not match linter styles
|
||||
"MD034": false,
|
||||
|
||||
// MD038 - Spaces inside code span elements - We display inline templates in codeblocks
|
||||
"MD038": false,
|
||||
|
||||
// MD046/code-block-style - Do not lint inline code blocks - we display formatted CLI output in code that does not match linter styles
|
||||
"MD046": false,
|
||||
|
||||
// MD049/emphasis-style - Emphasis style should be consistent
|
||||
"MD049": false,
|
||||
|
||||
// TODO: Enable the following rules as MD improvements:
|
||||
// MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content
|
||||
"MD024": false,
|
||||
|
||||
// MD033/no-inline-html - Inline HTML
|
||||
"MD033": false,
|
||||
|
||||
// MD051/link-fragments - Link fragments should be valid
|
||||
"MD051": false,
|
||||
|
||||
// MD052/reference-links-images - Reference links and images should use a label that is defined
|
||||
"MD052": false,
|
||||
|
||||
// MD037/no-space-in-emphasis - Inline lists (such as in tables) incorrectly trigger this warning.
|
||||
"MD037": false
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -0,0 +1,2 @@
|
||||
.yarn/releases/*.cjs
|
||||
src/content/examples/**/*.*
|
||||
@@ -0,0 +1,5 @@
|
||||
# Ignoring documentation style examples
|
||||
/src/content/api-examples
|
||||
/src/content/examples
|
||||
/src/content/tutorials
|
||||
node_modules/
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"customSyntax": "postcss-scss",
|
||||
"defaultSeverity": "error",
|
||||
"reportNeedlessDisables": true,
|
||||
"reportInvalidScopeDisables": true,
|
||||
"plugins": [
|
||||
"./tools/stylelint/loader-rule.js",
|
||||
"./tools/stylelint/no-concrete-rules.ts",
|
||||
"./tools/stylelint/no-import.ts",
|
||||
"./tools/stylelint/no-unused-import.ts",
|
||||
"./tools/stylelint/selector-no-deep.ts",
|
||||
"./tools/stylelint/single-line-comment-only.ts"
|
||||
],
|
||||
"rules": {
|
||||
"@angular/selector-no-deep": true,
|
||||
"@angular/no-unused-import": true,
|
||||
"@angular/single-line-comment-only": true,
|
||||
"@angular/no-import": true,
|
||||
|
||||
"color-hex-case": "lower",
|
||||
"color-no-invalid-hex": true,
|
||||
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"function-comma-space-after": "always-single-line",
|
||||
"function-comma-space-before": "never",
|
||||
"function-name-case": "lower",
|
||||
"function-url-quotes": "always",
|
||||
"function-url-scheme-disallowed-list": ["data"],
|
||||
"function-whitespace-after": "always",
|
||||
|
||||
"number-leading-zero": "always",
|
||||
"number-no-trailing-zeros": true,
|
||||
"length-zero-no-unit": true,
|
||||
|
||||
"string-no-newline": true,
|
||||
"string-quotes": "single",
|
||||
|
||||
"unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"unit-allowed-list": [
|
||||
"px",
|
||||
"%",
|
||||
"deg",
|
||||
"s",
|
||||
"ms",
|
||||
"em",
|
||||
"rem",
|
||||
"vh",
|
||||
"vw",
|
||||
"vmin",
|
||||
"fr",
|
||||
"ch",
|
||||
"lvh",
|
||||
"svh",
|
||||
"dvh"
|
||||
],
|
||||
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never",
|
||||
|
||||
"shorthand-property-no-redundant-values": true,
|
||||
|
||||
"property-case": "lower",
|
||||
"no-duplicate-at-import-rules": true,
|
||||
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
{
|
||||
"ignore": ["consecutive-duplicates-with-different-values"]
|
||||
}
|
||||
],
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
"declaration-block-single-line-max-declarations": 1,
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-semicolon-space-after": "always-single-line",
|
||||
"declaration-block-semicolon-newline-before": "never-multi-line",
|
||||
"declaration-block-semicolon-newline-after": "always-multi-line",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"declaration-property-value-disallowed-list": [
|
||||
{"/.*/": ["initial"]},
|
||||
{"message": "The `initial` value is not supported in IE."}
|
||||
],
|
||||
|
||||
"block-closing-brace-newline-after": "always",
|
||||
"block-closing-brace-newline-before": "always-multi-line",
|
||||
"block-opening-brace-newline-after": "always-multi-line",
|
||||
"block-opening-brace-space-before": "always-multi-line",
|
||||
|
||||
"selector-attribute-brackets-space-inside": "never",
|
||||
"selector-attribute-operator-space-after": "never",
|
||||
"selector-attribute-operator-space-before": "never",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-pseudo-class-case": "lower",
|
||||
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"selector-pseudo-element-no-unknown": true,
|
||||
"selector-type-case": "lower",
|
||||
"selector-max-id": 0,
|
||||
"no-missing-end-of-source-newline": true,
|
||||
"no-eol-whitespace": true,
|
||||
"max-line-length": [
|
||||
100,
|
||||
{
|
||||
"ignorePattern": "/https?://.*/"
|
||||
}
|
||||
],
|
||||
"linebreaks": "unix",
|
||||
"selector-class-pattern": [
|
||||
"^_?(docs-|adev-|cm-|mat-|mdc-|xterm|ͼ3|ͼ2u|ͼ1|ng-|hljs-|as-|cdk-)",
|
||||
{
|
||||
"resolveNestedSelectors": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["projects/shared/src/lib/styles/_split.scss"],
|
||||
"rules": {
|
||||
"selector-pseudo-element-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignorePseudoElements": ["ng-deep"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
|
||||
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
|
||||
load("@bazel_skylib//lib:collections.bzl", "collections")
|
||||
load("//adev/tools/local_deps:index.bzl", "ensure_local_package_deps", "link_local_packages")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
# All source and configuration files required to build the docs app
|
||||
APPLICATION_FILES = [
|
||||
"angular.json",
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.json",
|
||||
"tsconfig.worker.json",
|
||||
] + glob(
|
||||
["src/**/*"],
|
||||
exclude = [
|
||||
"src/**/*.spec.ts",
|
||||
# Temporarily exclude generated sources produced by the non-bazel
|
||||
# build until the whole project is built by bazel and this directory
|
||||
# isn't needed.
|
||||
"src/generated/**/*",
|
||||
],
|
||||
)
|
||||
|
||||
TEST_FILES = APPLICATION_FILES + [
|
||||
"karma.conf.js",
|
||||
"tsconfig.spec.json",
|
||||
] + glob(
|
||||
["**/*.spec.ts"],
|
||||
)
|
||||
|
||||
APPLICATION_ASSETS = [
|
||||
"//adev/src/assets/images",
|
||||
"//adev/src/assets/textures",
|
||||
"//adev/src/assets/previews",
|
||||
"//adev/src/assets:tutorials",
|
||||
"//adev/src/assets/icons",
|
||||
"//adev/src/assets:api",
|
||||
"//adev/src/assets:content",
|
||||
]
|
||||
|
||||
APPLICATION_DEPS = [
|
||||
"@npm//@angular/build",
|
||||
"@npm//@angular-devkit/build-angular",
|
||||
"@npm//@angular/animations",
|
||||
"@npm//@angular/cdk",
|
||||
"@npm//@angular/common",
|
||||
"@npm//@angular/compiler",
|
||||
"@npm//@angular/compiler-cli",
|
||||
"@npm//@angular/core",
|
||||
"@npm//@angular/docs",
|
||||
"@npm//@angular/forms",
|
||||
"@npm//@angular/material",
|
||||
"@npm//@angular/platform-browser",
|
||||
"@npm//@angular/platform-server",
|
||||
"@npm//@angular/router",
|
||||
"@npm//gsap",
|
||||
"@npm//marked",
|
||||
"@npm//ngx-progressbar",
|
||||
"@npm//ogl",
|
||||
"@npm//rxjs",
|
||||
"@npm//typescript",
|
||||
"@npm//@typescript/vfs",
|
||||
"@npm//@codemirror/state",
|
||||
"@npm//@codemirror/view",
|
||||
"@npm//@codemirror/language",
|
||||
"@npm//@codemirror/commands",
|
||||
"@npm//@codemirror/search",
|
||||
"@npm//@codemirror/autocomplete",
|
||||
"@npm//@codemirror/lint",
|
||||
"@npm//@codemirror/lang-html",
|
||||
"@npm//@codemirror/lang-angular",
|
||||
"@npm//@codemirror/lang-css",
|
||||
"@npm//@codemirror/lang-sass",
|
||||
"@npm//@codemirror/lang-javascript",
|
||||
"@npm//@lezer/highlight",
|
||||
"@npm//@lezer/javascript",
|
||||
"@npm//@lezer/common",
|
||||
"@npm//@xterm/xterm",
|
||||
"@npm//algoliasearch",
|
||||
"@npm//xterm",
|
||||
"@npm//xterm-addon-fit",
|
||||
"@npm//angular-split",
|
||||
"@npm//zone.js",
|
||||
]
|
||||
|
||||
TEST_DEPS = APPLICATION_DEPS + [
|
||||
"@npm//@angular/platform-browser-dynamic",
|
||||
"@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",
|
||||
]
|
||||
|
||||
# Create `npm_link` targets for all dependencies that correspond to a
|
||||
# first-party Angular package that can be built from `HEAD`.
|
||||
link_local_packages(
|
||||
all_deps = collections.uniq(APPLICATION_DEPS + TEST_DEPS),
|
||||
)
|
||||
|
||||
copy_to_bin(
|
||||
name = "application_files_bin",
|
||||
srcs = APPLICATION_FILES,
|
||||
)
|
||||
|
||||
bool_flag(
|
||||
name = "fast_build_mode",
|
||||
build_setting_default = False,
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "fast",
|
||||
flag_values = {
|
||||
":fast_build_mode": "true",
|
||||
},
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "full",
|
||||
flag_values = {
|
||||
":fast_build_mode": "false",
|
||||
},
|
||||
)
|
||||
|
||||
config_based_architect_flags = select({
|
||||
":fast": ["--no-prerender"],
|
||||
":full": ["--prerender"],
|
||||
})
|
||||
|
||||
architect(
|
||||
name = "build",
|
||||
args = [
|
||||
"angular-dev:build",
|
||||
"--output-path=build",
|
||||
] + config_based_architect_flags,
|
||||
chdir = "$(RULEDIR)",
|
||||
data = ensure_local_package_deps(APPLICATION_DEPS) + APPLICATION_ASSETS + [
|
||||
":application_files_bin",
|
||||
],
|
||||
# Network is required to inline fonts.
|
||||
exec_properties = ENABLE_NETWORK,
|
||||
output_dir = True,
|
||||
tags = [
|
||||
"no-remote-exec",
|
||||
],
|
||||
)
|
||||
|
||||
architect(
|
||||
name = "serve",
|
||||
args = [
|
||||
"angular-dev:serve",
|
||||
"--poll=1000",
|
||||
"--live-reload",
|
||||
"--watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = ensure_local_package_deps(APPLICATION_DEPS) + APPLICATION_ASSETS + [
|
||||
":application_files_bin",
|
||||
],
|
||||
tags = [
|
||||
"no-remote-exec",
|
||||
],
|
||||
)
|
||||
|
||||
architect_test(
|
||||
name = "test",
|
||||
args = [
|
||||
"angular-dev:test",
|
||||
"--no-watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = ensure_local_package_deps(TEST_DEPS) + TEST_FILES + APPLICATION_ASSETS + [
|
||||
"//aio/tools:windows-chromium-path",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
],
|
||||
env = {
|
||||
"CHROME_BIN": "../$(CHROMIUM)",
|
||||
},
|
||||
toolchains = [
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
],
|
||||
)
|
||||
+2
-36
@@ -1,37 +1,3 @@
|
||||
# [Angular.dev](https://www.angular.dev)
|
||||
# Angular.dev: The new home for Angular developers (https://angular.dev)
|
||||
|
||||
This site is built with Angular.
|
||||
|
||||
The content is written primarly in Markdown format located in `src/content`. For simple edits, you can directly edit the file on GitHub and generate a Pull Request.
|
||||
|
||||
## Local Development
|
||||
|
||||
For local development, [yarn](https://yarnpkg.com/) is the preferred package manager. You can setup a local environment with the following commands
|
||||
:
|
||||
|
||||
```bash
|
||||
# Clone Angular repo
|
||||
git clone https://github.com/angular/angular.git
|
||||
|
||||
# Navigate to project directory
|
||||
cd angular
|
||||
|
||||
# Install dependencies
|
||||
yarn
|
||||
|
||||
# Build and run local dev server
|
||||
# Note: Initial build will take some time
|
||||
yarn docs
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Want to report a bug, contribute some code, or improve the documentation? Excellent!
|
||||
|
||||
Read through our [contributing guidelines](CONTRIBUTING.md) to learn about our submission process, coding rules, and more.
|
||||
|
||||
And if you're new, check out one of our issues labeled as <kbd>[help wanted](https://github.com/angular/angular/labels/help%20wanted)</kbd> or <kbd>[good first issue](https://github.com/angular/angular/labels/good%20first%20issue)</kbd>.
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
Read more about our initial launch on the [Angular blog](https://goo.gle/angular-dot-dev).
|
||||
|
||||
+64
-24
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@angular/cli/lib/config/schema.json",
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": ".",
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-dev": {
|
||||
"projectType": "application",
|
||||
@@ -12,40 +12,56 @@
|
||||
"changeDetection": "OnPush"
|
||||
}
|
||||
},
|
||||
"root": ".",
|
||||
"root": "projects/angular-dev",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "adev",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"externalDependencies": ["path"],
|
||||
"outputPath": "dist/angular-dev",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"server": "src/main.server.ts",
|
||||
"prerender": true,
|
||||
"polyfills": ["src/polyfills.ts", "zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/robots.txt", "src/assets"],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/robots.txt",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": ["projects/shared/src/lib/styles/global-styles.scss"],
|
||||
"scripts": [],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||
"optimization": {
|
||||
"fonts": {
|
||||
// TODO(josephperrott): enabled inline scripts
|
||||
"inline": false
|
||||
}
|
||||
}
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "5kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
@@ -81,23 +97,47 @@
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"include": ["src/app"],
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
"styles": ["projects/shared/src/lib/styles/global-styles.scss"],
|
||||
"scripts": [],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"projectType": "library",
|
||||
"root": "projects/shared",
|
||||
"sourceRoot": "projects/shared/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/shared/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/shared/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/shared/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/shared/tsconfig.spec.json",
|
||||
"polyfills": ["zone.js", "zone.js/testing"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": "dca119a9-da31-47f7-a6cb-b60541037021",
|
||||
"cache": {
|
||||
// Disable build caching as the cache folder will just be dropped
|
||||
// when run under Bazel sandboxed execution.
|
||||
"enabled": false
|
||||
}
|
||||
"analytics": "dca119a9-da31-47f7-a6cb-b60541037021"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"hosting": {
|
||||
"target": "angular-docs",
|
||||
"public": "./build/browser",
|
||||
"ignore": ["**/.*"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "assets/tutorials/common/*.jpg",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Resource-Policy",
|
||||
"value": "cross-origin"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "*.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].+(css|js)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public,max-age=31536000,immutable"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/[0-9a-f][0-9a-f][0-9a-f].[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].js",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/@(ngsw-worker.js|ngsw.json)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Opener-Policy",
|
||||
"value": "same-origin"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
const {getAdjustedChromeBinPathForWindows} = require('../aio/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;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"name": "blossom",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "yarn generate-all && ng serve",
|
||||
"build": "ng build angular-dev",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"ng-dev": "cross-env TS_NODE_PROJECT=$PWD/.ng-dev/tsconfig.json TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm node_modules/@angular/ng-dev/bundles/cli.mjs",
|
||||
"lint": "tslint -c tslint.json --project tsconfig.json && stylelint \"projects/**/*.+(css|scss)\" --config .stylelintrc.json && markdownlint \"projects/**/*.md\"",
|
||||
"lint-fix": "markdownlint --fix '**/*.md'",
|
||||
"format": "yarn ng-dev format changed && tslint -c tslint.json --fix --project tsconfig.json",
|
||||
"serve:prerender": "http-server -c-l dist/angular-dev/browser",
|
||||
"postinstall": "yarn generate-all",
|
||||
"generate-all": "yarn generate-tutorials && yarn generate-docs && yarn generate-previews && yarn set-version && yarn generate-api && yarn generate-cli",
|
||||
"generate-api": "ts-node-esm ./prerender/api-gen/index.ts all ./src/assets/content/api",
|
||||
"generate-cli": "ts-node-esm ./prerender/api-gen/cli-index.ts all ./src/assets/content/cli",
|
||||
"generate-docs": "ts-node-esm ./prerender/markdown-pipeline/index.mts",
|
||||
"generate-previews": "ts-node-esm scripts/examples/generate-previews-components-map.mts",
|
||||
"generate-tutorials": "ts-node scripts/tutorials/generate-tutorials.ts",
|
||||
"check-links": "ts-node scripts/checks/check-links.ts",
|
||||
"new-example": "ts-node-esm scripts/examples/create-example.mts",
|
||||
"new-tutorial": "ts-node scripts/tutorials/new-tutorial.ts",
|
||||
"new-playground-template": "ts-node scripts/tutorials/new-playground-template.ts",
|
||||
"update-tutorials-package-lock": "ts-node scripts/tutorials/update-package-lock.ts",
|
||||
"set-version": "ts-node-esm scripts/versions/set-current-version.mts",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.0-rc.1",
|
||||
"@angular/cdk": "17.0.0-rc.1",
|
||||
"@angular/common": "^17.0.0-rc.1",
|
||||
"@angular/compiler": "^17.0.0-rc.1",
|
||||
"@angular/core": "^17.0.0-rc.1",
|
||||
"@angular/elements": "^17.0.0-rc.1",
|
||||
"@angular/forms": "^17.0.0-rc.1",
|
||||
"@angular/material": "17.0.0-rc.1",
|
||||
"@angular/platform-browser": "^17.0.0-rc.1",
|
||||
"@angular/platform-browser-dynamic": "^17.0.0-rc.1",
|
||||
"@angular/platform-server": "^17.0.0-rc.1",
|
||||
"@angular/router": "^17.0.0-rc.1",
|
||||
"@angular/service-worker": "^17.0.0-rc.1",
|
||||
"@codemirror/autocomplete": "^6.9.0",
|
||||
"@codemirror/commands": "^6.2.4",
|
||||
"@codemirror/lang-angular": "^0.1.2",
|
||||
"@codemirror/lang-css": "^6.2.1",
|
||||
"@codemirror/lang-html": "^6.4.3",
|
||||
"@codemirror/lang-javascript": "^6.1.8",
|
||||
"@codemirror/lang-sass": "^6.0.1",
|
||||
"@codemirror/language": "^6.7.0",
|
||||
"@codemirror/lint": "^6.4.0",
|
||||
"@codemirror/search": "^6.4.0",
|
||||
"@codemirror/state": "^6.2.1",
|
||||
"@codemirror/tooltip": "^0.19.16",
|
||||
"@codemirror/view": "^6.12.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@typescript/vfs": "^1.5.0",
|
||||
"@webcontainer/api": "^1.1.5",
|
||||
"algoliasearch": "^4.20.0",
|
||||
"angular-split": "16.2.1",
|
||||
"diff": "^5.1.0",
|
||||
"express": "^4.15.2",
|
||||
"gsap": "^3.12.2",
|
||||
"highlight.js": "^11.8.0",
|
||||
"jszip": "^3.10.1",
|
||||
"ngx-progressbar": "^11.1.0",
|
||||
"ogl": "^1.0.1",
|
||||
"preact": "^10.17.1",
|
||||
"preact-render-to-string": "^6.2.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"xterm": "^5.1.0",
|
||||
"xterm-addon-fit": "^0.7.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.0.0-rc.3",
|
||||
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#bb8a1cf74bda7e7a193cc449ca9558bc3ce55ad0",
|
||||
"@angular/cli": "17.0.0-rc.3",
|
||||
"@angular/compiler-cli": "^17.0.0-rc.1",
|
||||
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#09c58eaf121b8e71edbe863c373854c0a221e290",
|
||||
"@inquirer/prompts": "^3.0.2",
|
||||
"@mermaid-js/mermaid-cli": "^10.3.1",
|
||||
"@types/algoliasearch": "^4.0.0",
|
||||
"@types/diff": "^5.0.3",
|
||||
"@types/dom-navigation": "~1.0.0",
|
||||
"@types/dom-view-transitions": "~1.0.1",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/jasmine": "~4.3.0",
|
||||
"@types/jsdom": "^21.1.2",
|
||||
"@types/marked": "^5.0.1",
|
||||
"@types/node": "^18.11.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"firebase-tools": "latest",
|
||||
"glob": "^10.2.7",
|
||||
"html-entities": "^2.4.0",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^8.0.0",
|
||||
"jasmine-core": "~4.5.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"markdownlint-cli": "^0.36.0",
|
||||
"marked": "^5.1.1",
|
||||
"ng-packagr": "17.0.0-next.1",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"prettier": "^2.8.4",
|
||||
"stylelint": "^14.14.0",
|
||||
"ts-node": "^10.8.1",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-eslint-rules": "5.4.0",
|
||||
"typescript": "~5.2.0",
|
||||
"uuid": "^9.0.0",
|
||||
"vrsource-tslint-rules": "6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
/* tslint:disable:no-console */
|
||||
import {NavigationItem} from '@angular/docs-shared';
|
||||
import {readFileSync} from 'fs';
|
||||
import {glob} from 'glob';
|
||||
import {join} from 'path';
|
||||
|
||||
import {SUB_NAVIGATION_DATA} from '../../src/app/sub-navigation-data';
|
||||
|
||||
// PATHS
|
||||
const PROJECT_FOLDER_PATH = join(__dirname, '../../src');
|
||||
|
||||
// SKIP Files
|
||||
const SKIPED_ORPHAN = ['kitchen-sink', 'error', 'examples'];
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
const contentDir = join(PROJECT_FOLDER_PATH, 'content');
|
||||
const mdFiles = await retrieveAllMarkdownFiles(contentDir);
|
||||
|
||||
const allowedPaths = new Set([
|
||||
...SUB_NAVIGATION_DATA.docs.flatMap((item: any) => getPaths(item)),
|
||||
SUB_NAVIGATION_DATA.reference.flatMap((item: any) => getPaths(item)),
|
||||
SUB_NAVIGATION_DATA.footer.flatMap((item: any) => getPaths(item)),
|
||||
]);
|
||||
|
||||
const contentPaths = new Set([
|
||||
...SUB_NAVIGATION_DATA.docs.flatMap((item: any) => getContentPaths(item)),
|
||||
...SUB_NAVIGATION_DATA.reference.flatMap((item: any) => getContentPaths(item)),
|
||||
...SUB_NAVIGATION_DATA.footer.flatMap((item: any) => getContentPaths(item)),
|
||||
]);
|
||||
|
||||
// Regular expression to match relative links
|
||||
const regex = /\]\((?!https?:\/\/)([^)]+)\)/g;
|
||||
|
||||
const orphanFiles: string[] = [];
|
||||
for (const {path} of mdFiles) {
|
||||
if (
|
||||
!contentPaths.has(path.slice(0, -3)) &&
|
||||
!path.startsWith('tutorials') &&
|
||||
!SKIPED_ORPHAN.some((skip) => path.startsWith(skip))
|
||||
) {
|
||||
orphanFiles.push(path.slice(0, -3));
|
||||
}
|
||||
}
|
||||
if (orphanFiles.length > 0) {
|
||||
console.log('======== ORPHAN FILES =========\n');
|
||||
orphanFiles.forEach((file) => console.warn(file));
|
||||
}
|
||||
|
||||
const deadLinks: {path: string; links: string[]}[] = [];
|
||||
for (const {content, path} of mdFiles) {
|
||||
let match;
|
||||
const matches = [];
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const relativeLink = match[1].split('#')[0];
|
||||
if (!allowedPaths.has(relativeLink) && relativeLink.startsWith('guide')) {
|
||||
matches.push(relativeLink);
|
||||
}
|
||||
}
|
||||
if (matches.length) {
|
||||
deadLinks.push({path, links: matches});
|
||||
}
|
||||
}
|
||||
if (deadLinks.length > 0) {
|
||||
console.warn('\n\n======== DEAD RELATIVE LINKS =========\n');
|
||||
deadLinks.forEach(({path, links}) => {
|
||||
console.warn(path);
|
||||
links.forEach((path) => console.log(`\t ${path}`));
|
||||
});
|
||||
}
|
||||
|
||||
if (deadLinks.length > 0 || orphanFiles.length > 0) {
|
||||
throw new Error('Check fails');
|
||||
}
|
||||
}
|
||||
|
||||
function getPaths(navItem: NavigationItem): string[] {
|
||||
return [
|
||||
...(navItem.path ? [navItem.path] : []),
|
||||
...(navItem.children ?? []).flatMap((item) => getPaths(item)),
|
||||
];
|
||||
}
|
||||
|
||||
function getContentPaths(navItem: NavigationItem): string[] {
|
||||
return [
|
||||
...(navItem.contentPath ? [navItem.contentPath] : []),
|
||||
...(navItem.children ?? []).flatMap((item) => getContentPaths(item)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively search the provided directory for all markdown files and
|
||||
* asyncronously load them.
|
||||
*/
|
||||
async function retrieveAllMarkdownFiles(
|
||||
baseDir: string,
|
||||
): Promise<{path: string; content: string}[]> {
|
||||
const files = await glob('**/*.md', {
|
||||
root: baseDir,
|
||||
cwd: baseDir,
|
||||
ignore: ['**/node_modules/**'],
|
||||
});
|
||||
|
||||
return files.map((fullPathToFile) => ({
|
||||
path: fullPathToFile,
|
||||
content: readFileSync(join(baseDir, fullPathToFile), {encoding: 'utf-8'}),
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {input, confirm} from '@inquirer/prompts';
|
||||
import {existsSync} from 'fs';
|
||||
import {copyFile, mkdir, writeFile} from 'fs/promises';
|
||||
import {dirname, join} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {copyFolder} from './utils/fs.mjs';
|
||||
|
||||
const EXAMPLES_BASE = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'../../src/content/examples',
|
||||
);
|
||||
const STACKBLITZ_TEMPLATE = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'../../src/content/stackblitz-template',
|
||||
);
|
||||
|
||||
const MAIN_TS_FILENAME = 'main.ts';
|
||||
const STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
|
||||
|
||||
createExample();
|
||||
|
||||
async function createExample(): Promise<void> {
|
||||
console.info('Create example');
|
||||
const name = await input({message: 'What is the example name?'});
|
||||
const description = await input({message: 'Could you provide the description of the example?'});
|
||||
const tags: string[] = [];
|
||||
let addMoreTags = true;
|
||||
|
||||
while (addMoreTags) {
|
||||
const tag = await input({message: 'Could you provide the tag of the example?'});
|
||||
|
||||
if (!!tag) {
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
console.info();
|
||||
|
||||
addMoreTags = await confirm({message: 'Would you like to add another tag?', default: true});
|
||||
}
|
||||
|
||||
const shouldCopySrcAppFolderFromTemplate = await confirm({
|
||||
message: 'Would you like to copy /src/app/**.* from template?',
|
||||
default: true,
|
||||
});
|
||||
const shouldCopyMainTsFromTemplate = await confirm({
|
||||
message: 'Would you like to copy /src/main.ts from template?',
|
||||
default: false,
|
||||
});
|
||||
|
||||
const examplePath = join(EXAMPLES_BASE, name);
|
||||
|
||||
await createEmptyExample(examplePath, name, description, tags);
|
||||
await copyFilesFromTemplate(
|
||||
examplePath,
|
||||
shouldCopySrcAppFolderFromTemplate,
|
||||
shouldCopyMainTsFromTemplate,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the directory and marker files for the new example.
|
||||
*/
|
||||
async function createEmptyExample(
|
||||
examplePath: string,
|
||||
exampleName: string,
|
||||
description: string,
|
||||
tags: string[],
|
||||
): Promise<void> {
|
||||
validateExampleName(exampleName);
|
||||
|
||||
await ensureExamplePath(examplePath);
|
||||
await writeStackBlitzFile(examplePath, description, tags);
|
||||
}
|
||||
|
||||
function validateExampleName(exampleName: string): void {
|
||||
if (/\s/.test(exampleName)) {
|
||||
throw new Error(`Unable to create example. The example name contains spaces: '${exampleName}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the new example directory exists.
|
||||
*/
|
||||
async function ensureExamplePath(examplePath: string): Promise<void> {
|
||||
if (existsSync(examplePath)) {
|
||||
throw new Error(
|
||||
`Unable to create example. The path to the new example already exists: ${examplePath}`,
|
||||
);
|
||||
}
|
||||
await mkdir(examplePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the `stackblitz.json` file into the new example.
|
||||
*/
|
||||
async function writeStackBlitzFile(
|
||||
examplePath: string,
|
||||
description: string,
|
||||
tags: string[],
|
||||
): Promise<void> {
|
||||
const config = {
|
||||
description,
|
||||
ignore: ['**/*.d.ts', '**/*.js', '**/*.[1,2].*'],
|
||||
tags,
|
||||
};
|
||||
await writeFile(
|
||||
join(examplePath, STACKBLITZ_CONFIG_FILENAME),
|
||||
JSON.stringify(config, null, 2) + '\n',
|
||||
);
|
||||
}
|
||||
|
||||
async function copyFilesFromTemplate(
|
||||
examplePath: string,
|
||||
shouldCopySrcAppFolderFromTemplate: boolean,
|
||||
shouldCopyMainTsFromTemplate: boolean,
|
||||
): Promise<void> {
|
||||
if (shouldCopySrcAppFolderFromTemplate) {
|
||||
const appTemplateFolderPath = join(STACKBLITZ_TEMPLATE, 'src/app');
|
||||
const exampleAppFolderPath = join(examplePath, 'src/app');
|
||||
await copyFolder(appTemplateFolderPath, exampleAppFolderPath);
|
||||
}
|
||||
if (shouldCopyMainTsFromTemplate) {
|
||||
const mainTsTemplatePath = join(STACKBLITZ_TEMPLATE, 'src', MAIN_TS_FILENAME);
|
||||
const exampleMainTsPath = join(examplePath, 'src', MAIN_TS_FILENAME);
|
||||
await copyFile(mainTsTemplatePath, exampleMainTsPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import {dirname, join, relative} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {readFile, writeFile} from 'fs/promises';
|
||||
|
||||
const TYPESCRIPT_EXTENSION = '.ts';
|
||||
const SKIP_FILES_WITH_EXTENSIONS = ['.e2e-spec.ts', '.spec.ts', '.po.ts'];
|
||||
const EXAMPLES_PATH = `../../content/examples`;
|
||||
|
||||
interface File {
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface AnalyzedFiles {
|
||||
path: string;
|
||||
componentNames: string[];
|
||||
}
|
||||
|
||||
/** Absolute disk path to the project directory. */
|
||||
const examplesDir = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'../../src/content/examples',
|
||||
);
|
||||
const projectDir = join(dirname(fileURLToPath(import.meta.url)), '../../src');
|
||||
const previewsComponentMapFileName = 'previews.ts';
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
const files = await retrieveAllTypescriptFiles(
|
||||
examplesDir,
|
||||
(path) => !SKIP_FILES_WITH_EXTENSIONS.some((extensionToSkip) => path.endsWith(extensionToSkip)),
|
||||
);
|
||||
|
||||
const filesWithComponent = files
|
||||
.map((file) => ({
|
||||
componentNames: analyzeFile(file),
|
||||
path: file.path,
|
||||
}))
|
||||
.filter((result) => result.componentNames.length > 0);
|
||||
|
||||
const previewsComponentMap = generatePreviewsComponentMap(filesWithComponent);
|
||||
|
||||
fs.mkdirSync(join(projectDir, 'assets/previews'), {recursive: true});
|
||||
|
||||
await writeFile(
|
||||
join(projectDir, 'assets/previews', previewsComponentMapFileName),
|
||||
previewsComponentMap,
|
||||
);
|
||||
}
|
||||
|
||||
/** Recursively search the provided directory for all typescript files and asynchronously load them. */
|
||||
function retrieveAllTypescriptFiles(
|
||||
baseDir: string,
|
||||
predicateFn: (path: string) => boolean,
|
||||
): Promise<File[]> {
|
||||
const typescriptFiles: Promise<File>[] = [];
|
||||
|
||||
const checkFilesInDirectory = (dir: string) => {
|
||||
const files = fs.readdirSync(dir, {withFileTypes: true});
|
||||
for (const file of files) {
|
||||
const fullPathToFile = join(dir, file.name);
|
||||
const relativeFilePath = relative(baseDir, fullPathToFile);
|
||||
|
||||
if (
|
||||
file.isFile() &&
|
||||
file.name.endsWith(TYPESCRIPT_EXTENSION) &&
|
||||
predicateFn(relativeFilePath)
|
||||
) {
|
||||
typescriptFiles.push(
|
||||
readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => {
|
||||
return {
|
||||
path: relativeFilePath,
|
||||
content: fileContent,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} else if (file.isDirectory()) {
|
||||
checkFilesInDirectory(fullPathToFile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkFilesInDirectory(baseDir);
|
||||
|
||||
return Promise.all(typescriptFiles);
|
||||
}
|
||||
|
||||
/** Returns list of the `Standalone` @Component class names for given file */
|
||||
function analyzeFile(file: File): string[] {
|
||||
const componentClassNames: string[] = [];
|
||||
const sourceFile = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, false);
|
||||
|
||||
const visitNode = (node: ts.Node): void => {
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
const decorators = ts.getDecorators(node);
|
||||
const componentName = node.name ? node.name.text : null;
|
||||
|
||||
if (decorators && decorators.length) {
|
||||
for (const decorator of decorators) {
|
||||
const call = decorator.expression;
|
||||
|
||||
if (
|
||||
ts.isCallExpression(call) &&
|
||||
ts.isIdentifier(call.expression) &&
|
||||
call.expression.text === 'Component' &&
|
||||
call.arguments.length > 0 &&
|
||||
ts.isObjectLiteralExpression(call.arguments[0])
|
||||
) {
|
||||
const hasStandaloneProperty = call.arguments[0].properties.some(
|
||||
(property) =>
|
||||
property.name &&
|
||||
ts.isIdentifier(property.name) &&
|
||||
property.name.text === 'standalone',
|
||||
);
|
||||
|
||||
if (hasStandaloneProperty && componentName) {
|
||||
componentClassNames.push(componentName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visitNode);
|
||||
};
|
||||
|
||||
visitNode(sourceFile);
|
||||
|
||||
return componentClassNames;
|
||||
}
|
||||
|
||||
function generatePreviewsComponentMap(data: AnalyzedFiles[]): string {
|
||||
let result = '';
|
||||
for (const fileData of data) {
|
||||
for (const componentName of fileData.componentNames) {
|
||||
const key = `${fileData.path}${
|
||||
fileData.componentNames.length > 1 ? '_' + componentName : ''
|
||||
}`.replace(/\\/g, '/');
|
||||
result += `['${key}']: () => import('${EXAMPLES_PATH}/${fileData.path
|
||||
.replace(/\\/g, '/')
|
||||
.replace('.ts', '')}').then(c => c.${componentName}),\n`;
|
||||
}
|
||||
}
|
||||
return fs
|
||||
.readFileSync('scripts/examples/previews-components.template', 'utf8')
|
||||
.replace(/\${previewsComponents}/g, result);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
/**
|
||||
******************************************************************************
|
||||
* DO NOT MANUALLY EDIT THIS FILE. THIS FILE IS AUTOMATICALLY GENERATED.
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
import {Type} from '@angular/core';
|
||||
|
||||
/**
|
||||
* Map of the previews components, values are functions which returns the promise of the component type, which will be displayed as preview in the ExampleViewer component.
|
||||
* Keys has to be equal to paths written down in the docs markdown files.
|
||||
*/
|
||||
export const PREVIEWS_COMPONENTS_MAP: Record<string, () => Promise<Type<unknown>>> = {
|
||||
${previewsComponents}
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {dirname, join} from 'path';
|
||||
import {readFile, writeFile} from 'fs/promises';
|
||||
import {
|
||||
ASSETS_EXAMPLE_PATH,
|
||||
CSS_TS_COPYRIGHT,
|
||||
EXAMPLES_PATH,
|
||||
HTML_COPYRIGHT,
|
||||
STACKBLITZ_TEMPLATE_PATH,
|
||||
STACKBLITZ_CONFIG_FILENAME,
|
||||
TEMPORARY_EXAMPLES_PATH,
|
||||
EXCLUDE_FILES_FOR_STACKBLITZ,
|
||||
} from './utils/examples-constants.mjs';
|
||||
import {copyFolder, createFolder, removeFolder} from './utils/fs.mjs';
|
||||
import jsdom from 'jsdom';
|
||||
import {glob} from 'glob';
|
||||
import {regionParser} from './../../prerender/markdown-pipeline/regions/region-parser.mjs';
|
||||
|
||||
interface StackblitzConfig {
|
||||
ignore: string[];
|
||||
file: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
}
|
||||
|
||||
export async function generateStackblitzExample(
|
||||
exampleFolderName: string,
|
||||
primaryFilePath: string,
|
||||
title: string,
|
||||
) {
|
||||
const exampleDir = join(EXAMPLES_PATH, exampleFolderName);
|
||||
const temporaryExampleDir = join(TEMPORARY_EXAMPLES_PATH, exampleFolderName);
|
||||
const config = await readFile(join(exampleDir, STACKBLITZ_CONFIG_FILENAME), 'utf-8');
|
||||
const stackblitzConfig: StackblitzConfig = JSON.parse(config);
|
||||
primaryFilePath = join(...primaryFilePath.split('/'));
|
||||
|
||||
await createFolder(temporaryExampleDir);
|
||||
await combineTemplateWithExample(exampleDir, temporaryExampleDir);
|
||||
await generateStackblitzHtml(
|
||||
temporaryExampleDir,
|
||||
stackblitzConfig,
|
||||
exampleFolderName,
|
||||
primaryFilePath,
|
||||
title,
|
||||
);
|
||||
await removeFolder(temporaryExampleDir);
|
||||
}
|
||||
|
||||
async function combineTemplateWithExample(
|
||||
exampleDir: string,
|
||||
temporaryExampleDir: string,
|
||||
): Promise<void> {
|
||||
// Copy template files to TEMP folder
|
||||
await copyFolder(STACKBLITZ_TEMPLATE_PATH, temporaryExampleDir);
|
||||
|
||||
// Copy example files to TEMP folder
|
||||
await copyFolder(exampleDir, temporaryExampleDir);
|
||||
}
|
||||
|
||||
async function generateStackblitzHtml(
|
||||
temporaryExampleDir: string,
|
||||
stackBlitzConfig: StackblitzConfig,
|
||||
exampleFolderName: string,
|
||||
primaryFilePath: string,
|
||||
title: string,
|
||||
): Promise<void> {
|
||||
const defaultIncludes = [
|
||||
'**/*.ts',
|
||||
'**/*.js',
|
||||
'**/*.css',
|
||||
'**/*.html',
|
||||
'**/*.md',
|
||||
'**/*.json',
|
||||
'**/*.svg',
|
||||
];
|
||||
const exampleFilePaths = await glob(defaultIncludes, {
|
||||
cwd: temporaryExampleDir,
|
||||
nodir: true,
|
||||
dot: true,
|
||||
ignore: stackBlitzConfig.ignore,
|
||||
});
|
||||
|
||||
const postData = await createPostData(
|
||||
temporaryExampleDir,
|
||||
stackBlitzConfig,
|
||||
exampleFilePaths,
|
||||
title,
|
||||
);
|
||||
const primaryFile = getPrimaryFile(primaryFilePath ?? stackBlitzConfig.file, exampleFilePaths);
|
||||
const html = createStackblitzHtml(postData, primaryFile);
|
||||
|
||||
const stackblitzHtmlPath = join(
|
||||
join(ASSETS_EXAMPLE_PATH, exampleFolderName),
|
||||
`${primaryFile}.html`,
|
||||
);
|
||||
await createFolder(dirname(stackblitzHtmlPath));
|
||||
await writeFile(stackblitzHtmlPath, html, 'utf-8');
|
||||
}
|
||||
|
||||
function getPrimaryFile(primaryFilePath: string, exampleFilePaths: string[]): string {
|
||||
if (primaryFilePath) {
|
||||
if (!exampleFilePaths.some((filePath) => filePath === primaryFilePath)) {
|
||||
throw new Error(`The specified primary file (${primaryFilePath}) does not exist!`);
|
||||
}
|
||||
return primaryFilePath;
|
||||
} else {
|
||||
const defaultPrimaryFilePaths = [
|
||||
'src/app/app.component.html',
|
||||
'src/app/app.component.ts',
|
||||
'src/app/main.ts',
|
||||
];
|
||||
const primaryFile = defaultPrimaryFilePaths.find((path) =>
|
||||
exampleFilePaths.some((filePath) => filePath === path),
|
||||
);
|
||||
|
||||
if (!primaryFile) {
|
||||
throw new Error(
|
||||
`None of the default primary files (${defaultPrimaryFilePaths.join(', ')}) exists.`,
|
||||
);
|
||||
}
|
||||
|
||||
return primaryFile;
|
||||
}
|
||||
}
|
||||
|
||||
async function createPostData(
|
||||
exampleDir: string,
|
||||
config: StackblitzConfig,
|
||||
exampleFilePaths: string[],
|
||||
title: string,
|
||||
): Promise<Record<string, string>> {
|
||||
const postData: Record<string, string> = {};
|
||||
|
||||
for (const filePath of exampleFilePaths) {
|
||||
if (EXCLUDE_FILES_FOR_STACKBLITZ.some((excludedFile) => filePath.endsWith(excludedFile))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = await readFile(join(exampleDir, filePath), 'utf-8');
|
||||
content = appendCopyright(filePath, content);
|
||||
content = extractRegions(filePath, content);
|
||||
|
||||
postData[`project[files][${filePath}]`] = content;
|
||||
}
|
||||
|
||||
const tags = ['angular', 'example', ...(config.tags || [])];
|
||||
tags.forEach((tag, index) => (postData[`project[tags][${index}]`] = tag));
|
||||
|
||||
postData['project[description]'] = `Angular Example - ${config.description}`;
|
||||
postData['project[template]'] = 'node';
|
||||
postData['project[title]'] = title ?? 'Angular Example';
|
||||
|
||||
return postData;
|
||||
}
|
||||
|
||||
function createStackblitzHtml(postData: Record<string, string>, primaryFile: string): string {
|
||||
const baseHtml = createBaseStackblitzHtml(primaryFile);
|
||||
const doc = new jsdom.JSDOM(baseHtml).window.document;
|
||||
const form = doc.querySelector('form');
|
||||
|
||||
for (const [key, value] of Object.entries(postData)) {
|
||||
const element = htmlToElement(doc, `<input type="hidden" name="${key}">`);
|
||||
if (element && form) {
|
||||
element.setAttribute('value', value as string);
|
||||
form.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
return doc.documentElement.outerHTML;
|
||||
}
|
||||
|
||||
function createBaseStackblitzHtml(primaryFile: string) {
|
||||
const file = `?file=${primaryFile}`;
|
||||
const action = `https://stackblitz.com/run${file}`;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html><html lang="en"><body>
|
||||
<form id="mainForm" method="post" action="${action}" target="_self"></form>
|
||||
<script>
|
||||
var embedded = 'ctl=1';
|
||||
var isEmbedded = window.location.search.indexOf(embedded) > -1;
|
||||
|
||||
if (isEmbedded) {
|
||||
var form = document.getElementById('mainForm');
|
||||
var action = form.action;
|
||||
var actionHasParams = action.indexOf('?') > -1;
|
||||
var symbol = actionHasParams ? '&' : '?'
|
||||
form.action = form.action + symbol + embedded;
|
||||
}
|
||||
document.getElementById("mainForm").submit();
|
||||
</script>
|
||||
</body></html>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function appendCopyright(filename: string, content: string): string {
|
||||
if (filename.endsWith('.html')) {
|
||||
return `${HTML_COPYRIGHT}${content}`;
|
||||
} else if (filename.endsWith('.ts') || filename.endsWith('.css')) {
|
||||
return `${CSS_TS_COPYRIGHT}${content}`;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function htmlToElement(document: Document, html: string) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.firstElementChild;
|
||||
}
|
||||
|
||||
function extractRegions(path: string, contents: string): string {
|
||||
const regionParserResult = regionParser(contents, path);
|
||||
return regionParserResult.contents;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "es2020"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {dirname, join} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
const BASE = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Directory paths
|
||||
export const ASSETS_EXAMPLE_PATH = join(
|
||||
BASE,
|
||||
'../../../src/assets/content/examples',
|
||||
);
|
||||
export const EXAMPLES_PATH = join(BASE, '../../../src/content/examples');
|
||||
export const STACKBLITZ_TEMPLATE_PATH = join(
|
||||
BASE,
|
||||
'../../../src/content/stackblitz-template',
|
||||
);
|
||||
|
||||
export const TEMPORARY_EXAMPLES_PATH = join(fileURLToPath(import.meta.url), '../../../../TEMP');
|
||||
|
||||
// Extensions
|
||||
export const TEST_FILES_EXTENSION_SUFFIX = '.spec.ts';
|
||||
export const TEST_FILES_E2E_EXTENSION_SUFFIX = '.e2e-spec.ts';
|
||||
|
||||
// Filename
|
||||
// TODO: BUILD.bazel and example-config will be deprecated when migration to standalone examples will be done.
|
||||
export const BUILD_BAZEL_FILENAME = 'BUILD.bazel';
|
||||
export const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
|
||||
export const STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
|
||||
|
||||
// Copyright
|
||||
const PAD = '\n\n';
|
||||
const COPYRIGHT =
|
||||
'@license\n' +
|
||||
'Copyright Google LLC All Rights Reserved.\n' +
|
||||
'\n' +
|
||||
'Use of this source code is governed by an MIT-style license that can be\n' +
|
||||
'found in the LICENSE file at https://angular.dev/license\n';
|
||||
export const CSS_TS_COPYRIGHT = `/*\n${COPYRIGHT}\n*/${PAD}`;
|
||||
export const HTML_COPYRIGHT = `<!-- \n${COPYRIGHT}\n-->${PAD}`;
|
||||
|
||||
// Exclude
|
||||
export const EXCLUDE_FILES_FOR_STACKBLITZ = [
|
||||
STACKBLITZ_CONFIG_FILENAME,
|
||||
BUILD_BAZEL_FILENAME,
|
||||
EXAMPLE_CONFIG_FILENAME,
|
||||
TEST_FILES_EXTENSION_SUFFIX,
|
||||
TEST_FILES_E2E_EXTENSION_SUFFIX,
|
||||
];
|
||||
@@ -0,0 +1,77 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Dirent, existsSync, readdirSync} from 'fs';
|
||||
import {copyFile, mkdir, readFile, readdir, rm, stat} from 'fs/promises';
|
||||
import {join, relative} from 'path';
|
||||
|
||||
export async function copyFolder(source: string, destination: string) {
|
||||
if (!existsSync(destination)) {
|
||||
await mkdir(destination, {recursive: true});
|
||||
}
|
||||
|
||||
const files = await readdir(source);
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = join(source, file);
|
||||
const destPath = join(destination, file);
|
||||
|
||||
const stats = await stat(sourcePath);
|
||||
const isDirectory = await stats.isDirectory();
|
||||
|
||||
if (isDirectory) {
|
||||
await copyFolder(sourcePath, destPath);
|
||||
} else {
|
||||
await copyFile(sourcePath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Recursively search the provided directory for all files which satisfy predicate and asynchronously load them. */
|
||||
export function retrieveFiles(
|
||||
baseDir: string,
|
||||
predicate?: (file: Dirent) => boolean,
|
||||
): Promise<{path: string; content: string}[]> {
|
||||
const filesWithContent: Promise<{path: string; content: string}>[] = [];
|
||||
|
||||
const checkFilesInDirectory = (dir: string) => {
|
||||
const files = readdirSync(dir, {withFileTypes: true});
|
||||
for (const file of files) {
|
||||
const fullPathToFile = join(dir, file.name);
|
||||
|
||||
if (file.isFile() && (predicate ? predicate(file) : true)) {
|
||||
filesWithContent.push(
|
||||
readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => {
|
||||
return {
|
||||
path: relative(baseDir, fullPathToFile),
|
||||
content: fileContent,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} else if (file.isDirectory()) {
|
||||
checkFilesInDirectory(fullPathToFile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkFilesInDirectory(baseDir);
|
||||
|
||||
return Promise.all(filesWithContent);
|
||||
}
|
||||
|
||||
export async function createFolder(path: string): Promise<void> {
|
||||
if (!existsSync(path)) {
|
||||
await mkdir(path, {recursive: true});
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFolder(path: string): Promise<void> {
|
||||
if (existsSync(path)) {
|
||||
await rm(path, {recursive: true});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {dirname, join} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
// FOLDER NAMES
|
||||
export const ASSETS_FOLDER_NAME = 'assets';
|
||||
export const CONTENT_FOLDER_NAME = 'content';
|
||||
export const EXAMPLES_FOLDER_NAME = 'examples';
|
||||
export const TUTORIALS_FOLDER_NAME = 'tutorials';
|
||||
|
||||
// PATHS
|
||||
export const BASE_PATH = dirname(fileURLToPath(import.meta.url));
|
||||
export const ASSETS_EXAMPLES_PATH = 'assets/content/examples';
|
||||
export const PROJECT_FOLDER_PATH = join(BASE_PATH, '../../src');
|
||||
export const ASSETS_CONTENT_FOLDER_PATH = join(
|
||||
PROJECT_FOLDER_PATH,
|
||||
ASSETS_FOLDER_NAME,
|
||||
CONTENT_FOLDER_NAME,
|
||||
);
|
||||
export const CONTENT_FOLDER_PATH = join(PROJECT_FOLDER_PATH, CONTENT_FOLDER_NAME);
|
||||
export const EXAMPLES_FOLDER_PATH = join(CONTENT_FOLDER_PATH, EXAMPLES_FOLDER_NAME);
|
||||
export const TUTORIALS_FOLDER_PATH = join(CONTENT_FOLDER_PATH, TUTORIALS_FOLDER_NAME);
|
||||
|
||||
// TODO: Update the branch/sha
|
||||
// URLS
|
||||
export const GITHUB_CONTENT_URL = 'https://github.com/angular/angular/blob/main/adev/src/content';
|
||||
export const GITHUB_EDIT_CONTENT_URL =
|
||||
'https://github.com/angular/angular/edit/main/adev/src/content';
|
||||
@@ -0,0 +1,146 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {run} from '@mermaid-js/mermaid-cli';
|
||||
import {readFile, writeFile} from 'fs/promises';
|
||||
import {marked} from 'marked';
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import {
|
||||
DocsCodeMultifileToken,
|
||||
DocsCodeToken,
|
||||
DocsCodeTripleTickBlockToken,
|
||||
} from './tranformations/docs-code.mjs';
|
||||
import {generateStackblitzExample} from './../../scripts/examples/stackblitz-builder.mjs';
|
||||
|
||||
export const MERMAID_TEMP_FOLDER = 'TEMP';
|
||||
const MERMAID_INPUT = `${MERMAID_TEMP_FOLDER}/mermaid-input-#{id}.mmd`;
|
||||
const MERMAID_OUTPUT_SVG = `${MERMAID_TEMP_FOLDER}/mermaid-output-#{id}.svg`;
|
||||
|
||||
// If token type is 'docs-code' or 'docs-code-triple-tick' and language is 'mermaid' then we can try to render Mermaid svg.
|
||||
export async function docsWalkTokens(walkToken: marked.Tokens.Generic): Promise<void> {
|
||||
const token = walkToken as DocsCodeToken | DocsCodeTripleTickBlockToken | DocsCodeMultifileToken;
|
||||
await handleMermaid(token);
|
||||
await handleStackblitzExamples(token);
|
||||
}
|
||||
|
||||
async function handleMermaid(
|
||||
token: DocsCodeToken | DocsCodeMultifileToken | DocsCodeTripleTickBlockToken,
|
||||
) {
|
||||
if (
|
||||
token.language === 'mermaid' &&
|
||||
(token.type === 'docs-code' || token.type === 'docs-code-triple-tick')
|
||||
) {
|
||||
token.code = await renderMermaidToSvg(token.code);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStackblitzExamples(
|
||||
token: DocsCodeToken | DocsCodeTripleTickBlockToken | DocsCodeMultifileToken,
|
||||
) {
|
||||
if (
|
||||
(token.type !== 'docs-code' && token.type !== 'docs-code-multifile') ||
|
||||
!token.preview ||
|
||||
!token.path
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultCodeToken: marked.Tokens.Generic =
|
||||
token.type === 'docs-code-multifile' ? token.paneTokens[0] : token;
|
||||
const path: string = defaultCodeToken['diff'] ?? defaultCodeToken['path'];
|
||||
|
||||
const pathToExampleRule = /([^\/]+)\/(.+)/s;
|
||||
const pathToExampleSegments = path.match(pathToExampleRule);
|
||||
|
||||
if (!pathToExampleSegments) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exampleFolder = pathToExampleSegments[1];
|
||||
const primaryFilePath = pathToExampleSegments[2];
|
||||
|
||||
await generateStackblitzExample(exampleFolder, primaryFilePath, defaultCodeToken['header']);
|
||||
}
|
||||
|
||||
export async function renderMermaidToSvg(content: string): Promise<string> {
|
||||
// Generate input and output path
|
||||
const mermaidTempFileId = uuidv4();
|
||||
const input = MERMAID_INPUT.replace('#{id}', mermaidTempFileId);
|
||||
const output = MERMAID_OUTPUT_SVG.replace('#{id}', mermaidTempFileId) as `${string}.svg`;
|
||||
|
||||
// Create *.mmd file with provided content.
|
||||
await writeFile(input, content);
|
||||
// Execute rendering. The generated SVG file will be written in the output path.
|
||||
await run(input, output, {
|
||||
quiet: true,
|
||||
puppeteerConfig: {headless: 1},
|
||||
parseMMDOptions: {
|
||||
mermaidConfig: {
|
||||
themeCSS: `
|
||||
background-color: var(--page-background) !important; // svg background color
|
||||
g {
|
||||
rect {
|
||||
stroke: black !important; // border around the rectangles, same for dark/light theme
|
||||
filter: drop-shadow(5px 5px 0px var(--vivid-pink));
|
||||
}
|
||||
}
|
||||
.messageText, .pieTitleText {
|
||||
fill: var(--primary-contrast) !important; // pie chart title text and line labels
|
||||
}
|
||||
.pieOuterCircle {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
.pieCircle {
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
.legend {
|
||||
rect {
|
||||
filter: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
text {
|
||||
fill: var(--primary-contrast) !important; // legend label text color
|
||||
}
|
||||
}
|
||||
.slice { // e.g. text on the pie charts
|
||||
fill: var(--primary-contrast) !important;
|
||||
}
|
||||
.flowchart-link, line { // lines
|
||||
stroke: var(--primary-contrast) !important;
|
||||
}
|
||||
.marker,
|
||||
#statediagram-barbEnd,
|
||||
.transition,
|
||||
#arrowhead path { // arrows
|
||||
stroke: var(--primary-contrast) !important;
|
||||
fill: var(--primary-contrast) !important;
|
||||
}
|
||||
.cluster rect {
|
||||
stroke: var(--primary-contrast) !important;
|
||||
fill: var(--page-background) !important;
|
||||
}
|
||||
`,
|
||||
theme: 'base',
|
||||
themeVariables: {
|
||||
fontFamily: 'sans-serif',
|
||||
primaryColor: '#fff',
|
||||
primaryBorderColor: '#000',
|
||||
pie1: '#0546ff',
|
||||
pie2: '#f637e3',
|
||||
pie3: '#f11653',
|
||||
pie4: '#8001c6',
|
||||
pie5: '#00c572',
|
||||
pie6: '#fe3700',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Read the content of generated SVG file.
|
||||
return await readFile(output, 'utf8');
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {resetHeaderIdsOfCurrentDocument} from './state.mjs';
|
||||
|
||||
/**
|
||||
* Custom hooks for marked that will be used to post-transform markdown files with parent styles for Angular docs.
|
||||
*/
|
||||
export const hooks = {
|
||||
preprocess(html: string): string {
|
||||
resetHeaderIdsOfCurrentDocument();
|
||||
return html;
|
||||
},
|
||||
postprocess(html: string): string {
|
||||
return html;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {existsSync} from 'fs';
|
||||
import {copyFile, mkdir, readdir, rm, stat, writeFile} from 'fs/promises';
|
||||
import {marked} from 'marked';
|
||||
import {dirname, join} from 'path';
|
||||
import {PROJECT_FOLDER_PATH} from './constants.mjs';
|
||||
import {MERMAID_TEMP_FOLDER, docsWalkTokens} from './docs-walk-tokens.mjs';
|
||||
import {hooks} from './hooks.mjs';
|
||||
import {renderer} from './renderer.mjs';
|
||||
import {setCurrentParsedFilePath} from './state.mjs';
|
||||
import {docsAlertExtension} from './tranformations/docs-alert.mjs';
|
||||
import {docsCalloutExtension} from './tranformations/docs-callout.mjs';
|
||||
import {docsCardContainerExtension, docsCardExtension} from './tranformations/docs-card.mjs';
|
||||
import {
|
||||
docsCodeExtension,
|
||||
docsCodeMultifileExtension,
|
||||
docsTripleTickMarkdownCodeExtension,
|
||||
} from './tranformations/docs-code.mjs';
|
||||
import {docsDecorativeHeaderExtension} from './tranformations/docs-decorative-header.mjs';
|
||||
import {docsPillExtension, docsPillRowExtension} from './tranformations/docs-pill.mjs';
|
||||
import {docsVideoExtension} from './tranformations/docs-video.mjs';
|
||||
import {docsStepExtension, docsWorkflowExtension} from './tranformations/docs-workflow.mjs';
|
||||
import {retrieveAllMarkdownFiles} from './utils.mjs';
|
||||
|
||||
declare var logs: string[];
|
||||
(globalThis as any).logs = [];
|
||||
|
||||
/**
|
||||
* Fully configured processor instance.
|
||||
*
|
||||
* Marked was chosen as our processor as it is a popular and well supported by the community. Additionally,
|
||||
* its processing of custom HTML allows for a more direct interactions rather than being done within a custom
|
||||
* AST representation as other solutions provide.
|
||||
*/
|
||||
marked.use({
|
||||
mangle: false,
|
||||
headerIds: false,
|
||||
renderer,
|
||||
hooks,
|
||||
extensions: [
|
||||
/** Custom Extensions are @type marked.TokenizerAndRendererExtension but the this.renderer uses a custom Token that extends marked.Tokens.Generic which is not exported by @types/marked */
|
||||
// @ts-ignore @types/marked
|
||||
docsCardExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsCardContainerExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsCalloutExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsDecorativeHeaderExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsPillRowExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsPillExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsWorkflowExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsStepExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsCodeExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsCodeMultifileExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsTripleTickMarkdownCodeExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsAlertExtension,
|
||||
// @ts-ignore @types/marked
|
||||
docsVideoExtension,
|
||||
],
|
||||
// Marked will return a promise if the async option is true.
|
||||
// The async option will tell marked to await any walkTokens functions before parsing the tokens and returning an HTML string.
|
||||
// More details: https://marked.js.org/using_pro#async
|
||||
async: true,
|
||||
walkTokens: docsWalkTokens,
|
||||
});
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
/** Create temporary folder which helps to generate mermaid diagrams in the svg format */
|
||||
await createTempMermaidFolder();
|
||||
|
||||
/** Generate Docs Content */
|
||||
await generateContent(
|
||||
join(PROJECT_FOLDER_PATH, 'content'),
|
||||
join(PROJECT_FOLDER_PATH, 'assets', 'content'),
|
||||
);
|
||||
|
||||
/** Copy the images from source content folder to assets */
|
||||
await copyFolder(
|
||||
join(PROJECT_FOLDER_PATH, 'content', 'images'),
|
||||
join(PROJECT_FOLDER_PATH, 'assets', 'content', 'images'),
|
||||
);
|
||||
|
||||
/** Remove temp folder */
|
||||
await cleanUp();
|
||||
}
|
||||
|
||||
async function generateContent(baseDir: string, distDir: string) {
|
||||
const consoleTimeLabel = '\nMarkdown files processed in';
|
||||
|
||||
console.info(`Processing markdown files...`);
|
||||
console.time(consoleTimeLabel);
|
||||
|
||||
/** List of all markdown files in the content directory. */
|
||||
const files = await retrieveAllMarkdownFiles(baseDir);
|
||||
|
||||
/** These directories have no markdown destined to be in the docs */
|
||||
const skipDirectories = ['examples'];
|
||||
|
||||
const filteredFiles = files.filter((f) => !skipDirectories.some((dir) => f.path.startsWith(dir)));
|
||||
|
||||
await Promise.all(
|
||||
filteredFiles.map((file) =>
|
||||
mkdir(join(distDir, dirname(file.path)), {recursive: true}).catch(console.error),
|
||||
),
|
||||
);
|
||||
|
||||
let hadWarnings = false;
|
||||
for (let index = 0; index < filteredFiles.length; index++) {
|
||||
const file = filteredFiles[index];
|
||||
|
||||
// Basic progress report
|
||||
process.stdout.write('\r\x1b[K');
|
||||
process.stdout.write(`${index}/${filteredFiles.length} - Processing ${file.path}`);
|
||||
// keep in mind write doesn't write a newline like console.log does
|
||||
|
||||
// Logging is handcrafted because markedjs provides no solution
|
||||
// Moreover it has no context of filename as it is only string based
|
||||
// So we're using a global `logs` array
|
||||
if (logs.length) {
|
||||
console.warn('\n');
|
||||
logs.forEach((log) => {
|
||||
console.warn(`${file.path}: ${log}`);
|
||||
});
|
||||
console.warn('\n');
|
||||
logs.length = 0;
|
||||
hadWarnings = true;
|
||||
}
|
||||
|
||||
setCurrentParsedFilePath(file.path);
|
||||
const processedFile = await marked.parse(file.content, {async: true});
|
||||
const htmlFilePath = `${file.path}.html`;
|
||||
await writeFile(join(distDir, htmlFilePath), processedFile.toString());
|
||||
}
|
||||
|
||||
if (hadWarnings) {
|
||||
console.warn(
|
||||
`\n\nYou can resolve the ambiguities by editing the mentioned file(s) and adding a specific link using the Markdown syntax.`,
|
||||
);
|
||||
}
|
||||
|
||||
console.timeEnd(consoleTimeLabel);
|
||||
}
|
||||
|
||||
async function createTempMermaidFolder() {
|
||||
if (!existsSync(MERMAID_TEMP_FOLDER)) {
|
||||
await mkdir(MERMAID_TEMP_FOLDER);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanUp() {
|
||||
if (existsSync(MERMAID_TEMP_FOLDER)) {
|
||||
await rm(MERMAID_TEMP_FOLDER, {recursive: true});
|
||||
}
|
||||
}
|
||||
|
||||
async function copyFolder(source: string, destination: string) {
|
||||
if (!existsSync(destination)) {
|
||||
await mkdir(destination);
|
||||
}
|
||||
|
||||
const files = await readdir(source);
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = join(source, file);
|
||||
const destPath = join(destination, file);
|
||||
|
||||
const stats = await stat(sourcePath);
|
||||
|
||||
(await stats.isDirectory()) ? copyFolder(sourcePath, destPath) : copyFile(sourcePath, destPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Code Regions
|
||||
|
||||
All the files in the regions folder were implemented based on angular.io: <https://github.com/angular/angular/tree/main/aio/tools/transforms/examples-package/services>.
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
// These kind of comments are used CSS and other languages that do not support inline comments
|
||||
export const regionStartMatcher = /^\s*\/\*\s*#docregion\s*(.*)\s*\*\/\s*$/;
|
||||
export const regionEndMatcher = /^\s*\/\*\s*#enddocregion\s*(.*)\s*\*\/\s*$/;
|
||||
export const plasterMatcher = /^\s*\/\*\s*#docplaster\s*(.*)\s*\*\/\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `/* ${plaster} */`;
|
||||
@@ -0,0 +1,14 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
// These kind of comments are used in HTML
|
||||
|
||||
export const regionStartMatcher = /^\s*<!--\s*#docregion\s*(.*?)\s*(?:-->)?\s*$/;
|
||||
export const regionEndMatcher = /^\s*<!--\s*#enddocregion\s*(.*?)\s*-->\s*$/;
|
||||
export const plasterMatcher = /^\s*<!--\s*#docplaster\s*(.*?)\s*-->\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `<!-- ${plaster} -->`;
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
// These kind of comments are used in languages that do not support block comments, such as Jade
|
||||
export const regionStartMatcher = /^\s*\/\/\s*#docregion\s*(.*)\s*$/;
|
||||
export const regionEndMatcher = /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/;
|
||||
export const plasterMatcher = /^\s*\/\/\s*#docplaster\s*(.*)\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `// ${plaster}`;
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
// This comment type is used in C like languages such as JS, TS, etc
|
||||
export const regionStartMatcher = /^\s*\/\/\s*#docregion\s*(.*)\s*$/;
|
||||
export const regionEndMatcher = /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/;
|
||||
export const plasterMatcher = /^\s*\/\/\s*#docplaster\s*(.*)\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `/* ${plaster} */`;
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
// These type of comments are used in hash comment based languages such as bash and Yaml
|
||||
export const regionStartMatcher = /^\s*#\s*#docregion\s*(.*)\s*$/;
|
||||
export const regionEndMatcher = /^\s*#\s*#enddocregion\s*(.*)\s*$/;
|
||||
export const plasterMatcher = /^\s*#\s*#docplaster\s*(.*)\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `# ${plaster}`;
|
||||
@@ -0,0 +1,196 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import * as blockC from './region-matchers/block-c.mjs';
|
||||
import * as html from './region-matchers/html.mjs';
|
||||
import * as inlineC from './region-matchers/inline-c.mjs';
|
||||
import * as inlineCOnly from './region-matchers/inline-c-only.mjs';
|
||||
import * as inlineHash from './region-matchers/inline-hash.mjs';
|
||||
import {FileType, removeEslintComments} from './removeEslintComments.mjs';
|
||||
|
||||
const WHOLE_FILE_REGION_NAME = '';
|
||||
const DEFAULT_PLASTER = '. . .';
|
||||
const REGION_MATCHERS = {
|
||||
ts: inlineC,
|
||||
js: inlineC,
|
||||
mjs: inlineCOnly,
|
||||
es6: inlineC,
|
||||
html: html,
|
||||
svg: html,
|
||||
css: blockC,
|
||||
conf: inlineHash,
|
||||
yaml: inlineHash,
|
||||
yml: inlineHash,
|
||||
sh: inlineHash,
|
||||
jade: inlineCOnly,
|
||||
pug: inlineCOnly,
|
||||
json: inlineCOnly,
|
||||
'json.annotated': inlineCOnly,
|
||||
};
|
||||
|
||||
interface Region {
|
||||
lines: string[];
|
||||
open?: boolean;
|
||||
ranges: {from: number; to?: number}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: We assume that the tag defining the beginning and end of the region will be in different lines in each case.
|
||||
* For example, in HTML, we don't expect to have the following code on one line: <!-- docregion name -->content<!--enddocregion name-->
|
||||
*/
|
||||
export const regionParser = (contents: string, filePath: string) => {
|
||||
const fileType: FileType | undefined = filePath?.split('.').pop() as FileType;
|
||||
|
||||
if (!fileType) {
|
||||
throw new Error(`Incorrect file type for region parser: ${filePath}!`);
|
||||
}
|
||||
|
||||
const regionMatcher = REGION_MATCHERS[fileType];
|
||||
const openRegions: string[] = [];
|
||||
const regionMap: Record<string, Region> = {};
|
||||
|
||||
let countOfRegionLines = 0;
|
||||
|
||||
if (regionMatcher) {
|
||||
let plaster = regionMatcher.createPlasterComment(DEFAULT_PLASTER);
|
||||
const lines = removeEslintComments(contents, fileType)
|
||||
.split(/\r?\n/)
|
||||
.filter((line, index) => {
|
||||
const startRegion = line.match(regionMatcher.regionStartMatcher);
|
||||
const endRegion = line.match(regionMatcher.regionEndMatcher);
|
||||
const updatePlaster = line.match(regionMatcher.plasterMatcher);
|
||||
|
||||
// start region processing
|
||||
if (startRegion) {
|
||||
// open up the specified region
|
||||
handleStartRegion(
|
||||
startRegion,
|
||||
regionMap,
|
||||
index,
|
||||
countOfRegionLines,
|
||||
plaster,
|
||||
openRegions,
|
||||
);
|
||||
} else if (endRegion) {
|
||||
// end region processing
|
||||
handleEndRegion(openRegions, endRegion, regionMap, index, countOfRegionLines);
|
||||
} else if (updatePlaster) {
|
||||
// doc plaster processing
|
||||
const plasterString = updatePlaster[1].trim();
|
||||
plaster = plasterString ? regionMatcher.createPlasterComment(plasterString) : '';
|
||||
} else {
|
||||
// simple line of content processing
|
||||
openRegions.forEach((regionName) => regionMap[regionName].lines.push(line));
|
||||
// do not filter out this line from the content
|
||||
return true;
|
||||
}
|
||||
|
||||
// this line contained an annotation so let's filter it out
|
||||
countOfRegionLines++;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!regionMap[WHOLE_FILE_REGION_NAME]) {
|
||||
regionMap[WHOLE_FILE_REGION_NAME] = {lines, ranges: [{from: 1, to: lines.length + 1}]};
|
||||
}
|
||||
|
||||
return {
|
||||
contents: lines.join('\n'),
|
||||
regionMap,
|
||||
totalLinesCount: lines.length,
|
||||
};
|
||||
} else {
|
||||
return {contents, regionMap, totalLinesCount: 0};
|
||||
}
|
||||
};
|
||||
|
||||
function handleStartRegion(
|
||||
startRegion: RegExpMatchArray,
|
||||
regionMap: Record<string, Region>,
|
||||
index: number,
|
||||
countOfRegionLines: number,
|
||||
plaster: string,
|
||||
openRegions: string[],
|
||||
) {
|
||||
const regionNames = getRegionNames(startRegion[1]);
|
||||
if (regionNames.length === 0) {
|
||||
regionNames.push(WHOLE_FILE_REGION_NAME);
|
||||
}
|
||||
|
||||
for (const regionName of regionNames) {
|
||||
const region = regionMap[regionName];
|
||||
if (region) {
|
||||
if (region.open) {
|
||||
throw new Error(`Tried to open a region, named "${regionName}", that is already open`);
|
||||
}
|
||||
|
||||
// Region is opened, set from range value.
|
||||
region.open = true;
|
||||
region.ranges.push({from: getFromRangeValue(index, countOfRegionLines)});
|
||||
|
||||
if (plaster) {
|
||||
// Use the same indent as the docregion marker
|
||||
const indent = startRegion[0].split(/[^ ]/, 1);
|
||||
region.lines.push(indent + plaster);
|
||||
}
|
||||
} else {
|
||||
regionMap[regionName] = {
|
||||
lines: [],
|
||||
open: true,
|
||||
ranges: [
|
||||
{
|
||||
from: getFromRangeValue(index, countOfRegionLines),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
openRegions.push(regionName);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEndRegion(
|
||||
openRegions: string[],
|
||||
endRegion: RegExpMatchArray,
|
||||
regionMap: Record<string, Region>,
|
||||
index: number,
|
||||
countOfRegionLines: number,
|
||||
) {
|
||||
if (openRegions.length === 0) {
|
||||
throw new Error('Tried to close a region when none are open');
|
||||
}
|
||||
// close down the specified region (or most recent if no name is given)
|
||||
const regionNames = getRegionNames(endRegion[1]);
|
||||
if (regionNames.length === 0) {
|
||||
regionNames.push(openRegions[openRegions.length - 1]);
|
||||
}
|
||||
|
||||
for (const regionName of regionNames) {
|
||||
const region = regionMap[regionName];
|
||||
if (!region || !region.open) {
|
||||
throw new Error(`Tried to close a region, named "${regionName}", that is not open`);
|
||||
}
|
||||
|
||||
// Region is closed, we can define the last line number of the region
|
||||
region.open = false;
|
||||
region.ranges[region.ranges.length - 1].to = index - countOfRegionLines;
|
||||
removeLast(openRegions, regionName);
|
||||
}
|
||||
}
|
||||
|
||||
function getFromRangeValue(index: number, countOfRegionLines: number): number {
|
||||
return index - countOfRegionLines + 1;
|
||||
}
|
||||
|
||||
function getRegionNames(input: string): string[] {
|
||||
return input.trim() === '' ? [] : input.split(',').map((name) => name.trim());
|
||||
}
|
||||
|
||||
function removeLast(array: string[], item: string): void {
|
||||
const index = array.lastIndexOf(item);
|
||||
array.splice(index, 1);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export type FileType =
|
||||
| 'ts'
|
||||
| 'js'
|
||||
| 'mjs'
|
||||
| 'es6'
|
||||
| 'html'
|
||||
| 'svg'
|
||||
| 'css'
|
||||
| 'conf'
|
||||
| 'yaml'
|
||||
| 'yml'
|
||||
| 'sh'
|
||||
| 'jade'
|
||||
| 'pug'
|
||||
| 'json'
|
||||
| 'json.annotated';
|
||||
|
||||
export function removeEslintComments(input: string, fileType: FileType) {
|
||||
if (!input || (fileType !== 'ts' && fileType !== 'js' && fileType !== 'html')) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const regexForFileType = regexesForFileTypes[fileType];
|
||||
|
||||
return input.replace(regexForFileType, '');
|
||||
}
|
||||
|
||||
const jsRegexes = [
|
||||
/\/\/ *eslint-disable(?:-next-line)?(?: .*)?(?:\n *|$)/,
|
||||
/\n? *\/\/ *eslint-(?:disable-line|enable)(?: .*)?(?=\n|$)/,
|
||||
/\/\*\s*eslint-disable(?:-next-line)?(?: [\s\S]*?)?\*\/ *(?:\n *)?/,
|
||||
/\n? *\/\*\s*eslint-(?:disable-line|enable)(?: [\s\S]*?)?\*\//,
|
||||
];
|
||||
|
||||
const htmlRegexes = [
|
||||
/<!--\s*eslint-disable(?:-next-line)?(?: [\s\S]*?)?--> *(?:\n *)?/,
|
||||
/\n? *<!--\s*eslint-(?:disable-line|enable)(?: [\s\S]*?)?-->/,
|
||||
];
|
||||
|
||||
const joinRegexes = (regexes: any) =>
|
||||
new RegExp(regexes.map((regex: any) => `(?:${regex.source})`).join('|'), 'g');
|
||||
const htmlRegex = joinRegexes(htmlRegexes);
|
||||
// Note: the js regex needs to also include the html ones to account for inline templates in @Components
|
||||
const jsRegex = joinRegexes([...jsRegexes, ...htmlRegexes]);
|
||||
|
||||
const regexesForFileTypes = {
|
||||
js: jsRegex,
|
||||
ts: jsRegex,
|
||||
html: htmlRegex,
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Renderer as MarkedRenderer, marked} from 'marked';
|
||||
import {handleCode} from './tranformations/code.mjs';
|
||||
import {handleEmoji} from './tranformations/emoji.mjs';
|
||||
import {transformH1} from './tranformations/h1.mjs';
|
||||
import {transformHeader} from './tranformations/header.mjs';
|
||||
import {transformImage} from './tranformations/image.mjs';
|
||||
import {transformLink} from './tranformations/link.mjs';
|
||||
import {transformDocsList} from './tranformations/lists.mjs';
|
||||
import {transformDocsTable} from './tranformations/table.mjs';
|
||||
|
||||
/**
|
||||
* Custom renderer for marked that will be used to transform markdown files to HTML
|
||||
* files that can be used in the Angular docs.
|
||||
*/
|
||||
export const renderer: Partial<MarkedRenderer> = {
|
||||
// All headers have tabindex="-1"
|
||||
// so that we can focus relevant sections on
|
||||
// navigation for accessibility purposes.
|
||||
heading(label: string, level: number) {
|
||||
switch (level) {
|
||||
// Custom H1 header element with page breadcrumb and docs edit button link to GitHub
|
||||
case 1:
|
||||
return transformH1(label);
|
||||
// Custom headers include an anchor link and href
|
||||
default:
|
||||
return transformHeader(label, level);
|
||||
}
|
||||
},
|
||||
// Custom table styling
|
||||
table(header: string, body: string) {
|
||||
return transformDocsTable(header, body);
|
||||
},
|
||||
// Custom list styling
|
||||
list(body: string, ordered: boolean) {
|
||||
return transformDocsList(body, ordered);
|
||||
},
|
||||
// Custom list styling
|
||||
image(href: string | null, title: string | null, text: string) {
|
||||
return transformImage(href, title, text);
|
||||
},
|
||||
// Handles special behavior for external links
|
||||
link(href: string | null, title: string | null, text: string) {
|
||||
return transformLink(href, title, text);
|
||||
},
|
||||
// Custom code span styling
|
||||
codespan(this: marked.RendererThis, code: string): string {
|
||||
return handleCode(code);
|
||||
},
|
||||
// Custom text styling
|
||||
text(this: marked.RendererThis, code: string): string {
|
||||
return handleEmoji(code);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TUTORIALS_FOLDER_NAME} from './constants.mjs';
|
||||
|
||||
export const enum DocumentType {
|
||||
DOCS,
|
||||
TUTORIAL,
|
||||
}
|
||||
|
||||
let currentParsedPath: string | null = null;
|
||||
let currentParsedDocumentType: DocumentType = DocumentType.DOCS;
|
||||
const headerIds = new Map<string, number>();
|
||||
|
||||
export const setCurrentParsedFilePath = (path: string) => {
|
||||
currentParsedPath = path;
|
||||
setCurrentParsedDocumentType(
|
||||
path.startsWith(TUTORIALS_FOLDER_NAME) ? DocumentType.TUTORIAL : DocumentType.DOCS,
|
||||
);
|
||||
};
|
||||
|
||||
export const getCurrentParsedFilePath = (): string | null => currentParsedPath;
|
||||
|
||||
export const setCurrentParsedDocumentType = (type: DocumentType) => {
|
||||
currentParsedDocumentType = type;
|
||||
};
|
||||
|
||||
export const getCurrentParsedDocumentType = (): DocumentType => currentParsedDocumentType;
|
||||
|
||||
export const getHeaderId = (id: string): string => {
|
||||
const numberOfHeaderOccurrencesInTheDocument = headerIds.get(id) ?? 0;
|
||||
headerIds.set(id, numberOfHeaderOccurrencesInTheDocument + 1);
|
||||
|
||||
const cleanedUpId = id
|
||||
.toLowerCase()
|
||||
.replaceAll(/<code>(.*?)<\/code>/g, '$1') // remove <code>
|
||||
.replaceAll(/<strong>(.*?)<\/strong>/g, '$1') // remove <strong>
|
||||
.replaceAll(/<em>(.*?)<\/em>/g, '$1') // remove <em>
|
||||
.replace(/\s|\//g, '-') // remove spaces and slashes
|
||||
.replace(/gt;|lt;/g, '') // remove escaped < and >
|
||||
.replace(/&#\d+;/g, '') // remove HTML entities
|
||||
.replace(/[^0-9a-zA-Z\-]/g, ''); // only keep letters, digits & dashes
|
||||
|
||||
const headerId = numberOfHeaderOccurrencesInTheDocument
|
||||
? `${cleanedUpId}-${numberOfHeaderOccurrencesInTheDocument}`
|
||||
: cleanedUpId;
|
||||
|
||||
return headerId;
|
||||
};
|
||||
|
||||
export const resetHeaderIdsOfCurrentDocument = (): void => {
|
||||
headerIds.clear();
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {readFileSync, readdirSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {ClassEntry, DocEntry, EntryType} from '../../api-gen/entities';
|
||||
import {handleEmoji} from './emoji.mjs';
|
||||
|
||||
// global defined in the index
|
||||
declare const logs: string[];
|
||||
|
||||
// API
|
||||
interface EntryCollection {
|
||||
moduleName: string;
|
||||
entries: DocEntry[];
|
||||
}
|
||||
|
||||
function parseEntryData(srcs: string[]): EntryCollection[] {
|
||||
return srcs.map((jsonDataFilePath) => {
|
||||
const fileContent = readFileSync(jsonDataFilePath, {encoding: 'utf8'});
|
||||
return JSON.parse(fileContent) as EntryCollection;
|
||||
});
|
||||
}
|
||||
|
||||
interface ParsedEntry {
|
||||
module: string;
|
||||
members: string[];
|
||||
type: EntryType;
|
||||
isDeprecated: boolean;
|
||||
}
|
||||
|
||||
let apiEntries: undefined | Map<string, Array<ParsedEntry>>;
|
||||
|
||||
function getApiEntries(): Map<string, Array<ParsedEntry>> {
|
||||
const DEV_DATA_PATH = join('prerender', 'api-gen', 'dev-data', 'api');
|
||||
|
||||
const filePaths = readdirSync(DEV_DATA_PATH).map((fileName) => join(DEV_DATA_PATH, fileName));
|
||||
|
||||
const entryCollections: EntryCollection[] = parseEntryData(filePaths);
|
||||
|
||||
const entries = new Map<string, Array<ParsedEntry>>();
|
||||
|
||||
entryCollections.forEach((module) => {
|
||||
const moduleName = module.moduleName.slice(9); // removing "@angular/"" from the name
|
||||
|
||||
module.entries.forEach((entry) => {
|
||||
const members = (entry as ClassEntry).members?.map((m) => m.name) ?? [];
|
||||
const parsedEntry = {
|
||||
module: moduleName,
|
||||
members,
|
||||
type: entry.entryType,
|
||||
isDeprecated: entry.jsdocTags.some((tag) => tag.name === 'deprecated'),
|
||||
};
|
||||
if (!entries.has(entry.name)) {
|
||||
entries.set(entry.name, [parsedEntry]);
|
||||
} else {
|
||||
entries.get(entry.name)?.push(parsedEntry);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function defaultCodeBlock(text: string) {
|
||||
return `<code>${handleEmoji(text)}</code>`;
|
||||
}
|
||||
|
||||
export function handleCode(text: string): string {
|
||||
if (!apiEntries) {
|
||||
apiEntries = getApiEntries();
|
||||
}
|
||||
|
||||
// strip trailing the braces for functions,
|
||||
// remove leading @ for decorators
|
||||
let entity = text.replace(/^@|(\(\))$/g, '');
|
||||
let method: string | undefined;
|
||||
|
||||
// We're parsing a class.method
|
||||
if (entity.includes('.')) {
|
||||
[entity, method] = entity.split('.');
|
||||
}
|
||||
|
||||
if (apiEntries.has(entity)) {
|
||||
const isDecorator = text.startsWith('@');
|
||||
|
||||
const entries = apiEntries.get(entity);
|
||||
const nonDeprecated = entries?.find((e) => e.isDeprecated);
|
||||
let entry: ParsedEntry | undefined;
|
||||
|
||||
if (entries?.length === 1) {
|
||||
entry = entries[0];
|
||||
} else if (entries?.length === 2 && nonDeprecated) {
|
||||
// Sometime we move an entry from one package to another and deprecate the original entry.
|
||||
// In this case we have 2 entries with one deprecated, we take the non-deprecated one
|
||||
entry = nonDeprecated;
|
||||
}
|
||||
|
||||
if (entry) {
|
||||
if (isDecorator && entry.type !== 'decorator') {
|
||||
return defaultCodeBlock(text);
|
||||
}
|
||||
|
||||
if (entity && method && entry.members.includes(method)) {
|
||||
return `<a href="api/${entry.module}/${entity}#${method}"><code>${text}</code></a>`;
|
||||
}
|
||||
|
||||
return `<a href="api/${entry.module}/${entity}"><code>${text}</code></a>`;
|
||||
} else {
|
||||
logs.push(`Cannot resolve the ambiguity for: ${entity}`);
|
||||
}
|
||||
}
|
||||
|
||||
return defaultCodeBlock(text);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {marked} from 'marked';
|
||||
import {AlertSeverityLevel} from './enums/levels.mjs';
|
||||
|
||||
interface DocsAlertToken extends marked.Tokens.Generic {
|
||||
type: 'docs-alert';
|
||||
body: string;
|
||||
severityLevel: string;
|
||||
tokens: marked.Token[];
|
||||
}
|
||||
|
||||
interface DocsAlert {
|
||||
alert: RegExpExecArray | null;
|
||||
severityLevel: string;
|
||||
}
|
||||
|
||||
export const docsAlertExtension = {
|
||||
name: 'docs-alert',
|
||||
level: 'block',
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsAlertToken | undefined {
|
||||
const match: DocsAlert = {
|
||||
alert: null,
|
||||
severityLevel: AlertSeverityLevel.HELPFUL,
|
||||
};
|
||||
for (let level in AlertSeverityLevel) {
|
||||
// Capture group 1: all alert text content after the severity level
|
||||
const rule = new RegExp('^s*' + level + ': (.*?)\n', 's');
|
||||
const possibleMatch = rule.exec(src);
|
||||
|
||||
if (possibleMatch?.[1]) {
|
||||
match.alert = possibleMatch;
|
||||
match.severityLevel = level;
|
||||
}
|
||||
}
|
||||
|
||||
if (match?.alert) {
|
||||
const token: DocsAlertToken = {
|
||||
type: 'docs-alert',
|
||||
raw: match.alert[0],
|
||||
body: match.alert[1].trim(),
|
||||
severityLevel: match.severityLevel,
|
||||
tokens: [],
|
||||
};
|
||||
token.body = `**${
|
||||
token.severityLevel === AlertSeverityLevel.TLDR ? 'TL;DR' : token.severityLevel
|
||||
}:** ${token.body}`;
|
||||
this.lexer.blockTokens(token.body, token.tokens);
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsAlertToken) {
|
||||
return `
|
||||
<div class="docs-alert docs-alert-${token.severityLevel.toLowerCase()}">
|
||||
${this.parser.parse(token.tokens)}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {marked} from 'marked';
|
||||
import {CalloutSeverityLevel} from './enums/levels.mjs';
|
||||
interface DocsCalloutToken extends marked.Tokens.Generic {
|
||||
type: 'docs-callout';
|
||||
title: string;
|
||||
titleTokens: marked.Token[];
|
||||
severityLevel: CalloutSeverityLevel;
|
||||
body: string;
|
||||
bodyTokens: marked.Token[];
|
||||
}
|
||||
|
||||
// Capture group 1: all attributes on the opening tag
|
||||
// Capture group 2: all content between the open and close tags
|
||||
const calloutRule = /^<docs-callout([^>]*)>((?:.(?!\/docs-callout))*)<\/docs-callout>/s;
|
||||
|
||||
const titleRule = /title="([^"]*)"/;
|
||||
const isImportantRule = /important/;
|
||||
const isCriticalRule = /critical/;
|
||||
|
||||
export const docsCalloutExtension = {
|
||||
name: 'docs-callout',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\s*<docs-callout/m)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsCalloutToken | undefined {
|
||||
const match = calloutRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const attr = match[1].trim();
|
||||
const title = titleRule.exec(attr);
|
||||
|
||||
let severityLevel = CalloutSeverityLevel.HELPFUL;
|
||||
if (isImportantRule.exec(attr)) severityLevel = CalloutSeverityLevel.IMPORTANT;
|
||||
if (isCriticalRule.exec(attr)) severityLevel = CalloutSeverityLevel.CRITICAL;
|
||||
|
||||
const body = match[2].trim();
|
||||
|
||||
const token: DocsCalloutToken = {
|
||||
type: 'docs-callout',
|
||||
raw: match[0],
|
||||
severityLevel: severityLevel,
|
||||
title: title ? title[1] : '',
|
||||
titleTokens: [],
|
||||
body: body ?? '',
|
||||
bodyTokens: [],
|
||||
};
|
||||
this.lexer.inlineTokens(token.title, token.titleTokens);
|
||||
this.lexer.blockTokens(token.body, token.bodyTokens);
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsCalloutToken) {
|
||||
return `
|
||||
<div class="docs-callout docs-callout-${token.severityLevel.toLowerCase()}">
|
||||
<h3>${this.parser.parseInline(token.titleTokens)}</h3>
|
||||
<p>${this.parser.parse(token.bodyTokens)}</p>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {marked} from 'marked';
|
||||
import {targetForExternalHref} from './helpers.mjs';
|
||||
import {PROJECT_FOLDER_PATH} from '../constants.mjs';
|
||||
import {join} from 'path';
|
||||
import {readFileSync} from 'fs';
|
||||
|
||||
interface DocsCardContainerToken extends marked.Tokens.Generic {
|
||||
type: 'docs-card-container';
|
||||
cards: string;
|
||||
tokens: marked.Token[];
|
||||
}
|
||||
|
||||
const cardContainerRule = /^<docs-card-container>(.*?)<\/docs-card-container>/s;
|
||||
|
||||
export const docsCardContainerExtension = {
|
||||
name: 'docs-card-container',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\s*<docs-card-container/m)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsCardContainerToken | undefined {
|
||||
const match = cardContainerRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const body = match[1];
|
||||
const token: DocsCardContainerToken = {
|
||||
type: 'docs-card-container',
|
||||
raw: match[0],
|
||||
cards: body ?? '',
|
||||
tokens: [],
|
||||
};
|
||||
this.lexer.blockTokens(token.cards, token.tokens);
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsCardContainerToken) {
|
||||
return `
|
||||
<div class="docs-card-grid">
|
||||
${this.parser.parse(token.tokens)}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
interface DocsCardToken extends marked.Tokens.Generic {
|
||||
type: 'docs-card';
|
||||
title: string;
|
||||
body: string;
|
||||
link?: string;
|
||||
href?: string;
|
||||
imgSrc?: string;
|
||||
tokens: marked.Token[];
|
||||
}
|
||||
|
||||
// Capture group 1: all attributes on the opening tag
|
||||
// Capture group 2: all content between the open and close tags
|
||||
const cardRule = /^[^<]*<docs-card\s([^>]*)>((?:.(?!\/docs-card))*)<\/docs-card>/s;
|
||||
|
||||
const titleRule = /title="([^"]*)"/;
|
||||
const linkRule = /link="([^"]*)"/;
|
||||
const hrefRule = /href="([^"]*)"/;
|
||||
const imgSrcRule = /imgSrc="([^"]*)"/;
|
||||
|
||||
export const docsCardExtension = {
|
||||
name: 'docs-card',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\s*<docs-card\s*/m)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsCardToken | undefined {
|
||||
const match = cardRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const attr = match[1].trim();
|
||||
const title = titleRule.exec(attr);
|
||||
const link = linkRule.exec(attr);
|
||||
const href = hrefRule.exec(attr);
|
||||
const imgSrc = imgSrcRule.exec(attr);
|
||||
|
||||
const body = match[2].trim();
|
||||
|
||||
const token: DocsCardToken = {
|
||||
type: 'docs-card',
|
||||
raw: match[0],
|
||||
title: title ? title[1] : '',
|
||||
body: body ?? '',
|
||||
href: href ? href[1] : undefined,
|
||||
link: link ? link[1] : undefined,
|
||||
imgSrc: imgSrc ? imgSrc[1] : undefined,
|
||||
tokens: [],
|
||||
};
|
||||
this.lexer.blockTokens(token.body, token.tokens);
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsCardToken) {
|
||||
return token.imgSrc ? getCardWithSvgIllustration(this, token) : getStandardCard(this, token);
|
||||
},
|
||||
};
|
||||
|
||||
function getStandardCard(renderer: marked.RendererThis, token: DocsCardToken) {
|
||||
if (token.href) {
|
||||
return `
|
||||
<a href="${token.href}" ${targetForExternalHref(token.href)} class="docs-card">
|
||||
<div>
|
||||
<h3>${token.title}</h3>
|
||||
<p>${renderer.parser.parse(token.tokens)}</p>
|
||||
</div>
|
||||
<span>${token.link ? token.link : 'Learn more'}</span>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div class="docs-card">
|
||||
<div>
|
||||
<h3>${token.title}</h3>
|
||||
<p>${renderer.parser.parse(token.tokens)}</p>
|
||||
</div>
|
||||
${token.link ? `<span>${token.link}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getCardWithSvgIllustration(renderer: marked.RendererThis, token: DocsCardToken) {
|
||||
// We can assume that all illustrations are svg files
|
||||
// We need to read svg content, instead of renering svg with `img`,
|
||||
// cause we would like to use CSS variables to support dark and light mode.
|
||||
const illustration = getSvgIllustration(token.imgSrc!);
|
||||
|
||||
if (token.href) {
|
||||
return `
|
||||
<a href="${token.href}" ${targetForExternalHref(
|
||||
token.href,
|
||||
)} class="docs-card docs-card-with-svg">
|
||||
${illustration}
|
||||
<div class="docs-card-text-content">
|
||||
<div>
|
||||
<h3>${token.title}</h3>
|
||||
${renderer.parser.parse(token.tokens)}
|
||||
</div>
|
||||
<span>${token.link ? token.link : 'Learn more'}</span>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div class="docs-card docs-card-with-svg">
|
||||
${illustration}
|
||||
<div class="docs-card-text-content">
|
||||
<h3>${token.title}</h3>
|
||||
${renderer.parser.parse(token.tokens)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getSvgIllustration(path: string): string {
|
||||
return readFileSync(join(PROJECT_FOLDER_PATH, path), {encoding: 'utf-8'});
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {marked} from 'marked';
|
||||
import hljs from 'highlight.js';
|
||||
import * as diff from 'diff';
|
||||
import {join} from 'path';
|
||||
import {readFileSync} from 'fs';
|
||||
import {decode} from 'html-entities';
|
||||
import {regionParser} from '../regions/region-parser.mjs';
|
||||
import {EXAMPLES_FOLDER_PATH, TUTORIALS_FOLDER_PATH} from '../constants.mjs';
|
||||
import {DocumentType, getCurrentParsedDocumentType} from '../state.mjs';
|
||||
|
||||
/** Marked token for a multifile custom docs element. */
|
||||
export interface DocsCodeMultifileToken extends marked.Tokens.Generic {
|
||||
type: 'docs-code-multifile';
|
||||
// The example path used for linking to Stackblitz or rendering a preview
|
||||
path: string | undefined;
|
||||
// The raw nested Markdown of <docs-code> examples in the multifile example
|
||||
panes: string;
|
||||
// The DocsCodeToken of the nested <docs-code> examples
|
||||
paneTokens: marked.Token[];
|
||||
// True if we should display preview
|
||||
preview: boolean;
|
||||
}
|
||||
|
||||
/** Marked token for a custom docs element. */
|
||||
export interface DocsCodeToken extends marked.Tokens.Generic {
|
||||
type: 'docs-code';
|
||||
// Nested code OR the code from the optional file path
|
||||
code: string;
|
||||
// The example file path
|
||||
path: string | undefined;
|
||||
// The example display header
|
||||
header: string | undefined;
|
||||
// Code language
|
||||
language: string | undefined;
|
||||
// True if styled with line numbers
|
||||
linenums: boolean | undefined;
|
||||
// The lines to display highlighting on
|
||||
highlight: string | undefined;
|
||||
// The example path to determine diff (lines added/removed)
|
||||
diff: string | undefined;
|
||||
// The lines viewable in collapsed view
|
||||
visibleLines: string | undefined;
|
||||
// The name of the viewable region in the collapsed view
|
||||
visibleRegion: string | undefined;
|
||||
// True if we should display preview
|
||||
preview: boolean;
|
||||
}
|
||||
|
||||
export interface DocsCodeTripleTickBlockToken extends marked.Tokens.Generic {
|
||||
type: 'docs-code-triple-tick';
|
||||
// Nested code
|
||||
code: string;
|
||||
// Code language
|
||||
language: string | undefined;
|
||||
}
|
||||
|
||||
interface DiffMetadata {
|
||||
code: string;
|
||||
linesAdded: number[];
|
||||
linesRemoved: number[];
|
||||
}
|
||||
|
||||
// Capture group 1: all attributes on the opening tag
|
||||
// Capture group 2: all content between the open and close tags
|
||||
const multiFileCodeRule = /^\s*<docs-code-multifile(.*?)>(.*?)<\/docs-code-multifile>/s;
|
||||
const singleFileSelfClosingCodeRule = /^\s*<docs-code\s([^>]*)((?:.(?!\/>))*)\/>/s;
|
||||
const singleFileCodeRule = /^\s*<docs-code\s([^>]*)>((?:.(?!\/docs-code))*)<\/docs-code>/s;
|
||||
// TODO: use regex for code implemented in the marked package: https://github.com/markedjs/marked/blob/4e6acc8b8517eafe0036a914f58b6f53d4b12ca6/src/rules.ts#L72C1-L73C1
|
||||
const tripleTickCodeRule = /^\s*`{3}(\w+)[\r\n]+(.*?)[\r\n]+`{3}/s;
|
||||
|
||||
const pathRule = /path="([^"]*)"/;
|
||||
const headerRule = /header="([^"]*)"/;
|
||||
const linenumsRule = /linenums/;
|
||||
const highlightRule = /highlight="([^"]*)"/;
|
||||
const diffRule = /diff="([^"]*)"/;
|
||||
const languageRule = /language="([^"]*)"/;
|
||||
const visibleLinesRule = /visibleLines="([^"]*)"/;
|
||||
const visibleRegionRule = /visibleRegion="([^"]*)"/;
|
||||
const previewRule = /preview/;
|
||||
|
||||
const lineNumberClassName: string = 'hljs-ln-number';
|
||||
const lineMultifileClassName: string = 'hljs-ln-line';
|
||||
const lineAddedClassName: string = 'add';
|
||||
const lineRemovedClassName: string = 'remove';
|
||||
const lineHighlightedClassName: string = 'highlighted';
|
||||
|
||||
const mermaidClassName = 'mermaid';
|
||||
|
||||
export const docsCodeMultifileExtension = {
|
||||
name: 'docs-code-multifile',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\s*<docs-code-multifile/)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsCodeMultifileToken | undefined {
|
||||
const match = multiFileCodeRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const attr = match[1].trim();
|
||||
const path = pathRule.exec(attr);
|
||||
const preview = previewRule.exec(attr) ? true : false;
|
||||
|
||||
const token: DocsCodeMultifileToken = {
|
||||
type: 'docs-code-multifile',
|
||||
raw: match[0],
|
||||
path: path?.[1],
|
||||
panes: match[2].trim(),
|
||||
paneTokens: [],
|
||||
preview: preview,
|
||||
};
|
||||
this.lexer.blockTokens(token.panes, token.paneTokens);
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsCodeMultifileToken) {
|
||||
const attributes = getAttribute('path', token.path) + getAttribute('preview', token.preview);
|
||||
|
||||
return `
|
||||
<div class="docs-code-multifile"${attributes}>
|
||||
${this.parser.parse(token.paneTokens)}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
export const docsCodeExtension = {
|
||||
name: 'docs-code',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^<docs-code\s/)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsCodeToken | undefined {
|
||||
const code = singleFileCodeRule.exec(src);
|
||||
const selfClosingCode = singleFileSelfClosingCodeRule.exec(src);
|
||||
const match = selfClosingCode ?? code;
|
||||
|
||||
if (match) {
|
||||
const attr = match[1].trim();
|
||||
|
||||
const path = pathRule.exec(attr);
|
||||
const header = headerRule.exec(attr);
|
||||
const linenums = linenumsRule.exec(attr);
|
||||
const highlight = highlightRule.exec(attr);
|
||||
const diff = diffRule.exec(attr);
|
||||
const language = languageRule.exec(attr);
|
||||
const visibleLines = visibleLinesRule.exec(attr);
|
||||
const visibleRegion = visibleRegionRule.exec(attr);
|
||||
const preview = previewRule.exec(attr) ? true : false;
|
||||
|
||||
let code = match[2].trim();
|
||||
if (path && path[1]) {
|
||||
code = getCodeFromPath(path[1]);
|
||||
}
|
||||
|
||||
const token: DocsCodeToken = {
|
||||
type: 'docs-code',
|
||||
raw: match[0],
|
||||
code: code,
|
||||
path: path?.[1],
|
||||
header: header?.[1],
|
||||
linenums: !!linenums,
|
||||
highlight: highlight?.[1],
|
||||
diff: diff?.[1],
|
||||
language: language?.[1],
|
||||
visibleLines: visibleLines?.[1],
|
||||
visibleRegion: visibleRegion?.[1],
|
||||
preview: preview,
|
||||
};
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsCodeToken) {
|
||||
return formatDocsCode(token);
|
||||
},
|
||||
};
|
||||
|
||||
export const docsTripleTickMarkdownCodeExtension = {
|
||||
name: 'docs-code-triple-tick',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^(```)\s/)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsCodeTripleTickBlockToken | undefined {
|
||||
const match = tripleTickCodeRule.exec(src);
|
||||
if (match) {
|
||||
const token: DocsCodeTripleTickBlockToken = {
|
||||
raw: match[0],
|
||||
type: 'docs-code-triple-tick',
|
||||
code: match[2],
|
||||
language: match[1],
|
||||
};
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsCodeTripleTickBlockToken) {
|
||||
return formatDocsCode({code: token.code, language: token.language});
|
||||
},
|
||||
};
|
||||
|
||||
function formatDocsCode(token: Partial<DocsCodeToken>): string {
|
||||
let diffData: DiffMetadata | null = null;
|
||||
|
||||
if (!token.code) {
|
||||
throw new Error('Undefined code in DocsCodeToken!');
|
||||
}
|
||||
|
||||
let code = token.code;
|
||||
|
||||
if (token.diff) {
|
||||
diffData = getDiffData(token.diff, code);
|
||||
code = diffData.code;
|
||||
}
|
||||
|
||||
const highlightedLines = token.highlight ? getRangeValues(token.highlight) : [];
|
||||
|
||||
const extractRegionsResult = extractRegions(token, code);
|
||||
code = extractRegionsResult.code;
|
||||
|
||||
// Add highlighting based on the code language
|
||||
code = getHighlightedCode(token.language, code, diffData, highlightedLines, !!token.linenums);
|
||||
|
||||
const attributes =
|
||||
getAttribute(
|
||||
'visibleLines',
|
||||
getRangeValues(extractRegionsResult.visibleLines ?? token.visibleLines)?.toString(),
|
||||
) +
|
||||
getAttribute('path', token.diff ?? token.path) +
|
||||
getAttribute('preview', token.preview) +
|
||||
getAttribute('header', token.header) +
|
||||
getAttribute('mermaid', token.language === mermaidClassName);
|
||||
const classes = getClass('shell', token.language == 'shell');
|
||||
|
||||
const header = token.header ? `<div class="docs-code-header"><h3>${token.header}</h3></div>` : '';
|
||||
return `
|
||||
<div class="docs-code${classes}"${attributes}>
|
||||
${header}
|
||||
<pre class="adev-mini-scroll-track">
|
||||
<code>${code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getCodeFromPath(path: string): string {
|
||||
const rootFolder =
|
||||
getCurrentParsedDocumentType() === DocumentType.DOCS
|
||||
? EXAMPLES_FOLDER_PATH
|
||||
: TUTORIALS_FOLDER_PATH;
|
||||
return readFileSync(join(rootFolder, path), {encoding: 'utf-8'});
|
||||
}
|
||||
|
||||
function getHighlightedCode(
|
||||
language: string | undefined,
|
||||
code: string,
|
||||
diffData: DiffMetadata | null,
|
||||
highlightedLines: number[] | null,
|
||||
displayLineNums: boolean,
|
||||
): string {
|
||||
if (language == 'none' || language == 'file' || language == mermaidClassName || !highlightedLines)
|
||||
return code;
|
||||
|
||||
// Decode the code content to replace HTML entities to characters
|
||||
const decodedCode = decode(code);
|
||||
const highlightResult = language
|
||||
? hljs.highlight(decodedCode, {language})
|
||||
: hljs.highlightAuto(decodedCode);
|
||||
return appendModifierClassesToCodeLinesElements(
|
||||
highlightResult.value,
|
||||
diffData,
|
||||
highlightedLines,
|
||||
displayLineNums,
|
||||
);
|
||||
}
|
||||
|
||||
function appendModifierClassesToCodeLinesElements(
|
||||
htmlString: string,
|
||||
diffData: DiffMetadata | null,
|
||||
highlightedLines: number[],
|
||||
displayLineNums: boolean,
|
||||
): string {
|
||||
const lines = getLines(htmlString);
|
||||
let finalHtml = '';
|
||||
|
||||
let lineIndex = 0;
|
||||
let resultFileLineIndex = 1;
|
||||
const linesCount = lines.length;
|
||||
|
||||
if (linesCount === 0) {
|
||||
return htmlString;
|
||||
}
|
||||
|
||||
do {
|
||||
const isRemovedLine = diffData?.linesRemoved.includes(lineIndex);
|
||||
const isAddedLine = diffData?.linesAdded.includes(lineIndex);
|
||||
const isHighlighted = highlightedLines.includes(lineIndex);
|
||||
const statusClasses = `${getClass(lineAddedClassName, isAddedLine)}${getClass(
|
||||
lineRemovedClassName,
|
||||
isRemovedLine,
|
||||
)}${getClass(lineHighlightedClassName, isHighlighted)}`;
|
||||
|
||||
if (displayLineNums) {
|
||||
if (isRemovedLine) {
|
||||
finalHtml += `<span role="presentation" class="${lineNumberClassName}${statusClasses}">-</span>`;
|
||||
} else {
|
||||
finalHtml += `<span role="presentation" class="${lineNumberClassName}${statusClasses}">${
|
||||
isAddedLine ? '+' : resultFileLineIndex
|
||||
}</span>`;
|
||||
resultFileLineIndex++;
|
||||
}
|
||||
}
|
||||
finalHtml += `<div class="${lineMultifileClassName}${statusClasses}">${lines[lineIndex]}</div>`;
|
||||
lineIndex++;
|
||||
} while (lineIndex < linesCount);
|
||||
|
||||
return finalHtml;
|
||||
}
|
||||
|
||||
function getDiffData(diffPath: string, code: string): DiffMetadata {
|
||||
const changedCode = getCodeFromPath(diffPath);
|
||||
const change = diff.diffLines(code, changedCode);
|
||||
|
||||
const getLinesRange = (start: number, count: number): number[] =>
|
||||
Array.from(Array(count).keys()).map((i) => i + start);
|
||||
|
||||
let processedLines = 0;
|
||||
|
||||
return change.reduce(
|
||||
(prev: DiffMetadata, part: diff.Change) => {
|
||||
const diff: DiffMetadata = {
|
||||
code: `${prev.code}${part.value}`,
|
||||
linesAdded: part.added
|
||||
? [...prev.linesAdded, ...getLinesRange(processedLines, part.count ?? 0)]
|
||||
: prev.linesAdded,
|
||||
linesRemoved: part.removed
|
||||
? [...prev.linesRemoved, ...getLinesRange(processedLines, part.count ?? 0)]
|
||||
: prev.linesRemoved,
|
||||
};
|
||||
processedLines += part.count ?? 0;
|
||||
return diff;
|
||||
},
|
||||
{
|
||||
code: '',
|
||||
linesAdded: [],
|
||||
linesRemoved: [],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function extractRegions(
|
||||
token: Partial<DocsCodeToken>,
|
||||
code: string,
|
||||
): {code: string; visibleLines?: string} {
|
||||
if (!token.path) return {code};
|
||||
|
||||
const result = regionParser(code, token.path);
|
||||
|
||||
if (token.visibleRegion) {
|
||||
const region = result.regionMap[token.visibleRegion];
|
||||
|
||||
if (!region) throw new Error(`Cannot find ${token.visibleRegion} in ${token.path}!`);
|
||||
|
||||
return {
|
||||
code: result.contents,
|
||||
visibleLines: `[${region.ranges.map(
|
||||
(range) => `[${range.from}, ${range.to ?? result.totalLinesCount + 1}]`,
|
||||
)}]`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: result.contents,
|
||||
};
|
||||
}
|
||||
|
||||
function getLines(text: string): string[] {
|
||||
if (text.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return text.split(/\r\n|\r|\n/g);
|
||||
}
|
||||
|
||||
function getAttribute(name: string, value: boolean | number | string | undefined): string {
|
||||
return value ? ` ${name}="${value}"` : '';
|
||||
}
|
||||
|
||||
function getClass(name: string, value: boolean | number | string | undefined): string {
|
||||
return value ? ` ${name}` : ``;
|
||||
}
|
||||
|
||||
/**
|
||||
* The function used to generate ranges of highlighted or visible lines in code blocks
|
||||
*/
|
||||
function getRangeValues(token?: string): number[] | null {
|
||||
const getAllValuesFromRange = (range: any[]) => {
|
||||
const [start, end] = range;
|
||||
for (let i = start; i <= end; i++) {
|
||||
result.push(i - 1);
|
||||
}
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result: number[] = [];
|
||||
try {
|
||||
const boundaryValueArray = JSON.parse(token);
|
||||
if (!Array.isArray(boundaryValueArray)) {
|
||||
throw new Error('Provided token has wrong format!\n', boundaryValueArray);
|
||||
}
|
||||
// Flat Array
|
||||
if (
|
||||
boundaryValueArray.length === 2 &&
|
||||
!Array.isArray(boundaryValueArray[0]) &&
|
||||
!Array.isArray(boundaryValueArray[1])
|
||||
) {
|
||||
getAllValuesFromRange(boundaryValueArray);
|
||||
} else {
|
||||
for (const range of boundaryValueArray) {
|
||||
if (Array.isArray(range) && range.length === 2) {
|
||||
getAllValuesFromRange(range);
|
||||
} else if (!Number.isNaN(range)) {
|
||||
result.push(Number(range - 1));
|
||||
} else {
|
||||
throw new Error('Input has wrong format!\n', range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {marked} from 'marked';
|
||||
import {getPageTitle} from './h1.mjs';
|
||||
import {readFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {PROJECT_FOLDER_PATH} from '../constants.mjs';
|
||||
|
||||
interface DocsDecorativeHeaderToken extends marked.Tokens.Generic {
|
||||
type: 'docs-decorative-header';
|
||||
title: string;
|
||||
imgSrc: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
// Capture group 1: all attributes on the opening tag
|
||||
// Capture group 2: all content between the open and close tags
|
||||
const decorativeHeaderRule =
|
||||
/^[^<]*<docs-decorative-header\s([^>]*)>((?:.(?!\/docs-decorative-header))*)<\/docs-decorative-header>/s;
|
||||
|
||||
const imgSrcRule = /imgSrc="([^"]*)"/;
|
||||
const titleRule = /title="([^"]*)"/;
|
||||
|
||||
export const docsDecorativeHeaderExtension = {
|
||||
name: 'docs-decorative-header',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\s*<docs-decorative-header\s*/m)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsDecorativeHeaderToken | undefined {
|
||||
const match = decorativeHeaderRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const attr = match[1].trim();
|
||||
const body = match[2].trim();
|
||||
|
||||
const imgSrc = imgSrcRule.exec(attr);
|
||||
const title = titleRule.exec(attr);
|
||||
|
||||
const token: DocsDecorativeHeaderToken = {
|
||||
type: 'docs-decorative-header',
|
||||
raw: match[0],
|
||||
title: title ? title[1] : '',
|
||||
imgSrc: imgSrc ? imgSrc[1] : '../assets/images/globe.svg',
|
||||
body: body ?? '',
|
||||
};
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsDecorativeHeaderToken) {
|
||||
// We can assume that all illustrations are svg files
|
||||
// We need to read svg content, instead of renering svg with `img`,
|
||||
// cause we would like to use CSS variables to support dark and light mode.
|
||||
const illustration = getSvgIllustration(token.imgSrc);
|
||||
|
||||
return `
|
||||
<div class="docs-decorative-header-container">
|
||||
<div class="docs-decorative-header">
|
||||
<div class="docs-header-content">
|
||||
<docs-breadcrumb></docs-breadcrumb>
|
||||
|
||||
${getPageTitle(token.title)}
|
||||
|
||||
<p>${token.body}</p>
|
||||
</div>
|
||||
|
||||
<!-- illustration -->
|
||||
${illustration}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
function getSvgIllustration(path: string): string {
|
||||
return readFileSync(join(PROJECT_FOLDER_PATH, path), {encoding: 'utf-8'});
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {marked} from 'marked';
|
||||
import {isExternalLink, targetForExternalHref} from './helpers.mjs';
|
||||
|
||||
interface DocsPillRowToken extends marked.Tokens.Generic {
|
||||
type: 'docs-pill-row';
|
||||
pills: string;
|
||||
tokens: marked.Token[];
|
||||
}
|
||||
|
||||
// Capture group 1: all content between the open and close tags
|
||||
const pillRowRule = /^\s*<docs-pill-row>((?:.(?!docs-pill-row))*)<\/docs-pill-row>/s;
|
||||
|
||||
export const docsPillRowExtension = {
|
||||
name: 'docs-pill-row',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\s*<docs-pill-row/m)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsPillRowToken | undefined {
|
||||
const match = pillRowRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const body = match[1];
|
||||
const token: DocsPillRowToken = {
|
||||
type: 'docs-pill-row',
|
||||
raw: match[0],
|
||||
pills: body ?? '',
|
||||
tokens: [],
|
||||
};
|
||||
this.lexer.inlineTokens(token.pills, token.tokens);
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsPillRowToken) {
|
||||
return `
|
||||
<nav class="docs-pill-row">
|
||||
${this.parser.parseInline(token.tokens)}
|
||||
</nav>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
interface DocsPillToken extends marked.Tokens.Generic {
|
||||
type: 'docs-pill';
|
||||
title: string;
|
||||
href: string;
|
||||
tokens: marked.Token[];
|
||||
}
|
||||
|
||||
// Capture group 1: all attributes on the docs-pill tag
|
||||
const pillRule = /^\s*<docs-pill\s((?:.(?!\n))*)\/>/s;
|
||||
|
||||
const titleRule = /title="([^"]*)"/;
|
||||
const hrefRule = /href="([^"]*)"/;
|
||||
|
||||
export const docsPillExtension = {
|
||||
name: 'docs-pill',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
return src.indexOf('<docs-pill ');
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsPillToken | undefined {
|
||||
const match = pillRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const attr = match[1].trim();
|
||||
const title = titleRule.exec(attr);
|
||||
const href = hrefRule.exec(attr);
|
||||
|
||||
const token: DocsPillToken = {
|
||||
type: 'docs-pill',
|
||||
raw: match[0],
|
||||
title: title ? title[1] : '',
|
||||
href: href ? href[1] : '',
|
||||
tokens: [],
|
||||
};
|
||||
this.lexer.inlineTokens(token.title, token.tokens);
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsPillToken) {
|
||||
return `
|
||||
<a class="docs-pill" href="${token.href}" ${targetForExternalHref(token.href)}>
|
||||
${this.parser.parseInline(token.tokens)}
|
||||
${
|
||||
isExternalLink(token.href)
|
||||
? '<docs-icon class="docs-icon-small">open_in_new</docs-icon>'
|
||||
: ''
|
||||
}
|
||||
</a>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {marked} from 'marked';
|
||||
interface DocsVideoToken extends marked.Tokens.Generic {
|
||||
type: 'docs-video';
|
||||
src: string;
|
||||
title: string | undefined;
|
||||
}
|
||||
|
||||
// Capture group 1: all attributes on the opening tag
|
||||
const videoRule = /^<docs-video([^>]*)\/>/s;
|
||||
|
||||
const srcRule = /src="([^"]*)"/;
|
||||
const titleRule = /title="([^"]*)"/;
|
||||
const validYTUrlPrefix = 'https://www.youtube.com/embed/';
|
||||
|
||||
export const docsVideoExtension = {
|
||||
name: 'docs-video',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\s*<docs-video/m)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsVideoToken | undefined {
|
||||
const match = videoRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const attr = match[1].trim();
|
||||
const src = srcRule.exec(attr);
|
||||
const title = titleRule.exec(attr);
|
||||
|
||||
if (src) {
|
||||
return {
|
||||
type: 'docs-video',
|
||||
raw: match[0],
|
||||
src: src?.[1],
|
||||
title: title?.[1],
|
||||
};
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsVideoToken) {
|
||||
if (!token.src.startsWith(validYTUrlPrefix)) {
|
||||
process.stdout.write(
|
||||
`\n<docs-video> cannot load: ${token.src}. YouTube Player API expects src to begin with ${validYTUrlPrefix}.\n`,
|
||||
);
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="docs-video-container">
|
||||
<iframe
|
||||
class="docs-video"
|
||||
src="${token.src}"
|
||||
${token.title ? `title="${token.title}"` : ''}
|
||||
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
credentialless
|
||||
title="Video player"
|
||||
></iframe>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {marked} from 'marked';
|
||||
import {transformHeader} from './header.mjs';
|
||||
interface DocsWorkflowToken extends marked.Tokens.Generic {
|
||||
type: 'docs-workflow';
|
||||
steps: string;
|
||||
tokens: marked.Token[];
|
||||
}
|
||||
|
||||
// Capture group 1: all content between the open and close tags
|
||||
const workflowRule = /^<docs-workflow>(.*?)<\/docs-workflow>/s;
|
||||
|
||||
export const docsWorkflowExtension = {
|
||||
name: 'docs-workflow',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\s*<docs-workflow/m)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsWorkflowToken | undefined {
|
||||
const match = workflowRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const steps = match[1];
|
||||
|
||||
const token: DocsWorkflowToken = {
|
||||
type: 'docs-workflow',
|
||||
raw: match[0],
|
||||
steps: steps ?? '',
|
||||
tokens: [],
|
||||
};
|
||||
this.lexer.blockTokens(token.steps, token.tokens);
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsWorkflowToken) {
|
||||
return `
|
||||
<ol class="docs-steps">
|
||||
${this.parser.parse(token.tokens)}
|
||||
</ol>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
interface DocsStepToken extends marked.Tokens.Generic {
|
||||
type: 'docs-step';
|
||||
title: string;
|
||||
body: string;
|
||||
tokens: marked.Token[];
|
||||
}
|
||||
|
||||
// Capture group 1: all attributes on the opening tag
|
||||
// Capture group 2: all content between the open and close tags
|
||||
const stepRule = /^\s*<docs-step([^>]*)>((?:.(?!\/docs-step))*)<\/docs-step>/s;
|
||||
const titleRule = /title="([^"]*)"/;
|
||||
|
||||
export const docsStepExtension = {
|
||||
name: 'docs-step',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\s*<docs-step/m)?.index;
|
||||
},
|
||||
tokenizer(this: marked.TokenizerThis, src: string): DocsStepToken | undefined {
|
||||
const match = stepRule.exec(src);
|
||||
|
||||
if (match) {
|
||||
const attr = match[1].trim();
|
||||
const title = titleRule.exec(attr);
|
||||
const body = match[2].trim();
|
||||
|
||||
const token: DocsStepToken = {
|
||||
type: 'docs-step',
|
||||
raw: match[0],
|
||||
title: title ? title[1] : '',
|
||||
body: body,
|
||||
tokens: [],
|
||||
};
|
||||
this.lexer.blockTokens(token.body, token.tokens);
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: marked.RendererThis, token: DocsStepToken) {
|
||||
return `
|
||||
<li>
|
||||
<span class="docs-step-number" aria-hidden="true"></span>
|
||||
${transformHeader(token.title, 3)}
|
||||
${this.parser.parse(token.tokens)}
|
||||
</li>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
const UNICODE_EMOJI_REGEX = /&#x[\dA-Fa-f]+;/g;
|
||||
|
||||
const regex = emojiRegex();
|
||||
|
||||
export function handleEmoji(text: string): string {
|
||||
return text.match(regex) || text.match(UNICODE_EMOJI_REGEX)
|
||||
? `<span class="docs-emoji">${text}</span>`
|
||||
: text;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
// Enum with the severity levels for alerts and callouts
|
||||
export enum CalloutSeverityLevel {
|
||||
HELPFUL = 'HELPFUL',
|
||||
IMPORTANT = 'IMPORTANT',
|
||||
CRITICAL = 'CRITICAL',
|
||||
}
|
||||
|
||||
export enum AlertSeverityLevel {
|
||||
Note = 'Note',
|
||||
Tip = 'Tip',
|
||||
TODO = 'TODO',
|
||||
QUESTION = 'QUESTION',
|
||||
Summary = 'Summary',
|
||||
TLDR = 'TLDR',
|
||||
CRITICAL = 'CRITICAL',
|
||||
IMPORTANT = 'IMPORTANT',
|
||||
HELPFUL = 'HELPFUL',
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {GITHUB_EDIT_CONTENT_URL} from '../constants.mjs';
|
||||
import {DocumentType, getCurrentParsedDocumentType, getCurrentParsedFilePath} from '../state.mjs';
|
||||
|
||||
export function transformH1(text: string) {
|
||||
// For tutorials pages, we don't have to display docs-breadcrumb
|
||||
return `
|
||||
<!-- Document Header -->
|
||||
<header class="docs-header">
|
||||
${
|
||||
getCurrentParsedDocumentType() === DocumentType.TUTORIAL
|
||||
? ''
|
||||
: '<docs-breadcrumb></docs-breadcrumb>'
|
||||
}
|
||||
|
||||
${getPageTitle(text)}
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
|
||||
// TODO: Add back pencil icon once code is open sourced
|
||||
export function getPageTitle(text: string): string {
|
||||
return `
|
||||
<!-- Page title -->
|
||||
<div class="docs-page-title">
|
||||
<h1 tabindex="-1">${text}</h1>
|
||||
<a class="docs-github-links" target="_blank" href="${GITHUB_EDIT_CONTENT_URL}/${getCurrentParsedFilePath()}" title="Edit this page" aria-label="Edit this page">
|
||||
<!-- Pencil -->
|
||||
<!-- <docs-icon role="presentation">edit</docs-icon> -->
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {getHeaderId} from '../state.mjs';
|
||||
import {mapBackticksToCodeElement} from '../utils.mjs';
|
||||
|
||||
export function transformHeader(text: string, level: number) {
|
||||
// Nested anchor elements are invalid in HTML
|
||||
// They might happen when we have a code block in a heading
|
||||
// regex aren't perfect for that but this one should be "good enough"
|
||||
const regex = /<a\s+(?:[^>]*?\s+)?href.*?>(.*?)<\/a>/gi;
|
||||
const anchorLessText = text.replace(regex, '$1');
|
||||
|
||||
const link = getHeaderId(anchorLessText);
|
||||
const label = mapBackticksToCodeElement(anchorLessText);
|
||||
|
||||
return `
|
||||
<h${level} id="${link}">
|
||||
<a href="#${link}" class="docs-anchor" tabindex="-1">${label}</a>
|
||||
</h${level}>`;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const isExternalLink = (href: string | undefined | null) =>
|
||||
href?.startsWith('http') ?? false;
|
||||
|
||||
export const targetForExternalHref = (href: string | undefined | null) =>
|
||||
isExternalLink(href) ? `target="_blank"` : '';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {normalize} from 'path';
|
||||
|
||||
// TODO: Make sure we're setting up correct image optimization
|
||||
export function transformImage(href: string | null, title: string | null, text: string) {
|
||||
const src = href?.startsWith('./') ? `assets/content/${normalize(href)}` : href;
|
||||
return `
|
||||
<img src="${src}" alt="${text}" title="${title}" class="docs-image">
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {targetForExternalHref} from './helpers.mjs';
|
||||
|
||||
export function transformLink(href: string | null, title: string | null, text: string) {
|
||||
return `<a href="${href}" ${title ? `title=${title}` : ''} ${targetForExternalHref(
|
||||
href,
|
||||
)}>${text}</a>`;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export function transformDocsList(body: string, ordered: boolean) {
|
||||
if (ordered) {
|
||||
return `
|
||||
<ol class="docs-ordered-list">
|
||||
${body}
|
||||
</ol>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<ul class="docs-list">
|
||||
${body}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export function transformDocsTable(header: string, body: string) {
|
||||
return `
|
||||
<div class="docs-table adev-scroll-track-transparent">
|
||||
<table>
|
||||
<thead>
|
||||
${header}
|
||||
</thead>
|
||||
<tbody>
|
||||
${body}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {readFileSync} from 'fs';
|
||||
import {glob} from 'glob';
|
||||
import {join} from 'path';
|
||||
|
||||
export function mapBackticksToCodeElement(text: string): string {
|
||||
return text.replaceAll(/`(.*?)`/g, '<code>$1</code>');
|
||||
}
|
||||
|
||||
/** Recursively search the provided directory for all markdown files and asyncronously load them. */
|
||||
export async function retrieveAllMarkdownFiles(
|
||||
baseDir: string,
|
||||
): Promise<{path: string; content: string}[]> {
|
||||
const files = await glob('**/*.md', {
|
||||
root: baseDir,
|
||||
cwd: baseDir,
|
||||
ignore: ['**/node_modules/**'],
|
||||
});
|
||||
|
||||
return files.map((filePath) => ({
|
||||
path: filePath,
|
||||
content: readFileSync(join(baseDir, filePath), {encoding: 'utf-8'}),
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Tutorial Scripts
|
||||
|
||||
The tutorials scripts are responsible for generating the tutorials source code and metadata by reading the tutorials content from [`src/content/tutorials`](/src/content/tutorials).
|
||||
|
||||
_See [the tutorials content README](/src/content/tutorials/README.md) for more information about the tutorials content._
|
||||
|
||||
For each tutorial, the following JSON files are generated and stored at `src/assets/tutorials`:
|
||||
|
||||
- `source-code/<tutorial-pathname>.json`: contains the tutorial source code in the [WebContainer](https://webcontainers.io) `FileSystemTree` format. It's
|
||||
used to mount the tutorial files in the WebContainer. See the WebContainer docs for more info: <https://webcontainers.io/guides/working-with-the-file-system>
|
||||
- `metadata/<tutorial-pathname>.json`: the tutorial metadata has all the information necessary to manage the tutorial in app.
|
||||
It contains the tutorial `config.json`, the previous and next step, the next tutorial, and the code editor file paths and contents.
|
||||
- `routes.json`: defines all routes for the tutorials, used at [`src/app/sub-navigation-data.ts`](/src/app/sub-navigation-data.ts) to populate the tutorials nav.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
yarn generate-tutorials
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
import {NavigationItem} from '@angular/docs-shared';
|
||||
|
||||
import {PagePrefix} from '../../src/app/core/enums/pages';
|
||||
|
||||
import type {TutorialNavigationData, TutorialNavigationItem, TutorialStep} from './tutorials-types';
|
||||
import {createDirectoryAndWriteFile} from './utils/filesystem';
|
||||
import {TUTORIALS_ROUTES_NODE_PATH} from './utils/node-constants';
|
||||
|
||||
/** the step is used only in this function to sort the nav items */
|
||||
export type TutorialNavigationItemWithStep = TutorialNavigationItem & {
|
||||
tutorialData: TutorialNavigationData & {
|
||||
step: TutorialStep['step'];
|
||||
};
|
||||
};
|
||||
|
||||
export async function buildTutorialsNavigationItems(
|
||||
initialNavigationItems: TutorialNavigationItemWithStep[],
|
||||
) {
|
||||
const sortedInitialNavigationItems = initialNavigationItems.sort((a, b) => {
|
||||
if (!a.tutorialData?.step || !b.tutorialData?.step) return 0;
|
||||
|
||||
if (a.tutorialData.step < b.tutorialData.step) return -1;
|
||||
else if (a.tutorialData.step > b.tutorialData.step) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const addTutorialPagePrefixToNavItemPath = (path: string) => `${PagePrefix.TUTORIALS}/${path}`;
|
||||
|
||||
const navItemMap = new Map<NavigationItem['path'], TutorialNavigationItem>();
|
||||
|
||||
for (const navItem of sortedInitialNavigationItems) {
|
||||
const pathParts = navItem.path.split('/');
|
||||
const topLevelPath = pathParts[0];
|
||||
const topLevelNavItem = navItemMap.get(topLevelPath);
|
||||
|
||||
const isNavItemTopLevelPath = pathParts.length === 1;
|
||||
|
||||
const newNavItem: TutorialNavigationItem = {
|
||||
path: addTutorialPagePrefixToNavItemPath(navItem.path),
|
||||
label: navItem.label,
|
||||
contentPath: navItem.contentPath,
|
||||
tutorialData: navItem.tutorialData,
|
||||
};
|
||||
|
||||
if (!topLevelNavItem) {
|
||||
// create new map entry
|
||||
navItemMap.set(
|
||||
topLevelPath,
|
||||
isNavItemTopLevelPath ? newNavItem : ({children: [newNavItem]} as TutorialNavigationItem),
|
||||
);
|
||||
} else if (isNavItemTopLevelPath) {
|
||||
// add top level path to existing map entry
|
||||
navItemMap.set(topLevelPath, {
|
||||
...newNavItem,
|
||||
...topLevelNavItem,
|
||||
});
|
||||
} else if (!topLevelNavItem.children) {
|
||||
// create children in existing map entry
|
||||
topLevelNavItem.children = [newNavItem];
|
||||
|
||||
navItemMap.set(topLevelPath, topLevelNavItem);
|
||||
} else {
|
||||
// add children to existing map entry
|
||||
topLevelNavItem.children.push(newNavItem);
|
||||
|
||||
navItemMap.set(topLevelPath, topLevelNavItem);
|
||||
}
|
||||
}
|
||||
|
||||
const tutorialNavigationItems = Array.from(navItemMap.values());
|
||||
|
||||
await createDirectoryAndWriteFile(
|
||||
TUTORIALS_ROUTES_NODE_PATH,
|
||||
JSON.stringify(tutorialNavigationItems),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
import {mkdir, rm, writeFile} from 'fs/promises';
|
||||
import {basename, join, sep} from 'path';
|
||||
|
||||
import {
|
||||
GLOB_OPTIONS,
|
||||
INTRO_DIRECTORY,
|
||||
PLAYGROUND_ROUTE_NODE_PATH,
|
||||
STEPS_DIRECTORY,
|
||||
TUTORIALS_ASSETS_NODE_PATH,
|
||||
TUTORIALS_COMMON_NODE_PATH,
|
||||
TUTORIALS_METADATA_NODE_PATH,
|
||||
TUTORIALS_PROJECT_PATH,
|
||||
TUTORIALS_SOURCE_CODE_NODE_PATH,
|
||||
} from './utils/node-constants';
|
||||
|
||||
import {createDirectoryAndWriteFile, getFilesContents, globWithCwdPath} from './utils/filesystem';
|
||||
|
||||
import {
|
||||
TutorialNavigationItemWithStep,
|
||||
buildTutorialsNavigationItems,
|
||||
} from './generate-tutorials-routes';
|
||||
|
||||
import {copyCommonAssetsToAppAssets, getTutorialCommonFiles} from './tutorial-type/common';
|
||||
import {getHomepagePlaygroundFiles} from './tutorial-type/homepage';
|
||||
import {getIntroAndStepsTutorialFiles} from './tutorial-type/intro-and-steps';
|
||||
import {getTutorialPlaygroundFiles, isPlaygroundRouteData} from './tutorial-type/playground';
|
||||
import {PlaygroundFiles, TutorialFiles} from './tutorials-types';
|
||||
import {
|
||||
TUTORIALS_COMMON_DIRECTORY,
|
||||
TUTORIALS_HOMEPAGE_DIRECTORY,
|
||||
TUTORIALS_PLAYGROUND_DIRECTORY,
|
||||
} from './utils/web-constants';
|
||||
|
||||
main();
|
||||
|
||||
/**
|
||||
* Builds and stores the tutorials source codes and metadata into JSON files.
|
||||
* These files are later used to load the tutorials into the WebContainer file system
|
||||
* and manage the tutorials in the embedded editor.
|
||||
*
|
||||
* @see https://webcontainers.io/guides/working-with-the-file-system
|
||||
*/
|
||||
export async function main() {
|
||||
const consoleTimeLabel = 'Tutorials files build done in';
|
||||
|
||||
// Informs the developers that this script is running and how much time it took
|
||||
console.info('Building tutorials files...');
|
||||
console.time(consoleTimeLabel);
|
||||
|
||||
const tutorialsFiles = await getTutorialsFiles();
|
||||
|
||||
// Clean up the directory before writing the files
|
||||
await rm(TUTORIALS_ASSETS_NODE_PATH, {recursive: true}).catch(() => {
|
||||
// `rm` throws if the directory doesn't exist, meaning this script is
|
||||
// being executed for the first time
|
||||
});
|
||||
await Promise.all([
|
||||
mkdir(TUTORIALS_ASSETS_NODE_PATH, {recursive: true}),
|
||||
mkdir(TUTORIALS_SOURCE_CODE_NODE_PATH, {recursive: true}),
|
||||
mkdir(TUTORIALS_METADATA_NODE_PATH, {recursive: true}),
|
||||
]);
|
||||
|
||||
const writeFilesPromises: Promise<void>[] = [];
|
||||
const tutorialsRoutes: TutorialNavigationItemWithStep[] = [];
|
||||
|
||||
tutorialsFiles.forEach(({sourceCode, metadata, route, sourceCodeZip}, tutorialName) => {
|
||||
if (sourceCode) {
|
||||
const filePath = join(TUTORIALS_SOURCE_CODE_NODE_PATH, tutorialName + '.json');
|
||||
const fileContents = JSON.stringify(sourceCode);
|
||||
|
||||
if (tutorialName.includes(sep)) {
|
||||
writeFilesPromises.push(createDirectoryAndWriteFile(filePath, fileContents));
|
||||
} else {
|
||||
writeFilesPromises.push(writeFile(filePath, fileContents));
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
const filePath = join(TUTORIALS_METADATA_NODE_PATH, tutorialName + '.json');
|
||||
const fileContents = JSON.stringify(metadata);
|
||||
|
||||
if (tutorialName.includes(sep)) {
|
||||
writeFilesPromises.push(createDirectoryAndWriteFile(filePath, fileContents));
|
||||
} else {
|
||||
writeFilesPromises.push(writeFile(filePath, fileContents));
|
||||
}
|
||||
}
|
||||
|
||||
if (route) {
|
||||
if (isPlaygroundRouteData(route)) {
|
||||
writeFilesPromises.push(
|
||||
createDirectoryAndWriteFile(PLAYGROUND_ROUTE_NODE_PATH, JSON.stringify(route)),
|
||||
);
|
||||
} else {
|
||||
tutorialsRoutes.push({
|
||||
...route,
|
||||
path: tutorialName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceCodeZip) {
|
||||
const filePath = join(TUTORIALS_SOURCE_CODE_NODE_PATH, tutorialName + '.zip');
|
||||
writeFilesPromises.push(createDirectoryAndWriteFile(filePath, sourceCodeZip));
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
...writeFilesPromises,
|
||||
copyCommonAssetsToAppAssets(),
|
||||
buildTutorialsNavigationItems(tutorialsRoutes),
|
||||
]);
|
||||
|
||||
console.timeEnd(consoleTimeLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the tutorials source code and generate a Map representing the files
|
||||
* and contents that will be stored in the filesystem.
|
||||
*
|
||||
* Those files are later used by the `/tutorials` pages.
|
||||
*
|
||||
* The map key is the project name and the value contain the contents for each
|
||||
* .json file.
|
||||
*/
|
||||
async function getTutorialsFiles(): Promise<Map<string, TutorialFiles | PlaygroundFiles>> {
|
||||
const [tutorialsProjects, commonTutorialFiles] = await Promise.all([
|
||||
globWithCwdPath('**', {
|
||||
...GLOB_OPTIONS,
|
||||
ignore: [...(GLOB_OPTIONS.ignore as string[]), '*.md', TUTORIALS_COMMON_DIRECTORY],
|
||||
maxDepth: 1,
|
||||
cwd: TUTORIALS_PROJECT_PATH,
|
||||
}),
|
||||
globWithCwdPath('**', {
|
||||
...GLOB_OPTIONS,
|
||||
cwd: TUTORIALS_COMMON_NODE_PATH,
|
||||
nodir: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const tutorialsFiles = new Map<string, TutorialFiles | PlaygroundFiles>();
|
||||
|
||||
// tutorials depends on the common project files so here we handle it first
|
||||
const commonFilesContents = await getFilesContents(commonTutorialFiles);
|
||||
|
||||
tutorialsFiles.set(
|
||||
TUTORIALS_COMMON_DIRECTORY,
|
||||
await getTutorialCommonFiles(commonTutorialFiles, commonFilesContents),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
tutorialsProjects.map(async (tutorialPath) => {
|
||||
const tutorialName = basename(tutorialPath);
|
||||
|
||||
const [tutorialDirectories, tutorialFiles] = await Promise.all([
|
||||
globWithCwdPath('**/', {...GLOB_OPTIONS, cwd: tutorialPath}),
|
||||
globWithCwdPath('**', {...GLOB_OPTIONS, nodir: true, cwd: tutorialPath}),
|
||||
]);
|
||||
|
||||
try {
|
||||
if (tutorialName === TUTORIALS_PLAYGROUND_DIRECTORY) {
|
||||
const playgroundFiles = await getTutorialPlaygroundFiles(commonFilesContents);
|
||||
|
||||
playgroundFiles.forEach((playgroundFiles, playgroundName) => {
|
||||
tutorialsFiles.set(playgroundName, playgroundFiles);
|
||||
});
|
||||
} else if (tutorialName === TUTORIALS_HOMEPAGE_DIRECTORY) {
|
||||
const homepageFiles = await getHomepagePlaygroundFiles(commonFilesContents);
|
||||
tutorialsFiles.set(TUTORIALS_HOMEPAGE_DIRECTORY, homepageFiles);
|
||||
} else if (
|
||||
tutorialDirectories.some((directory) => directory.includes(STEPS_DIRECTORY)) &&
|
||||
tutorialDirectories.some((directory) => directory.includes(INTRO_DIRECTORY))
|
||||
) {
|
||||
const tutorialStepsFiles = await getIntroAndStepsTutorialFiles(
|
||||
tutorialName,
|
||||
tutorialDirectories,
|
||||
tutorialFiles,
|
||||
commonFilesContents,
|
||||
);
|
||||
|
||||
tutorialStepsFiles.forEach((stepFiles, stepName) => {
|
||||
tutorialsFiles.set(stepName, stepFiles);
|
||||
});
|
||||
} else {
|
||||
throw `Could not find "${INTRO_DIRECTORY}" and/or "${STEPS_DIRECTORY}" directories.`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`\n❌ Failed to parse tutorial at ${TUTORIALS_PROJECT_PATH}/${tutorialName}`);
|
||||
console.error(`\t${error}\n`);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return tutorialsFiles;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {join} from 'path';
|
||||
import {glob} from 'glob';
|
||||
import {input} from '@inquirer/prompts';
|
||||
|
||||
import {TutorialType} from './utils/web-constants';
|
||||
import {parseConfig} from './tutorials-config';
|
||||
import {createDirectoryAndWriteFile} from './utils/filesystem';
|
||||
import {CONFIG_FILE, GLOB_OPTIONS, TUTORIALS_PLAYGROUND_NODE_PATH} from './utils/node-constants';
|
||||
import {validatePathAnswer} from './utils/cli';
|
||||
|
||||
createTemplate();
|
||||
|
||||
async function createTemplate() {
|
||||
const existingTemplates = await glob('**/', {
|
||||
...GLOB_OPTIONS,
|
||||
absolute: false,
|
||||
cwd: TUTORIALS_PLAYGROUND_NODE_PATH,
|
||||
maxDepth: 1,
|
||||
});
|
||||
|
||||
const path = await askTemplatePath(existingTemplates);
|
||||
const title = await input({message: 'What is the title for the template?'});
|
||||
|
||||
const templatePath = `${TUTORIALS_PLAYGROUND_NODE_PATH}/${path}`;
|
||||
|
||||
const config = await parseConfig({
|
||||
type: TutorialType.EDITOR_ONLY,
|
||||
title,
|
||||
openFiles: ['src/main.ts'],
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
createDirectoryAndWriteFile(join(templatePath, CONFIG_FILE), JSON.stringify(config, null, 2)),
|
||||
createDirectoryAndWriteFile(join(templatePath, 'src', 'main.ts'), ''),
|
||||
]);
|
||||
|
||||
console.info(`\n✅ Template created at ${templatePath}`);
|
||||
}
|
||||
|
||||
function askTemplatePath(existingTemplates: string[]) {
|
||||
const placeholder = 'path-to-template';
|
||||
|
||||
return input({
|
||||
message: 'What is the path of the template?',
|
||||
default: placeholder,
|
||||
validate: (answer) => validatePathAnswer(answer, existingTemplates, placeholder),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {join} from 'path';
|
||||
import {select, input, confirm} from '@inquirer/prompts';
|
||||
import {glob} from 'glob';
|
||||
|
||||
import {createDirectoryAndWriteFile} from './utils/filesystem';
|
||||
import {
|
||||
CONFIG_FILE,
|
||||
CONTENT_PLACEHOLDER,
|
||||
GLOB_OPTIONS,
|
||||
INTRO_DIRECTORY,
|
||||
STEPS_DIRECTORY,
|
||||
TUTORIALS_PROJECT_PATH,
|
||||
TUTORIAL_CONTENT_FILENAME,
|
||||
} from './utils/node-constants';
|
||||
|
||||
import type {TutorialConfig} from './tutorials-types';
|
||||
import {parseConfig} from './tutorials-config';
|
||||
import {
|
||||
TUTORIALS_COMMON_DIRECTORY,
|
||||
TUTORIALS_PLAYGROUND_DIRECTORY,
|
||||
TutorialType,
|
||||
} from './utils/web-constants';
|
||||
import {validatePathAnswer} from './utils/cli';
|
||||
|
||||
type TutorialFields = TutorialConfig & {
|
||||
path: string;
|
||||
};
|
||||
|
||||
type TutorialToCreate = {
|
||||
intro: TutorialFields;
|
||||
steps: Array<TutorialFields & {step: number}>;
|
||||
};
|
||||
|
||||
createTutorial();
|
||||
|
||||
async function createTutorial() {
|
||||
const existingTutorials = await glob('**/', {
|
||||
ignore: [
|
||||
...(GLOB_OPTIONS.ignore as string[]),
|
||||
TUTORIALS_COMMON_DIRECTORY,
|
||||
TUTORIALS_PLAYGROUND_DIRECTORY,
|
||||
],
|
||||
cwd: TUTORIALS_PROJECT_PATH,
|
||||
maxDepth: 1,
|
||||
});
|
||||
|
||||
const introPath = await askTutorialPath(existingTutorials);
|
||||
const introType = await askTutorialType('What is the type of the tutorial intro?');
|
||||
const introTitle = await input({message: 'What is the title for the tutorial intro?'});
|
||||
const introNextTutorial = await askAboutNextTutorial(existingTutorials);
|
||||
|
||||
const tutorialToCreate: TutorialToCreate = {
|
||||
intro: {
|
||||
type: introType as any, // Note: casting to any here as a workaround for the enum type error
|
||||
title: introTitle,
|
||||
path: introPath,
|
||||
openFiles: [''],
|
||||
nextTutorial: introNextTutorial,
|
||||
},
|
||||
steps: [],
|
||||
};
|
||||
|
||||
let addMoreSteps = true;
|
||||
let step = 1;
|
||||
|
||||
while (addMoreSteps) {
|
||||
console.info(`\nStep ${step}`);
|
||||
const path = await askTutorialStepPath(tutorialToCreate.steps);
|
||||
const type = await askTutorialType('What is the type of the step?');
|
||||
const title = await input({message: 'What is the title of the step?'});
|
||||
|
||||
tutorialToCreate.steps.push({
|
||||
step,
|
||||
type: type as any, // Note: casting to any here as a workaround for the enum type error
|
||||
path,
|
||||
title,
|
||||
openFiles: [''],
|
||||
});
|
||||
|
||||
step++;
|
||||
|
||||
console.info();
|
||||
addMoreSteps = await confirm({message: 'Would you like to add another step?', default: true});
|
||||
}
|
||||
|
||||
const tutorialPath = `${TUTORIALS_PROJECT_PATH}/${tutorialToCreate.intro.path}`;
|
||||
|
||||
await Promise.all([
|
||||
createIntroFiles(tutorialToCreate.intro),
|
||||
...createStepsFiles(tutorialPath, tutorialToCreate.steps),
|
||||
]);
|
||||
|
||||
console.info(`\n✅ Tutorial created at ${tutorialPath}`);
|
||||
}
|
||||
|
||||
function askTutorialType(message: string = 'What is the type of the tutorial?') {
|
||||
return select<TutorialConfig['type']>({
|
||||
message: message,
|
||||
choices: [
|
||||
{
|
||||
name: 'CLI only: a tutorial for the Angular CLI in-browser unsing the embedded terminal',
|
||||
value: TutorialType.CLI,
|
||||
},
|
||||
{name: 'Local app: a tutorial for building an app locally', value: TutorialType.LOCAL},
|
||||
{
|
||||
name: 'Embedded editor: a tutorial for building an app in-browser using the embedded editor',
|
||||
value: TutorialType.EDITOR,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function askAboutNextTutorial(
|
||||
existingTutorials: string[],
|
||||
message: string = 'What should be the next tutorial?',
|
||||
) {
|
||||
return select({
|
||||
message,
|
||||
choices: [
|
||||
...existingTutorials.map((tutorial) => ({name: tutorial, value: tutorial})),
|
||||
{name: 'None', value: undefined},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function askTutorialPath(existingTutorials: string[]) {
|
||||
const placeholder = 'path-to-tutorial';
|
||||
|
||||
return input({
|
||||
message: 'What is the path of the tutorial?',
|
||||
default: placeholder,
|
||||
validate: (answer) => validatePathAnswer(answer, existingTutorials, placeholder),
|
||||
});
|
||||
}
|
||||
|
||||
function askTutorialStepPath(createdSteps: TutorialToCreate['steps']) {
|
||||
const placeholder = 'path-to-step';
|
||||
|
||||
const existingSteps = createdSteps.map((step) => step.path);
|
||||
|
||||
return input({
|
||||
message: 'What is the path of the step?',
|
||||
default: placeholder,
|
||||
validate: (answer) => validatePathAnswer(answer, existingSteps, placeholder),
|
||||
});
|
||||
}
|
||||
|
||||
async function createIntroFiles(intro: TutorialToCreate['intro']): Promise<Promise<void>[]> {
|
||||
const tutorialPath = `${TUTORIALS_PROJECT_PATH}/${intro.path}`;
|
||||
|
||||
const config = await parseConfig({
|
||||
type: intro.type as any, // Note: casting to any here as a workaround for the enum type error
|
||||
title: intro.title,
|
||||
openFiles: intro.openFiles,
|
||||
nextTutorial: intro.nextTutorial,
|
||||
});
|
||||
|
||||
return [
|
||||
createDirectoryAndWriteFile(
|
||||
join(tutorialPath, INTRO_DIRECTORY, CONFIG_FILE),
|
||||
JSON.stringify(config, null, 2),
|
||||
),
|
||||
createDirectoryAndWriteFile(
|
||||
join(tutorialPath, INTRO_DIRECTORY, TUTORIAL_CONTENT_FILENAME),
|
||||
CONTENT_PLACEHOLDER,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function createStepsFiles(
|
||||
tutorialPath: string,
|
||||
steps: TutorialToCreate['steps'],
|
||||
): Promise<Promise<void>[]>[] {
|
||||
const tutorialStepsPath = join(tutorialPath, STEPS_DIRECTORY);
|
||||
|
||||
return steps
|
||||
.map(async (step) => {
|
||||
const stepPath = join(tutorialStepsPath, `${step.step}-${step.path}`);
|
||||
|
||||
const config = await parseConfig({
|
||||
type: step.type as any, // Note: casting to any here as a workaround for the enum type error
|
||||
title: step.title,
|
||||
openFiles: step.openFiles,
|
||||
});
|
||||
|
||||
return [
|
||||
createDirectoryAndWriteFile(`${stepPath}/${CONFIG_FILE}`, JSON.stringify(config, null, 2)),
|
||||
createDirectoryAndWriteFile(
|
||||
`${stepPath}/${TUTORIAL_CONTENT_FILENAME}`,
|
||||
CONTENT_PLACEHOLDER,
|
||||
),
|
||||
];
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {copyFile, mkdir} from 'fs/promises';
|
||||
import {glob} from 'glob';
|
||||
|
||||
import {filesAndContentsToRecord} from '../utils/filesystem';
|
||||
import {TUTORIALS_COMMON_DIRECTORY} from '../utils/web-constants';
|
||||
import {getFileSystemTree, shouldUseFileInWebContainer} from '../utils/webcontainers';
|
||||
|
||||
import type {FileAndContent, TutorialFiles} from '../tutorials-types';
|
||||
import {TUTORIALS_COMMON_ASSETS_DEST, TUTORIALS_COMMON_ASSETS_SRC} from '../utils/node-constants';
|
||||
|
||||
const BEFORE_COMMON_PATH_REGEX = new RegExp(`.*\/${TUTORIALS_COMMON_DIRECTORY}\/`);
|
||||
|
||||
export async function getTutorialCommonFiles(
|
||||
files: string[],
|
||||
filesContents: FileAndContent[],
|
||||
): Promise<TutorialFiles> {
|
||||
const filesPath = files.filter(shouldUseFileInWebContainer).map(getCleanFilePath);
|
||||
|
||||
const filesContentsWithoutPath = filesContents.map(({path, content}) => ({
|
||||
path: getCleanFilePath(path),
|
||||
content,
|
||||
}));
|
||||
|
||||
const fileSystemTree = getFileSystemTree(
|
||||
filesPath,
|
||||
filesAndContentsToRecord(filesContentsWithoutPath),
|
||||
);
|
||||
|
||||
return {sourceCode: fileSystemTree};
|
||||
}
|
||||
|
||||
export function getCleanFilePath(path: string) {
|
||||
return path.replace(BEFORE_COMMON_PATH_REGEX, '');
|
||||
}
|
||||
|
||||
export async function copyCommonAssetsToAppAssets() {
|
||||
const [commonAssets] = await Promise.all([
|
||||
// List of files for the assets from the tutorial common directory/app.
|
||||
glob(['**/*.png', '**/*.jpg'], {
|
||||
cwd: TUTORIALS_COMMON_ASSETS_SRC,
|
||||
nodir: true,
|
||||
absolute: true,
|
||||
}),
|
||||
|
||||
// Ensure the common directory for tutorial assets exists.
|
||||
mkdir(TUTORIALS_COMMON_ASSETS_DEST, {recursive: true}),
|
||||
]);
|
||||
|
||||
await Promise.all(
|
||||
commonAssets.map((asset) =>
|
||||
copyFile(asset, asset.replace(TUTORIALS_COMMON_ASSETS_SRC, TUTORIALS_COMMON_ASSETS_DEST)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {basename} from 'path';
|
||||
|
||||
import {
|
||||
filesAndContentsToRecord,
|
||||
getFilesContents,
|
||||
getPackageJsonFromFiles,
|
||||
globWithCwdPath,
|
||||
} from '../utils/filesystem';
|
||||
import {
|
||||
GLOB_OPTIONS,
|
||||
TUTORIALS_COMMON_NODE_PATH,
|
||||
TUTORIALS_HOMEPAGE_NODE_PATH,
|
||||
} from '../utils/node-constants';
|
||||
import {getCleanFilePath as getCleanCommonFilePath} from './common';
|
||||
|
||||
import {getTutorialConfig, validateOpenFilesConfig} from '../tutorials-config';
|
||||
import {
|
||||
FileAndContent,
|
||||
FileAndContentRecord,
|
||||
TutorialFiles,
|
||||
TutorialMetadata,
|
||||
} from '../tutorials-types';
|
||||
import {getAllFiles, validatePackageJson} from '../utils/metadata';
|
||||
import {TUTORIALS_HOMEPAGE_DIRECTORY, TutorialType} from '../utils/web-constants';
|
||||
import {getFileSystemTree, shouldUseFileInWebContainer} from '../utils/webcontainers';
|
||||
|
||||
const BEFORE_HOMEPAGE_PATH_REGEX = new RegExp(`.*\/${TUTORIALS_HOMEPAGE_DIRECTORY}\/`);
|
||||
|
||||
export async function getHomepagePlaygroundFiles(
|
||||
commonFiles: FileAndContent[],
|
||||
): Promise<TutorialFiles> {
|
||||
const homepagePlaygroundFiles = await globWithCwdPath('**', {
|
||||
...GLOB_OPTIONS,
|
||||
cwd: TUTORIALS_HOMEPAGE_NODE_PATH,
|
||||
nodir: true,
|
||||
});
|
||||
|
||||
const config = await getTutorialConfig(homepagePlaygroundFiles);
|
||||
|
||||
if (config.type !== TutorialType.EDITOR_ONLY)
|
||||
throw new Error(`Homepage must be of type "${TutorialType.EDITOR_ONLY}"`);
|
||||
|
||||
const filesContents = await getFilesContents(homepagePlaygroundFiles);
|
||||
|
||||
const tutorialFiles = getTutorialFiles(filesContents, commonFiles);
|
||||
|
||||
if (config.openFiles) {
|
||||
validateOpenFilesConfig(
|
||||
TUTORIALS_HOMEPAGE_NODE_PATH,
|
||||
config.openFiles,
|
||||
Object.keys(tutorialFiles),
|
||||
);
|
||||
} else {
|
||||
config.openFiles = Object.keys(tutorialFiles);
|
||||
}
|
||||
|
||||
const hasPackageJson = homepagePlaygroundFiles.some((file) => basename(file) === 'package.json');
|
||||
|
||||
let dependencies: TutorialMetadata['dependencies'];
|
||||
|
||||
if (hasPackageJson) {
|
||||
const packageJson = getPackageJsonFromFiles(filesContents);
|
||||
|
||||
dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
};
|
||||
|
||||
validatePackageJson(homepagePlaygroundFiles, packageJson, getPackageJsonFromFiles(commonFiles));
|
||||
}
|
||||
|
||||
const allFiles = getAllFiles(
|
||||
homepagePlaygroundFiles,
|
||||
commonFiles.map(({path}) => path),
|
||||
getCleanFilePath,
|
||||
);
|
||||
|
||||
return {
|
||||
sourceCode: getSourceCode(homepagePlaygroundFiles, filesContents),
|
||||
metadata: {
|
||||
type: config.type,
|
||||
allFiles,
|
||||
dependencies,
|
||||
tutorialFiles,
|
||||
openFiles: config.openFiles ?? Object.keys(tutorialFiles),
|
||||
hiddenFiles: config.openFiles
|
||||
? Object.keys(tutorialFiles).filter((filename) => !config.openFiles!.includes(filename))
|
||||
: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getSourceCode(tutorialFiles: string[], filesContents: FileAndContent[]) {
|
||||
// TODO: reuse this as it's duplicated in getTutorialFiles
|
||||
const fileSystemTreeContents = filesContents
|
||||
.filter(({path}) => shouldUseFileInWebContainer(path))
|
||||
// remove steps paths from file contents
|
||||
.map(({path, content}) => ({
|
||||
path: getCleanFilePath(path),
|
||||
content,
|
||||
}));
|
||||
|
||||
const fileSystemTreeFiles = tutorialFiles
|
||||
.filter((filename) => shouldUseFileInWebContainer(filename))
|
||||
.map(getCleanFilePath);
|
||||
|
||||
return getFileSystemTree(fileSystemTreeFiles, filesAndContentsToRecord(fileSystemTreeContents));
|
||||
}
|
||||
|
||||
function getTutorialFiles(
|
||||
filesContents: FileAndContent[],
|
||||
commonFiles: FileAndContent[],
|
||||
): FileAndContentRecord {
|
||||
const playgroundFilesForCodeEditor = filesContents
|
||||
.filter(({path}) => shouldUseFileInWebContainer(getCleanFilePath(path)))
|
||||
.map(({path, content}) => ({
|
||||
path: getCleanFilePath(path),
|
||||
content,
|
||||
}));
|
||||
|
||||
const commonFilesForCodeEditor = commonFiles
|
||||
.filter(({path}) => shouldUseFileInWebContainer(path))
|
||||
.map(({path, content}) => ({
|
||||
path: getCleanCommonFilePath(path),
|
||||
content,
|
||||
}));
|
||||
|
||||
return {
|
||||
...filesAndContentsToRecord(commonFilesForCodeEditor),
|
||||
...filesAndContentsToRecord(playgroundFilesForCodeEditor),
|
||||
};
|
||||
}
|
||||
|
||||
function getCleanFilePath(path: string) {
|
||||
if (path.includes(TUTORIALS_COMMON_NODE_PATH)) return getCleanCommonFilePath(path);
|
||||
|
||||
return path.replace(BEFORE_HOMEPAGE_PATH_REGEX, '');
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user