mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
288 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -38,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/
|
||||
@@ -22,10 +22,10 @@ jobs:
|
||||
(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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
# 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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
workflow-artifact-name: 'aio'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: aio/
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'aio'
|
||||
|
||||
@@ -13,9 +13,9 @@ jobs:
|
||||
assistant_to_the_branch_manager:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- 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@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3
|
||||
- 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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- 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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- 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@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3
|
||||
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
|
||||
with:
|
||||
issue-number: ${{github.event.issue.number}}
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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.
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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
|
||||
|
||||
+25
-25
@@ -23,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -60,13 +60,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Run unit tests
|
||||
@@ -78,13 +78,13 @@ jobs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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
|
||||
@@ -94,15 +94,15 @@ jobs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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
|
||||
@@ -115,15 +115,15 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Run AIO tests with local packages
|
||||
@@ -138,15 +138,15 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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
|
||||
@@ -182,13 +182,13 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -208,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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: |
|
||||
@@ -251,7 +251,7 @@ jobs:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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.
|
||||
@@ -259,9 +259,9 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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:
|
||||
|
||||
@@ -12,14 +12,14 @@ jobs:
|
||||
labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- 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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- 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 }}
|
||||
|
||||
@@ -14,6 +14,6 @@ jobs:
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/feature-request@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- 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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- 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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
- 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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0
|
||||
uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: 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@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9
|
||||
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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
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
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
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}}]'
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
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
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- 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@031962443584a0ac5cbd9d1c1b78b241453e4702
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
branch-prefix: docs-update-events
|
||||
pr-title: 'docs: update events'
|
||||
|
||||
@@ -29,6 +29,8 @@ export const format: FormatConfig = {
|
||||
'!packages/core/src/i18n/locale_en.ts',
|
||||
'!packages/common/locales/closure-locale.ts',
|
||||
'!packages/common/src/i18n/currencies.ts',
|
||||
// Temporarily disable formatting for adev
|
||||
'!adev/**',
|
||||
],
|
||||
},
|
||||
'buildifier': true,
|
||||
|
||||
+25
-5
@@ -182,6 +182,8 @@ groups:
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- jessicajaniuk
|
||||
- AndrewKushnir
|
||||
|
||||
# =========================================================
|
||||
# Framework: Core
|
||||
@@ -339,7 +341,9 @@ groups:
|
||||
'aio/content/special-elements/**/{*,.*}',
|
||||
'aio/content/blocks/**/{*,.*}',
|
||||
'aio/content/guide/hydration.md',
|
||||
'aio/content/guide/defer.md',
|
||||
'aio/content/guide/signals.md',
|
||||
'aio/content/guide/control_flow.md',
|
||||
'aio/content/examples/injection-token/**/{*,.*}',
|
||||
])
|
||||
reviewers:
|
||||
@@ -517,10 +521,8 @@ groups:
|
||||
contains_any_globs(files, [
|
||||
'packages/platform-server/**/{*,.*}',
|
||||
'aio/content/guide/prerendering.md',
|
||||
'aio/content/guide/universal.md',
|
||||
'aio/content/examples/universal/**/{*,.*}',
|
||||
'aio/content/guide/universal-ngmodule.md',
|
||||
'aio/content/examples/universal-ngmodule/**/{*,.*}'
|
||||
'aio/content/guide/ssr.md',
|
||||
'aio/content/examples/ssr/**/{*,.*}',
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
@@ -938,6 +940,7 @@ groups:
|
||||
- alxhub
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- jessicajaniuk
|
||||
- jelbourn
|
||||
|
||||
# =========================================================
|
||||
@@ -1054,6 +1057,22 @@ groups:
|
||||
- dylhunn
|
||||
- mgechev
|
||||
|
||||
# =========================================================
|
||||
# Docs: Angular Dev Site
|
||||
# =========================================================
|
||||
angular-dev:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'adev/**/{.*,*}'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- twerske
|
||||
- josephperrott
|
||||
- jelbourn
|
||||
|
||||
# =========================================================
|
||||
# Docs-infra
|
||||
# =========================================================
|
||||
@@ -1165,10 +1184,11 @@ groups:
|
||||
'tools/esm-interop/**/{*,.*}',
|
||||
'tools/gulp-tasks/**/{*,.*}',
|
||||
'tools/legacy-saucelabs/**/{*,.*}',
|
||||
'tools/manual_api_docs/**/{*,.*}',
|
||||
'tools/npm-patches/**/{*,.*}',
|
||||
'tools/rxjs/**/{*,.*}',
|
||||
'tools/saucelabs/**/{*,.*}',
|
||||
'tools/saucelabs-daemon/**/{*,.*}',
|
||||
'tools/saucelabs/**/{*,.*}',
|
||||
'tools/symbol-extractor/**/{*,.*}',
|
||||
'tools/testing/**/{*,.*}',
|
||||
'tools/tslint/**/{*,.*}',
|
||||
|
||||
+330
-270
@@ -1,16 +1,158 @@
|
||||
<a name="16.2.9"></a>
|
||||
# 16.2.9 (2023-10-11)
|
||||
### forms
|
||||
<a name="17.0.1"></a>
|
||||
# 17.0.1 (2023-11-08)
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [51a5baace3](https://github.com/angular/angular/commit/51a5baace3b1eee130fa911327773066139cc68e) | fix | reset() call with null values on nested group ([#48830](https://github.com/angular/angular/pull/48830)) |
|
||||
| [5c6f3f8ec0](https://github.com/angular/angular/commit/5c6f3f8ec0f1dd9b5505f3c94e654a675e75f147) | fix | Don't override the backend when using the InMemoryWebAPI ([#52425](https://github.com/angular/angular/pull/52425)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [70d30c28e0](https://github.com/angular/angular/commit/70d30c28e04f4ead51145e4e47df342492bfb336) | fix | Add support for ng-templates with i18n attributes ([#52597](https://github.com/angular/angular/pull/52597)) |
|
||||
| [4f125c5f9a](https://github.com/angular/angular/commit/4f125c5f9ae572a8216ec1fbb88f52e47b875e1e) | fix | Switches to multiple passes to fix several reported bugs ([#52592](https://github.com/angular/angular/pull/52592)) |
|
||||
|
||||
Web Frameworks: the internet frontier.<br/>
|
||||
These are the voyages of the framework Angular.<br/>
|
||||
Its continuing mission:<br/>
|
||||
To explore strange, new technologies.<br/>
|
||||
To seek out new users and new applications.<br/>
|
||||
To boldly go where no web framework has gone before.<br/>
|
||||
|
||||
In honor of v17.0.1
|
||||
|
||||
```
|
||||
______
|
||||
___.--------'------`---------.____
|
||||
_.---'----------------------------------`---.__
|
||||
.'___=]===========================================
|
||||
,-----------------------..__/.' >--.______ _______.---'
|
||||
]====================<==||(__) .' `------'
|
||||
`-----------------------`' ----.___--/
|
||||
/ /---' `/
|
||||
/_______(______________________/
|
||||
`-------------.--------------.'
|
||||
\________|_.-'
|
||||
```
|
||||
|
||||
Live long and prosper 🖖🏻
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.0-next.7"></a>
|
||||
# 17.0.0-next.7 (2023-10-04)
|
||||
## Deprecations
|
||||
<a name="17.0.0"></a>
|
||||
# 17.0.0 (2023-11-08)
|
||||
|
||||
[Blog post "Angular v17 is now available"](http://goo.gle/angular-v17).
|
||||
|
||||
## Breaking Changes
|
||||
###
|
||||
- Node.js v16 support has been removed and the minimum support version has been bumped to 18.13.0.
|
||||
|
||||
Node.js v16 is planned to be End-of-Life on 2023-09-11. Angular will stop supporting Node.js v16 in Angular v17. For Node.js release schedule details, please see: https://github.com/nodejs/release#release-schedule
|
||||
### common
|
||||
-
|
||||
- the NgSwitch directive now defaults to the === equality operator,
|
||||
migrating from the previously used ==. NgSwitch expressions and / or
|
||||
individual condition values need adjusting to this stricter equality
|
||||
check. The added warning message should help pinpointing NgSwitch
|
||||
usages where adjustements are needed.
|
||||
### core
|
||||
- Angular now required `zone.js` version `~0.14.0`
|
||||
- Versions of TypeScript older than 5.2 are no longer supported.
|
||||
- The `mutate` method was removed from the `WritableSignal` interface and completely
|
||||
dropped from the public API surface. As an alternative please use the update method and
|
||||
make immutable changes to the object.
|
||||
|
||||
Example before:
|
||||
|
||||
```typescript
|
||||
items.mutate(itemsArray => itemsArray.push(newItem));
|
||||
```
|
||||
|
||||
Example after:
|
||||
|
||||
```typescript
|
||||
items.update(itemsArray => [itemsArray, …newItem]);
|
||||
```
|
||||
- The `mutate` method was removed from the `WritableSignal` interface and completely
|
||||
dropped from the public API surface. As an alternative please use the update method and
|
||||
make immutable changes to the object.
|
||||
|
||||
Example before:
|
||||
|
||||
```typescript
|
||||
items.mutate(itemsArray => itemsArray.push(newItem));
|
||||
```
|
||||
|
||||
Example after:
|
||||
|
||||
```typescript
|
||||
items.update(itemsArray => [itemsArray, …newItem]);
|
||||
```
|
||||
- `OnPush` components that are created dynamically now
|
||||
only have their host bindings refreshed and `ngDoCheck run` during change
|
||||
detection if they are dirty.
|
||||
Previously, a bug in the change detection would result in the `OnPush`
|
||||
configuration of dynamically created components to be ignored when
|
||||
executing host bindings and the `ngDoCheck` function. This is
|
||||
rarely encountered but can happen if code has a handle on the
|
||||
`ComponentRef` instance and updates values read in the `OnPush`
|
||||
component template without then calling either `markForCheck` or
|
||||
`detectChanges` on that component's `ChangeDetectorRef`.
|
||||
### platform-browser
|
||||
- `REMOVE_STYLES_ON_COMPONENT_DESTROY` default value is now `true`. This causes CSS of components to be removed from the DOM when destroyed. You retain the previous behaviour by providing the `REMOVE_STYLES_ON_COMPONENT_DESTROY` injection token.
|
||||
|
||||
```ts
|
||||
import {REMOVE_STYLES_ON_COMPONENT_DESTROY} from '@angular/platform-browser';
|
||||
...
|
||||
providers: [{
|
||||
provide: REMOVE_STYLES_ON_COMPONENT_DESTROY,
|
||||
useValue: false,
|
||||
}]
|
||||
```
|
||||
- The `withNoDomReuse()` function was removed from the public API. If you need to disable hydration, you can exclude the `provideClientHydration()` call from provider list in your application (which would disable hydration features for the entire application) or use `ngSkipHydration` attribute to disable hydration for particular components. See this guide for additional information: https://angular.io/guide/hydration#how-to-skip-hydration-for-particular-components.
|
||||
### router
|
||||
- Absolute redirects no longer prevent further redirects.
|
||||
Route configurations may need to be adjusted to prevent infinite
|
||||
redirects where additional redirects were previously ignored after an
|
||||
absolute redirect occurred.
|
||||
- Routes with `loadComponent` would incorrectly cause
|
||||
child routes to inherit their data by default. The default
|
||||
`paramsInheritanceStrategy` is `emptyOnly`. If parent data should be
|
||||
inherited in child routes, this should be manually set to `always`.
|
||||
- `urlHandlingStrategy` has been removed from the Router public API.
|
||||
This should instead be configured through the provideRouter or RouterModule.forRoot APIs.
|
||||
- The following Router properties have been removed from
|
||||
the public API:
|
||||
|
||||
- canceledNavigationResolution
|
||||
- paramsInheritanceStrategy
|
||||
- titleStrategy
|
||||
- urlUpdateStrategy
|
||||
- malformedUriErrorHandler
|
||||
|
||||
These should instead be configured through the `provideRouter` or
|
||||
`RouterModule.forRoot` APIs.
|
||||
- The `setupTestingRouter` function has been removed. Use
|
||||
`RouterModule.forRoot` or `provideRouter` to setup the `Router` for
|
||||
tests instead.
|
||||
- `malformedUriErrorHandler` is no longer available in
|
||||
the `RouterModule.forRoot` options. URL parsing errors should instead be
|
||||
handled in the `UrlSerializer.parse` method.
|
||||
### zone.js
|
||||
- Deep and legacy `dist/` imports like `zone.js/bundles/zone-testing.js` and `zone.js/dist/zone` are no longer allowed. `zone-testing-bundle` and `zone-testing-node-bundle` are also no longer part of the package.
|
||||
|
||||
The proper way to import `zone.js` and `zone.js/testing` is:
|
||||
```js
|
||||
import 'zone.js';
|
||||
import 'zone.js/testing';
|
||||
```
|
||||
## Deprecations
|
||||
### animations
|
||||
- The `AnimationDriver.NOOP` symbol is deprecated, use `NoopAnimationDriver` instead.
|
||||
### core
|
||||
- `ChangeDetectorRef.checkNoChanges` is deprecated.
|
||||
|
||||
Test code should use `ComponentFixture` instead of `ChangeDetectorRef`.
|
||||
Application code should not call `ChangeDetectorRef.checkNoChanges` directly.
|
||||
- Swapping out the context object for `EmbeddedViewRef`
|
||||
is no longer supported. Support for this was introduced with v12.0.0, but
|
||||
this pattern is rarely used. There is no replacement, but you can use
|
||||
@@ -19,41 +161,219 @@
|
||||
as an example).
|
||||
|
||||
Also adds a warning if the deprecated
|
||||
- NgProbeToken
|
||||
|
||||
The `NgProbeToken` is not used internally since the transition from View Engine to Ivy. The token has no utility and can be removed from applications and libraries.
|
||||
###
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [59aa0634f4](https://github.com/angular/angular/commit/59aa0634f4d4694203f2a69c40017fe5a3962514) | build | remove support for Node.js v16 ([#51755](https://github.com/angular/angular/pull/51755)) |
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e753278faa](https://github.com/angular/angular/commit/e753278faae79a53e235e0d8e03f89555a712d80) | feat | Add the possibility of lazy loading animations code. ([#50738](https://github.com/angular/angular/pull/50738)) |
|
||||
| [698c058e1c](https://github.com/angular/angular/commit/698c058e1c975c573722407f4843a4a774ceb92a) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
|
||||
| [0598613950](https://github.com/angular/angular/commit/0598613950c76f4a13601c6942e30ab4ce1e3b67) | refactor | deprecation of `AnimationDriver.NOOP` ([#51843](https://github.com/angular/angular/pull/51843)) |
|
||||
### benchpress
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2da3551a70](https://github.com/angular/angular/commit/2da3551a703ebef401d76a8e88e388437e851d85) | feat | report gc and render time spent in script ([#50771](https://github.com/angular/angular/pull/50771)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fe2fd7e1a8](https://github.com/angular/angular/commit/fe2fd7e1a898a4525c219065a6d0908988dfd7e2) | feat | make the warning for lazy-loaded lcp image an error ([#51748](https://github.com/angular/angular/pull/51748)) |
|
||||
| [dde3fdabbd](https://github.com/angular/angular/commit/dde3fdabbd24b48dd6afd120d23e92a3605eb04d) | feat | upgrade warning to logged error for lazy-loaded LCP images using NgOptimizedImage ([#52004](https://github.com/angular/angular/pull/52004)) |
|
||||
| [da056a1fe2](https://github.com/angular/angular/commit/da056a1fe2816299319fb3f87416316be2029479) | fix | add missing types field for @angular/common/locales of exports in package.json ([#52080](https://github.com/angular/angular/pull/52080)) |
|
||||
| [85843e8212](https://github.com/angular/angular/commit/85843e8212e99deb8b70f3d3f8dfe002b978cbb1) | fix | allow to specify only some properties of `DatePipeConfig` ([#51287](https://github.com/angular/angular/pull/51287)) |
|
||||
| [3bd85fb7b0](https://github.com/angular/angular/commit/3bd85fb7b0723ed807bca771e9fa95af60a3cfaf) | fix | apply fixed_srcset_width value only to fixed srcsets ([#52459](https://github.com/angular/angular/pull/52459)) |
|
||||
| [65b460448e](https://github.com/angular/angular/commit/65b460448ec5fdcee5aecca0cdc3cf498b0832cb) | fix | missing space in ngSwitch equality warning ([#52180](https://github.com/angular/angular/pull/52180)) |
|
||||
| [86c5e34601](https://github.com/angular/angular/commit/86c5e34601d7901a11688124aa902646524177eb) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
|
||||
| [28a5925f53](https://github.com/angular/angular/commit/28a5925f53790067d45f1f68d204a36088dbf5e3) | fix | use === operator to match NgSwitch cases ([#51504](https://github.com/angular/angular/pull/51504)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1934524a0c](https://github.com/angular/angular/commit/1934524a0c673fb65cd927c55c712f59446f9c93) | feat | add docs extraction for type aliases ([#52118](https://github.com/angular/angular/pull/52118)) |
|
||||
| [7f6d9a73ab](https://github.com/angular/angular/commit/7f6d9a73ab8b658d0d8148080dfefb2550bee6b4) | feat | expand class api doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [a7fa25306f](https://github.com/angular/angular/commit/a7fa25306f8ce47d8aa330531382106efec55a55) | feat | extract api docs for interfaces ([#52006](https://github.com/angular/angular/pull/52006)) |
|
||||
| [7bfe20707f](https://github.com/angular/angular/commit/7bfe20707fedff7290e12356a1545644b436d41c) | feat | extract api for fn overloads and abtract classes ([#52040](https://github.com/angular/angular/pull/52040)) |
|
||||
| [c7daf7ea16](https://github.com/angular/angular/commit/c7daf7ea1692391f7cac8f794ed777887a2764af) | feat | extract directive docs info ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [e0b1bb33d7](https://github.com/angular/angular/commit/e0b1bb33d77babe881f77f52cb1b71e345f5696b) | feat | extract doc info for JsDoc ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [b9c70158ab](https://github.com/angular/angular/commit/b9c70158abecd81a5af512c8b4da685851cf159f) | feat | extract docs for accessors, rest params, and types ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [a24ae994a0](https://github.com/angular/angular/commit/a24ae994a0470fdac09a69937fd0580cff6c6d68) | feat | extract docs for top level functions and consts ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [2e41488296](https://github.com/angular/angular/commit/2e41488296879685b19dfba8d78037690347bda3) | feat | extract docs info for enums, pipes, and NgModules ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [34495b3533](https://github.com/angular/angular/commit/34495b35337892ab209d9955ff7fe2897a0c5d41) | feat | extract docs via exports ([#51828](https://github.com/angular/angular/pull/51828)) |
|
||||
| [7e82df45c5](https://github.com/angular/angular/commit/7e82df45c5bb72ec3dafaa07dc1eaa5d463b006c) | feat | initial skeleton for API doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [6795cccbbb](https://github.com/angular/angular/commit/6795cccbbbfc17bbf88fb8197aa172cca67fa2d2) | fix | account for type-only imports in defer blocks ([#52343](https://github.com/angular/angular/pull/52343)) |
|
||||
| [23bfa10ac8](https://github.com/angular/angular/commit/23bfa10ac809f6b27d32647210c52329f0e4262e) | fix | add diagnostic for inaccessible deferred trigger ([#51922](https://github.com/angular/angular/pull/51922)) |
|
||||
| [31295a3cf9](https://github.com/angular/angular/commit/31295a3cf907a61e7115d9039a83a232b263a676) | fix | allocating unnecessary slots in conditional instruction ([#51913](https://github.com/angular/angular/pull/51913)) |
|
||||
| [2aaddd3f64](https://github.com/angular/angular/commit/2aaddd3f64bb8891bb4bdcadf05d427a89338112) | fix | allow comments between switch cases ([#52449](https://github.com/angular/angular/pull/52449)) |
|
||||
| [ddd9df68bb](https://github.com/angular/angular/commit/ddd9df68bb2e907dd820f239aaf819425cb95df8) | fix | allow decimals in defer block time values ([#52433](https://github.com/angular/angular/pull/52433)) |
|
||||
| [7dbd47fb30](https://github.com/angular/angular/commit/7dbd47fb3015117c420f984181bfcb48e533525a) | fix | allow newlines in track and let expressions ([#52137](https://github.com/angular/angular/pull/52137)) |
|
||||
| [0eae992c4e](https://github.com/angular/angular/commit/0eae992c4e03b7c9039476e03b72e92d662293df) | fix | allow nullable values in for loop block ([#51997](https://github.com/angular/angular/pull/51997)) |
|
||||
| [073ebfe09e](https://github.com/angular/angular/commit/073ebfe09eccd5d01d27fcc46fc5d4465c1851ff) | fix | apply style on :host attributes in prod builds. ([#49118](https://github.com/angular/angular/pull/49118)) |
|
||||
| [81a287a79a](https://github.com/angular/angular/commit/81a287a79afc16d43c0fd24d7aea54be4414940a) | fix | avoid error in template parser for tag names that can occur in object prototype ([#52225](https://github.com/angular/angular/pull/52225)) |
|
||||
| [6c58252521](https://github.com/angular/angular/commit/6c582525217197dd777d5bb9e62d6aaa2c70a996) | fix | compilation error when for loop block expression contains new line ([#52447](https://github.com/angular/angular/pull/52447)) |
|
||||
| [9d19c8e317](https://github.com/angular/angular/commit/9d19c8e31752d211f575246282358b83afe90969) | fix | don't allocate variable to for loop expression ([#52158](https://github.com/angular/angular/pull/52158)) |
|
||||
| [9acd2ac98b](https://github.com/angular/angular/commit/9acd2ac98bc3b6ffc5a8d6c19f7290d05fe1f896) | fix | enable block syntax in the linker ([#51979](https://github.com/angular/angular/pull/51979)) |
|
||||
| [1d871c03a5](https://github.com/angular/angular/commit/1d871c03a523e10bb838cb0f9550595cfbd9d14d) | fix | forward referenced dependencies not identified as deferrable ([#52017](https://github.com/angular/angular/pull/52017)) |
|
||||
| [16ff08ec70](https://github.com/angular/angular/commit/16ff08ec70bfa192041ba050e550676e8d505a05) | fix | narrow the type of expressions in event listeners inside if blocks ([#52069](https://github.com/angular/angular/pull/52069)) |
|
||||
| [ac0d5dcfd6](https://github.com/angular/angular/commit/ac0d5dcfd6015ec4283ed1a5cf241f130f4c5cf5) | fix | narrow the type of expressions in event listeners inside switch blocks ([#52069](https://github.com/angular/angular/pull/52069)) |
|
||||
| [02edb43067](https://github.com/angular/angular/commit/02edb4306736e6f12e87a4164c17eca6cbdfe151) | fix | narrow the type of the aliased if block expression ([#51952](https://github.com/angular/angular/pull/51952)) |
|
||||
| [83067b3ef2](https://github.com/angular/angular/commit/83067b3ef257dbc7b1c20d50645615d19023ba51) | fix | ng-template directive invoke twice at the root of control flow ([#52515](https://github.com/angular/angular/pull/52515)) |
|
||||
| [17078a3fe1](https://github.com/angular/angular/commit/17078a3fe1e9b90e48952b6c12b6e6b774b97810) | fix | pipes used inside defer triggers not being picked up ([#52071](https://github.com/angular/angular/pull/52071)) |
|
||||
| [861ce3a7c5](https://github.com/angular/angular/commit/861ce3a7c574340a6164ad0de13f49bda3e172da) | fix | pipes using DI not working in blocks ([#52112](https://github.com/angular/angular/pull/52112)) |
|
||||
| [1f5039bbd6](https://github.com/angular/angular/commit/1f5039bbd6de8450e5511af00044ddd2f4314016) | fix | project control flow root elements into correct slot ([#52414](https://github.com/angular/angular/pull/52414)) |
|
||||
| [81c315ec6e](https://github.com/angular/angular/commit/81c315ec6ea37c55d951d3b38b6c551226173be5) | fix | template type checking not reporting diagnostics for incompatible type comparisons ([#52322](https://github.com/angular/angular/pull/52322)) |
|
||||
| [1beef49d80](https://github.com/angular/angular/commit/1beef49d80809fbb0e7c8e95f17096c39ac8940a) | fix | update the minVersion if component uses block syntax ([#51979](https://github.com/angular/angular/pull/51979)) |
|
||||
| [386e1e9500](https://github.com/angular/angular/commit/386e1e950033ad98661e5077a4f119df0e7b3008) | fix | work around TypeScript bug when narrowing switch statements ([#52110](https://github.com/angular/angular/pull/52110)) |
|
||||
| [e5bca43224](https://github.com/angular/angular/commit/e5bca432248add0a19102f6afeae145f1a33ee8a) | perf | further reduce bundle size using arrow functions ([#52010](https://github.com/angular/angular/pull/52010)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5b66330329](https://github.com/angular/angular/commit/5b66330329fd066a7c347f040a330b4c7f2a0a2b) | fix | allow non-array imports for standalone component in local compilation mode ([#51819](https://github.com/angular/angular/pull/51819)) |
|
||||
| [377a7abfda](https://github.com/angular/angular/commit/377a7abfda60a6ddd55a41531e3653bcad78b0a2) | fix | bypass static resolving of the component's changeDetection field in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
|
||||
| [19c3dc18d3](https://github.com/angular/angular/commit/19c3dc18d3c0cfd83efec2c8f81b40860d570346) | fix | fix NgModule injector def in local compilation mode when imports/exports are non-array expressions ([#51819](https://github.com/angular/angular/pull/51819)) |
|
||||
| [11bb19cafc](https://github.com/angular/angular/commit/11bb19cafcf447b7ce6ade146d431a43c3e2c682) | fix | handle nested qualified names in ctor injection in local compilation mode ([#51947](https://github.com/angular/angular/pull/51947)) |
|
||||
| [f91f222b55](https://github.com/angular/angular/commit/f91f222b55f249089d267c72a9c0ab5b09d7c932) | fix | resolve component encapsulation enum in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [59b6ec6be8](https://github.com/angular/angular/commit/59b6ec6be8cc464a6e74449ac7e0f2554ac7392b) | docs | Deprecate `ChangeDetectorRef.checkNoChanges` ([#52431](https://github.com/angular/angular/pull/52431)) |
|
||||
| [4f04d1cdab](https://github.com/angular/angular/commit/4f04d1cdab2fc5217566c0c01b7df10c74a93afa) | feat | add new list reconcilation algorithm ([#51980](https://github.com/angular/angular/pull/51980)) |
|
||||
| [c7127b98b5](https://github.com/angular/angular/commit/c7127b98b555449f99e24a81c828ab7b1c6c4a4e) | feat | add schematic to escape block syntax characters ([#51905](https://github.com/angular/angular/pull/51905)) |
|
||||
| [50275e58b8](https://github.com/angular/angular/commit/50275e58b80acfc52239908a6c61523e99f6731c) | feat | Add schematic to migrate control flow syntax ([#52035](https://github.com/angular/angular/pull/52035)) |
|
||||
| [81b67aa987](https://github.com/angular/angular/commit/81b67aa98767078aebae22150d3441372772c28f) | feat | add support for zone.js 0.14.0 ([#51774](https://github.com/angular/angular/pull/51774)) |
|
||||
| [048f400efc](https://github.com/angular/angular/commit/048f400efc75011e888ea25d214a0211f91b96d4) | feat | add warnings for oversized images and lazy-lcp ([#51846](https://github.com/angular/angular/pull/51846)) |
|
||||
| [93675dc797](https://github.com/angular/angular/commit/93675dc797cb9f897c19fe298455dec52b900113) | feat | conditional built-in control flow ([#51346](https://github.com/angular/angular/pull/51346)) |
|
||||
| [4427e1ebc2](https://github.com/angular/angular/commit/4427e1ebc29f5541cfe6a404f212de4359441812) | feat | create function to assert not running inside reactive context ([#52049](https://github.com/angular/angular/pull/52049)) |
|
||||
| [e23aaa7d75](https://github.com/angular/angular/commit/e23aaa7d75efdd52be4dd7ca9267bc60d36059c2) | feat | drop support for older TypeScript versions ([#51792](https://github.com/angular/angular/pull/51792)) |
|
||||
| [43e6fb0606](https://github.com/angular/angular/commit/43e6fb0606e15584dcb4478ad4eaa8e825dda83e) | feat | enable block syntax ([#51994](https://github.com/angular/angular/pull/51994)) |
|
||||
| [3cbb2a8ecf](https://github.com/angular/angular/commit/3cbb2a8ecf202c814c37ab241ce9f57fb574692e) | feat | implement deferred block interaction triggers ([#51830](https://github.com/angular/angular/pull/51830)) |
|
||||
| [8be2c48b7c](https://github.com/angular/angular/commit/8be2c48b7cda5e99f3d1efa9f26eb2615fea6a8b) | feat | implement new block syntax ([#51891](https://github.com/angular/angular/pull/51891)) |
|
||||
| [a54713c831](https://github.com/angular/angular/commit/a54713c8316787eea160cfdb7f2778a087fe59ed) | feat | implement ɵgetInjectorMetadata debug API ([#51900](https://github.com/angular/angular/pull/51900)) |
|
||||
| [5b88d136af](https://github.com/angular/angular/commit/5b88d136affdaa35e7015c00281b86cae040321b) | feat | mark core signal APIs as stable ([#51821](https://github.com/angular/angular/pull/51821)) |
|
||||
| [8eef694def](https://github.com/angular/angular/commit/8eef694def3dc660779168925a380179c7e30993) | feat | Provide a diagnostic for missing Signal invocation in template interpolation. ([#49660](https://github.com/angular/angular/pull/49660)) |
|
||||
| [40113f653c](https://github.com/angular/angular/commit/40113f653c2468315e1dea64f17e545840cc5e22) | feat | Remove deprecated `CompilerOptions.useJit` and`CompilerOptions.missingTranslation`. ([#49672](https://github.com/angular/angular/pull/49672)) |
|
||||
| [68ba798ae3](https://github.com/angular/angular/commit/68ba798ae3551b789a86d46b0a3bb61d42ef3f87) | feat | revamp the runtime error message for orphan components to include full component info ([#51919](https://github.com/angular/angular/pull/51919)) |
|
||||
| [1a4aee7e49](https://github.com/angular/angular/commit/1a4aee7e49074e3bc3f3099bff88eaee9b2ab255) | feat | show runtime error for orphan component rendering ([#52061](https://github.com/angular/angular/pull/52061)) |
|
||||
| [687b96186c](https://github.com/angular/angular/commit/687b96186c7da731927e55e714061ea2de718505) | feat | support deferred hover triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
|
||||
| [e2e3d69a27](https://github.com/angular/angular/commit/e2e3d69a277990ab79aac7aae43cbdd398e13ed9) | feat | support deferred triggers with implicit triggers ([#51922](https://github.com/angular/angular/pull/51922)) |
|
||||
| [16f5fc40a4](https://github.com/angular/angular/commit/16f5fc40a49cee0d29711df1783f297ff30b5c6e) | feat | support deferred viewport triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
|
||||
| [59387ee476](https://github.com/angular/angular/commit/59387ee476dff1a893a01fe5cbee3c95b93c0cdb) | feat | support styles and styleUrl as strings ([#51715](https://github.com/angular/angular/pull/51715)) |
|
||||
| [9cc52b9b85](https://github.com/angular/angular/commit/9cc52b9b85ffa5cb65c6886e81b5bff10dde8d52) | feat | support TypeScript 5.2 ([#51334](https://github.com/angular/angular/pull/51334)) |
|
||||
| [7d42dc3c02](https://github.com/angular/angular/commit/7d42dc3c023391e12ea607beb227fd4426e1694d) | feat | the new list reconciliation algorithm for built-in for ([#51980](https://github.com/angular/angular/pull/51980)) |
|
||||
| [935c1816fd](https://github.com/angular/angular/commit/935c1816fd04caab24be66f8ef67851c88d3d4da) | fix | add `rejectErrors` option to `toSignal` ([#52474](https://github.com/angular/angular/pull/52474)) |
|
||||
| [5411864c2e](https://github.com/angular/angular/commit/5411864c2e74b52e7df8022719f0fd792b50a849) | fix | adjust toSignal types to handle more common cases ([#51991](https://github.com/angular/angular/pull/51991)) |
|
||||
| [dcf18dc74c](https://github.com/angular/angular/commit/dcf18dc74c260253bbf394626beb712a831824f3) | fix | allow toSignal calls in reactive context ([#51831](https://github.com/angular/angular/pull/51831)) |
|
||||
| [dbffdc09c2](https://github.com/angular/angular/commit/dbffdc09c25c93868aa13ae368c9fd21a4c359fb) | fix | avoid duplicated code between entry-points (primary, testing, rxjs-interop) ([#51500](https://github.com/angular/angular/pull/51500)) |
|
||||
| [4f69d620d9](https://github.com/angular/angular/commit/4f69d620d94663592780b2875acbc2b1918775f9) | fix | deferred blocks not removing content immediately when animations are enabled ([#51971](https://github.com/angular/angular/pull/51971)) |
|
||||
| [df58c0b714](https://github.com/angular/angular/commit/df58c0b714e37152ddf81855ee31f93f9fa71e30) | fix | disallow `afterRender` in reactive contexts ([#52138](https://github.com/angular/angular/pull/52138)) |
|
||||
| [5d61221ed7](https://github.com/angular/angular/commit/5d61221ed7b4a5d1ef005183045d45238b19a446) | fix | disallow using `effect` inside reactive contexts ([#52138](https://github.com/angular/angular/pull/52138)) |
|
||||
| [99e7629159](https://github.com/angular/angular/commit/99e7629159afbb8550957a265c5bd75f7e13f052) | fix | do not remove used ng-template nodes in control flow migration ([#52186](https://github.com/angular/angular/pull/52186)) |
|
||||
| [c7ff9dff2c](https://github.com/angular/angular/commit/c7ff9dff2c14aba70e92b9e216a2d4d97d6ef71e) | fix | drop mutate function from the signals public API ([#51821](https://github.com/angular/angular/pull/51821)) |
|
||||
| [00128e3853](https://github.com/angular/angular/commit/00128e38538f12fe9bc72ede9b55149e0be5ead0) | fix | drop mutate function from the signals public API ([#51821](https://github.com/angular/angular/pull/51821)) ([#51986](https://github.com/angular/angular/pull/51986)) |
|
||||
| [ddef3ac9a4](https://github.com/angular/angular/commit/ddef3ac9a42677b900c998b2af7cd23a8213aa3a) | fix | effects wait for ngOnInit for their first run ([#52473](https://github.com/angular/angular/pull/52473)) |
|
||||
| [5ead7d412d](https://github.com/angular/angular/commit/5ead7d412d847c85176a321e58d12dcdfc0dab67) | fix | ensure a consumer drops all its stale producers ([#51722](https://github.com/angular/angular/pull/51722)) |
|
||||
| [1dd8558f82](https://github.com/angular/angular/commit/1dd8558f82aeb5f5819629d5e25b616343d27f1f) | fix | Ensure backwards-referenced transplanted views are refreshed ([#51854](https://github.com/angular/angular/pull/51854)) |
|
||||
| [50ad074505](https://github.com/angular/angular/commit/50ad074505c15d09fe5d85fb443d9a2775125f87) | fix | framework debug APIs getDependenciesForTokenInInjector and getInjectorMetadata ([#51719](https://github.com/angular/angular/pull/51719)) |
|
||||
| [80e7a0f8fa](https://github.com/angular/angular/commit/80e7a0f8fa13ad72a32b07cd1722efab3722fa49) | fix | guard usages of `performance.mark` ([#52505](https://github.com/angular/angular/pull/52505)) |
|
||||
| [b9ea2d6900](https://github.com/angular/angular/commit/b9ea2d690015dd5b919df239a5ded66f2969b97b) | fix | handle aliased index with no space in control flow migration ([#52444](https://github.com/angular/angular/pull/52444)) |
|
||||
| [ffe9b1fcc2](https://github.com/angular/angular/commit/ffe9b1fcc2ecb8dc0d36c7f9228ac1a052554eef) | fix | handle for alias with as in control flow migration ([#52183](https://github.com/angular/angular/pull/52183)) |
|
||||
| [e5720edb46](https://github.com/angular/angular/commit/e5720edb460a1bb51475c78d3bd442da52991a46) | fix | handle if alias in control flow migration ([#52181](https://github.com/angular/angular/pull/52181)) |
|
||||
| [4461cefa4f](https://github.com/angular/angular/commit/4461cefa4f8db21009ab10a2a53de664163a86d2) | fix | handle trackBy and aliased index in control flow migration ([#52423](https://github.com/angular/angular/pull/52423)) |
|
||||
| [7368b8aaeb](https://github.com/angular/angular/commit/7368b8aaeba2ef0972a8bb261208c7281e050bb9) | fix | host directive validation not picking up duplicate directives on component node ([#52073](https://github.com/angular/angular/pull/52073)) |
|
||||
| [696f003553](https://github.com/angular/angular/commit/696f003553a0ca6886329728511dd46761de909b) | fix | mutation bug in `getDependenciesFromInjectable` ([#52450](https://github.com/angular/angular/pull/52450)) |
|
||||
| [d487014785](https://github.com/angular/angular/commit/d48701478518d97a1fd5b4744963530494f93958) | fix | Remove no longer needed build rule related to removed migration ([#52143](https://github.com/angular/angular/pull/52143)) |
|
||||
| [4da08dc2ef](https://github.com/angular/angular/commit/4da08dc2ef439d3eced7199afb9a104cfd7b54cc) | fix | remove unnecessary migration ([#52141](https://github.com/angular/angular/pull/52141)) |
|
||||
| [384d7aacd0](https://github.com/angular/angular/commit/384d7aacd04dfbb951d9d4ab493759c12cf35645) | fix | replace assertion with more intentional error ([#52427](https://github.com/angular/angular/pull/52427)) |
|
||||
| [40bb45f329](https://github.com/angular/angular/commit/40bb45f3297359866cab39044dba06b3e809b096) | fix | Respect OnPush change detection strategy for dynamically created components ([#51356](https://github.com/angular/angular/pull/51356)) |
|
||||
| [3a19d6b743](https://github.com/angular/angular/commit/3a19d6b7437e1812ae70b3784fd6a8a185b330b1) | fix | run afterRender callbacks outside of the Angular zone ([#51385](https://github.com/angular/angular/pull/51385)) |
|
||||
| [a2ba5482c3](https://github.com/angular/angular/commit/a2ba5482c3032df808cb684444f76e2825a4fd36) | fix | use TNode instead of LView for mapping injector providers ([#52436](https://github.com/angular/angular/pull/52436)) |
|
||||
| [d5dad3eb4c](https://github.com/angular/angular/commit/d5dad3eb4cd837032da72899f0796c6d431cb2c9) | fix | viewport trigger deregistering callbacks multiple times ([#52115](https://github.com/angular/angular/pull/52115)) |
|
||||
| [8e4a7ab52b](https://github.com/angular/angular/commit/8e4a7ab52bc85172efd12e42304e1b8da446ff75) | perf | avoid repeated access to LContainer and trackBy calculation ([#52227](https://github.com/angular/angular/pull/52227)) |
|
||||
| [1dc14d9853](https://github.com/angular/angular/commit/1dc14d98539b9063b14c6463a534a4129b0a4643) | perf | avoid unnecessary callbacks in after render hooks ([#52292](https://github.com/angular/angular/pull/52292)) |
|
||||
| [e90694259e](https://github.com/angular/angular/commit/e90694259e31f264f05d4aa9ebe275638577ce1a) | perf | build-in for should update indexes only when views were added / removed ([#52051](https://github.com/angular/angular/pull/52051)) |
|
||||
| [1032c1e1a5](https://github.com/angular/angular/commit/1032c1e1a5f5de28f38ede1786cf973f8e8b7a53) | perf | cache LiveCollectionLContainerImpl ([#52227](https://github.com/angular/angular/pull/52227)) |
|
||||
| [685d01e106](https://github.com/angular/angular/commit/685d01e1065dad6dc52eaac9eb9527100994f5ce) | perf | chain template instructions ([#51546](https://github.com/angular/angular/pull/51546)) |
|
||||
| [88a0af64fd](https://github.com/angular/angular/commit/88a0af64fde58cbf71e8e2a22c39fabb8f0ee8fb) | perf | generate arrow functions for pure function calls ([#51668](https://github.com/angular/angular/pull/51668)) |
|
||||
| [37d627dbd4](https://github.com/angular/angular/commit/37d627dbd4083662b103de2e28102e6ff31a9192) | perf | minimze trackBy calculations ([#52227](https://github.com/angular/angular/pull/52227)) |
|
||||
| [3861a73135](https://github.com/angular/angular/commit/3861a73135ca9111c0ec10d52ee7db0a0e95f262) | perf | Update LView consumer to only mark component for check ([#52302](https://github.com/angular/angular/pull/52302)) |
|
||||
| [9b9e11fcaf](https://github.com/angular/angular/commit/9b9e11fcaf5d8d639ff1d7b8feddb01751b47e14) | refactor | deprecate allowing full context object to be replaced in `EmbeddedViewRef` ([#51887](https://github.com/angular/angular/pull/51887)) |
|
||||
| [ba9fc2419e](https://github.com/angular/angular/commit/ba9fc2419eee0d72c573463016a872a4b69f71c1) | refactor | deprecate the `NgProbeToken` ([#51396](https://github.com/angular/angular/pull/51396)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7dde42a5df](https://github.com/angular/angular/commit/7dde42a5dfdab30e9420708722e0bef9f1467d1f) | feat | allow customization of the HttpTransferCache. ([#52029](https://github.com/angular/angular/pull/52029)) |
|
||||
| [8156b3d4ec](https://github.com/angular/angular/commit/8156b3d4ec44a3b0489cc21763790a3be2969f7e) | fix | Don't override the backend when using the InMemoryWebAPI ([#52425](https://github.com/angular/angular/pull/52425)) |
|
||||
| [bd9e91ecf7](https://github.com/angular/angular/commit/bd9e91ecf7af877e4ecf08a16eda7b4e59707541) | perf | reduce data transfer when using HTTP caching ([#52347](https://github.com/angular/angular/pull/52347)) |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [08482f2c7d](https://github.com/angular/angular/commit/08482f2c7dcbcd100981dfb266a6e63f64432328) | fix | Retain correct language service when `ts.Project` reloads ([#51912](https://github.com/angular/angular/pull/51912)) |
|
||||
### service-worker
|
||||
| [449830f24e](https://github.com/angular/angular/commit/449830f24e78ebd977ca3210ab3541912d959245) | feat | Complete inside @switch ([#52153](https://github.com/angular/angular/pull/52153)) |
|
||||
| [e2416a284f](https://github.com/angular/angular/commit/e2416a284ff086752c809689ef74588f02e5f0e4) | feat | Enable go to definition of styleUrl ([#51746](https://github.com/angular/angular/pull/51746)) |
|
||||
| [023a181ba5](https://github.com/angular/angular/commit/023a181ba5f489deb0a47bbc9b290621ad38304a) | feat | Implement outlining spans for control flow blocks ([#52062](https://github.com/angular/angular/pull/52062)) |
|
||||
| [7c052bb6ef](https://github.com/angular/angular/commit/7c052bb6efde580afc61d6c50e787353c103e3e1) | feat | Support autocompletion for blocks ([#52121](https://github.com/angular/angular/pull/52121)) |
|
||||
| [9d565cd6d6](https://github.com/angular/angular/commit/9d565cd6d682e5c86ee8d43e1ee1c0f8866eb274) | fix | Autocomplete block keywords in more cases ([#52198](https://github.com/angular/angular/pull/52198)) |
|
||||
### localize
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [cc7973f5a5](https://github.com/angular/angular/commit/cc7973f5a5cddbc5288db7d572757819327a40c3) | fix | throw a critical error when handleFetch fails ([#51960](https://github.com/angular/angular/pull/51960)) |
|
||||
| [5a20a44c64](https://github.com/angular/angular/commit/5a20a44c64066e47894ca3cbe26327766ca89a42) | fix | ng-add schematics for application builder ([#51777](https://github.com/angular/angular/pull/51777)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f0da7c2e44](https://github.com/angular/angular/commit/f0da7c2e44a29c5a71cf4880388989d129f4c6e8) | feat | schematic to remove deprecated CompilerOptions properties ([#49672](https://github.com/angular/angular/pull/49672)) |
|
||||
| [965ce5a8c5](https://github.com/angular/angular/commit/965ce5a8c514237aa8e4c03a5e4b5527a1a19d96) | feat | Schematics for `TransferState`, `StateKey` and `makeStateKey` migration. ([#49594](https://github.com/angular/angular/pull/49594)) |
|
||||
| [09e905ad67](https://github.com/angular/angular/commit/09e905ad672294d36f36eb2728b3483ab2e729fc) | fix | account for separator characters inside strings ([#52525](https://github.com/angular/angular/pull/52525)) |
|
||||
| [4c878f90d2](https://github.com/angular/angular/commit/4c878f90d2cba689703d8def9dade3b7f8f6e8f1) | fix | Add support for nested structures inside a switch statement ([#52358](https://github.com/angular/angular/pull/52358)) |
|
||||
| [d7397fb29b](https://github.com/angular/angular/commit/d7397fb29b400f193a2cda7af70058ac39247e6f) | fix | Ensure control flow migration ignores new block syntax ([#52402](https://github.com/angular/angular/pull/52402)) |
|
||||
| [6a01d62b9d](https://github.com/angular/angular/commit/6a01d62b9df33732ea3dbef8de1385c8c0cb8a88) | fix | fix broken migration when no control flow is present ([#52399](https://github.com/angular/angular/pull/52399)) |
|
||||
| [9c2be715a3](https://github.com/angular/angular/commit/9c2be715a3f6ea6b1d0184f5d79f5a3dfef4b576) | fix | Fixes a bug in the ngFor pre-v5 alias translation ([#52531](https://github.com/angular/angular/pull/52531)) |
|
||||
| [54fed68bbf](https://github.com/angular/angular/commit/54fed68bbfed34a631df64ac6a32e99d2fade50a) | fix | Fixes the root level template offset in control flow migration ([#52355](https://github.com/angular/angular/pull/52355)) |
|
||||
| [57404d4723](https://github.com/angular/angular/commit/57404d4723d3634f2b5dfdc9af1af50d8f61da70) | fix | handle comma-separated syntax in ngFor ([#52525](https://github.com/angular/angular/pull/52525)) |
|
||||
| [54bc384661](https://github.com/angular/angular/commit/54bc384661ae7b884dbb822592e75df281aa02dd) | fix | handle nested classes in block entities migration ([#52309](https://github.com/angular/angular/pull/52309)) |
|
||||
| [c9b1ddff4d](https://github.com/angular/angular/commit/c9b1ddff4d16cca149ca2b1faae729bf454b61e5) | fix | handle nested classes in control flow migration ([#52309](https://github.com/angular/angular/pull/52309)) |
|
||||
| [6988a0070e](https://github.com/angular/angular/commit/6988a0070e9849b58738bba82d6f9eb9e3b27330) | fix | handle ngIf else condition with no whitespaces ([#52504](https://github.com/angular/angular/pull/52504)) |
|
||||
| [e40e55d902](https://github.com/angular/angular/commit/e40e55d902e09c36da4cf73b5ab31b6b65584518) | fix | Remove unhelpful parsing errors from the log ([#52401](https://github.com/angular/angular/pull/52401)) |
|
||||
| [c267f54bc3](https://github.com/angular/angular/commit/c267f54bc36e1c92f526071e2d78455daf8a588c) | fix | Update regex to better match ng-templates ([#52529](https://github.com/angular/angular/pull/52529)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c340d6e044](https://github.com/angular/angular/commit/c340d6e0440bd982dff6f9f4f4229931c62d2c08) | feat | enable removal of styles on component destroy by default ([#51571](https://github.com/angular/angular/pull/51571)) |
|
||||
| [c5daa6ce77](https://github.com/angular/angular/commit/c5daa6ce776724d44c02cc97f1a349a85cb2a819) | feat | expose `EventManagerPlugin` in the public API. ([#49969](https://github.com/angular/angular/pull/49969)) |
|
||||
| [5b375d106f](https://github.com/angular/angular/commit/5b375d106f2e02afadad8f5a60c37558318ea091) | fix | Fire Animations events when using async animations. ([#52087](https://github.com/angular/angular/pull/52087)) |
|
||||
| [65786b2b96](https://github.com/angular/angular/commit/65786b2b96ba198034ff23bb14571a659a491b50) | fix | prevent duplicate stylesheets from being created ([#52019](https://github.com/angular/angular/pull/52019)) |
|
||||
| [75d610d420](https://github.com/angular/angular/commit/75d610d420ce3a1ec6429d79c72ec6ef6c2c9a10) | fix | set animation properties when using async animations. ([#52087](https://github.com/angular/angular/pull/52087)) |
|
||||
| [3c0577f991](https://github.com/angular/angular/commit/3c0577f99140b75688cb8ae969738325cc871548) | perf | disable styles of removed components instead of removing ([#51808](https://github.com/angular/angular/pull/51808)) |
|
||||
| [c9cde3ab10](https://github.com/angular/angular/commit/c9cde3ab103699bc3f941d8176ee0b0373fcf7e0) | perf | only append style element on creation ([#52237](https://github.com/angular/angular/pull/52237)) |
|
||||
| [dbc14eb41d](https://github.com/angular/angular/commit/dbc14eb41d540ab3f7509e41cdf64ac6fe33e13a) | refactor | remove `withNoDomReuse` function ([#52057](https://github.com/angular/angular/pull/52057)) |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0c66e2424c](https://github.com/angular/angular/commit/0c66e2424c84f92765c727a98f8d2199f4b1a809) | fix | resolve relative requests URL ([#52326](https://github.com/angular/angular/pull/52326)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1da28f4825](https://github.com/angular/angular/commit/1da28f482517ea53a18e4eb526c7c9708e6fcb55) | feat | Add callback to execute when a view transition is created ([#52002](https://github.com/angular/angular/pull/52002)) |
|
||||
| [73e4bf2ed2](https://github.com/angular/angular/commit/73e4bf2ed2471faf44a49b591e19a390d5867449) | feat | Add feature to support the View Transitions API ([#51314](https://github.com/angular/angular/pull/51314)) |
|
||||
| [86e91463af](https://github.com/angular/angular/commit/86e91463afc1f3d3d71a669fb2919f2b8bc5a1ca) | feat | Add option to skip the first view transition ([#51825](https://github.com/angular/angular/pull/51825)) |
|
||||
| [ce1b915868](https://github.com/angular/angular/commit/ce1b915868e654cdb679e9381db9d3bd3d68d5c4) | fix | Allow redirects after an absolute redirect ([#51731](https://github.com/angular/angular/pull/51731)) |
|
||||
| [37df395be0](https://github.com/angular/angular/commit/37df395be070a11b8cd84c0ff3af9290d15c4e9d) | fix | children of routes with loadComponent should not inherit parent data by default ([#52114](https://github.com/angular/angular/pull/52114)) |
|
||||
| [4dce8766f8](https://github.com/angular/angular/commit/4dce8766f8a3a33ffab0b3df5981ad209db42c77) | fix | Ensure newly resolved data is inherited by child routes ([#52167](https://github.com/angular/angular/pull/52167)) |
|
||||
| [f464e39364](https://github.com/angular/angular/commit/f464e39364da6436fc4b5a703f66fe7dee70818c) | fix | Ensure title observable gets latest values ([#51561](https://github.com/angular/angular/pull/51561)) |
|
||||
| [b2aff43621](https://github.com/angular/angular/commit/b2aff4362129feb746856fc3d0f8e73b1927a037) | fix | Remove `urlHandlingStrategy` from public Router properties ([#51631](https://github.com/angular/angular/pull/51631)) |
|
||||
| [c62e680098](https://github.com/angular/angular/commit/c62e680098a8c26fb2234336613185f7ab273483) | fix | Remove deprecated Router properties ([#51502](https://github.com/angular/angular/pull/51502)) |
|
||||
| [3c6258c85b](https://github.com/angular/angular/commit/3c6258c85b37535c1178e84509b7c9ed3a1359e4) | fix | Remove deprecated setupTestingRouter function ([#51826](https://github.com/angular/angular/pull/51826)) |
|
||||
| [0b3e6a41d0](https://github.com/angular/angular/commit/0b3e6a41d025997d2947125d875ac26ecd1b86d9) | fix | Remove malformedUriErrorHandler from `ExtraOptions` ([#51745](https://github.com/angular/angular/pull/51745)) |
|
||||
| [c03baed854](https://github.com/angular/angular/commit/c03baed8547c2c1da576307c708d2682dfdf3742) | fix | use DOCUMENT token instead of document directly in view transitions ([#51814](https://github.com/angular/angular/pull/51814)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.9"></a>
|
||||
# 16.2.9 (2023-10-11)
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [51a5baace3](https://github.com/angular/angular/commit/51a5baace3b1eee130fa911327773066139cc68e) | fix | reset() call with null values on nested group ([#48830](https://github.com/angular/angular/pull/48830)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
@@ -79,61 +399,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.0-next.6"></a>
|
||||
# 17.0.0-next.6 (2023-09-27)
|
||||
## Breaking Changes
|
||||
### router
|
||||
- Absolute redirects no longer prevent further redirects.
|
||||
Route configurations may need to be adjusted to prevent infinite
|
||||
redirects where additional redirects were previously ignored after an
|
||||
absolute redirect occurred.
|
||||
- The `setupTestingRouter` function has been removed. Use
|
||||
`RouterModule.forRoot` or `provideRouter` to setup the `Router` for
|
||||
tests instead.
|
||||
## Deprecations
|
||||
### animations
|
||||
- The `AnimationDriver.NOOP` symbol is deprecated, use `NoopAnimationDriver` instead.
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0598613950](https://github.com/angular/angular/commit/0598613950c76f4a13601c6942e30ab4ce1e3b67) | refactor | deprecation of `AnimationDriver.NOOP` ([#51843](https://github.com/angular/angular/pull/51843)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [23bfa10ac8](https://github.com/angular/angular/commit/23bfa10ac809f6b27d32647210c52329f0e4262e) | fix | add diagnostic for inaccessible deferred trigger ([#51922](https://github.com/angular/angular/pull/51922)) |
|
||||
| [31295a3cf9](https://github.com/angular/angular/commit/31295a3cf907a61e7115d9039a83a232b263a676) | fix | allocating unnecessary slots in conditional instruction ([#51913](https://github.com/angular/angular/pull/51913)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [377a7abfda](https://github.com/angular/angular/commit/377a7abfda60a6ddd55a41531e3653bcad78b0a2) | fix | bypass static resolving of the component's changeDetection field in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
|
||||
| [f91f222b55](https://github.com/angular/angular/commit/f91f222b55f249089d267c72a9c0ab5b09d7c932) | fix | resolve component encapsulation enum in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c7127b98b5](https://github.com/angular/angular/commit/c7127b98b555449f99e24a81c828ab7b1c6c4a4e) | feat | add schematic to escape block syntax characters ([#51905](https://github.com/angular/angular/pull/51905)) |
|
||||
| [3cbb2a8ecf](https://github.com/angular/angular/commit/3cbb2a8ecf202c814c37ab241ce9f57fb574692e) | feat | implement deferred block interaction triggers ([#51830](https://github.com/angular/angular/pull/51830)) |
|
||||
| [8be2c48b7c](https://github.com/angular/angular/commit/8be2c48b7cda5e99f3d1efa9f26eb2615fea6a8b) | feat | implement new block syntax ([#51891](https://github.com/angular/angular/pull/51891)) |
|
||||
| [687b96186c](https://github.com/angular/angular/commit/687b96186c7da731927e55e714061ea2de718505) | feat | support deferred hover triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
|
||||
| [e2e3d69a27](https://github.com/angular/angular/commit/e2e3d69a277990ab79aac7aae43cbdd398e13ed9) | feat | support deferred triggers with implicit triggers ([#51922](https://github.com/angular/angular/pull/51922)) |
|
||||
| [16f5fc40a4](https://github.com/angular/angular/commit/16f5fc40a49cee0d29711df1783f297ff30b5c6e) | feat | support deferred viewport triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
|
||||
| [dcf18dc74c](https://github.com/angular/angular/commit/dcf18dc74c260253bbf394626beb712a831824f3) | fix | allow toSignal calls in reactive context ([#51831](https://github.com/angular/angular/pull/51831)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3c0577f991](https://github.com/angular/angular/commit/3c0577f99140b75688cb8ae969738325cc871548) | perf | disable styles of removed components instead of removing ([#51808](https://github.com/angular/angular/pull/51808)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [86e91463af](https://github.com/angular/angular/commit/86e91463afc1f3d3d71a669fb2919f2b8bc5a1ca) | feat | Add option to skip the first view transition ([#51825](https://github.com/angular/angular/pull/51825)) |
|
||||
| [ce1b915868](https://github.com/angular/angular/commit/ce1b915868e654cdb679e9381db9d3bd3d68d5c4) | fix | Allow redirects after an absolute redirect ([#51731](https://github.com/angular/angular/pull/51731)) |
|
||||
| [3c6258c85b](https://github.com/angular/angular/commit/3c6258c85b37535c1178e84509b7c9ed3a1359e4) | fix | Remove deprecated setupTestingRouter function ([#51826](https://github.com/angular/angular/pull/51826)) |
|
||||
### service-worker
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [dcaad169ec](https://github.com/angular/angular/commit/dcaad169ec8bf0a61d032ae1ae68fb90d1face09) | fix | throw a critical error when `handleFetch` fails ([#51885](https://github.com/angular/angular/pull/51885)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.7"></a>
|
||||
# 16.2.7 (2023-09-27)
|
||||
### core
|
||||
@@ -147,76 +412,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.0-next.5"></a>
|
||||
# 17.0.0-next.5 (2023-09-20)
|
||||
## Breaking Changes
|
||||
###
|
||||
- Node.js v16 support has been removed and the minimum support version has been bumped to 18.13.0.
|
||||
|
||||
Node.js v16 is planned to be End-of-Life on 2023-09-11. Angular will stop supporting Node.js v16 in Angular v17. For Node.js release schedule details, please see: https://github.com/nodejs/release#release-schedule
|
||||
### common
|
||||
-
|
||||
### core
|
||||
- Angular now required `zone.js` version `~0.14.0`
|
||||
- Versions of TypeScript older than 5.2 are no longer supported.
|
||||
### router
|
||||
- `malformedUriErrorHandler` is no longer available in
|
||||
the `RouterModule.forRoot` options. URL parsing errors should instead be
|
||||
handled in the `UrlSerializer.parse` method.
|
||||
### zone.js
|
||||
- Deep and legacy `dist/` imports like `zone.js/bundles/zone-testing.js` and `zone.js/dist/zone` are no longer allowed. `zone-testing-bundle` and `zone-testing-node-bundle` are also no longer part of the package.
|
||||
|
||||
The proper way to import `zone.js` and `zone.js/testing` is:
|
||||
```js
|
||||
import 'zone.js';
|
||||
import 'zone.js/testing';
|
||||
```
|
||||
###
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [59aa0634f4](https://github.com/angular/angular/commit/59aa0634f4d4694203f2a69c40017fe5a3962514) | build | remove support for Node.js v16 ([#51755](https://github.com/angular/angular/pull/51755)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fe2fd7e1a8](https://github.com/angular/angular/commit/fe2fd7e1a898a4525c219065a6d0908988dfd7e2) | feat | make the warning for lazy-loaded lcp image an error ([#51748](https://github.com/angular/angular/pull/51748)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7f6d9a73ab](https://github.com/angular/angular/commit/7f6d9a73ab8b658d0d8148080dfefb2550bee6b4) | feat | expand class api doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [c7daf7ea16](https://github.com/angular/angular/commit/c7daf7ea1692391f7cac8f794ed777887a2764af) | feat | extract directive docs info ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [e0b1bb33d7](https://github.com/angular/angular/commit/e0b1bb33d77babe881f77f52cb1b71e345f5696b) | feat | extract doc info for JsDoc ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [b9c70158ab](https://github.com/angular/angular/commit/b9c70158abecd81a5af512c8b4da685851cf159f) | feat | extract docs for accessors, rest params, and types ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [a24ae994a0](https://github.com/angular/angular/commit/a24ae994a0470fdac09a69937fd0580cff6c6d68) | feat | extract docs for top level functions and consts ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [2e41488296](https://github.com/angular/angular/commit/2e41488296879685b19dfba8d78037690347bda3) | feat | extract docs info for enums, pipes, and NgModules ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [34495b3533](https://github.com/angular/angular/commit/34495b35337892ab209d9955ff7fe2897a0c5d41) | feat | extract docs via exports ([#51828](https://github.com/angular/angular/pull/51828)) |
|
||||
| [7e82df45c5](https://github.com/angular/angular/commit/7e82df45c5bb72ec3dafaa07dc1eaa5d463b006c) | feat | initial skeleton for API doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5b66330329](https://github.com/angular/angular/commit/5b66330329fd066a7c347f040a330b4c7f2a0a2b) | fix | allow non-array imports for standalone component in local compilation mode ([#51819](https://github.com/angular/angular/pull/51819)) |
|
||||
| [19c3dc18d3](https://github.com/angular/angular/commit/19c3dc18d3c0cfd83efec2c8f81b40860d570346) | fix | fix NgModule injector def in local compilation mode when imports/exports are non-array expressions ([#51819](https://github.com/angular/angular/pull/51819)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [81b67aa987](https://github.com/angular/angular/commit/81b67aa98767078aebae22150d3441372772c28f) | feat | add support for zone.js 0.14.0 ([#51774](https://github.com/angular/angular/pull/51774)) |
|
||||
| [e23aaa7d75](https://github.com/angular/angular/commit/e23aaa7d75efdd52be4dd7ca9267bc60d36059c2) | feat | drop support for older TypeScript versions ([#51792](https://github.com/angular/angular/pull/51792)) |
|
||||
| [5ead7d412d](https://github.com/angular/angular/commit/5ead7d412d847c85176a321e58d12dcdfc0dab67) | fix | ensure a consumer drops all its stale producers ([#51722](https://github.com/angular/angular/pull/51722)) |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e2416a284f](https://github.com/angular/angular/commit/e2416a284ff086752c809689ef74588f02e5f0e4) | feat | Enable go to definition of styleUrl ([#51746](https://github.com/angular/angular/pull/51746)) |
|
||||
### localize
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5a20a44c64](https://github.com/angular/angular/commit/5a20a44c64066e47894ca3cbe26327766ca89a42) | fix | ng-add schematics for application builder ([#51777](https://github.com/angular/angular/pull/51777)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0b3e6a41d0](https://github.com/angular/angular/commit/0b3e6a41d025997d2947125d875ac26ecd1b86d9) | fix | Remove malformedUriErrorHandler from `ExtraOptions` ([#51745](https://github.com/angular/angular/pull/51745)) |
|
||||
| [c03baed854](https://github.com/angular/angular/commit/c03baed8547c2c1da576307c708d2682dfdf3742) | fix | use DOCUMENT token instead of document directly in view transitions ([#51814](https://github.com/angular/angular/pull/51814)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.6"></a>
|
||||
# 16.2.6 (2023-09-20)
|
||||
### core
|
||||
@@ -226,124 +421,16 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.0-next.4"></a>
|
||||
# 17.0.0-next.4 (2023-09-13)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [59387ee476](https://github.com/angular/angular/commit/59387ee476dff1a893a01fe5cbee3c95b93c0cdb) | feat | support styles and styleUrl as strings ([#51715](https://github.com/angular/angular/pull/51715)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [73e4bf2ed2](https://github.com/angular/angular/commit/73e4bf2ed2471faf44a49b591e19a390d5867449) | feat | Add feature to support the View Transitions API ([#51314](https://github.com/angular/angular/pull/51314)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.5"></a>
|
||||
# 16.2.5 (2023-09-13)
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.0-next.3"></a>
|
||||
# 17.0.0-next.3 (2023-09-06)
|
||||
## Breaking Changes
|
||||
### core
|
||||
- `OnPush` components that are created dynamically now
|
||||
only have their host bindings refreshed and `ngDoCheck run` during change
|
||||
detection if they are dirty.
|
||||
Previously, a bug in the change detection would result in the `OnPush`
|
||||
configuration of dynamically created components to be ignored when
|
||||
executing host bindings and the `ngDoCheck` function. This is
|
||||
rarely encountered but can happen if code has a handle on the
|
||||
`ComponentRef` instance and updates values read in the `OnPush`
|
||||
component template without then calling either `markForCheck` or
|
||||
`detectChanges` on that component's `ChangeDetectorRef`.
|
||||
### router
|
||||
- `urlHandlingStrategy` has been removed from the Router public API.
|
||||
This should instead be configured through the provideRouter or RouterModule.forRoot APIs.
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [40bb45f329](https://github.com/angular/angular/commit/40bb45f3297359866cab39044dba06b3e809b096) | fix | Respect OnPush change detection strategy for dynamically created components ([#51356](https://github.com/angular/angular/pull/51356)) |
|
||||
| [88a0af64fd](https://github.com/angular/angular/commit/88a0af64fde58cbf71e8e2a22c39fabb8f0ee8fb) | perf | generate arrow functions for pure function calls ([#51668](https://github.com/angular/angular/pull/51668)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f464e39364](https://github.com/angular/angular/commit/f464e39364da6436fc4b5a703f66fe7dee70818c) | fix | Ensure title observable gets latest values ([#51561](https://github.com/angular/angular/pull/51561)) |
|
||||
| [b2aff43621](https://github.com/angular/angular/commit/b2aff4362129feb746856fc3d0f8e73b1927a037) | fix | Remove `urlHandlingStrategy` from public Router properties ([#51631](https://github.com/angular/angular/pull/51631)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.4"></a>
|
||||
# 16.2.4 (2023-09-06)
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.0-next.2"></a>
|
||||
# 17.0.0-next.2 (2023-08-30)
|
||||
## Breaking Changes
|
||||
### platform-browser
|
||||
- `REMOVE_STYLES_ON_COMPONENT_DESTROY` default value is now `true`. This causes CSS of components to be removed from the DOM when destroyed. You retain the previous behaviour by providing the `REMOVE_STYLES_ON_COMPONENT_DESTROY` injection token.
|
||||
|
||||
```ts
|
||||
import {REMOVE_STYLES_ON_COMPONENT_DESTROY} from '@angular/platform-browser';
|
||||
...
|
||||
providers: [{
|
||||
provide: REMOVE_STYLES_ON_COMPONENT_DESTROY,
|
||||
useValue: false,
|
||||
}]
|
||||
```
|
||||
### router
|
||||
- The following Router properties have been removed from
|
||||
the public API:
|
||||
|
||||
- canceledNavigationResolution
|
||||
- paramsInheritanceStrategy
|
||||
- titleStrategy
|
||||
- urlUpdateStrategy
|
||||
- malformedUriErrorHandler
|
||||
|
||||
These should instead be configured through the `provideRouter` or
|
||||
`RouterModule.forRoot` APIs.
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [698c058e1c](https://github.com/angular/angular/commit/698c058e1c975c573722407f4843a4a774ceb92a) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
|
||||
| [18be804c03](https://github.com/angular/angular/commit/18be804c038e8d81a60c9a72521cfa640c8a1d5a) | fix | remove unnecessary escaping in regex expressions ([#51554](https://github.com/angular/angular/pull/51554)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [86c5e34601](https://github.com/angular/angular/commit/86c5e34601d7901a11688124aa902646524177eb) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [de2550d988](https://github.com/angular/angular/commit/de2550d9886394e1ecde586d72bf2bab5b65cb39) | fix | correct incomplete escaping ([#51557](https://github.com/angular/angular/pull/51557)) |
|
||||
| [3bca9db4a5](https://github.com/angular/angular/commit/3bca9db4a56d61ac22b4ce87591d8862606177c8) | fix | remove unnecessary escaping in regex expressions ([#51554](https://github.com/angular/angular/pull/51554)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [dbffdc09c2](https://github.com/angular/angular/commit/dbffdc09c25c93868aa13ae368c9fd21a4c359fb) | fix | avoid duplicated code between entry-points (primary, testing, rxjs-interop) ([#51500](https://github.com/angular/angular/pull/51500)) |
|
||||
| [45d2ded0ea](https://github.com/angular/angular/commit/45d2ded0ea9ef414948256099f8dc9c4598fdc2b) | fix | correct incomplete escaping ([#51557](https://github.com/angular/angular/pull/51557)) |
|
||||
| [006577f39c](https://github.com/angular/angular/commit/006577f39c0e46e37491e44687142521fe7fab54) | fix | handle hydration of view containers that use component hosts as anchors ([#51456](https://github.com/angular/angular/pull/51456)) |
|
||||
| [1423bfbf8f](https://github.com/angular/angular/commit/1423bfbf8ffa3b43d0dea41054c8f950e669a697) | fix | remove unnecessary escaping in regex expressions ([#51554](https://github.com/angular/angular/pull/51554)) |
|
||||
| [3a19d6b743](https://github.com/angular/angular/commit/3a19d6b7437e1812ae70b3784fd6a8a185b330b1) | fix | run afterRender callbacks outside of the Angular zone ([#51385](https://github.com/angular/angular/pull/51385)) |
|
||||
| [685d01e106](https://github.com/angular/angular/commit/685d01e1065dad6dc52eaac9eb9527100994f5ce) | perf | chain template instructions ([#51546](https://github.com/angular/angular/pull/51546)) |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [88b1575271](https://github.com/angular/angular/commit/88b157527172d70ed3e5aa11aa8b7963a8612e49) | fix | correct incomplete escaping ([#51557](https://github.com/angular/angular/pull/51557)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c340d6e044](https://github.com/angular/angular/commit/c340d6e0440bd982dff6f9f4f4229931c62d2c08) | feat | enable removal of styles on component destroy by default ([#51571](https://github.com/angular/angular/pull/51571)) |
|
||||
| [c5daa6ce77](https://github.com/angular/angular/commit/c5daa6ce776724d44c02cc97f1a349a85cb2a819) | feat | expose `EventManagerPlugin` in the public API. ([#49969](https://github.com/angular/angular/pull/49969)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c62e680098](https://github.com/angular/angular/commit/c62e680098a8c26fb2234336613185f7ab273483) | fix | Remove deprecated Router properties ([#51502](https://github.com/angular/angular/pull/51502)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.3"></a>
|
||||
# 16.2.3 (2023-08-30)
|
||||
### animations
|
||||
@@ -369,28 +456,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.0-next.1"></a>
|
||||
# 17.0.0-next.1 (2023-08-23)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d910bf8a84](https://github.com/angular/angular/commit/d910bf8a843c07a096969d5e47b49f60981b00e9) | fix | Allow safeUrl for ngSrc in NgOptimizedImage ([#51351](https://github.com/angular/angular/pull/51351)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5bd9fbd2c3](https://github.com/angular/angular/commit/5bd9fbd2c3ab4467074fac5e4d689b3c85bf08cd) | fix | enforce a minimum version to be used when a library uses input transform ([#51413](https://github.com/angular/angular/pull/51413)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9cc52b9b85](https://github.com/angular/angular/commit/9cc52b9b85ffa5cb65c6886e81b5bff10dde8d52) | feat | support TypeScript 5.2 ([#51334](https://github.com/angular/angular/pull/51334)) |
|
||||
| [a9b3c006f8](https://github.com/angular/angular/commit/a9b3c006f8593e0187298df21418644070312a40) | fix | guard the jasmine hooks ([#51394](https://github.com/angular/angular/pull/51394)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [96d94ad130](https://github.com/angular/angular/commit/96d94ad13072032326446e8a20658c9f38fd1b8e) | fix | Ensure `canceledNavigationResolution: 'computed'` works on first page ([#51441](https://github.com/angular/angular/pull/51441)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.2"></a>
|
||||
# 16.2.2 (2023-08-23)
|
||||
### common
|
||||
@@ -412,11 +477,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.0-next.0"></a>
|
||||
# 17.0.0-next.0 (2023-08-16)
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.1"></a>
|
||||
# 16.2.1 (2023-08-16)
|
||||
### router
|
||||
|
||||
@@ -129,7 +129,7 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
|
||||
|
||||
Join the conversation and help the community.
|
||||
|
||||
- [Twitter][twitter]
|
||||
- [X (formerly Twitter)][X (formerly Twitter)]
|
||||
- [Discord][discord]
|
||||
- [Gitter][gitter]
|
||||
- [YouTube][youtube]
|
||||
@@ -152,13 +152,13 @@ Join the conversation and help the community.
|
||||
[forms]: https://angular.io/guide/forms-overview
|
||||
[api]: https://angular.io/api
|
||||
[angularelements]: https://angular.io/guide/elements
|
||||
[ssr]: https://angular.io/guide/universal
|
||||
[ssr]: https://angular.io/guide/ssr
|
||||
[schematics]: https://angular.io/guide/schematics
|
||||
[lazyloading]: https://angular.io/guide/lazy-loading-ngmodules
|
||||
[node.js]: https://nodejs.org/
|
||||
[npm]: https://www.npmjs.com/get-npm
|
||||
[codeofconduct]: CODE_OF_CONDUCT.md
|
||||
[twitter]: https://www.twitter.com/angular
|
||||
[X (formerly Twitter)]: https://www.twitter.com/angular
|
||||
[discord]: https://discord.gg/angular
|
||||
[gitter]: https://gitter.im/angular/angular
|
||||
[stackoverflow]: https://stackoverflow.com/questions/tagged/angular
|
||||
|
||||
@@ -80,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-680aab4562e5bb684518ca496cb449a6c447601d.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",
|
||||
@@ -196,10 +196,10 @@ cldr_xml_data_repository(
|
||||
# sass rules
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
sha256 = "923b2c470e981b6e96b17e5b9ec7a6141eab808d52ebffdbd94695f5a42a5184",
|
||||
strip_prefix = "rules_sass-9a327e5badc3fdd5141479410675c356133e1e41",
|
||||
sha256 = "7a90567717c3417a23ff4b21963a698a6e9aa669ac9e682121ea33f218a2816a",
|
||||
strip_prefix = "rules_sass-425ecafa8268bc013d684216363fe75bc802323e",
|
||||
urls = [
|
||||
"https://github.com/bazelbuild/rules_sass/archive/9a327e5badc3fdd5141479410675c356133e1e41.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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Angular.dev: The new home for Angular developers (https://angular.dev)
|
||||
|
||||
Read more about our initial launch on the [Angular blog](https://goo.gle/angular-dot-dev).
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-dev": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss",
|
||||
"standalone": true,
|
||||
"changeDetection": "OnPush"
|
||||
}
|
||||
},
|
||||
"root": "projects/angular-dev",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"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": ["projects/shared/src/lib/styles/global-styles.scss"],
|
||||
"scripts": [],
|
||||
"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,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "angular-dev:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular-dev:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "web-ui:build",
|
||||
"headers": {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "angular-dev:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"include": ["src/app"],
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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, '');
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
/*!
|
||||
* @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, dirname, join} from 'path';
|
||||
|
||||
import {
|
||||
filesAndContentsToRecord,
|
||||
getFilesContents,
|
||||
getPackageJsonFromFiles,
|
||||
getZipBuffer,
|
||||
globWithCwdPath,
|
||||
recordToFilesAndContents,
|
||||
} from '../utils/filesystem';
|
||||
import {
|
||||
ANSWER_DIRECTORY,
|
||||
GLOB_OPTIONS,
|
||||
INTRO_DIRECTORY,
|
||||
STEPS_DIRECTORY,
|
||||
TUTORIALS_PROJECT_PATH,
|
||||
} from '../utils/node-constants';
|
||||
import {getFileSystemTree, shouldUseFileInWebContainer} from '../utils/webcontainers';
|
||||
import {getCleanFilePath as getCleanCommonFilePath} from './common';
|
||||
|
||||
import {TutorialNavigationItemWithStep} from '../generate-tutorials-routes';
|
||||
import {getTutorialConfig, validateOpenFilesConfig} from '../tutorials-config';
|
||||
import type {
|
||||
FileAndContent,
|
||||
FileAndContentRecord,
|
||||
TutorialConfig,
|
||||
TutorialFiles,
|
||||
TutorialMetadata,
|
||||
TutorialStep,
|
||||
} from '../tutorials-types';
|
||||
import {
|
||||
TUTORIALS_COMMON_DIRECTORY,
|
||||
TUTORIALS_DIST_PATH,
|
||||
TUTORIALS_SOURCE_CODE_WEB_PATH,
|
||||
TutorialType,
|
||||
} from '../utils/web-constants';
|
||||
import {getAllFiles, validatePackageJson} from '../utils/metadata';
|
||||
|
||||
export const TUTORIALS_DIRECTORY_REGEX = /^[a-z-0-9]*$/;
|
||||
export const STEPS_DIRECTORY_REGEX = /^([0-9]|[0-9][0-9])-[A-Za-z-0-9]*$/;
|
||||
const BEFORE_STEP_PATH_REGEX = new RegExp(`.*\/${STEPS_DIRECTORY}\/[^\/]+\/`);
|
||||
const BEFORE_INTRO_PATH_REGEX = new RegExp(`.*\/${INTRO_DIRECTORY}\/`);
|
||||
|
||||
export async function getIntroAndStepsTutorialFiles(
|
||||
tutorialName: string,
|
||||
tutorialDirectories: string[],
|
||||
tutorialFiles: string[],
|
||||
commonTutorialFiles: FileAndContent[],
|
||||
): Promise<Map<string, TutorialFiles>> {
|
||||
const tutorialSteps = getTutorialSteps(tutorialName, tutorialDirectories);
|
||||
|
||||
const introAndStepsFiles = new Map<TutorialStep['url'], TutorialFiles>();
|
||||
|
||||
const tutorialCommonFiles = tutorialFiles.filter((file) =>
|
||||
isCommonTutorialFile(tutorialName, file),
|
||||
);
|
||||
|
||||
const introFiles = await getTutorialIntroFiles(
|
||||
tutorialName,
|
||||
[
|
||||
...tutorialCommonFiles,
|
||||
...tutorialFiles.filter((file) => file.includes(`/${INTRO_DIRECTORY}/`)),
|
||||
],
|
||||
tutorialSteps,
|
||||
commonTutorialFiles,
|
||||
);
|
||||
|
||||
introAndStepsFiles.set(tutorialName, introFiles);
|
||||
|
||||
await Promise.all(
|
||||
tutorialSteps.map(async (step) => {
|
||||
const stepFiles = tutorialFiles.filter((file) => file.includes(step.path));
|
||||
|
||||
let nextTutorial;
|
||||
if (introFiles.route && 'tutorialData' in introFiles.route) {
|
||||
nextTutorial = introFiles.route.tutorialData.nextTutorial;
|
||||
}
|
||||
|
||||
const stepSourceCodeAndMetadata = await getStepFiles(
|
||||
[...tutorialCommonFiles, ...stepFiles],
|
||||
{
|
||||
...step,
|
||||
nextTutorial,
|
||||
},
|
||||
commonTutorialFiles,
|
||||
);
|
||||
|
||||
introAndStepsFiles.set(step.url, stepSourceCodeAndMetadata);
|
||||
}),
|
||||
);
|
||||
|
||||
return introAndStepsFiles;
|
||||
}
|
||||
|
||||
async function getTutorialIntroFiles(
|
||||
tutorialName: string,
|
||||
introTutorialFiles: string[],
|
||||
tutorialSteps: TutorialStep[],
|
||||
commonFilesContents: FileAndContent[],
|
||||
) {
|
||||
return getStepFiles(
|
||||
introTutorialFiles,
|
||||
{
|
||||
step: 0,
|
||||
name: tutorialName,
|
||||
path: dirname(introTutorialFiles[0]),
|
||||
nextStep: tutorialSteps.find((step) => step.step === 0 || step.step === 1)?.url, // assume the first tutorialSteps array item is the first step
|
||||
url: tutorialName,
|
||||
},
|
||||
commonFilesContents,
|
||||
);
|
||||
}
|
||||
|
||||
async function getStepFiles(
|
||||
stepFiles: string[],
|
||||
step: TutorialStep,
|
||||
commonFilesContents: FileAndContent[],
|
||||
): Promise<TutorialFiles> {
|
||||
const config = await getTutorialConfig(stepFiles);
|
||||
|
||||
// if src is present in the config, add files to the step files
|
||||
if (config.src) {
|
||||
const srcFiles = await getExternalTutorialFiles(step, config.src);
|
||||
|
||||
stepFiles.push(...srcFiles);
|
||||
}
|
||||
|
||||
const filesContents = await getFilesContents(stepFiles);
|
||||
|
||||
const route: Omit<TutorialNavigationItemWithStep, 'path'> = {
|
||||
label: config.title,
|
||||
|
||||
contentPath: getTutorialContentPath(stepFiles),
|
||||
|
||||
tutorialData: {
|
||||
type: config.type,
|
||||
title: config.title,
|
||||
|
||||
step: step.step,
|
||||
nextStep: step.nextStep,
|
||||
previousStep: step.previousStep,
|
||||
nextTutorial: config.nextTutorial ?? step.nextTutorial,
|
||||
},
|
||||
};
|
||||
|
||||
if (config.type === TutorialType.LOCAL) {
|
||||
const hasFilesToDownload =
|
||||
filesContents.filter(({path}) => shouldUseFileInWebContainer(path)).length > 0;
|
||||
|
||||
if (!hasFilesToDownload) {
|
||||
return {route};
|
||||
} else {
|
||||
const filesToDownload = getFilesToDownload(commonFilesContents, filesContents);
|
||||
|
||||
route.tutorialData.sourceCodeZipPath = join(
|
||||
TUTORIALS_SOURCE_CODE_WEB_PATH,
|
||||
step.url + '.zip',
|
||||
);
|
||||
|
||||
return {
|
||||
sourceCodeZip: await getZipBuffer(filesToDownload),
|
||||
route,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const tutorialFiles = getTutorialFiles(filesContents);
|
||||
|
||||
if (config.openFiles) {
|
||||
validateOpenFilesConfig(step.path, config.openFiles, Object.keys(tutorialFiles));
|
||||
} else if (config.type === TutorialType.EDITOR) {
|
||||
config.openFiles = Object.keys(tutorialFiles);
|
||||
}
|
||||
|
||||
const hasPackageJson = stepFiles.some((file) => basename(file) === 'package.json');
|
||||
|
||||
let dependencies: TutorialMetadata['dependencies'];
|
||||
|
||||
if (hasPackageJson) {
|
||||
const packageJson = getPackageJsonFromFiles(filesContents);
|
||||
|
||||
dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
};
|
||||
|
||||
validatePackageJson(stepFiles, packageJson, getPackageJsonFromFiles(commonFilesContents));
|
||||
}
|
||||
|
||||
const fileSystemTreeContents = filesContents
|
||||
.filter(({path}) => shouldUseFileInWebContainer(path))
|
||||
.map(({path, content}) => ({path: getCleanFilePath(path), content}));
|
||||
|
||||
const fileSystemTreeFiles = stepFiles.filter(shouldUseFileInWebContainer).map(getCleanFilePath);
|
||||
|
||||
const allFiles = getAllFiles(
|
||||
stepFiles,
|
||||
commonFilesContents.map(({path}) => path),
|
||||
getCleanFilePath,
|
||||
);
|
||||
|
||||
const metadata: TutorialMetadata = {
|
||||
type: config.type,
|
||||
allFiles,
|
||||
tutorialFiles,
|
||||
answerFiles: await getAnswerFiles(step, filesContents, config.answerSrc),
|
||||
openFiles: config.openFiles ?? Object.keys(tutorialFiles),
|
||||
hiddenFiles: config.openFiles
|
||||
? Object.keys(tutorialFiles).filter((filename) => !config.openFiles!.includes(filename))
|
||||
: [],
|
||||
dependencies,
|
||||
};
|
||||
|
||||
return {
|
||||
metadata,
|
||||
sourceCode: getFileSystemTree(
|
||||
fileSystemTreeFiles,
|
||||
filesAndContentsToRecord(fileSystemTreeContents),
|
||||
),
|
||||
route,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTutorialSteps(
|
||||
tutorialName: string,
|
||||
tutorialDirectories: string[],
|
||||
): TutorialStep[] {
|
||||
const stepsDirectories = tutorialDirectories.filter((directory) => {
|
||||
const isStepDirectory = directory.includes(`${tutorialName}/${STEPS_DIRECTORY}/`);
|
||||
if (!isStepDirectory) return false;
|
||||
|
||||
const stepPathParts = directory.split('/');
|
||||
|
||||
// steps directory is always right after STEPS_DIRECTORY in the path, e.g. `learn-angular/steps/1-getting-started`
|
||||
const stepDirectoryIndex = stepPathParts.indexOf(STEPS_DIRECTORY) + 1;
|
||||
const step = stepPathParts[stepDirectoryIndex];
|
||||
|
||||
return step === stepPathParts.at(-1);
|
||||
});
|
||||
|
||||
const steps: TutorialStep[] = [];
|
||||
|
||||
for (const stepDirectoryPath of stepsDirectories) {
|
||||
const stepDirectory = basename(stepDirectoryPath);
|
||||
|
||||
// force the tutorials steps to follow the pattern steps/{step}-{name}
|
||||
if (!STEPS_DIRECTORY_REGEX.test(stepDirectory))
|
||||
throw `Invalid step directory '${stepDirectory}'. The step directory must start with a number > 0, followed by a hyphen and a name. For example: '1-intro'`;
|
||||
|
||||
const [step, ...nameParts] = stepDirectory.replace(`${STEPS_DIRECTORY}/`, '').split('-');
|
||||
const name = nameParts.join('-');
|
||||
|
||||
steps.push({
|
||||
step: Number(step),
|
||||
name,
|
||||
path: stepDirectoryPath,
|
||||
url: join(tutorialName, name),
|
||||
});
|
||||
}
|
||||
|
||||
return steps.map((step) => {
|
||||
const previousStep = steps.find(({step: previousStep}) => previousStep === step.step - 1);
|
||||
const nextStep = steps.find(({step: nextStep}) => nextStep === step.step + 1);
|
||||
|
||||
return {
|
||||
...step,
|
||||
nextStep: nextStep?.url,
|
||||
previousStep: previousStep?.url ?? tutorialName,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter files that won't be used in the WebContainer.
|
||||
*/
|
||||
function getTutorialFiles(
|
||||
tutorialFilesContents: FileAndContent[],
|
||||
): Record<FileAndContent['path'], FileAndContent['content']> {
|
||||
const tutorialFiles = tutorialFilesContents
|
||||
.filter(({path}) => shouldUseFileInWebContainer(path))
|
||||
.map(({path, content}) => ({
|
||||
path: getCleanFilePath(path),
|
||||
content,
|
||||
}));
|
||||
|
||||
return filesAndContentsToRecord(tutorialFiles);
|
||||
}
|
||||
|
||||
async function getAnswerFiles(
|
||||
step: TutorialStep,
|
||||
filesContents: FileAndContent[],
|
||||
answerSrc: TutorialConfig['answerSrc'],
|
||||
): Promise<FileAndContentRecord> {
|
||||
const removeAnswerPathFromFiles = (filesAndContents: FileAndContent[]) =>
|
||||
filesAndContents.map(({path, content}) => ({
|
||||
path: path.replace(`${ANSWER_DIRECTORY}/`, ''),
|
||||
content,
|
||||
}));
|
||||
|
||||
if (answerSrc) {
|
||||
const answerSrcFiles = await getExternalTutorialFiles(step, answerSrc);
|
||||
|
||||
const answerFiles = await getFilesContents(answerSrcFiles);
|
||||
|
||||
return getTutorialFiles(removeAnswerPathFromFiles(answerFiles));
|
||||
}
|
||||
|
||||
const answerFiles = filesContents.filter(({path}) => path.includes(`${ANSWER_DIRECTORY}/`));
|
||||
|
||||
return getTutorialFiles(removeAnswerPathFromFiles(answerFiles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files external to the current tutorial directory.
|
||||
*/
|
||||
async function getExternalTutorialFiles(
|
||||
step: TutorialStep,
|
||||
relativePath: string,
|
||||
): Promise<string[]> {
|
||||
const externalFilesPath = join(step.path, relativePath);
|
||||
|
||||
const files = await globWithCwdPath('**', {
|
||||
...GLOB_OPTIONS,
|
||||
cwd: externalFilesPath,
|
||||
nodir: true,
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function getTutorialContentPath(stepFiles: string[]) {
|
||||
const tutorialContentPath = stepFiles.find((filename) => filename.endsWith('README.md'));
|
||||
|
||||
if (!tutorialContentPath) {
|
||||
const tutorialPath = dirname(stepFiles[0]);
|
||||
|
||||
throw `Tutorial at ${tutorialPath} is missing a README.md for content`;
|
||||
}
|
||||
|
||||
const contentPathWithDistPath = tutorialContentPath.replace(
|
||||
TUTORIALS_PROJECT_PATH,
|
||||
TUTORIALS_DIST_PATH,
|
||||
);
|
||||
|
||||
return contentPathWithDistPath.replace(/\.[^.]+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the file path by making it relative to the project root.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* getCleanFilePath('path-to-tutorial/intro/src/app/app.component.ts') => 'src/app/app.component.ts'
|
||||
* getCleanFilePath('path-to-tutorial/steps/1-step/src/app/app.component.ts') => 'src/app/app.component.ts'
|
||||
* ```
|
||||
*/
|
||||
function getCleanFilePath(path: string) {
|
||||
if (path.includes(TUTORIALS_COMMON_DIRECTORY)) return getCleanCommonFilePath(path);
|
||||
if (path.includes(STEPS_DIRECTORY)) return path.replace(BEFORE_STEP_PATH_REGEX, '');
|
||||
|
||||
return path.replace(BEFORE_INTRO_PATH_REGEX, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge common project files with the step files to get the final files to download.
|
||||
*/
|
||||
function getFilesToDownload(
|
||||
commonFilesContents: FileAndContent[],
|
||||
stepFilesWithoutAnswer: FileAndContent[],
|
||||
) {
|
||||
const commonFilesWithoutPath = commonFilesContents.map(({path, content}) => ({
|
||||
path: getCleanCommonFilePath(path),
|
||||
content,
|
||||
}));
|
||||
|
||||
const stepFilesWithoutPath = stepFilesWithoutAnswer
|
||||
.filter(({path}) => shouldUseFileInWebContainer(path))
|
||||
.map(({path, content}) => ({
|
||||
path: getCleanFilePath(path),
|
||||
content,
|
||||
}));
|
||||
|
||||
// use object to easily override common files with step files
|
||||
const filesToDownload = {
|
||||
...filesAndContentsToRecord(commonFilesWithoutPath),
|
||||
...filesAndContentsToRecord(stepFilesWithoutPath),
|
||||
};
|
||||
|
||||
return recordToFilesAndContents(filesToDownload);
|
||||
}
|
||||
|
||||
function isCommonTutorialFile(tutorialPath: string, filePath: string) {
|
||||
return filePath.includes(join(tutorialPath, TUTORIALS_COMMON_DIRECTORY));
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
/*!
|
||||
* @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 {glob} from 'glob';
|
||||
import {basename, join, sep} from 'path';
|
||||
|
||||
import {
|
||||
filesAndContentsToRecord,
|
||||
getFilesContents,
|
||||
getPackageJsonFromFiles,
|
||||
globWithCwdPath,
|
||||
} from '../utils/filesystem';
|
||||
import {CONFIG_FILE, GLOB_OPTIONS, TUTORIALS_PLAYGROUND_NODE_PATH} from '../utils/node-constants';
|
||||
import {
|
||||
DEFAULT_PLAYGROUND_TEMPLATE,
|
||||
STARTER_PLAYGROUND_TEMPLATE,
|
||||
TUTORIALS_COMMON_DIRECTORY,
|
||||
TUTORIALS_PLAYGROUND_DIRECTORY,
|
||||
TutorialType,
|
||||
} from '../utils/web-constants';
|
||||
import {getFileSystemTree, shouldUseFileInWebContainer} from '../utils/webcontainers';
|
||||
|
||||
import {getTutorialConfig, validateOpenFilesConfig} from '../tutorials-config';
|
||||
import {
|
||||
FileAndContent,
|
||||
FileAndContentRecord,
|
||||
PlaygroundFiles,
|
||||
PlaygroundRouteData,
|
||||
PlaygroundTemplate,
|
||||
TutorialConfig,
|
||||
TutorialFiles,
|
||||
TutorialMetadata,
|
||||
} from '../tutorials-types';
|
||||
import {getAllFiles, validatePackageJson} from '../utils/metadata';
|
||||
import {getCleanFilePath as getCleanCommonFilePath} from './common';
|
||||
|
||||
// used to avoid recreating a regex in loops
|
||||
const beforePlaygroundPathRegexCache = new Map<string, RegExp>();
|
||||
|
||||
export async function getTutorialPlaygroundFiles(
|
||||
commonFiles: FileAndContent[],
|
||||
): Promise<Map<string, PlaygroundFiles>> {
|
||||
const cwd = TUTORIALS_PLAYGROUND_NODE_PATH;
|
||||
|
||||
const [playgroundProjects, playgroundFiles] = await Promise.all([
|
||||
glob('*/', {
|
||||
ignore: [TUTORIALS_COMMON_DIRECTORY],
|
||||
cwd,
|
||||
}),
|
||||
|
||||
globWithCwdPath('**', {
|
||||
...GLOB_OPTIONS,
|
||||
nodir: true,
|
||||
cwd,
|
||||
}),
|
||||
]);
|
||||
|
||||
const playgroundCommonFiles = playgroundFiles.filter(isPlaygroundCommonFile);
|
||||
|
||||
const playgroundFilesMap = new Map<string, PlaygroundFiles>();
|
||||
|
||||
const templates: PlaygroundTemplate[] = [];
|
||||
|
||||
await Promise.all(
|
||||
playgroundProjects.map(async (project) => {
|
||||
const projectFiles = playgroundFiles.filter((file) =>
|
||||
file.startsWith(`${cwd}${sep}${project}${sep}`),
|
||||
);
|
||||
|
||||
if (!projectFiles.length) {
|
||||
throw new Error(
|
||||
`Project at ${join(TUTORIALS_PLAYGROUND_NODE_PATH, project)} has no files.`,
|
||||
);
|
||||
}
|
||||
|
||||
const projectFilesAndPlaygroundCommonFiles = [...playgroundCommonFiles, ...projectFiles];
|
||||
|
||||
const projectPath = join(TUTORIALS_PLAYGROUND_DIRECTORY, project);
|
||||
|
||||
const {sourceCode, metadata, route} = await getPlaygroundFiles(
|
||||
projectPath,
|
||||
projectFilesAndPlaygroundCommonFiles,
|
||||
commonFiles,
|
||||
);
|
||||
|
||||
// store only sourceCode and metadata per playground template
|
||||
playgroundFilesMap.set(projectPath, {sourceCode, metadata});
|
||||
|
||||
// compose playground templates from each project route object
|
||||
if (route) templates.push(...route!.templates);
|
||||
}),
|
||||
);
|
||||
|
||||
const defaultTemplate = templates.find((template) =>
|
||||
template.path.endsWith(DEFAULT_PLAYGROUND_TEMPLATE),
|
||||
);
|
||||
if (!defaultTemplate) {
|
||||
throw new Error(`Invalid default playground template path '${DEFAULT_PLAYGROUND_TEMPLATE}'`);
|
||||
}
|
||||
|
||||
const starterTemplate = templates.find((template) =>
|
||||
template.path.endsWith(STARTER_PLAYGROUND_TEMPLATE),
|
||||
);
|
||||
if (!starterTemplate) {
|
||||
throw new Error(`Invalid starter playground template path '${STARTER_PLAYGROUND_TEMPLATE}'`);
|
||||
}
|
||||
|
||||
playgroundFilesMap.set(TUTORIALS_PLAYGROUND_DIRECTORY, {
|
||||
route: {
|
||||
templates: templates.sort((a, b) => {
|
||||
const aPath = a.path.toLowerCase();
|
||||
const bPath = b.path.toLowerCase();
|
||||
|
||||
if (aPath < bPath) return -1;
|
||||
if (aPath > bPath) return 1;
|
||||
return 0;
|
||||
}),
|
||||
defaultTemplate,
|
||||
starterTemplate,
|
||||
},
|
||||
});
|
||||
|
||||
return playgroundFilesMap;
|
||||
}
|
||||
|
||||
async function getPlaygroundFiles(
|
||||
project: string,
|
||||
playgroundFiles: string[],
|
||||
commonFiles: FileAndContent[],
|
||||
): Promise<PlaygroundFiles> {
|
||||
const config = await getTutorialConfig(playgroundFiles);
|
||||
|
||||
if (config.type !== TutorialType.EDITOR_ONLY)
|
||||
throw new Error(`Playground must be of type "${TutorialType.EDITOR_ONLY}"`);
|
||||
|
||||
const filesContents = await getFilesContents(playgroundFiles);
|
||||
|
||||
const projectFiles = getProjectFiles(project, config.openFiles, filesContents, commonFiles);
|
||||
|
||||
if (config.openFiles) {
|
||||
validateOpenFilesConfig(
|
||||
TUTORIALS_PLAYGROUND_NODE_PATH,
|
||||
config.openFiles,
|
||||
Object.keys(projectFiles),
|
||||
);
|
||||
}
|
||||
|
||||
const hasPackageJson = playgroundFiles.some((file) => basename(file) === 'package.json');
|
||||
|
||||
let dependencies: TutorialMetadata['dependencies'];
|
||||
|
||||
if (hasPackageJson) {
|
||||
const packageJson = getPackageJsonFromFiles(filesContents);
|
||||
|
||||
dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
};
|
||||
|
||||
validatePackageJson(playgroundFiles, packageJson, getPackageJsonFromFiles(commonFiles));
|
||||
}
|
||||
|
||||
const allFiles = getAllFiles(
|
||||
playgroundFiles,
|
||||
commonFiles.map(({path}) => path),
|
||||
(path: string) => getCleanFilePath(project, path),
|
||||
);
|
||||
|
||||
return {
|
||||
sourceCode: getSourceCode(project, playgroundFiles, filesContents),
|
||||
metadata: {
|
||||
type: config.type,
|
||||
allFiles,
|
||||
dependencies,
|
||||
tutorialFiles: projectFiles,
|
||||
openFiles: config.openFiles ?? Object.keys(projectFiles),
|
||||
hiddenFiles: config.openFiles
|
||||
? Object.keys(projectFiles).filter((filename) => !config.openFiles!.includes(filename))
|
||||
: [],
|
||||
},
|
||||
route: {
|
||||
templates: [{path: project, label: config.title}],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getProjectFiles(
|
||||
project: string,
|
||||
openFiles: TutorialConfig['openFiles'],
|
||||
playgroundFilesContents: FileAndContent[],
|
||||
commonFiles: FileAndContent[],
|
||||
): FileAndContentRecord {
|
||||
const playgroundFilesForCodeEditor = playgroundFilesContents
|
||||
// remove files that should not be shown in the code editor
|
||||
.filter(({path}) => shouldUseFileInWebContainer(path))
|
||||
// remove playground path from file paths
|
||||
.map(({path, content}) => ({
|
||||
path: getCleanFilePath(project, path),
|
||||
content,
|
||||
}));
|
||||
|
||||
if (!openFiles) return filesAndContentsToRecord(playgroundFilesForCodeEditor);
|
||||
|
||||
const commonFilesForCodeEditor = commonFiles
|
||||
// remove files that should not be shown in the code editor
|
||||
.filter(({path}) => shouldUseFileInWebContainer(getCleanCommonFilePath(path)))
|
||||
// remove common path from file paths
|
||||
.map(({path, content}) => ({
|
||||
path: getCleanCommonFilePath(path),
|
||||
content,
|
||||
}));
|
||||
|
||||
const commonAndPlaygroundFiles = {
|
||||
...filesAndContentsToRecord(commonFilesForCodeEditor),
|
||||
...filesAndContentsToRecord(playgroundFilesForCodeEditor),
|
||||
};
|
||||
|
||||
const openFilesConfigWithContents: FileAndContentRecord = {};
|
||||
|
||||
for (const openFile of openFiles) {
|
||||
if (commonAndPlaygroundFiles[openFile]) {
|
||||
openFilesConfigWithContents[openFile] = commonAndPlaygroundFiles[openFile];
|
||||
} else {
|
||||
throw `At ${TUTORIALS_PLAYGROUND_NODE_PATH}/${CONFIG_FILE}.\n\tCould not find file "${openFile}" in playground or common files`;
|
||||
}
|
||||
}
|
||||
|
||||
return openFilesConfigWithContents;
|
||||
}
|
||||
|
||||
export function getSourceCode(
|
||||
project: string,
|
||||
tutorialFiles: string[],
|
||||
filesContents: FileAndContent[],
|
||||
) {
|
||||
const fileSystemTreeContents = filesContents
|
||||
// filter out config files
|
||||
.filter(({path}) => shouldUseFileInWebContainer(path))
|
||||
// remove steps paths from file contents
|
||||
.map(({path, content}) => ({
|
||||
path: getCleanFilePath(project, path),
|
||||
content,
|
||||
}));
|
||||
|
||||
const fileSystemTreeFiles = tutorialFiles
|
||||
.filter(shouldUseFileInWebContainer)
|
||||
.map((filename) => getCleanFilePath(project, filename));
|
||||
|
||||
return getFileSystemTree(fileSystemTreeFiles, filesAndContentsToRecord(fileSystemTreeContents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a regex that matches the string before the project name in a path.
|
||||
*
|
||||
* This is used to remove the project name from the path, keeping only the path
|
||||
* relative to the project root.
|
||||
*/
|
||||
function getBeforePlaygroundProjectRegex(project: string): RegExp {
|
||||
const projectRegex = beforePlaygroundPathRegexCache.get(project);
|
||||
if (projectRegex) return projectRegex;
|
||||
|
||||
const newRegex = new RegExp(`.*\/${project}\/`);
|
||||
beforePlaygroundPathRegexCache.set(project, newRegex);
|
||||
|
||||
return newRegex;
|
||||
}
|
||||
|
||||
function getCleanFilePath(project: string, path: string) {
|
||||
if (isPlaygroundCommonFile(path)) {
|
||||
return getCleanCommonFilePath(path);
|
||||
}
|
||||
|
||||
return path.replace(getBeforePlaygroundProjectRegex(project), '');
|
||||
}
|
||||
|
||||
export function isPlaygroundRouteData(
|
||||
routeData: PlaygroundRouteData | TutorialFiles['route'],
|
||||
): routeData is PlaygroundRouteData {
|
||||
return (routeData as PlaygroundRouteData).templates !== undefined;
|
||||
}
|
||||
|
||||
function isPlaygroundCommonFile(path: string) {
|
||||
return path.includes(TUTORIALS_COMMON_DIRECTORY);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*!
|
||||
* @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, join} from 'path';
|
||||
import {readdir} from 'fs/promises';
|
||||
import {glob} from 'glob';
|
||||
|
||||
import {getFileContents} from './utils/filesystem';
|
||||
import {
|
||||
CONFIG_FILE,
|
||||
CONFIG_KEYS,
|
||||
REQUIRED_CONFIGS,
|
||||
TUTORIALS_PROJECT_PATH,
|
||||
} from './utils/node-constants';
|
||||
|
||||
import type {TutorialConfig} from './tutorials-types';
|
||||
import {TutorialType} from './utils/web-constants';
|
||||
|
||||
export async function getTutorialConfig(tutorialFiles: string[]): Promise<TutorialConfig> {
|
||||
const configFilePath = tutorialFiles.find((file) => basename(file) === CONFIG_FILE);
|
||||
|
||||
if (!configFilePath) throw `Can't find ${CONFIG_FILE}`;
|
||||
|
||||
const configFileContents = await getFileContents(configFilePath);
|
||||
|
||||
let config: TutorialConfig;
|
||||
|
||||
try {
|
||||
config = JSON.parse(String(configFileContents));
|
||||
config = await parseConfig(config);
|
||||
|
||||
return config;
|
||||
} catch (err) {
|
||||
throw `${configFilePath.replace(`${TUTORIALS_PROJECT_PATH}/`, '')}: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseConfig(config: TutorialConfig): Promise<TutorialConfig> {
|
||||
const tutorialConfigKeys = Object.keys(config);
|
||||
|
||||
REQUIRED_CONFIGS.forEach((requiredConfig) => {
|
||||
if (!tutorialConfigKeys.includes(requiredConfig)) {
|
||||
throw `missing required config '${requiredConfig}'`;
|
||||
}
|
||||
});
|
||||
|
||||
for (const key of tutorialConfigKeys) {
|
||||
// @ts-expect-error
|
||||
if (!CONFIG_KEYS.includes(key)) throw `invalid config '${key}'`;
|
||||
}
|
||||
|
||||
if (config.nextTutorial) {
|
||||
if (typeof config.nextTutorial !== 'string') {
|
||||
throw '"nextTutorial" must be a string';
|
||||
}
|
||||
|
||||
const path = join(TUTORIALS_PROJECT_PATH, config.nextTutorial);
|
||||
|
||||
// check if nextTutorial directory exists
|
||||
try {
|
||||
await readdir(path);
|
||||
} catch (error) {
|
||||
throw `the nextTutorial "${config.nextTutorial}" doesn't exist at ${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.src) {
|
||||
if (typeof config.src !== 'string') {
|
||||
throw '"src" must be a string';
|
||||
}
|
||||
|
||||
// check if src directory exists
|
||||
const srcFiles = await glob(`**/${config.src}`, {cwd: TUTORIALS_PROJECT_PATH});
|
||||
|
||||
if (!srcFiles.length) throw `could not find "src" "${config.src}"`;
|
||||
}
|
||||
|
||||
if (config.answerSrc) {
|
||||
if (typeof config.answerSrc !== 'string') {
|
||||
throw '"answerSrc" must be a string';
|
||||
}
|
||||
|
||||
// check if answerSrc directory exists
|
||||
const answerSrcFiles = await glob(`**/${config.answerSrc}`, {cwd: TUTORIALS_PROJECT_PATH});
|
||||
|
||||
if (!answerSrcFiles.length) throw `could not find "answerSrc" "${config.answerSrc}"`;
|
||||
}
|
||||
|
||||
switch (config.type) {
|
||||
case TutorialType.EDITOR:
|
||||
assertOpenFiles(config.openFiles);
|
||||
|
||||
return config;
|
||||
|
||||
case TutorialType.CLI:
|
||||
return config;
|
||||
|
||||
case TutorialType.LOCAL:
|
||||
return config;
|
||||
|
||||
case TutorialType.EDITOR_ONLY:
|
||||
assertOpenFiles(config.openFiles);
|
||||
|
||||
return config;
|
||||
|
||||
default:
|
||||
throw 'invalid config';
|
||||
}
|
||||
}
|
||||
|
||||
function assertOpenFiles(
|
||||
openFiles: TutorialConfig['openFiles'],
|
||||
): asserts openFiles is string[] | undefined {
|
||||
if (openFiles === undefined) return;
|
||||
|
||||
const errorMessage = '"openFiles" must be an array of strings';
|
||||
|
||||
if (!Array.isArray(openFiles)) {
|
||||
throw errorMessage;
|
||||
}
|
||||
|
||||
for (const openFile of openFiles) {
|
||||
if (typeof openFile !== 'string') throw errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if openFiles exist in tutorial files.
|
||||
*/
|
||||
export function validateOpenFilesConfig(
|
||||
tutorialPath: string,
|
||||
openFiles: NonNullable<TutorialConfig['openFiles']>,
|
||||
tutorialFiles: string[],
|
||||
) {
|
||||
for (const openFile of openFiles) {
|
||||
if (!tutorialFiles.includes(openFile)) {
|
||||
throw `At: ${tutorialPath}/${CONFIG_FILE}\n\tCould not find "${openFile}" in the tutorial files`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*!
|
||||
* @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 type {FileSystemTree} from '@webcontainer/api';
|
||||
import type {TutorialNavigationItemWithStep} from './generate-tutorials-routes';
|
||||
import {NavigationItem} from '@angular/docs-shared';
|
||||
import {TutorialType} from './utils/web-constants';
|
||||
|
||||
/**
|
||||
* Represents the contents of the tutorial files to be generated by the build script
|
||||
*/
|
||||
export type TutorialFiles = {
|
||||
sourceCode?: FileSystemTree;
|
||||
metadata?: TutorialMetadata;
|
||||
sourceCodeZip?: Buffer;
|
||||
route?: Omit<TutorialNavigationItemWithStep, 'path'>;
|
||||
};
|
||||
|
||||
export type PlaygroundFiles = {
|
||||
sourceCode?: FileSystemTree;
|
||||
metadata?: TutorialMetadata;
|
||||
sourceCodeZip?: undefined;
|
||||
route?: PlaygroundRouteData;
|
||||
};
|
||||
|
||||
/** Represents the contents of the tutorial config file */
|
||||
export type TutorialMetadata = {
|
||||
type: TutorialConfig['type'];
|
||||
|
||||
/** a record of all tutorials filenames and its contents */
|
||||
tutorialFiles: FileAndContentRecord;
|
||||
|
||||
/** a record of filenames and contents for the tutorial answer */
|
||||
answerFiles?: FileAndContentRecord;
|
||||
|
||||
/** files that are part of the project but are not visible in the code editor */
|
||||
hiddenFiles: string[];
|
||||
|
||||
/**
|
||||
* All files in the project, used to find the difference between new and old projects
|
||||
* when changing projects
|
||||
*/
|
||||
allFiles: string[];
|
||||
|
||||
openFiles: NonNullable<TutorialConfig['openFiles']>;
|
||||
|
||||
/** whether a package.json exists */
|
||||
dependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type TutorialStep = {
|
||||
step: number;
|
||||
name: string;
|
||||
path: string;
|
||||
url: string;
|
||||
nextStep?: TutorialStep['url'];
|
||||
previousStep?: TutorialStep['url'];
|
||||
nextTutorial?: string;
|
||||
};
|
||||
|
||||
export type TutorialConfig =
|
||||
| EditorTutorialConfig
|
||||
| LocalTutorialConfig
|
||||
| CliTutorialConfig
|
||||
| EditorOnlyTutorialConfig;
|
||||
|
||||
export interface TutorialConfigBase {
|
||||
type: TutorialType;
|
||||
|
||||
/** The tutorial title */
|
||||
title: string;
|
||||
|
||||
/** The name of the tutorial folder that will be started after the current one ends. */
|
||||
nextTutorial?: string;
|
||||
|
||||
/** The path to the tutorial src folder when it's external to the tutorial */
|
||||
src?: string;
|
||||
|
||||
/** The path to the tutorial answer folder when it's external to the tutorial */
|
||||
answerSrc?: string;
|
||||
|
||||
/** An array of files to be open in the editor */
|
||||
openFiles?: string[];
|
||||
}
|
||||
|
||||
/** Represents a tutorial config with all the embedded editor components enabled */
|
||||
export interface EditorTutorialConfig extends TutorialConfigBase {
|
||||
type: TutorialType.EDITOR;
|
||||
}
|
||||
|
||||
/** Represents a tutorial config that won't use the embedded editor */
|
||||
export interface LocalTutorialConfig extends TutorialConfigBase {
|
||||
type: TutorialType.LOCAL;
|
||||
|
||||
// fields that must be undefined for local app tutorials
|
||||
openFiles?: undefined;
|
||||
src?: undefined;
|
||||
answerSrc?: undefined;
|
||||
}
|
||||
|
||||
/** Represents a tutorial config that supports only the interactive terminal for the Angular CLI */
|
||||
export type CliTutorialConfig = Omit<LocalTutorialConfig, 'type'> & {
|
||||
type: TutorialType.CLI;
|
||||
};
|
||||
|
||||
export type EditorOnlyTutorialConfig = Omit<EditorTutorialConfig, 'type'> & {
|
||||
type: TutorialType.EDITOR_ONLY;
|
||||
};
|
||||
|
||||
export type FileAndContent = {
|
||||
path: string;
|
||||
content: string | Buffer;
|
||||
};
|
||||
|
||||
export type FileAndContentRecord = Record<FileAndContent['path'], FileAndContent['content']>;
|
||||
|
||||
export type TutorialNavigationItem = {
|
||||
path: NonNullable<NavigationItem['path']>;
|
||||
label: NonNullable<NavigationItem['label']>;
|
||||
children?: TutorialNavigationItem[];
|
||||
parent?: TutorialNavigationItem;
|
||||
contentPath?: string;
|
||||
tutorialData: TutorialNavigationData;
|
||||
};
|
||||
|
||||
export type TutorialNavigationData = {
|
||||
type: TutorialConfig['type'];
|
||||
title: TutorialConfig['title'];
|
||||
nextStep?: string;
|
||||
previousStep?: string;
|
||||
nextTutorial?: string;
|
||||
sourceCodeZipPath?: string;
|
||||
};
|
||||
|
||||
export type PlaygroundRouteData = {
|
||||
templates: PlaygroundTemplate[];
|
||||
defaultTemplate?: PlaygroundTemplate;
|
||||
starterTemplate?: PlaygroundTemplate;
|
||||
};
|
||||
|
||||
export type PlaygroundTemplate = Required<Pick<NavigationItem, 'path' | 'label'>>;
|
||||
|
||||
// Note: only the fields being used are defined in this type
|
||||
export interface PackageJson {
|
||||
dependencies: Record<string, string>;
|
||||
devDependencies: Record<string, string>;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*!
|
||||
* @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 {glob} from 'glob';
|
||||
import {TUTORIALS_CONTENT_NODE_PATH} from './utils/node-constants';
|
||||
import {dirname} from 'path';
|
||||
import {execSync} from 'child_process';
|
||||
|
||||
main();
|
||||
|
||||
/**
|
||||
* This script updates the package-lock.json files for all tutorials
|
||||
* after manually changing the dependencies in the package.json files
|
||||
*/
|
||||
async function main() {
|
||||
const tutorialsPackageJsons = await glob('**/package.json', {
|
||||
ignore: ['**/node_modules'],
|
||||
absolute: true,
|
||||
cwd: TUTORIALS_CONTENT_NODE_PATH,
|
||||
});
|
||||
|
||||
for (const path of tutorialsPackageJsons) {
|
||||
const directory = dirname(path);
|
||||
|
||||
console.info(`\nUpdating lock file at ${directory}\n`);
|
||||
execSync('npm install --package-lock-only', {
|
||||
cwd: directory,
|
||||
stdio: [null, null, 'inherit'],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*!
|
||||
* @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_DIRECTORY_REGEX} from '../tutorial-type/intro-and-steps';
|
||||
|
||||
export function validatePathAnswer(
|
||||
answer: string,
|
||||
existingPaths: string[],
|
||||
placeholder: string,
|
||||
): string | true {
|
||||
if (answer === placeholder) {
|
||||
return 'Please define the path';
|
||||
}
|
||||
|
||||
if (!TUTORIALS_DIRECTORY_REGEX.test(answer)) {
|
||||
return `The path must be in the format of "${placeholder}"`;
|
||||
}
|
||||
|
||||
if (existingPaths.includes(answer)) {
|
||||
return `'${answer}' already exists`;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*!
|
||||
* @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, dirname} from 'path';
|
||||
import {mkdir, readFile, writeFile} from 'fs/promises';
|
||||
import {GlobOptionsWithFileTypesFalse, glob} from 'glob';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import type {FileAndContent, FileAndContentRecord, PackageJson} from '../tutorials-types';
|
||||
|
||||
const fileContentsCache = new Map<string, string | Buffer>();
|
||||
|
||||
export async function getFileContents(path: string): Promise<string | Buffer> {
|
||||
if (fileContentsCache.get(path)) {
|
||||
return fileContentsCache.get(path)!;
|
||||
}
|
||||
|
||||
const fileBuffer = await readFile(path);
|
||||
|
||||
if (isBufferImage(fileBuffer)) {
|
||||
fileContentsCache.set(path, fileBuffer);
|
||||
return fileBuffer;
|
||||
}
|
||||
|
||||
fileContentsCache.set(path, fileBuffer.toString());
|
||||
return fileBuffer.toString();
|
||||
}
|
||||
|
||||
export async function getFilesContents(files: string[]): Promise<FileAndContent[]> {
|
||||
const filesContents: FileAndContent[] = [];
|
||||
|
||||
await Promise.all(
|
||||
files.map((path) =>
|
||||
getFileContents(path).then((fileContents) => {
|
||||
filesContents.push({path, content: fileContents});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return filesContents;
|
||||
}
|
||||
|
||||
export async function createDirectoryAndWriteFile(path: string, fileContents: string | Buffer) {
|
||||
await mkdir(dirname(path), {recursive: true}).catch(() => {
|
||||
// ignore error if directory already exists
|
||||
});
|
||||
|
||||
await writeFile(path, fileContents);
|
||||
}
|
||||
|
||||
export function filesAndContentsToRecord(filesAndContents: FileAndContent[]): FileAndContentRecord {
|
||||
return Object.fromEntries(filesAndContents.map(({path, content}) => [path, content]));
|
||||
}
|
||||
|
||||
export function recordToFilesAndContents(filesRecord: FileAndContentRecord): FileAndContent[] {
|
||||
return Object.entries(filesRecord).map(([path, content]) => ({path, content}));
|
||||
}
|
||||
|
||||
export async function getZipBuffer(files: FileAndContent[]): Promise<Buffer> {
|
||||
const zip = new JSZip();
|
||||
|
||||
for (const file of files) {
|
||||
zip.file(file.path, file.content, {binary: true});
|
||||
}
|
||||
|
||||
return await zip.generateAsync({type: 'nodebuffer'});
|
||||
}
|
||||
|
||||
export async function globWithCwdPath(
|
||||
pattern: string | string[],
|
||||
optionsWithCwd: Omit<GlobOptionsWithFileTypesFalse, 'cwd'> & {cwd: string},
|
||||
) {
|
||||
const files = await glob(pattern, optionsWithCwd);
|
||||
|
||||
return getFilesWithCwdPath(optionsWithCwd.cwd, files).sort();
|
||||
}
|
||||
|
||||
export function getFilesWithCwdPath(cwdPath: string, files: string[]) {
|
||||
return files.map((file) => file.substring(file.indexOf(cwdPath)));
|
||||
}
|
||||
|
||||
function isBufferImage(buffer: Buffer) {
|
||||
return isBufferPng(buffer) || isBufferJpg(buffer) || isBufferGif(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check initial bytes of the buffer(aka magic numbers) to see if it's a PNG file.
|
||||
*
|
||||
* See https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files
|
||||
*/
|
||||
function isBufferPng(buffer: Buffer) {
|
||||
const pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
||||
|
||||
return bufferMatchInitialBytes(buffer, pngSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check initial bytes of the buffer(aka magic numbers) to see if it's a JPG file.
|
||||
*
|
||||
* See https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files
|
||||
*/
|
||||
function isBufferJpg(buffer: Buffer) {
|
||||
const jpgSignature = [0xff, 0xd8, 0xff];
|
||||
|
||||
return bufferMatchInitialBytes(buffer, jpgSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check initial bytes of the buffer(aka magic numbers) to see if it's a GIF file.
|
||||
*
|
||||
* See https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files
|
||||
*/
|
||||
function isBufferGif(buffer: Buffer) {
|
||||
const gifSignature = [0x47, 0x49, 0x46];
|
||||
|
||||
return bufferMatchInitialBytes(buffer, gifSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the initial bytes of a buffer matches the expected bytes.
|
||||
*/
|
||||
function bufferMatchInitialBytes(buffer: Buffer, expectedInitialBytes: number[]) {
|
||||
for (const [index, byte] of expectedInitialBytes.entries()) {
|
||||
if (buffer[index] !== byte) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getPackageJsonFromFiles(filesAndContents: FileAndContent[]): PackageJson {
|
||||
const packageJson = filesAndContents.find(({path}) => basename(path) === 'package.json');
|
||||
|
||||
if (!packageJson)
|
||||
throw new Error(`Could not find ${dirname(filesAndContents[0].path)}/package.json`);
|
||||
|
||||
let parsedPackageJsonContent: PackageJson;
|
||||
|
||||
try {
|
||||
parsedPackageJsonContent = JSON.parse(packageJson.content as string);
|
||||
} catch (e) {
|
||||
throw new Error(`Could not JSON.parse ${dirname(filesAndContents[0].path)}/package.json`);
|
||||
}
|
||||
|
||||
return parsedPackageJsonContent;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*!
|
||||
* @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, dirname} from 'path';
|
||||
import {PackageJson} from '../tutorials-types';
|
||||
import {shouldUseFileInWebContainer} from './webcontainers';
|
||||
|
||||
export function validatePackageJson(
|
||||
files: string[],
|
||||
projectPackageJson: PackageJson,
|
||||
commonPackageJson: PackageJson,
|
||||
): void {
|
||||
if (!files.some((file) => basename(file) === 'package-lock.json')) {
|
||||
throw new Error(
|
||||
`Tutorial at ${dirname(
|
||||
files[0],
|
||||
)} has a package.json but is missing a package-lock.json. A lock file is required for faster installs.`,
|
||||
);
|
||||
}
|
||||
|
||||
const commonDependencies = {
|
||||
...commonPackageJson.dependencies,
|
||||
...commonPackageJson.devDependencies,
|
||||
};
|
||||
|
||||
const projectDependencies = {
|
||||
...projectPackageJson.dependencies,
|
||||
...projectPackageJson.devDependencies,
|
||||
};
|
||||
|
||||
const projectDependenciesNames = Object.keys(projectDependencies);
|
||||
|
||||
for (const dependency of Object.keys(commonDependencies)) {
|
||||
if (!projectDependenciesNames.includes(dependency)) {
|
||||
throw new Error(
|
||||
`Tutorial at ${dirname(files[0])} is missing '${dependency}' as a dependency.`,
|
||||
);
|
||||
} else {
|
||||
const commonDependencyVersion = commonDependencies[dependency];
|
||||
const projectDependencyVersion = projectDependencies[dependency];
|
||||
|
||||
if (commonDependencyVersion !== projectDependencyVersion) {
|
||||
throw new Error(
|
||||
`${dirname(
|
||||
files[0],
|
||||
)} has a different version of '${dependency}' than the common package.json.
|
||||
The common version is ${commonDependencyVersion}.
|
||||
The project version is ${projectDependencyVersion}.
|
||||
|
||||
Please update the project package.json to match the common package.json, or update both.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllFiles(
|
||||
projectFiles: string[],
|
||||
commonFiles: string[],
|
||||
getCleanFilePath: (file: string) => string,
|
||||
) {
|
||||
const commonAndProjectPaths = [...commonFiles, ...projectFiles]
|
||||
.map((file) => getCleanFilePath(file))
|
||||
.filter(shouldUseFileInWebContainer);
|
||||
|
||||
const uniquePaths = Array.from(new Set(commonAndProjectPaths));
|
||||
|
||||
return uniquePaths;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*!
|
||||
* @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 type {TutorialConfigBase} from '../tutorials-types';
|
||||
import {
|
||||
TUTORIALS_ASSETS_METADATA_DIRECTORY,
|
||||
TUTORIALS_ASSETS_ROUTES_DIRECTORY,
|
||||
TUTORIALS_ASSETS_SOURCE_CODE_DIRECTORY,
|
||||
TUTORIALS_COMMON_DIRECTORY,
|
||||
TUTORIALS_HOMEPAGE_DIRECTORY,
|
||||
TUTORIALS_PLAYGROUND_DIRECTORY,
|
||||
} from './web-constants';
|
||||
import {GlobOptionsWithFileTypesFalse} from 'glob';
|
||||
|
||||
export const CONTENT_PATH = join('projects', 'angular-dev', 'src', 'content');
|
||||
export const TUTORIALS_PROJECT_PATH = join(CONTENT_PATH, 'tutorials');
|
||||
export const TUTORIALS_ASSETS_NODE_PATH = join(
|
||||
'projects',
|
||||
'angular-dev',
|
||||
'src',
|
||||
'assets',
|
||||
'tutorials',
|
||||
);
|
||||
|
||||
export const TUTORIALS_PLAYGROUND_NODE_PATH = join(
|
||||
TUTORIALS_PROJECT_PATH,
|
||||
TUTORIALS_PLAYGROUND_DIRECTORY,
|
||||
);
|
||||
export const TUTORIALS_HOMEPAGE_NODE_PATH = join(
|
||||
TUTORIALS_PROJECT_PATH,
|
||||
TUTORIALS_HOMEPAGE_DIRECTORY,
|
||||
);
|
||||
export const TUTORIALS_COMMON_NODE_PATH = join(TUTORIALS_PROJECT_PATH, TUTORIALS_COMMON_DIRECTORY);
|
||||
|
||||
/** The common location where the tutorial assets are stored. */
|
||||
export const TUTORIALS_COMMON_ASSETS_SRC = join(TUTORIALS_COMMON_NODE_PATH, 'src', 'assets');
|
||||
|
||||
/** The common location for the tutorial assets to be served from. */
|
||||
export const TUTORIALS_COMMON_ASSETS_DEST = join(TUTORIALS_ASSETS_NODE_PATH, 'common');
|
||||
|
||||
export const TUTORIALS_SOURCE_CODE_NODE_PATH = join(
|
||||
TUTORIALS_ASSETS_NODE_PATH,
|
||||
TUTORIALS_ASSETS_SOURCE_CODE_DIRECTORY,
|
||||
);
|
||||
export const TUTORIALS_METADATA_NODE_PATH = join(
|
||||
TUTORIALS_ASSETS_NODE_PATH,
|
||||
TUTORIALS_ASSETS_METADATA_DIRECTORY,
|
||||
);
|
||||
export const TUTORIALS_CONTENT_NODE_PATH = join(
|
||||
'projects',
|
||||
'angular-dev',
|
||||
'src',
|
||||
'content',
|
||||
'tutorials',
|
||||
);
|
||||
export const TUTORIALS_ROUTES_NODE_PATH = join(
|
||||
TUTORIALS_ASSETS_NODE_PATH,
|
||||
TUTORIALS_ASSETS_ROUTES_DIRECTORY,
|
||||
'tutorials.json',
|
||||
);
|
||||
export const PLAYGROUND_ROUTE_NODE_PATH = join(
|
||||
TUTORIALS_ASSETS_NODE_PATH,
|
||||
TUTORIALS_ASSETS_ROUTES_DIRECTORY,
|
||||
'playground.json',
|
||||
);
|
||||
export const TUTORIALS_COMMON_ASSETS_PATH = join(TUTORIALS_COMMON_NODE_PATH, 'src', 'assets');
|
||||
|
||||
export const IGNORED_BASENAMES = [
|
||||
'.DS_Store',
|
||||
'LICENSE',
|
||||
'.gitignore',
|
||||
'.editorconfig',
|
||||
'.gitkeep',
|
||||
'favicon.ico',
|
||||
];
|
||||
export const IGNORED_EXTENSIONS = ['.map'];
|
||||
export const IGNORED_DIRECTORIES = ['.git'];
|
||||
|
||||
export const TUTORIAL_CONTENT_FILENAME = 'README.md';
|
||||
export const ANSWER_DIRECTORY = 'answer';
|
||||
export const INTRO_DIRECTORY = 'intro';
|
||||
export const STEPS_DIRECTORY = 'steps';
|
||||
export const CONFIG_FILE = 'config.json';
|
||||
export const CONFIG_KEYS: Array<keyof TutorialConfigBase> = [
|
||||
'title',
|
||||
'type',
|
||||
'nextTutorial',
|
||||
'src',
|
||||
'answerSrc',
|
||||
'openFiles',
|
||||
];
|
||||
export const REQUIRED_CONFIGS: Array<keyof Pick<TutorialConfigBase, 'title' | 'type'>> = [
|
||||
'title',
|
||||
'type',
|
||||
];
|
||||
|
||||
export const CONTENT_PLACEHOLDER = 'Insert content here';
|
||||
|
||||
export const GLOB_OPTIONS: GlobOptionsWithFileTypesFalse = {
|
||||
dot: false,
|
||||
absolute: true,
|
||||
posix: true,
|
||||
ignore: [
|
||||
'.',
|
||||
'**/node_modules/**',
|
||||
...IGNORED_BASENAMES.map((ignoredBasename) => `**/${ignoredBasename}`),
|
||||
...IGNORED_EXTENSIONS.map((ext) => `**/*${ext}`),
|
||||
...IGNORED_DIRECTORIES.map((dir) => `**/${dir}/**`),
|
||||
],
|
||||
};
|
||||
@@ -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
|
||||
*/
|
||||
export const TUTORIALS_ASSETS_WEB_PATH = '/assets/tutorials';
|
||||
export const TUTORIALS_DIST_PATH = 'tutorials';
|
||||
|
||||
export const TUTORIALS_PLAYGROUND_DIRECTORY = 'playground';
|
||||
|
||||
/** the playground template that loads by default when entering the /playground page */
|
||||
export const DEFAULT_PLAYGROUND_TEMPLATE = '0-minigame';
|
||||
export const STARTER_PLAYGROUND_TEMPLATE = '3-hello-world';
|
||||
|
||||
export const TUTORIALS_HOMEPAGE_DIRECTORY = 'homepage';
|
||||
export const TUTORIALS_COMMON_DIRECTORY = 'common';
|
||||
export const TUTORIALS_ASSETS_SOURCE_CODE_DIRECTORY = 'source-code';
|
||||
export const TUTORIALS_ASSETS_METADATA_DIRECTORY = 'metadata';
|
||||
export const TUTORIALS_ASSETS_ROUTES_DIRECTORY = 'routes';
|
||||
|
||||
export const TUTORIALS_SOURCE_CODE_WEB_PATH = `${TUTORIALS_ASSETS_WEB_PATH}/${TUTORIALS_ASSETS_SOURCE_CODE_DIRECTORY}`;
|
||||
export const TUTORIALS_METADATA_WEB_PATH = `${TUTORIALS_ASSETS_WEB_PATH}/${TUTORIALS_ASSETS_METADATA_DIRECTORY}`;
|
||||
|
||||
export const enum TutorialType {
|
||||
CLI = 'cli',
|
||||
LOCAL = 'local',
|
||||
EDITOR = 'editor',
|
||||
EDITOR_ONLY = 'editor-only',
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*!
|
||||
* @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, dirname, extname} from 'path';
|
||||
import {DirectoryNode, FileNode, FileSystemTree} from '@webcontainer/api';
|
||||
import {ANSWER_DIRECTORY, CONFIG_FILE} from './node-constants';
|
||||
import {FileAndContent, FileAndContentRecord} from '../tutorials-types';
|
||||
|
||||
/**
|
||||
* Create a WebContainer's FileSystemTree from a list of files and its contents
|
||||
*/
|
||||
export function getFileSystemTree(files: string[], filesContents: FileAndContentRecord) {
|
||||
const fileSystemTree: FileSystemTree = {};
|
||||
|
||||
for (let filepath of files) {
|
||||
const dir = dirname(filepath);
|
||||
const filename = basename(filepath);
|
||||
|
||||
if (dir === '.') {
|
||||
const fileNode: FileNode = {file: {contents: filesContents[filepath]}};
|
||||
|
||||
fileSystemTree[filename] = fileNode;
|
||||
} else {
|
||||
const dirParts = dir.split('/');
|
||||
|
||||
buildFileSystemTree(fileSystemTree, dirParts, filename, filesContents[filepath]);
|
||||
}
|
||||
}
|
||||
|
||||
return fileSystemTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a WebContainer's file system tree object recursively, mutating the
|
||||
* `fileSystemTree` parameter.
|
||||
*
|
||||
* @see https://webcontainers.io/api#filesystemtree
|
||||
*/
|
||||
export function buildFileSystemTree(
|
||||
fileSystemTree: FileSystemTree,
|
||||
fileDirectories: string[],
|
||||
filename: FileAndContent['path'],
|
||||
fileContents: FileAndContent['content'],
|
||||
): void {
|
||||
if (fileDirectories.length === 1) {
|
||||
const directory = fileDirectories[0];
|
||||
|
||||
const fileNode: FileNode = {file: {contents: fileContents}};
|
||||
|
||||
fileSystemTree[directory] = {
|
||||
...fileSystemTree[directory],
|
||||
|
||||
directory: {
|
||||
...(fileSystemTree[directory]
|
||||
? (fileSystemTree[directory] as DirectoryNode).directory
|
||||
: undefined),
|
||||
|
||||
[filename]: fileNode,
|
||||
},
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nextDirectory = fileDirectories.shift();
|
||||
if (!nextDirectory) return;
|
||||
|
||||
if (!fileSystemTree[nextDirectory]) {
|
||||
fileSystemTree[nextDirectory] = {directory: {}};
|
||||
}
|
||||
|
||||
buildFileSystemTree(
|
||||
(fileSystemTree[nextDirectory] as DirectoryNode).directory,
|
||||
fileDirectories,
|
||||
filename,
|
||||
fileContents,
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldUseFileInWebContainer(filename: string) {
|
||||
return (
|
||||
basename(filename) !== CONFIG_FILE &&
|
||||
['.md', '.png', '.jpg'].includes(extname(filename)) === false &&
|
||||
!filename.includes(`${ANSWER_DIRECTORY}/`)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*!
|
||||
* @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 {readFile, writeFile} from 'fs/promises';
|
||||
import {dirname, join} from 'path';
|
||||
import {argv} from 'process';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {VersionsConfig, VersionMode} from './types.mjs';
|
||||
|
||||
// CONSTANTS
|
||||
const MODE_ARG = 'v=';
|
||||
const VERSIONS_CONFIG = 'versions.json';
|
||||
|
||||
// PATHS
|
||||
const ASSETS = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'../../src/assets',
|
||||
);
|
||||
const VERSIONS_CONFIG_PATH = join(ASSETS, VERSIONS_CONFIG);
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
console.info('Updating version...');
|
||||
|
||||
const versionsConfig: VersionsConfig = JSON.parse(await readFile(VERSIONS_CONFIG_PATH, 'utf-8'));
|
||||
|
||||
const currentVersion = (await getCurrentVersion()) ?? 'stable';
|
||||
|
||||
if (
|
||||
currentVersion !== 'next' &&
|
||||
currentVersion !== 'rc' &&
|
||||
currentVersion !== 'stable' &&
|
||||
Number.isNaN(currentVersion)
|
||||
) {
|
||||
throw new Error('Invalid mode value provided!');
|
||||
}
|
||||
|
||||
versionsConfig.currentVersion = currentVersion;
|
||||
|
||||
await updateConfig(versionsConfig);
|
||||
|
||||
console.info('Updating version successfully done!');
|
||||
}
|
||||
|
||||
async function updateConfig(config: VersionsConfig): Promise<void> {
|
||||
await writeFile(VERSIONS_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function getCurrentVersion(): Promise<VersionMode | undefined> {
|
||||
return argv.find((arg) => arg.startsWith(MODE_ARG))?.replaceAll(MODE_ARG, '') as VersionMode;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "es2020"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*!
|
||||
* @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 interface VersionUrl {
|
||||
version: VersionMode;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type VersionMode = 'stable' | 'rc' | 'next' | number;
|
||||
|
||||
export interface VersionsConfig {
|
||||
currentVersion: VersionMode;
|
||||
currentVersions: VersionUrl[];
|
||||
historicalVersions: VersionUrl[];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/shared",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "shared",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^16.0.1",
|
||||
"@angular/core": "^16.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<!-- Algolia logo -->
|
||||
<svg
|
||||
id="Layer_1"
|
||||
class="adev-algolia-logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 2196.2 500"
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1,
|
||||
.cls-2 {
|
||||
fill: #003dff;
|
||||
}
|
||||
.cls-2 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
|
||||
/>
|
||||
<rect class="cls-1" x="1845.88" y="104.73" width="62.58" height="277.9" rx="5.9" ry="5.9" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -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 {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-algolia-icon',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './algolia-icon.component.html',
|
||||
})
|
||||
export class AlgoliaIcon {}
|
||||
@@ -0,0 +1,13 @@
|
||||
@for (breadcrumb of breadcrumbItems(); track breadcrumb) {
|
||||
<div class="docs-breadcrumb">
|
||||
@if (breadcrumb.path) {
|
||||
@if (breadcrumb.isExternal) {
|
||||
<a [href]="breadcrumb.path">{{ breadcrumb.label }}</a>
|
||||
} @else {
|
||||
<a [routerLink]="'/' + breadcrumb.path">{{ breadcrumb.label }}</a>
|
||||
}
|
||||
} @else {
|
||||
<span>{{ breadcrumb.label }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-block-end: 1.5rem;
|
||||
}
|
||||
|
||||
.docs-breadcrumb {
|
||||
span {
|
||||
color: var(--quaternary-contrast);
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
span {
|
||||
&::after {
|
||||
content: 'chevron_right';
|
||||
font-family: var(--icons);
|
||||
margin-inline: 0.5rem;
|
||||
color: var(--quinary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Breadcrumb} from './breadcrumb.component';
|
||||
|
||||
describe('Breadcrumb', () => {
|
||||
let component: Breadcrumb;
|
||||
let fixture: ComponentFixture<Breadcrumb>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [Breadcrumb],
|
||||
});
|
||||
fixture = TestBed.createComponent(Breadcrumb);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectionStrategy, Component, OnInit, inject, signal} from '@angular/core';
|
||||
import {NavigationState} from '../../services';
|
||||
import {NavigationItem} from '../../interfaces';
|
||||
import {NgFor, NgIf} from '@angular/common';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-breadcrumb',
|
||||
standalone: true,
|
||||
imports: [NgIf, NgFor, RouterLink],
|
||||
templateUrl: './breadcrumb.component.html',
|
||||
styleUrls: ['./breadcrumb.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Breadcrumb implements OnInit {
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
|
||||
breadcrumbItems = signal<NavigationItem[]>([]);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setBreadcrumbItemsBasedOnNavigationStructure();
|
||||
}
|
||||
|
||||
private setBreadcrumbItemsBasedOnNavigationStructure(): void {
|
||||
let breadcrumbs: NavigationItem[] = [];
|
||||
|
||||
const traverse = (node: NavigationItem | null) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.parent) {
|
||||
breadcrumbs = [node.parent, ...breadcrumbs];
|
||||
traverse(node.parent);
|
||||
}
|
||||
};
|
||||
|
||||
traverse(this.navigationState.activeNavigationItem());
|
||||
|
||||
this.breadcrumbItems.set(breadcrumbs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@if (!hasAccepted()) {
|
||||
<div class="docs-cookies-popup adev-invert-mode">
|
||||
<p>This site uses cookies from Google to deliver its services and to analyze traffic.</p>
|
||||
|
||||
<div>
|
||||
<a href="https://policies.google.com/technologies/cookies" target="_blank" rel="noopener">
|
||||
<button class="adev-primary-btn" [attr.text]="'Learn more'" aria-label="Learn More">
|
||||
Learn more
|
||||
</button>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
(click)="accept()"
|
||||
class="adev-primary-btn"
|
||||
[attr.text]="'Ok, Got it'"
|
||||
aria-label="Ok, Got it"
|
||||
>
|
||||
Ok, Got it
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user