mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
362 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5307bf119 | |||
| 151a544277 | |||
| 4e200bf13b | |||
| 5de7575be8 | |||
| a523801b25 | |||
| 290e26a2ab | |||
| 95e4806ce3 | |||
| cc8dbb61c4 | |||
| 64d9e658a4 | |||
| b393e3727e | |||
| 78f817c358 | |||
| 60f979e01f | |||
| eb44643bdf | |||
| 561cb1d8da | |||
| 031a1a8c8e | |||
| 24c6b0eafb | |||
| a2a5646253 | |||
| d033540d0f | |||
| 8b6fd8dd18 | |||
| c8ff0be38f | |||
| a00beec2e0 | |||
| 76aba9ef0e | |||
| a55c8bb0e0 | |||
| 3831942771 | |||
| 30757bd4d5 | |||
| f5872c9921 | |||
| 7c066a4af4 | |||
| 6a1d4ed667 | |||
| e46162d3e4 | |||
| 0fbdb63bbc | |||
| e1f84a31dc | |||
| aa2d815648 | |||
| 54a7b33245 | |||
| 38aab6aa9d | |||
| 12ffb25e94 | |||
| cb5d9a480e | |||
| 825718f2b1 | |||
| 0b7e7fd3a5 | |||
| 22ff9cacce | |||
| 6b164dc83a | |||
| c01af4302f | |||
| 01efb1b807 | |||
| b16fc2610a | |||
| cfb42ee2c8 | |||
| 47756d81e5 | |||
| 90018f9327 | |||
| 7b60b5196f | |||
| 35e998effb | |||
| 158b1984bf | |||
| 27bf371e32 | |||
| 56ec37dcbc | |||
| d5fd614836 | |||
| e2f3ae51df | |||
| f61519ab90 | |||
| b651648db6 | |||
| a548c0333e | |||
| 7a95cccf50 | |||
| 44c48a4835 | |||
| 2cea80c6e2 | |||
| 7d6752a806 | |||
| 354340d643 | |||
| e3950b4e1f | |||
| 881bb4d719 | |||
| 6c7d2b6fe3 | |||
| 96d554d8e7 | |||
| e994305d70 | |||
| a87d50639b | |||
| 7e5d71de3e | |||
| bd8ac477c3 | |||
| 1ce0f7b07a | |||
| a08232793d | |||
| 8c3d41b5b3 | |||
| 387051b078 | |||
| d6fec2fc75 | |||
| 4626691198 | |||
| 5c6f3f8ec0 | |||
| 70d30c28e0 | |||
| 6240613477 | |||
| 227de98201 | |||
| a46c8cbf4e | |||
| 1d31a0c129 | |||
| bd0f811405 | |||
| f729215f0a | |||
| bd275b1b26 | |||
| 181d5755ee | |||
| e1c0493135 | |||
| 4f125c5f9a | |||
| c30dde257b | |||
| 1cc87b26cb | |||
| da429c59b6 | |||
| fc6b2f2669 | |||
| fecd141dfa | |||
| 6b83328577 | |||
| d4c20aca71 | |||
| a86661ff24 | |||
| 590c61889f | |||
| b44f2f9e47 | |||
| a48d65cdf4 | |||
| 42899c5aa0 | |||
| 2367b038fb | |||
| d8eab6f807 | |||
| dab522f011 | |||
| 8400533555 | |||
| 43acd44cf8 | |||
| cab7e003b3 | |||
| 2c3daebc5d | |||
| 91dbcb5eab | |||
| fa9bca23f7 | |||
| 51d73ed957 | |||
| 9c2be715a3 | |||
| c267f54bc3 | |||
| 83067b3ef2 | |||
| 09e905ad67 | |||
| 57404d4723 | |||
| 1c92609719 | |||
| e5bd6e9ccc | |||
| 5a816b8739 | |||
| c0d7bac111 | |||
| 3afc9d0cdb | |||
| de136105aa | |||
| 95accb07b4 | |||
| a2ba5482c3 | |||
| 842b2cceb6 | |||
| 6988a0070e | |||
| 236b448364 | |||
| 1f9ff84e28 | |||
| 7ff5152547 | |||
| 80e7a0f8fa | |||
| 739c0d27ec | |||
| a8e57a1fab | |||
| 32f417f77f | |||
| c99e5cf168 | |||
| 2ce2ad1c42 | |||
| 0d88e25311 | |||
| 79b21662fd | |||
| f9b226f9dc | |||
| 6d39551f53 | |||
| 5dd4bd6cf5 | |||
| 6d4271c340 | |||
| 0717c08b7d | |||
| 2837342852 | |||
| 1841d7903b | |||
| 90af00a07c | |||
| e06a25b6c3 | |||
| 8358dcd7e3 | |||
| cc13d6f1cd | |||
| 0754122232 | |||
| bf3281a6a5 | |||
| 356da9c9f6 | |||
| 4e4c678180 | |||
| 0f37b12a17 | |||
| 7c950794ea | |||
| c9a3708e55 | |||
| ddef3ac9a4 | |||
| 935c1816fd | |||
| 2e12e277c5 | |||
| 3bd85fb7b0 | |||
| af46256043 | |||
| 34a59967d7 | |||
| 792026bc7d | |||
| d2fca84bc4 | |||
| 1f5039bbd6 | |||
| 696f003553 | |||
| e5c3b25496 | |||
| b9ea2d6900 | |||
| abb4a9bad3 | |||
| d517a75c9b | |||
| 96fd73dc5d | |||
| fdd82857b1 | |||
| ddd9df68bb | |||
| 59b6ec6be8 | |||
| 3f456821b5 | |||
| 2aaddd3f64 | |||
| 6c58252521 | |||
| d8280ace7d | |||
| b2c38399f9 | |||
| 65ccaa4169 | |||
| 8156b3d4ec | |||
| 59a3475d77 | |||
| 6e82ae5871 | |||
| de3454a507 | |||
| 0451e7eabc | |||
| e355f05b9a | |||
| 595009ecf4 | |||
| 1d7d53caa3 | |||
| 6a11e9e8b4 | |||
| 877ba3e3c1 | |||
| 3861a73135 | |||
| e74b578bd9 | |||
| b14f0dc5d6 | |||
| 4461cefa4f | |||
| ce54ead2b5 | |||
| f6c2b39812 | |||
| 8e17dd719d | |||
| 57b9798cf6 | |||
| 2579c7121e | |||
| d7397fb29b | |||
| e40e55d902 | |||
| ffd0b4c30b | |||
| 5dd2822523 | |||
| 7e8a214474 | |||
| e92eda9a31 | |||
| 499c22251d | |||
| becbcdcff0 | |||
| 384d7aacd0 | |||
| 612986bcd3 | |||
| da1b71e0a5 | |||
| 6a01d62b9d | |||
| befd197933 | |||
| 9376e7f05e | |||
| 576338a9f5 | |||
| 8580542153 | |||
| 57630c3769 | |||
| f1d42cef87 | |||
| 48816e8c22 | |||
| d3cc0e998f | |||
| 13b16b905f | |||
| 40f75364d4 | |||
| 2b0698def0 | |||
| 6858149eb1 | |||
| a803c89a21 | |||
| 957effa404 | |||
| ea6d75291f | |||
| 403d217a59 | |||
| 1acff00468 | |||
| 4c878f90d2 | |||
| 938007df9d | |||
| cd903cc9f4 | |||
| 9b32d9148e | |||
| 0536b61bb8 | |||
| df68ad4731 | |||
| 6d0362cb44 | |||
| 1660f667e0 | |||
| 1dd8558f82 | |||
| c9b1ddff4d | |||
| 54bc384661 | |||
| bd9e91ecf7 | |||
| 4e00851c04 | |||
| 54fed68bbf | |||
| 114a2f8153 | |||
| 7002cfc811 | |||
| 0d23baa740 | |||
| 3c08f91a01 | |||
| 20993f7a87 | |||
| 6ade8c9c8c | |||
| e4df686ec6 | |||
| 1fa5020739 | |||
| d2df9fb59b | |||
| fe82292583 | |||
| 439e83a974 | |||
| 4ade911e66 | |||
| d2f8412322 | |||
| 00dee8384c | |||
| ceb7f1eaf0 | |||
| f069b3a71a | |||
| ec1e652f8a | |||
| a87586538a | |||
| 6a3a7b0d13 | |||
| 0c1cf7ac6b | |||
| 394c0fb719 | |||
| 6795cccbbb | |||
| 96ad3bf065 | |||
| 3f7d081112 | |||
| 2560115793 | |||
| 9060cebc02 | |||
| db81193ce6 | |||
| af6a75a837 | |||
| f206d1041f | |||
| f5aed23924 | |||
| 8473675db2 | |||
| 0c66e2424c | |||
| 9d565cd6d6 | |||
| 182d574fa3 | |||
| 569f57daa9 | |||
| dfc77e5f58 | |||
| 6005fe2d70 | |||
| 1dc14d9853 | |||
| 129b5d8bde | |||
| 7a064e372a | |||
| 81c315ec6e | |||
| 4dce8766f8 | |||
| 0b4c013aac | |||
| 958346107f | |||
| a207421ada | |||
| 7cc9f193fe | |||
| 30f993a356 | |||
| aaf00092c8 | |||
| 44f08baf63 | |||
| b4c1ffbfa1 | |||
| 4567a090c9 | |||
| fc3cde6cff | |||
| 1a8a5cf68f | |||
| 866d0f422d | |||
| b00dd23846 | |||
| 6fbe371b2c | |||
| edd060efbf | |||
| 1091239d2d | |||
| dc6ca34d4c | |||
| 33bd523be3 | |||
| 08553046bf | |||
| 603f30cab0 | |||
| eccbf36dbd | |||
| 419855629d | |||
| 37768da011 | |||
| 1e941f6f84 | |||
| 920f072824 | |||
| 37d627dbd4 | |||
| 1032c1e1a5 | |||
| 8e4a7ab52b | |||
| 3fec271fa3 | |||
| f9ef762bc7 | |||
| e87ac245e0 | |||
| 39a6f62551 | |||
| 96267922fe | |||
| ba37440cbf | |||
| 5d3f66d1a6 | |||
| 36993f3459 | |||
| d811d82a12 | |||
| c6e4cf12f0 | |||
| a318e6ac7c | |||
| c9cde3ab10 | |||
| 35fd10a9fd | |||
| e760fd7865 | |||
| 4e135947e6 | |||
| 6fe4f44b1d | |||
| 920820a1a8 | |||
| 6735f5f634 | |||
| 2d197f0c65 | |||
| e5720edb46 | |||
| eaf735da58 | |||
| bd1e78d746 | |||
| 81a287a79a | |||
| 2fa5e702de | |||
| 65c2f02ccb | |||
| a6be2e221f | |||
| 6ae68f39b9 | |||
| 450f360bd7 | |||
| c1dc86d186 | |||
| a58301f980 | |||
| b2ba678e73 | |||
| 2d98726ec9 | |||
| 030a68b3df | |||
| 6fd863d6e1 | |||
| ba9fc2419e | |||
| e90f5e5f22 | |||
| 8e5d54a3d5 | |||
| 54f7578d55 | |||
| a6fcf75423 | |||
| 99e7629159 | |||
| 6f19117a6a | |||
| ffe9b1fcc2 | |||
| 0792424df2 | |||
| 5c70148d20 | |||
| 65b460448e | |||
| 4cd1b3d38b | |||
| 4e9d6878a5 | |||
| 2bf088e7bb | |||
| 2e5ee1c198 | |||
| 371e26f6b3 | |||
| d50d3b85bd | |||
| 073ebfe09e | |||
| 926db6dd36 |
+4
-1
@@ -16,7 +16,6 @@ integration/cli-hello-world/node_modules
|
||||
integration/cli-hello-world-ivy-i18n/node_modules
|
||||
integration/cli-hello-world-lazy/node_modules
|
||||
integration/cli-hello-world-mocha/node_modules
|
||||
integration/cli-signal-inputs/node_modules
|
||||
integration/defer/node_modules
|
||||
integration/dynamic-compiler/node_modules
|
||||
integration/forms/node_modules
|
||||
@@ -39,3 +38,7 @@ integration/typings_test_ts49/node_modules
|
||||
integration/typings_test_ts50/node_modules
|
||||
integration/typings_test_ts51/node_modules
|
||||
integration/typings_test_ts52/node_modules
|
||||
|
||||
|
||||
# Temporarily ignore all of adev
|
||||
adev/
|
||||
@@ -1,7 +1,7 @@
|
||||
# Image metadata and config.
|
||||
# Ideally, the Node.js version should match what we use on CI.
|
||||
# See `executors > default-executor` in `.circleci/config.yml`.
|
||||
FROM cimg/node:18.13.0@sha256:45d0ee279f522c4562a464df71108104cc9b2187205333ffd0707d4f05589731
|
||||
FROM cimg/node:16.13-browsers@sha256:6c7a82701665dda03e92d682360fa2e4358e07dab8d1b6a7e48f7d36e3d98117
|
||||
|
||||
|
||||
LABEL name="Angular dev environment" \
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# This workflow builds the previews for pull requests when a certain label is applied.
|
||||
# The actual deployment happens as part of a dedicated second workflow to avoid security
|
||||
# issues where the building would otherwise occur in an authorized context where secrets
|
||||
# could be leaked. More details can be found here:
|
||||
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Build adev for preview deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
adev-build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'adev: preview') ||
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build --config=aio_local_deps //adev:build
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/adev/build/browser'
|
||||
@@ -1,50 +0,0 @@
|
||||
# This workflow runs whenever the ADEV build workflow has completed. Deployment happens
|
||||
# as part of a dedicated second workflow to avoid security issues where the building would
|
||||
# otherwise occur in an authorized context where secrets could be leaked.
|
||||
#
|
||||
# More details can be found here:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Deploying adev preview to Firebase
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build adev for preview deployment']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
# Needed in order to be able to comment on the pull request.
|
||||
pull-requests: write
|
||||
# Needed in order to checkout the repository
|
||||
contents: read
|
||||
# Needed in order to retrieve the artifacts from the previous job
|
||||
actions: read
|
||||
|
||||
env:
|
||||
PREVIEW_PROJECT: ng-dev-previews
|
||||
PREVIEW_SITE: ng-dev-previews-fw
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # tag=v3
|
||||
with:
|
||||
token: '${{secrets.GITHUB_TOKEN}}'
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: ./
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
firebase-config-dir: './adev'
|
||||
firebase-public-dir: './adev/build/browser'
|
||||
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
|
||||
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- 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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
workflow-artifact-name: 'aio'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
|
||||
@@ -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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'aio'
|
||||
|
||||
@@ -16,6 +16,6 @@ jobs:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
|
||||
@@ -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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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
|
||||
|
||||
+29
-40
@@ -23,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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
|
||||
@@ -110,35 +110,20 @@ jobs:
|
||||
- name: Check generated bundle sizes
|
||||
run: yarn --cwd aio payload-size
|
||||
|
||||
adev:
|
||||
runs-on:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel test --config=aio_local_deps //adev:test
|
||||
|
||||
aio-local:
|
||||
runs-on:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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
|
||||
@@ -153,15 +138,15 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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
|
||||
@@ -197,13 +182,13 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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
|
||||
@@ -215,7 +200,7 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -223,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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: |
|
||||
@@ -233,6 +218,7 @@ jobs:
|
||||
//packages/zone.js/bundles:zone.umd.js \
|
||||
//packages/zone.js:npm_package \
|
||||
//packages/zone.js/test/closure:closure_js \
|
||||
//packages/zone.js:zone_externs
|
||||
|
||||
- run: |
|
||||
mkdir -p packages/zone.js/build/
|
||||
@@ -243,14 +229,17 @@ jobs:
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-mix.umd.js ./packages/zone.js/test/extra/
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-patch-electron.umd.js ./packages/zone.js/test/extra/
|
||||
cp dist/bin/packages/zone.js/test/closure/zone.closure.mjs ./packages/zone.js/build/test/zone.closure.mjs
|
||||
cp dist/bin/packages/zone.js/zone_externs.js ./packages/zone.js/build/zone_externs.js
|
||||
|
||||
# Install
|
||||
- run: yarn --cwd packages/zone.js install --frozen-lockfile --non-interactive
|
||||
# Run zone.js tools tests
|
||||
- run: yarn --cwd packages/zone.js promisetest
|
||||
- run: yarn --cwd packages/zone.js promisefinallytest
|
||||
- run: yarn --cwd packages/zone.js jest:test
|
||||
- run: yarn --cwd packages/zone.js jest:nodetest
|
||||
- run: yarn --cwd packages/zone.js electrontest
|
||||
- run: yarn --cwd packages/zone.js closuretest
|
||||
- run: yarn --cwd packages/zone.js/test/typings install --frozen-lockfile --non-interactive
|
||||
- run: yarn --cwd packages/zone.js/test/typings test
|
||||
|
||||
@@ -262,7 +251,7 @@ jobs:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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.
|
||||
@@ -270,9 +259,9 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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:
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- 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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- uses: angular/dev-infra/github-actions/feature-request@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- 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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
- 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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
|
||||
uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11
|
||||
uses: github/codeql-action/upload-sarif@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
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}}]'
|
||||
|
||||
@@ -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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@1173ab9b7174e4ec6ff3a3455226ca75594edaa0
|
||||
with:
|
||||
branch-prefix: docs-update-events
|
||||
pr-title: 'docs: update events'
|
||||
|
||||
@@ -57,7 +57,3 @@ baseline.json
|
||||
# Husky
|
||||
.husky/_
|
||||
aio/content/examples/.DS_Store
|
||||
|
||||
|
||||
# Ignore cache created with the Angular CLI.
|
||||
.angular/
|
||||
+1
-23
@@ -5,18 +5,7 @@ import {FormatConfig} from '@angular/ng-dev';
|
||||
*/
|
||||
export const format: FormatConfig = {
|
||||
'prettier': {
|
||||
'matchers': [
|
||||
'**/*.{yaml,yml}',
|
||||
'devtools/**/*.{js,ts}',
|
||||
'tools/**/*.{js,ts}',
|
||||
'modules/**/*.{js,ts}',
|
||||
'scripts/**/*.{js,ts}',
|
||||
'packages/animations/**/*.{js,ts}',
|
||||
'packages/bazel/**/*.{js,ts}',
|
||||
'packages/benchpress/**/*.{js,ts}',
|
||||
'packages/elements/**/*.{js,ts}',
|
||||
'packages/misc/**/*.{js,ts}',
|
||||
],
|
||||
'matchers': ['**/*.{yaml,yml}'],
|
||||
},
|
||||
'clang-format': {
|
||||
'matchers': [
|
||||
@@ -42,17 +31,6 @@ export const format: FormatConfig = {
|
||||
'!packages/common/src/i18n/currencies.ts',
|
||||
// Temporarily disable formatting for adev
|
||||
'!adev/**',
|
||||
|
||||
// Migrated to prettier
|
||||
'!devtools/**/*.{js,ts}',
|
||||
'!tools/**/*.{js,ts}',
|
||||
'!modules/**/*.{js,ts}',
|
||||
'!scripts/**/*.{js,ts}',
|
||||
'!packages/animations/**/*.{js,ts}',
|
||||
'!packages/bazel/**/*.{js,ts}',
|
||||
'!packages/benchpress/**/*.{js,ts}',
|
||||
'!packages/elements/**/*.{js,ts}',
|
||||
'!packages/misc/**/*.{js,ts}',
|
||||
],
|
||||
},
|
||||
'buildifier': true,
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"packages/http/**",
|
||||
"**/.gitignore",
|
||||
"**/.gitkeep",
|
||||
"**/yarn.lock",
|
||||
"**/package.json",
|
||||
"**/third_party/**",
|
||||
"**/tsconfig-build.json",
|
||||
"**/tsconfig-tsec.json",
|
||||
|
||||
@@ -1150,7 +1150,6 @@ groups:
|
||||
- josephperrott
|
||||
- mgechev
|
||||
- twerske
|
||||
- ~jeanmeche
|
||||
|
||||
# =========================================================
|
||||
# Dev-infra
|
||||
@@ -1395,7 +1394,6 @@ groups:
|
||||
- devversion # Paul Gschwendtner
|
||||
- dgp1130 # Doug Parker
|
||||
- dylhunn # Dylan Hunn
|
||||
- jeanmeche # Matthieu Riegler
|
||||
- jelbourn # Jeremy Elbourn
|
||||
- jessicajaniuk # Jessica Janiuk
|
||||
- JiaLiPassion # Jia Li
|
||||
|
||||
@@ -13,7 +13,6 @@ exports_files([
|
||||
"scripts/ci/payload-size.sh",
|
||||
"scripts/ci/payload-size.js",
|
||||
"package.json",
|
||||
"angular.json",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
|
||||
+1
-278
@@ -1,242 +1,3 @@
|
||||
<a name="17.1.0"></a>
|
||||
# 17.1.0 (2024-01-17)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [79ff91a813](https://github.com/angular/angular/commit/79ff91a813e544929cb5eb5f9aab762a9f3d0435) | fix | allow TS jsDocParsingMode host option to be programmatically set ([#53126](https://github.com/angular/angular/pull/53126)) |
|
||||
| [5613051a8b](https://github.com/angular/angular/commit/5613051a8bd2626ae347292807b2bf21085c4c02) | fix | allow TS jsDocParsingMode host option to be programmatically set again ([#53292](https://github.com/angular/angular/pull/53292)) |
|
||||
| [df8a825910](https://github.com/angular/angular/commit/df8a825910951bebf34a4eede42f3ce5cd3e6fb7) | fix | project empty block root node ([#53620](https://github.com/angular/angular/pull/53620)) |
|
||||
| [478d622265](https://github.com/angular/angular/commit/478d6222650884478314985e3d5132587c4f670c) | fix | project empty block root node in template pipeline ([#53620](https://github.com/angular/angular/pull/53620)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [abdc7e4578](https://github.com/angular/angular/commit/abdc7e45786667e4283912024a641975f1917d97) | feat | support type-checking for generic signal inputs ([#53521](https://github.com/angular/angular/pull/53521)) |
|
||||
| [e620b3a724](https://github.com/angular/angular/commit/e620b3a724cb615af24b7779c0ab492d24efb8cc) | fix | add compiler option to disable control flow content projection diagnostic ([#53311](https://github.com/angular/angular/pull/53311)) |
|
||||
| [4c1d69e288](https://github.com/angular/angular/commit/4c1d69e2880f22745c820eee630d10071e4fa86b) | fix | add diagnostic for control flow that prevents content projection ([#53190](https://github.com/angular/angular/pull/53190)) |
|
||||
| [76ceebad04](https://github.com/angular/angular/commit/76ceebad047f62972654a8c934c77d8d02d9fa14) | fix | do not throw fatal error if extended type check fails ([#53896](https://github.com/angular/angular/pull/53896)) |
|
||||
| [1a6eaa0fea](https://github.com/angular/angular/commit/1a6eaa0fea1024b919e17ac9d2e8c07df7916de8) | fix | input transform in local compilation mode ([#53645](https://github.com/angular/angular/pull/53645)) |
|
||||
| [56a76d73e0](https://github.com/angular/angular/commit/56a76d73e037aeea1975808d5c51608fd23d4fa6) | fix | modify `getConstructorDependencies` helper to work with reflection host after the previous change ([#52215](https://github.com/angular/angular/pull/52215)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [863be4b698](https://github.com/angular/angular/commit/863be4b6981dc60ca0610b0e61d2ba1f5759e2a3) | feat | expose new `input` API for signal-based inputs ([#53872](https://github.com/angular/angular/pull/53872)) |
|
||||
| [94096c6ede](https://github.com/angular/angular/commit/94096c6ede67436a349ae07901f2bb418bf9f461) | feat | support TypeScript 5.3 ([#52572](https://github.com/angular/angular/pull/52572)) |
|
||||
| [69b384c0d1](https://github.com/angular/angular/commit/69b384c0d16f631741339d8757c32ef08260cfce) | fix | `SignalNode` reactive node incorrectly exposing unset field ([#53571](https://github.com/angular/angular/pull/53571)) |
|
||||
| [6f79507ea7](https://github.com/angular/angular/commit/6f79507ea7f272d8d09250e222ca831f407867d8) | fix | Change defer block fixture default behavior to playthrough ([#53956](https://github.com/angular/angular/pull/53956)) |
|
||||
| [32f908ab70](https://github.com/angular/angular/commit/32f908ab70f1b9ed3f92df1cae05ddde68932404) | fix | do not accidentally inherit input transforms when overridden ([#53571](https://github.com/angular/angular/pull/53571)) |
|
||||
| [bdd61c768a](https://github.com/angular/angular/commit/bdd61c768a28b56c68634b99c036986499829f45) | fix | replace assertion with more intentional error ([#52234](https://github.com/angular/angular/pull/52234)) |
|
||||
| [0daca457bb](https://github.com/angular/angular/commit/0daca457bb5bb6ffe14b7037264f8497eb5b3daf) | fix | TestBed should still use the microtask queue to schedule effects ([#53843](https://github.com/angular/angular/pull/53843)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5c1d441029](https://github.com/angular/angular/commit/5c1d4410298e20cb03d7a1ddf7931538b6a181b4) | feat | Add info property to `NavigationExtras` ([#53303](https://github.com/angular/angular/pull/53303)) |
|
||||
| [50d7916278](https://github.com/angular/angular/commit/50d79162785bb8d3e158a7a4a3733f4c75d3b127) | feat | Add router configuration to resolve navigation promise on error ([#48910](https://github.com/angular/angular/pull/48910)) |
|
||||
| [a5a9b408e2](https://github.com/angular/angular/commit/a5a9b408e2eb64dcf1d3ca16da4897649dd2fc34) | feat | Add transient info to RouterLink input ([#53784](https://github.com/angular/angular/pull/53784)) |
|
||||
| [726530a9af](https://github.com/angular/angular/commit/726530a9af9c8daf7295cc3548f24e70f380d70e) | feat | Allow `onSameUrlNavigation: 'ignore'` in `navigateByUrl` ([#52265](https://github.com/angular/angular/pull/52265)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.9"></a>
|
||||
# 17.0.9 (2024-01-10)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c22b513b3f](https://github.com/angular/angular/commit/c22b513b3f45c49baf4d6e571735aa4aa33b7845) | fix | remove unused parameters from the ngClass constructor ([#53831](https://github.com/angular/angular/pull/53831)) |
|
||||
| [bd9f89d1c8](https://github.com/angular/angular/commit/bd9f89d1c8e295f00ef3399c6bedca4e2ce0e89e) | fix | server-side rendering error when using in-memory scrolling ([#53683](https://github.com/angular/angular/pull/53683)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [92fd6cc42e](https://github.com/angular/angular/commit/92fd6cc42e0a217f4575404ea8e2af462f14ae18) | fix | generate less code for advance instructions ([#53845](https://github.com/angular/angular/pull/53845)) |
|
||||
| [6a41961fbd](https://github.com/angular/angular/commit/6a41961fbdf921f7a3ab82e92185eab751c0d153) | fix | ignore empty switch blocks ([#53776](https://github.com/angular/angular/pull/53776)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7309463697](https://github.com/angular/angular/commit/7309463697110d848781bfe81f04b6070c759928) | fix | interpolatedSignalNotInvoked diagnostic ([#53585](https://github.com/angular/angular/pull/53585)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [441db5123f](https://github.com/angular/angular/commit/441db5123f368c5fe3a505a79b97309e8400250f) | fix | `afterRender` hooks now only run on `ApplicationRef.tick` ([#52455](https://github.com/angular/angular/pull/52455)) |
|
||||
| [f9120d79cb](https://github.com/angular/angular/commit/f9120d79cb88a9f14c4baa6981f71a5afbd984e1) | fix | allow effect to be used inside an ErrorHandler ([#53713](https://github.com/angular/angular/pull/53713)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e92c86b77f](https://github.com/angular/angular/commit/e92c86b77ff7d400c034740ee0ad68acb626a22f) | fix | Fix empty switch case offset bug in cf migration ([#53839](https://github.com/angular/angular/pull/53839)) |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [91cb16fde9](https://github.com/angular/angular/commit/91cb16fde9ff68bcdc760428c47f4ebf3e476bd2) | fix | Do not delete global Event ([#53659](https://github.com/angular/angular/pull/53659)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.8"></a>
|
||||
# 17.0.8 (2023-12-21)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [de5c9ca8e9](https://github.com/angular/angular/commit/de5c9ca8e9a026ad752aab348bd137f647cc3cc9) | fix | correctly intercept index in loop tracking function ([#53604](https://github.com/angular/angular/pull/53604)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d79489255a](https://github.com/angular/angular/commit/d79489255a48a55b136746856af0d8be51bbe665) | fix | avoid repeated work when parsing version ([#53598](https://github.com/angular/angular/pull/53598)) |
|
||||
| [513fee871e](https://github.com/angular/angular/commit/513fee871eb5d1c8a12bfe64878276b1d9c07705) | fix | tree shake version class ([#53598](https://github.com/angular/angular/pull/53598)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [eb7c29c7b6](https://github.com/angular/angular/commit/eb7c29c7b64a64853c7f7691912835240b62dfeb) | fix | cf migration - detect and error when result is invalid i18n nesting ([#53638](https://github.com/angular/angular/pull/53638)) |
|
||||
| [ed936ba0e9](https://github.com/angular/angular/commit/ed936ba0e9d5323095aa486dc58d8b088b4d64cc) | fix | cf migration - detect and error when result is invalid i18n nesting ([#53638](https://github.com/angular/angular/pull/53638)) ([#53639](https://github.com/angular/angular/pull/53639)) |
|
||||
| [5c2f2539e2](https://github.com/angular/angular/commit/5c2f2539e27bd18fd586c5977d853cd827f7b004) | fix | cf migration - ensure full check runs for all imports ([#53637](https://github.com/angular/angular/pull/53637)) |
|
||||
| [817dc1b27f](https://github.com/angular/angular/commit/817dc1b27fcf32db8f8d8417bdd46a1763460f11) | fix | cf migration - fix bug in attribute formatting ([#53636](https://github.com/angular/angular/pull/53636)) |
|
||||
| [7ac60bab9a](https://github.com/angular/angular/commit/7ac60bab9a4bb7a5af037a2de47339bd7837b157) | fix | cf migration - improve import declaration handling ([#53622](https://github.com/angular/angular/pull/53622)) |
|
||||
| [c3f85e51a9](https://github.com/angular/angular/commit/c3f85e51a970aa458b920835573fa4e392e6f909) | fix | cf migration - preserve indentation on attribute strings ([#53625](https://github.com/angular/angular/pull/53625)) |
|
||||
| [e73205ff5a](https://github.com/angular/angular/commit/e73205ff5ae4c382924266520f271b56972a0db6) | fix | cf migration - stop removing empty newlines from i18n blocks ([#53578](https://github.com/angular/angular/pull/53578)) |
|
||||
| [886aa7b2a9](https://github.com/angular/angular/commit/886aa7b2a99a301eb6e35dc1a59ef918f0bea348) | fix | Fix cf migration bug with parsing for loop conditions properly ([#53558](https://github.com/angular/angular/pull/53558)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0696ab6a5b](https://github.com/angular/angular/commit/0696ab6a5bea8acd6dafde488151150bf3332b79) | fix | Should not freeze original object used for route data ([#53635](https://github.com/angular/angular/pull/53635)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.7"></a>
|
||||
# 17.0.7 (2023-12-13)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [4fd5409090](https://github.com/angular/angular/commit/4fd5409090b5556ecde3b4e2e1f09955e71443f4) | fix | handle ambient types in input transform function ([#51474](https://github.com/angular/angular/pull/51474)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a603338fe8](https://github.com/angular/angular/commit/a603338fe800a133c64b1573f2f805978c947725) | fix | generate less type checking code in for loops ([#53515](https://github.com/angular/angular/pull/53515)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [58ed76be93](https://github.com/angular/angular/commit/58ed76be937c23713e05609da3195ebafeb49794) | fix | Avoid refreshing a host view twice when using transplanted views ([#53021](https://github.com/angular/angular/pull/53021)) |
|
||||
| [c16b5e8290](https://github.com/angular/angular/commit/c16b5e82901b28b8ca0b72009c56aaeca68c58b0) | fix | Multiple subscribers to ApplicationRef.isStable should all see values ([#53541](https://github.com/angular/angular/pull/53541)) |
|
||||
| [17dbf8b8e2](https://github.com/angular/angular/commit/17dbf8b8e2a0979515d07341b4a869a78fab8583) | fix | remove signal equality check short-circuit ([#53446](https://github.com/angular/angular/pull/53446)) |
|
||||
| [5b4add27b6](https://github.com/angular/angular/commit/5b4add27b6d9d3ba55740818dc00659b6d462b87) | fix | update feature usage marker ([#53542](https://github.com/angular/angular/pull/53542)) |
|
||||
| [68d111c841](https://github.com/angular/angular/commit/68d111c8416dff43a83bb9de1241b4847d80eae7) | perf | avoid changes Observable creation on QueryList ([#53498](https://github.com/angular/angular/pull/53498)) |
|
||||
| [044cb553b4](https://github.com/angular/angular/commit/044cb553b4b8ed2f5f9a80131c5da84b3c964d9f) | perf | optimize memory allocation when reconcilling lists ([#52245](https://github.com/angular/angular/pull/52245)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [96ab999698](https://github.com/angular/angular/commit/96ab999698ca186b8e2b95177ea6f2c84e43628a) | fix | CF Migration - ensure bound ngIfElse cases ignore line breaks ([#53435](https://github.com/angular/angular/pull/53435)) |
|
||||
| [c9a1c6f1c7](https://github.com/angular/angular/commit/c9a1c6f1c7455431755dc31e00d72864931bf819) | fix | cf migration - undo changes when html fails to parse post migration ([#53530](https://github.com/angular/angular/pull/53530)) |
|
||||
| [b75aca1d74](https://github.com/angular/angular/commit/b75aca1d747e1bb55d4f3f6996bb2cb363e34f5b) | fix | CF migration only remove newlines of changed template content ([#53508](https://github.com/angular/angular/pull/53508)) |
|
||||
| [e88a12d5b3](https://github.com/angular/angular/commit/e88a12d5b3cf0e7c55c15c6820362d4d1e25ecaa) | fix | cf migration validate structure of ngswitch before migrating ([#53530](https://github.com/angular/angular/pull/53530)) |
|
||||
| [543df3dca5](https://github.com/angular/angular/commit/543df3dca52597aae298a01271337949d891dc65) | fix | ensure we do not overwrite prior template replacements in migration ([#53393](https://github.com/angular/angular/pull/53393)) |
|
||||
| [d232ea143f](https://github.com/angular/angular/commit/d232ea143f99fffc190c126d01920e965ffde3a3) | fix | fix cf migration import removal when errors occur ([#53502](https://github.com/angular/angular/pull/53502)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d5c631bf36](https://github.com/angular/angular/commit/d5c631bf3663b414b6d7734352804e594bfa2772) | fix | Get correct base path when using "." as base href when serving from the file:// protocol. ([#53547](https://github.com/angular/angular/pull/53547)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e750e4edcf](https://github.com/angular/angular/commit/e750e4edcfe812bcee82d4a2f53fd8acd8792f08) | fix | provide more actionable error message when route is not matched in production mode ([#53523](https://github.com/angular/angular/pull/53523)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.6"></a>
|
||||
# 17.0.6 (2023-12-06)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a2e5f483f5](https://github.com/angular/angular/commit/a2e5f483f5a869c0cca205f092049e252a02b710) | fix | generate proper code for nullish coalescing in styling host bindings ([#53305](https://github.com/angular/angular/pull/53305)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [66ecf4c274](https://github.com/angular/angular/commit/66ecf4c2748b43b7e53af00499b153bbe70dd684) | fix | add compiler option to disable control flow content projection diagnostic ([#53387](https://github.com/angular/angular/pull/53387)) |
|
||||
| [74e6ce5d23](https://github.com/angular/angular/commit/74e6ce5d233c8763b6c437fab5d81d7b89ae6cd4) | fix | add diagnostic for control flow that prevents content projection ([#53387](https://github.com/angular/angular/pull/53387)) |
|
||||
| [6ec7a42b95](https://github.com/angular/angular/commit/6ec7a42b9578aa34a66bb7c81ac491bb18f98941) | fix | avoid conflicts with built-in global variables in for loop blocks ([#53319](https://github.com/angular/angular/pull/53319)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0a53f96094](https://github.com/angular/angular/commit/0a53f9609462fc59cf20c7fe1436d153c23e0412) | fix | cleanup signal consumers for all views ([#53351](https://github.com/angular/angular/pull/53351)) |
|
||||
| [4fc1581bbc](https://github.com/angular/angular/commit/4fc1581bbcd98e607eb2bbd9976c64c92a70d827) | fix | handle hydration of multiple nodes projected in a single slot ([#53270](https://github.com/angular/angular/pull/53270)) |
|
||||
| [14e66533ec](https://github.com/angular/angular/commit/14e66533ec49184723a66652253e9ae863a972e0) | fix | support hydration for cases when content is re-projected using ng-template ([#53304](https://github.com/angular/angular/pull/53304)) |
|
||||
| [8e366e8911](https://github.com/angular/angular/commit/8e366e8911434d5b91e83e215320caae72f6adf8) | fix | support swapping hydrated views in `@for` loops ([#53274](https://github.com/angular/angular/pull/53274)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [45064f1ae1](https://github.com/angular/angular/commit/45064f1ae1ceb9c5a1f1053077f77370e3e9fdb2) | fix | CF migration - ensure NgIfElse attributes are properly removed ([#53298](https://github.com/angular/angular/pull/53298)) |
|
||||
| [a6275cfa54](https://github.com/angular/angular/commit/a6275cfa54d680d6612a1c91e5d3f6b86b828fbc) | fix | CF Migration - Fix case of aliases on i18n ng-templates preventing removal ([#53299](https://github.com/angular/angular/pull/53299)) |
|
||||
| [58a96e0f50](https://github.com/angular/angular/commit/58a96e0f50f2d59376aa357188ad0792b39e4e70) | fix | CF Migration add support for ngIf with just a then ([#53297](https://github.com/angular/angular/pull/53297)) |
|
||||
| [26e40c7f89](https://github.com/angular/angular/commit/26e40c7f8916612034b03dae4f74e53b61d39d86) | fix | CF Migration fix missing alias for bound ngifs ([#53296](https://github.com/angular/angular/pull/53296)) |
|
||||
| [836aeba01d](https://github.com/angular/angular/commit/836aeba01db526676610802daff4e8ebca8cef1e) | fix | Change CF Migration ng-template placeholder generation and handling ([#53394](https://github.com/angular/angular/pull/53394)) |
|
||||
| [72d22ba7ee](https://github.com/angular/angular/commit/72d22ba7eea87e2873a765aa69b3d923b9d6cc6d) | fix | fix regexp for else and then in cf migration ([#53257](https://github.com/angular/angular/pull/53257)) |
|
||||
| [7a2facae8a](https://github.com/angular/angular/commit/7a2facae8af3240b21fc17857a770dad793b7b6d) | fix | handle aliases on bound ngIf migrations ([#53261](https://github.com/angular/angular/pull/53261)) |
|
||||
| [5104a89b30](https://github.com/angular/angular/commit/5104a89b3035fb07ce23e09974dc9998ef6932ca) | fix | handle nested ng-template replacement safely in CF migration ([#53368](https://github.com/angular/angular/pull/53368)) |
|
||||
| [2a4e3f5373](https://github.com/angular/angular/commit/2a4e3f5373dfbc2a1634d67e646c30d8bbe4fea8) | fix | handle templates outside of component in cf migration ([#53368](https://github.com/angular/angular/pull/53368)) |
|
||||
| [0db75ab5b1](https://github.com/angular/angular/commit/0db75ab5b1c8c79a0e7ca1d6094092f0cb3e3939) | fix | remove setting that removes comments in CF migration ([#53350](https://github.com/angular/angular/pull/53350)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [13ade13a15](https://github.com/angular/angular/commit/13ade13a15f0c5b5f782d2fda0f7a96b3c606198) | fix | Ensure canMatch guards run on wildcard routes ([#53239](https://github.com/angular/angular/pull/53239)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.5"></a>
|
||||
# 17.0.5 (2023-11-29)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6be88040d1](https://github.com/angular/angular/commit/6be88040d134b8c8d7efd2068296f68b8b7efbdd) | fix | avoid stale provider info when TestBed.overrideProvider is used ([#52918](https://github.com/angular/angular/pull/52918)) |
|
||||
| [dee50f1d78](https://github.com/angular/angular/commit/dee50f1d78d009f74edfa53f11fd6998b1af5441) | fix | inherit host directives ([#52992](https://github.com/angular/angular/pull/52992)) |
|
||||
| [07920d96d4](https://github.com/angular/angular/commit/07920d96d4e9831fd14ff01a3dd44af1017b28ca) | fix | Reattached views that are dirty from a signal update should refresh ([#53001](https://github.com/angular/angular/pull/53001)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [aab7fb8654](https://github.com/angular/angular/commit/aab7fb865486b94d6aabbe548894f8083f4e6cfe) | fix | Add ngForTemplate support to control flow migration ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [dbd6f386ea](https://github.com/angular/angular/commit/dbd6f386ea81e7f29bc1cb34fb8efbba8c290ea8) | fix | allows colons in ngIf else cases to migrate ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [5b9f896009](https://github.com/angular/angular/commit/5b9f896009d9c57c91d44e1cb7adf80c29fbfb4c) | fix | cf migration fix migrating empty switch default ([#53237](https://github.com/angular/angular/pull/53237)) |
|
||||
| [2b3d3b0fe1](https://github.com/angular/angular/commit/2b3d3b0fe10134739ec9982a776837a273ec8e1d) | fix | CF migration log warning when collection aliasing detected in `@for` ([#53238](https://github.com/angular/angular/pull/53238)) |
|
||||
| [dffeac8386](https://github.com/angular/angular/commit/dffeac8386bed73ef0b2337c33f122af9884bb82) | fix | cf migration removes unnecessary bound ngifelse attribute ([#53236](https://github.com/angular/angular/pull/53236)) |
|
||||
| [00cb3339ba](https://github.com/angular/angular/commit/00cb3339bab738100e4164339420f2492be91fd1) | fix | control flow migration formatting fixes ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [c22af72f75](https://github.com/angular/angular/commit/c22af72f753b9ad3886561228a0cfd938a2a6de9) | fix | fix off by one issue with template removal in CF migration ([#53255](https://github.com/angular/angular/pull/53255)) |
|
||||
| [ba6d7fe018](https://github.com/angular/angular/commit/ba6d7fe0184dd1d9dbab088d767097948b78d9a4) | fix | fixes CF migration i18n ng-template offsets ([#53212](https://github.com/angular/angular/pull/53212)) |
|
||||
| [8f6affdd64](https://github.com/angular/angular/commit/8f6affdd64c6022c6a96fddac564c0ec05c5da9b) | fix | fixes control flow migration common module removal ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [6ae408847c](https://github.com/angular/angular/commit/6ae408847ce5943ff34c7432a76533b69f08cb63) | fix | properly handle ngIfThen cases in CF migration ([#53256](https://github.com/angular/angular/pull/53256)) |
|
||||
| [0fcef65cea](https://github.com/angular/angular/commit/0fcef65cea8a7687afdf90e6e0e0c396f5792606) | fix | Update CF migration to skip templates with duplicate ng-template names ([#53204](https://github.com/angular/angular/pull/53204)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [91486aaf07](https://github.com/angular/angular/commit/91486aaf0754cb38bc9846f1d9532b1a0941f53a) | fix | Resolvers in different parts of the route tree should be able to execute together ([#52934](https://github.com/angular/angular/pull/52934)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.4"></a>
|
||||
# 17.0.4 (2023-11-20)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7f1c55755d](https://github.com/angular/angular/commit/7f1c55755d94444aa2c07fc62c276bb158e69f24) | fix | remove `load` on image once it fails to load ([#52990](https://github.com/angular/angular/pull/52990)) |
|
||||
| [fafcb0d23f](https://github.com/angular/angular/commit/fafcb0d23f1f687a2fe5c8349b916586ffadc375) | fix | scan images once page is loaded ([#52991](https://github.com/angular/angular/pull/52991)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [98376f2c09](https://github.com/angular/angular/commit/98376f2c09e9c28d1473123a2a1f4fb1c9d1cb1e) | fix | changed after checked error in for loops ([#52935](https://github.com/angular/angular/pull/52935)) |
|
||||
| [291deac663](https://github.com/angular/angular/commit/291deac6636a6f99a98dd0c9096ebe3b0547bb9e) | fix | generate i18n instructions for blocks ([#52958](https://github.com/angular/angular/pull/52958)) |
|
||||
| [49dca36880](https://github.com/angular/angular/commit/49dca36880a1c1c394533e8a94db9c5ef412ebd2) | fix | nested for loops incorrectly calculating computed variables ([#52931](https://github.com/angular/angular/pull/52931)) |
|
||||
| [f01b7183d2](https://github.com/angular/angular/commit/f01b7183d2064f41c0f5e30ee976cc91c15e06c5) | fix | produce placeholder for blocks in i18n bundles ([#52958](https://github.com/angular/angular/pull/52958)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f671f86ac2](https://github.com/angular/angular/commit/f671f86ac28d434b2fd492ef005749fe0275ece9) | fix | add diagnostic for control flow that prevents content projection ([#52726](https://github.com/angular/angular/pull/52726)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [db1a8ebdb4](https://github.com/angular/angular/commit/db1a8ebdb4da8673107ba4ba08c42d484b733c03) | fix | cleanup loading promise when no dependencies are defined ([#53031](https://github.com/angular/angular/pull/53031)) |
|
||||
| [31a1575334](https://github.com/angular/angular/commit/31a1575334ef78822d947ed858d8365ca5665f2f) | fix | handle local refs when `getDeferBlocks` is invoked in tests ([#52973](https://github.com/angular/angular/pull/52973)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ac9cd6108f](https://github.com/angular/angular/commit/ac9cd6108f6fe25e9c7a11db9816c6e07d241515) | fix | control flow migration fails for async pipe with unboxing of observable ([#52756](https://github.com/angular/angular/pull/52756)) ([#52972](https://github.com/angular/angular/pull/52972)) |
|
||||
| [13bf5b7007](https://github.com/angular/angular/commit/13bf5b700739aadb2e5a210441fb815a8501ad65) | fix | Fixes control flow migration if then else case ([#53006](https://github.com/angular/angular/pull/53006)) |
|
||||
| [492ad4698a](https://github.com/angular/angular/commit/492ad4698aaef51a3d24ae90f617a2ba3fae901e) | fix | fixes migrations of nested switches in control flow ([#53010](https://github.com/angular/angular/pull/53010)) |
|
||||
| [0fad36eff2](https://github.com/angular/angular/commit/0fad36eff2b228baa3b8868810d4ac86eb6db459) | fix | tweaks to formatting in control flow migration ([#53058](https://github.com/angular/angular/pull/53058)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.3"></a>
|
||||
# 17.0.3 (2023-11-15)
|
||||
### animations
|
||||
@@ -275,7 +36,7 @@
|
||||
| -- | -- | -- |
|
||||
| [2cea80c6e2](https://github.com/angular/angular/commit/2cea80c6e21c113d12c38c4b3219c5f3f5944bd8) | fix | error code in image performance warning ([#52727](https://github.com/angular/angular/pull/52727)) |
|
||||
| [b16fc2610a](https://github.com/angular/angular/commit/b16fc2610a37b7407713e1e0018d92372f1349e9) | fix | limit rate of markers invocations ([#52742](https://github.com/angular/angular/pull/52742)) |
|
||||
| [44c48a4835](https://github.com/angular/angular/commit/44c48a48358c92c32301b578966a8e1ee9a867d8) | fix | properly update collection with repeated keys in `@for` ([#52697](https://github.com/angular/angular/pull/52697)) |
|
||||
| [44c48a4835](https://github.com/angular/angular/commit/44c48a48358c92c32301b578966a8e1ee9a867d8) | fix | properly update collection with repeated keys in @for ([#52697](https://github.com/angular/angular/pull/52697)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
@@ -649,44 +410,6 @@ Live long and prosper 🖖🏻
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.12"></a>
|
||||
# 16.2.12 (2023-11-02)
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [03f4050636](https://github.com/angular/angular/commit/03f4050636a385cf2e920b06e7ec94d6b5f32383) | fix | remove `finish` listener once player is destroyed ([#51136](https://github.com/angular/angular/pull/51136)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e092184a5c](https://github.com/angular/angular/commit/e092184a5c3d98f4be329e4037c9039c1b420d75) | fix | apply fixed_srcset_width values only to fixed srcsets ([#52486](https://github.com/angular/angular/pull/52486)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b3b4ae4c3a](https://github.com/angular/angular/commit/b3b4ae4c3ae5f307e68e3a57f84c9e2dfc9938fb) | fix | properly emit literal types in input coercion function arguments ([#52437](https://github.com/angular/angular/pull/52437)) |
|
||||
| [873c4f2454](https://github.com/angular/angular/commit/873c4f2454716520881f9d684364bdd6ab6ef722) | fix | use originally used module specifier for transform functions ([#52437](https://github.com/angular/angular/pull/52437)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.11"></a>
|
||||
# 16.2.11 (2023-10-25)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [54ea3b65c3](https://github.com/angular/angular/commit/54ea3b65c35254b220054541f2fbc52384486156) | fix | emit provider configured event when a service is configured with `providedIn` ([#52365](https://github.com/angular/angular/pull/52365)) |
|
||||
| [78533324dc](https://github.com/angular/angular/commit/78533324dcda2e884fbe489aaa6b37fa9a6fadbc) | fix | get root and platform injector providers in special cases ([#52365](https://github.com/angular/angular/pull/52365)) |
|
||||
| [019a0f4c22](https://github.com/angular/angular/commit/019a0f4c225d6bb6f741b37bf272b62899fe1f69) | fix | load global utils before creating platform injector in the standalone case ([#52365](https://github.com/angular/angular/pull/52365)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b79b4aca91](https://github.com/angular/angular/commit/b79b4aca914aefda1605fc8d069ac8d84283f1b3) | fix | `RouterTestingHarness` should throw if a component is expected but navigation fails ([#52357](https://github.com/angular/angular/pull/52357)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.10"></a>
|
||||
# 16.2.10 (2023-10-18)
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.9"></a>
|
||||
# 16.2.9 (2023-10-11)
|
||||
### forms
|
||||
|
||||
@@ -61,25 +61,6 @@ Get started with Angular, learn the fundamentals and explore advanced topics on
|
||||
- [Lazy Loading][lazyloading]
|
||||
- [Animations][animations]
|
||||
|
||||
### Local Development
|
||||
|
||||
To contribute to Angular docs, you can setup a local environment with the following commands:
|
||||
|
||||
```bash
|
||||
# Clone Angular repo
|
||||
git clone https://github.com/angular/angular.git
|
||||
|
||||
# Navigate to project directory
|
||||
cd angular
|
||||
|
||||
# Install dependencies
|
||||
yarn
|
||||
|
||||
# Build and run local dev server
|
||||
# Note: Initial build will take some time
|
||||
yarn docs
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -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-e0ec7b60641d7f6369be45d8d02663fd50f320be.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",
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
/src/assets/tutorials
|
||||
/src/assets/previews
|
||||
/TEMP
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# Ignore ng-dev logs and user-config
|
||||
.ng-dev.err*
|
||||
.ng-dev.log
|
||||
.ng-dev.user*
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
# Yarn v2+. Zero installs disabled since we are not using PnP.
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
!.yarn/yarn.js
|
||||
|
||||
|
||||
# Firebase
|
||||
.firebase
|
||||
*-debug.log
|
||||
.runtimeconfig.json
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
// Default state for all rules
|
||||
"default": true,
|
||||
|
||||
// MD013/line-length - Line length
|
||||
"MD013": {
|
||||
// Number of characters
|
||||
"line_length": 750,
|
||||
// Include code blocks
|
||||
"code_blocks": false,
|
||||
// Include tables
|
||||
"tables": false
|
||||
},
|
||||
|
||||
// MD034/bare-URL-used - Do not lint bare url - we display links in config code that does not match linter styles
|
||||
"MD034": false,
|
||||
|
||||
// MD038 - Spaces inside code span elements - We display inline templates in codeblocks
|
||||
"MD038": false,
|
||||
|
||||
// MD046/code-block-style - Do not lint inline code blocks - we display formatted CLI output in code that does not match linter styles
|
||||
"MD046": false,
|
||||
|
||||
// MD049/emphasis-style - Emphasis style should be consistent
|
||||
"MD049": false,
|
||||
|
||||
// TODO: Enable the following rules as MD improvements:
|
||||
// MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content
|
||||
"MD024": false,
|
||||
|
||||
// MD033/no-inline-html - Inline HTML
|
||||
"MD033": false,
|
||||
|
||||
// MD051/link-fragments - Link fragments should be valid
|
||||
"MD051": false,
|
||||
|
||||
// MD052/reference-links-images - Reference links and images should use a label that is defined
|
||||
"MD052": false,
|
||||
|
||||
// MD037/no-space-in-emphasis - Inline lists (such as in tables) incorrectly trigger this warning.
|
||||
"MD037": false
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -0,0 +1,2 @@
|
||||
.yarn/releases/*.cjs
|
||||
src/content/examples/**/*.*
|
||||
@@ -0,0 +1,5 @@
|
||||
# Ignoring documentation style examples
|
||||
/src/content/api-examples
|
||||
/src/content/examples
|
||||
/src/content/tutorials
|
||||
node_modules/
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"customSyntax": "postcss-scss",
|
||||
"defaultSeverity": "error",
|
||||
"reportNeedlessDisables": true,
|
||||
"reportInvalidScopeDisables": true,
|
||||
"plugins": [
|
||||
"./tools/stylelint/loader-rule.js",
|
||||
"./tools/stylelint/no-concrete-rules.ts",
|
||||
"./tools/stylelint/no-import.ts",
|
||||
"./tools/stylelint/no-unused-import.ts",
|
||||
"./tools/stylelint/selector-no-deep.ts",
|
||||
"./tools/stylelint/single-line-comment-only.ts"
|
||||
],
|
||||
"rules": {
|
||||
"@angular/selector-no-deep": true,
|
||||
"@angular/no-unused-import": true,
|
||||
"@angular/single-line-comment-only": true,
|
||||
"@angular/no-import": true,
|
||||
|
||||
"color-hex-case": "lower",
|
||||
"color-no-invalid-hex": true,
|
||||
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"function-comma-space-after": "always-single-line",
|
||||
"function-comma-space-before": "never",
|
||||
"function-name-case": "lower",
|
||||
"function-url-quotes": "always",
|
||||
"function-url-scheme-disallowed-list": ["data"],
|
||||
"function-whitespace-after": "always",
|
||||
|
||||
"number-leading-zero": "always",
|
||||
"number-no-trailing-zeros": true,
|
||||
"length-zero-no-unit": true,
|
||||
|
||||
"string-no-newline": true,
|
||||
"string-quotes": "single",
|
||||
|
||||
"unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"unit-allowed-list": [
|
||||
"px",
|
||||
"%",
|
||||
"deg",
|
||||
"s",
|
||||
"ms",
|
||||
"em",
|
||||
"rem",
|
||||
"vh",
|
||||
"vw",
|
||||
"vmin",
|
||||
"fr",
|
||||
"ch",
|
||||
"lvh",
|
||||
"svh",
|
||||
"dvh"
|
||||
],
|
||||
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never",
|
||||
|
||||
"shorthand-property-no-redundant-values": true,
|
||||
|
||||
"property-case": "lower",
|
||||
"no-duplicate-at-import-rules": true,
|
||||
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
{
|
||||
"ignore": ["consecutive-duplicates-with-different-values"]
|
||||
}
|
||||
],
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
"declaration-block-single-line-max-declarations": 1,
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-semicolon-space-after": "always-single-line",
|
||||
"declaration-block-semicolon-newline-before": "never-multi-line",
|
||||
"declaration-block-semicolon-newline-after": "always-multi-line",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"declaration-property-value-disallowed-list": [
|
||||
{"/.*/": ["initial"]},
|
||||
{"message": "The `initial` value is not supported in IE."}
|
||||
],
|
||||
|
||||
"block-closing-brace-newline-after": "always",
|
||||
"block-closing-brace-newline-before": "always-multi-line",
|
||||
"block-opening-brace-newline-after": "always-multi-line",
|
||||
"block-opening-brace-space-before": "always-multi-line",
|
||||
|
||||
"selector-attribute-brackets-space-inside": "never",
|
||||
"selector-attribute-operator-space-after": "never",
|
||||
"selector-attribute-operator-space-before": "never",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-pseudo-class-case": "lower",
|
||||
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"selector-pseudo-element-no-unknown": true,
|
||||
"selector-type-case": "lower",
|
||||
"selector-max-id": 0,
|
||||
"no-missing-end-of-source-newline": true,
|
||||
"no-eol-whitespace": true,
|
||||
"max-line-length": [
|
||||
100,
|
||||
{
|
||||
"ignorePattern": "/https?://.*/"
|
||||
}
|
||||
],
|
||||
"linebreaks": "unix",
|
||||
"selector-class-pattern": [
|
||||
"^_?(docs-|adev-|cm-|mat-|mdc-|xterm|ͼ3|ͼ2u|ͼ1|ng-|hljs-|as-|cdk-)",
|
||||
{
|
||||
"resolveNestedSelectors": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["projects/shared/src/lib/styles/_split.scss"],
|
||||
"rules": {
|
||||
"selector-pseudo-element-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignorePseudoElements": ["ng-deep"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
load("//:packages.bzl", "link_packages")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
|
||||
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
# All source and configuration files required to build the docs app
|
||||
APPLICATION_FILES = [
|
||||
"angular.json",
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.json",
|
||||
"tsconfig.worker.json",
|
||||
] + glob(
|
||||
["src/**/*"],
|
||||
exclude = [
|
||||
"src/**/*.spec.ts",
|
||||
# Temporarily exclude generated sources produced by the non-bazel
|
||||
# build until the whole project is built by bazel and this directory
|
||||
# isn't needed.
|
||||
"src/generated/**/*",
|
||||
],
|
||||
)
|
||||
|
||||
TEST_FILES = APPLICATION_FILES + [
|
||||
"karma.conf.js",
|
||||
"tsconfig.spec.json",
|
||||
] + glob(
|
||||
["**/*.spec.ts"],
|
||||
)
|
||||
|
||||
APPLICATION_DEPS = link_packages([
|
||||
"@npm//@angular-devkit/build-angular",
|
||||
"@npm//@angular/animations",
|
||||
"@npm//@angular/cdk",
|
||||
"@npm//@angular/common",
|
||||
"@npm//@angular/compiler",
|
||||
"@npm//@angular/compiler-cli",
|
||||
"@npm//@angular/core",
|
||||
"@npm//@angular/docs",
|
||||
"@npm//@angular/forms",
|
||||
"@npm//@angular/material",
|
||||
"@npm//@angular/platform-browser",
|
||||
"@npm//@angular/platform-server",
|
||||
"@npm//@angular/router",
|
||||
"@npm//gsap",
|
||||
"@npm//marked",
|
||||
"@npm//ngx-progressbar",
|
||||
"@npm//ogl",
|
||||
"@npm//rxjs",
|
||||
"@npm//typescript",
|
||||
"//adev/src/assets/images",
|
||||
"//adev/src/assets/textures",
|
||||
"//adev/src/assets/previews",
|
||||
"//adev/src/assets:tutorials",
|
||||
"//adev/src/assets/icons",
|
||||
"//adev/src/assets:api",
|
||||
"//adev/src/assets:content",
|
||||
"@npm//@typescript/vfs",
|
||||
"@npm//@codemirror/state",
|
||||
"@npm//@codemirror/view",
|
||||
"@npm//@codemirror/language",
|
||||
"@npm//@codemirror/commands",
|
||||
"@npm//@codemirror/search",
|
||||
"@npm//@codemirror/autocomplete",
|
||||
"@npm//@codemirror/lint",
|
||||
"@npm//@codemirror/lang-html",
|
||||
"@npm//@codemirror/lang-angular",
|
||||
"@npm//@codemirror/lang-css",
|
||||
"@npm//@codemirror/lang-sass",
|
||||
"@npm//@codemirror/lang-javascript",
|
||||
"@npm//@lezer/highlight",
|
||||
"@npm//@lezer/javascript",
|
||||
"@npm//@lezer/common",
|
||||
"@npm//xterm",
|
||||
"@npm//xterm-addon-fit",
|
||||
"@npm//angular-split",
|
||||
])
|
||||
|
||||
TEST_DEPS = APPLICATION_DEPS + link_packages([
|
||||
"@npm//@angular/platform-browser-dynamic",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@npm//@types/jasmine",
|
||||
"@npm//@types/node",
|
||||
"@npm//assert",
|
||||
"@npm//jasmine",
|
||||
"@npm//jasmine-core",
|
||||
"@npm//karma-chrome-launcher",
|
||||
"@npm//karma-coverage",
|
||||
"@npm//karma-jasmine",
|
||||
"@npm//karma-jasmine-html-reporter",
|
||||
"//aio/tools:windows-chromium-path",
|
||||
])
|
||||
|
||||
copy_to_bin(
|
||||
name = "application_files_bin",
|
||||
srcs = APPLICATION_FILES,
|
||||
)
|
||||
|
||||
architect(
|
||||
name = "build",
|
||||
args = [
|
||||
"angular-dev:build",
|
||||
"--output-path=build",
|
||||
],
|
||||
chdir = "$(RULEDIR)",
|
||||
data = APPLICATION_DEPS + [
|
||||
":application_files_bin",
|
||||
],
|
||||
output_dir = True,
|
||||
)
|
||||
|
||||
architect(
|
||||
name = "serve",
|
||||
args = [
|
||||
"angular-dev:serve",
|
||||
"--poll=1000",
|
||||
"--live-reload",
|
||||
"--watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = APPLICATION_DEPS + [
|
||||
":application_files_bin",
|
||||
],
|
||||
)
|
||||
|
||||
architect_test(
|
||||
name = "test",
|
||||
args = [
|
||||
"angular-dev:test",
|
||||
"--no-watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = TEST_DEPS + TEST_FILES,
|
||||
env = {
|
||||
"CHROME_BIN": "../$(CHROMIUM)",
|
||||
},
|
||||
toolchains = [
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
],
|
||||
)
|
||||
@@ -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).
|
||||
+56
-18
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@angular/cli/lib/config/schema.json",
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": ".",
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-dev": {
|
||||
"projectType": "application",
|
||||
@@ -12,16 +12,13 @@
|
||||
"changeDetection": "OnPush"
|
||||
}
|
||||
},
|
||||
"root": ".",
|
||||
"root": "projects/angular-dev",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "adev",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"externalDependencies": [
|
||||
"path"
|
||||
],
|
||||
"outputPath": "dist/angular-dev",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
@@ -35,24 +32,36 @@
|
||||
"src/robots.txt",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
"styles": ["projects/shared/src/lib/styles/global-styles.scss"],
|
||||
"scripts": [],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||
"optimization": {
|
||||
"fonts": {
|
||||
// TODO(josephperrott): enabled inline scripts
|
||||
"inline": false
|
||||
}
|
||||
}
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "5kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
@@ -88,15 +97,44 @@
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"include": ["src/app"],
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
"styles": ["projects/shared/src/lib/styles/global-styles.scss"],
|
||||
"scripts": [],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"projectType": "library",
|
||||
"root": "projects/shared",
|
||||
"sourceRoot": "projects/shared/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/shared/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/shared/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/shared/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/shared/tsconfig.spec.json",
|
||||
"polyfills": ["zone.js", "zone.js/testing"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"hosting": {
|
||||
"target": "angular-docs",
|
||||
"public": "./build/browser",
|
||||
"ignore": ["**/.*"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "assets/tutorials/common/*.jpg",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Resource-Policy",
|
||||
"value": "cross-origin"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "*.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].+(css|js)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public,max-age=31536000,immutable"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/[0-9a-f][0-9a-f][0-9a-f].[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].js",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/@(ngsw-worker.js|ngsw.json)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Opener-Policy",
|
||||
"value": "same-origin"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
const {getAdjustedChromeBinPathForWindows} = require('../aio/tools/windows-chromium-path');
|
||||
|
||||
process.env.CHROME_BIN = getAdjustedChromeBinPathForWindows();
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma'),
|
||||
{'reporter:jasmine-seed': ['type', JasmineSeedReporter]},
|
||||
],
|
||||
proxies: {
|
||||
'/dummy/image': 'src/assets/images/logos/angular/angular.png',
|
||||
},
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
random: true,
|
||||
seed: '',
|
||||
},
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/site'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
],
|
||||
},
|
||||
reporters: ['progress', 'kjhtml', 'jasmine-seed'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
customLaunchers: {
|
||||
ChromeHeadlessNoSandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
// See /integration/README.md#browser-tests for more info on these args
|
||||
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
|
||||
},
|
||||
},
|
||||
browsers: ['ChromeHeadlessNoSandbox'],
|
||||
browserNoActivityTimeout: 60000,
|
||||
singleRun: false,
|
||||
restartOnFileChange: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Helpers
|
||||
function JasmineSeedReporter(baseReporterDecorator) {
|
||||
baseReporterDecorator(this);
|
||||
|
||||
this.onBrowserComplete = (browser, result) => {
|
||||
const seed = result.order && result.order.random && result.order.seed;
|
||||
if (seed) this.write(`${browser}: Randomized with seed ${seed}.\n`);
|
||||
};
|
||||
|
||||
this.onRunComplete = () => undefined;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"name": "blossom",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "yarn generate-all && ng serve",
|
||||
"build": "ng build angular-dev",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"ng-dev": "cross-env TS_NODE_PROJECT=$PWD/.ng-dev/tsconfig.json TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm node_modules/@angular/ng-dev/bundles/cli.mjs",
|
||||
"lint": "tslint -c tslint.json --project tsconfig.json && stylelint \"projects/**/*.+(css|scss)\" --config .stylelintrc.json && markdownlint \"projects/**/*.md\"",
|
||||
"lint-fix": "markdownlint --fix '**/*.md'",
|
||||
"format": "yarn ng-dev format changed && tslint -c tslint.json --fix --project tsconfig.json",
|
||||
"serve:prerender": "http-server -c-l dist/angular-dev/browser",
|
||||
"postinstall": "yarn generate-all",
|
||||
"generate-all": "yarn generate-tutorials && yarn generate-docs && yarn generate-previews && yarn set-version && yarn generate-api && yarn generate-cli",
|
||||
"generate-api": "ts-node-esm ./prerender/api-gen/index.ts all ./src/assets/content/api",
|
||||
"generate-cli": "ts-node-esm ./prerender/api-gen/cli-index.ts all ./src/assets/content/cli",
|
||||
"generate-docs": "ts-node-esm ./prerender/markdown-pipeline/index.mts",
|
||||
"generate-previews": "ts-node-esm scripts/examples/generate-previews-components-map.mts",
|
||||
"generate-tutorials": "ts-node scripts/tutorials/generate-tutorials.ts",
|
||||
"check-links": "ts-node scripts/checks/check-links.ts",
|
||||
"new-example": "ts-node-esm scripts/examples/create-example.mts",
|
||||
"new-tutorial": "ts-node scripts/tutorials/new-tutorial.ts",
|
||||
"new-playground-template": "ts-node scripts/tutorials/new-playground-template.ts",
|
||||
"update-tutorials-package-lock": "ts-node scripts/tutorials/update-package-lock.ts",
|
||||
"set-version": "ts-node-esm scripts/versions/set-current-version.mts",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.0-rc.1",
|
||||
"@angular/cdk": "17.0.0-rc.1",
|
||||
"@angular/common": "^17.0.0-rc.1",
|
||||
"@angular/compiler": "^17.0.0-rc.1",
|
||||
"@angular/core": "^17.0.0-rc.1",
|
||||
"@angular/elements": "^17.0.0-rc.1",
|
||||
"@angular/forms": "^17.0.0-rc.1",
|
||||
"@angular/material": "17.0.0-rc.1",
|
||||
"@angular/platform-browser": "^17.0.0-rc.1",
|
||||
"@angular/platform-browser-dynamic": "^17.0.0-rc.1",
|
||||
"@angular/platform-server": "^17.0.0-rc.1",
|
||||
"@angular/router": "^17.0.0-rc.1",
|
||||
"@angular/service-worker": "^17.0.0-rc.1",
|
||||
"@codemirror/autocomplete": "^6.9.0",
|
||||
"@codemirror/commands": "^6.2.4",
|
||||
"@codemirror/lang-angular": "^0.1.2",
|
||||
"@codemirror/lang-css": "^6.2.1",
|
||||
"@codemirror/lang-html": "^6.4.3",
|
||||
"@codemirror/lang-javascript": "^6.1.8",
|
||||
"@codemirror/lang-sass": "^6.0.1",
|
||||
"@codemirror/language": "^6.7.0",
|
||||
"@codemirror/lint": "^6.4.0",
|
||||
"@codemirror/search": "^6.4.0",
|
||||
"@codemirror/state": "^6.2.1",
|
||||
"@codemirror/tooltip": "^0.19.16",
|
||||
"@codemirror/view": "^6.12.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@typescript/vfs": "^1.5.0",
|
||||
"@webcontainer/api": "^1.1.5",
|
||||
"algoliasearch": "^4.20.0",
|
||||
"angular-split": "16.2.1",
|
||||
"diff": "^5.1.0",
|
||||
"express": "^4.15.2",
|
||||
"gsap": "^3.12.2",
|
||||
"highlight.js": "^11.8.0",
|
||||
"jszip": "^3.10.1",
|
||||
"ngx-progressbar": "^11.1.0",
|
||||
"ogl": "^1.0.1",
|
||||
"preact": "^10.17.1",
|
||||
"preact-render-to-string": "^6.2.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"xterm": "^5.1.0",
|
||||
"xterm-addon-fit": "^0.7.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.0.0-rc.3",
|
||||
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#bb8a1cf74bda7e7a193cc449ca9558bc3ce55ad0",
|
||||
"@angular/cli": "17.0.0-rc.3",
|
||||
"@angular/compiler-cli": "^17.0.0-rc.1",
|
||||
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#09c58eaf121b8e71edbe863c373854c0a221e290",
|
||||
"@inquirer/prompts": "^3.0.2",
|
||||
"@mermaid-js/mermaid-cli": "^10.3.1",
|
||||
"@types/algoliasearch": "^4.0.0",
|
||||
"@types/diff": "^5.0.3",
|
||||
"@types/dom-navigation": "~1.0.0",
|
||||
"@types/dom-view-transitions": "~1.0.1",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/jasmine": "~4.3.0",
|
||||
"@types/jsdom": "^21.1.2",
|
||||
"@types/marked": "^5.0.1",
|
||||
"@types/node": "^18.11.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"firebase-tools": "latest",
|
||||
"glob": "^10.2.7",
|
||||
"html-entities": "^2.4.0",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^8.0.0",
|
||||
"jasmine-core": "~4.5.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"markdownlint-cli": "^0.36.0",
|
||||
"marked": "^5.1.1",
|
||||
"ng-packagr": "17.0.0-next.1",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"prettier": "^2.8.4",
|
||||
"stylelint": "^14.14.0",
|
||||
"ts-node": "^10.8.1",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-eslint-rules": "5.4.0",
|
||||
"typescript": "~5.2.0",
|
||||
"uuid": "^9.0.0",
|
||||
"vrsource-tslint-rules": "6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
/* tslint:disable:no-console */
|
||||
import {NavigationItem} from '@angular/docs-shared';
|
||||
import {readFileSync} from 'fs';
|
||||
import {glob} from 'glob';
|
||||
import {join} from 'path';
|
||||
|
||||
import {SUB_NAVIGATION_DATA} from '../../src/app/sub-navigation-data';
|
||||
|
||||
// PATHS
|
||||
const PROJECT_FOLDER_PATH = join(__dirname, '../../src');
|
||||
|
||||
// SKIP Files
|
||||
const SKIPED_ORPHAN = ['kitchen-sink', 'error', 'examples'];
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
const contentDir = join(PROJECT_FOLDER_PATH, 'content');
|
||||
const mdFiles = await retrieveAllMarkdownFiles(contentDir);
|
||||
|
||||
const allowedPaths = new Set([
|
||||
...SUB_NAVIGATION_DATA.docs.flatMap((item: any) => getPaths(item)),
|
||||
SUB_NAVIGATION_DATA.reference.flatMap((item: any) => getPaths(item)),
|
||||
SUB_NAVIGATION_DATA.footer.flatMap((item: any) => getPaths(item)),
|
||||
]);
|
||||
|
||||
const contentPaths = new Set([
|
||||
...SUB_NAVIGATION_DATA.docs.flatMap((item: any) => getContentPaths(item)),
|
||||
...SUB_NAVIGATION_DATA.reference.flatMap((item: any) => getContentPaths(item)),
|
||||
...SUB_NAVIGATION_DATA.footer.flatMap((item: any) => getContentPaths(item)),
|
||||
]);
|
||||
|
||||
// Regular expression to match relative links
|
||||
const regex = /\]\((?!https?:\/\/)([^)]+)\)/g;
|
||||
|
||||
const orphanFiles: string[] = [];
|
||||
for (const {path} of mdFiles) {
|
||||
if (
|
||||
!contentPaths.has(path.slice(0, -3)) &&
|
||||
!path.startsWith('tutorials') &&
|
||||
!SKIPED_ORPHAN.some((skip) => path.startsWith(skip))
|
||||
) {
|
||||
orphanFiles.push(path.slice(0, -3));
|
||||
}
|
||||
}
|
||||
if (orphanFiles.length > 0) {
|
||||
console.log('======== ORPHAN FILES =========\n');
|
||||
orphanFiles.forEach((file) => console.warn(file));
|
||||
}
|
||||
|
||||
const deadLinks: {path: string; links: string[]}[] = [];
|
||||
for (const {content, path} of mdFiles) {
|
||||
let match;
|
||||
const matches = [];
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const relativeLink = match[1].split('#')[0];
|
||||
if (!allowedPaths.has(relativeLink) && relativeLink.startsWith('guide')) {
|
||||
matches.push(relativeLink);
|
||||
}
|
||||
}
|
||||
if (matches.length) {
|
||||
deadLinks.push({path, links: matches});
|
||||
}
|
||||
}
|
||||
if (deadLinks.length > 0) {
|
||||
console.warn('\n\n======== DEAD RELATIVE LINKS =========\n');
|
||||
deadLinks.forEach(({path, links}) => {
|
||||
console.warn(path);
|
||||
links.forEach((path) => console.log(`\t ${path}`));
|
||||
});
|
||||
}
|
||||
|
||||
if (deadLinks.length > 0 || orphanFiles.length > 0) {
|
||||
throw new Error('Check fails');
|
||||
}
|
||||
}
|
||||
|
||||
function getPaths(navItem: NavigationItem): string[] {
|
||||
return [
|
||||
...(navItem.path ? [navItem.path] : []),
|
||||
...(navItem.children ?? []).flatMap((item) => getPaths(item)),
|
||||
];
|
||||
}
|
||||
|
||||
function getContentPaths(navItem: NavigationItem): string[] {
|
||||
return [
|
||||
...(navItem.contentPath ? [navItem.contentPath] : []),
|
||||
...(navItem.children ?? []).flatMap((item) => getContentPaths(item)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively search the provided directory for all markdown files and
|
||||
* asyncronously load them.
|
||||
*/
|
||||
async function retrieveAllMarkdownFiles(
|
||||
baseDir: string,
|
||||
): Promise<{path: string; content: string}[]> {
|
||||
const files = await glob('**/*.md', {
|
||||
root: baseDir,
|
||||
cwd: baseDir,
|
||||
ignore: ['**/node_modules/**'],
|
||||
});
|
||||
|
||||
return files.map((fullPathToFile) => ({
|
||||
path: fullPathToFile,
|
||||
content: readFileSync(join(baseDir, fullPathToFile), {encoding: 'utf-8'}),
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,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}/**`),
|
||||
],
|
||||
};
|
||||
@@ -6,12 +6,26 @@
|
||||
* 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 TUTORIALS_COMMON_DIRECTORY = 'common';
|
||||
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();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user